Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

azp-ext: Add task, error diagnostics and more #279

Merged
merged 3 commits into from
Dec 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions src/Sdk/AzurePipelines/AzureDevops.cs
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ public static async Task ParseSteps(Runner.Server.Azure.Devops.Context context,
}
}

private static async Task<PipelineContextData> ConvertValue(Runner.Server.Azure.Devops.Context context, TemplateToken val, string type, TemplateToken values) {
private static async Task<PipelineContextData> ConvertValue(Runner.Server.Azure.Devops.Context context, TemplateToken val, StringToken type, TemplateToken values) {
var steps = new List<TaskStep>();
var jobs = new List<Job>();
var stages = new List<Stage>();
Expand All @@ -456,7 +456,7 @@ private static async Task<PipelineContextData> ConvertValue(Runner.Server.Azure.
}
return s;
};
switch(type) {
switch(type.Value) {
case "object":
return val == null ? null : val.ToContextData();
case "boolean":
Expand Down Expand Up @@ -558,7 +558,7 @@ private static async Task<PipelineContextData> ConvertValue(Runner.Server.Azure.
}
return containers;
default:
throw new Exception("This parameter type is not supported: " + type);
throw new Exception($"{GitHub.DistributedTask.ObjectTemplating.Tokens.TemplateTokenExtensions.GetAssertPrefix(type)}This parameter type is not supported: " + type);
}
}

Expand Down Expand Up @@ -685,7 +685,7 @@ public static async Task<MappingToken> ReadTemplate(Runner.Server.Azure.Devops.C
{
var varm = mparam.AssertMapping("varm");
string name = null;
string type = "string";
StringToken type = new StringToken(null, null, null, "string");
TemplateToken def = null;
TemplateToken values = null;
foreach (var kv in varm)
Expand All @@ -696,7 +696,7 @@ public static async Task<MappingToken> ReadTemplate(Runner.Server.Azure.Devops.C
name = kv.Value.AssertLiteralString("name");
break;
case "type":
type = kv.Value.AssertLiteralString("type");
type = new StringToken(kv.Value.FileId, kv.Value.Line, kv.Value.Column, kv.Value.AssertLiteralString("type"));
break;
case "default":
def = kv.Value;
Expand All @@ -708,7 +708,7 @@ public static async Task<MappingToken> ReadTemplate(Runner.Server.Azure.Devops.C
}
if (name == null)
{
templateContext.Error(sparameters, "A value for the 'name' parameter must be provided.");
templateContext.Error(varm, "A value for the 'name' parameter must be provided.");
continue;
}
var defCtxData = def == null ? null : await ConvertValue(context, def, type, values);
Expand Down Expand Up @@ -736,9 +736,9 @@ public static async Task<MappingToken> ReadTemplate(Runner.Server.Azure.Devops.C

if (cparameters != null)
{
foreach (var unexpectedParameter in cparameters.Keys.Where(i => !parametersData.ContainsKey(i)))
foreach (var unexpectedParameter in cparameters.Where(kv => !parametersData.ContainsKey(kv.Key)))
{
templateContext.Error(parameters, $"Unexpected parameter '{unexpectedParameter}'");
templateContext.Error(unexpectedParameter.Value ?? parameters, $"Unexpected parameter '{unexpectedParameter.Key}'");
}
}

Expand Down
8 changes: 7 additions & 1 deletion src/Sdk/AzurePipelines/Stage.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using GitHub.DistributedTask.ObjectTemplating;
using GitHub.DistributedTask.ObjectTemplating.Tokens;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.DistributedTask.WebApi;
Expand Down Expand Up @@ -69,7 +70,12 @@ public static async Task ParseStages(Context context, List<Stage> stages, Sequen
if(mstep.Count == 2 && (mstep[1].Key as StringToken)?.Value != "parameters") {
throw new Exception($"Unexpected yaml key {(mstep[1].Key as StringToken)?.Value} expected parameters");
}
var file = await AzureDevops.ReadTemplate(context, path, mstep.Count == 2 ? mstep[1].Value.AssertMapping("param").ToDictionary(kv => kv.Key.AssertString("").Value, kv => kv.Value) : null, "stage-template-root");
MappingToken file;
try {
file = await AzureDevops.ReadTemplate(context, path, mstep.Count == 2 ? mstep[1].Value.AssertMapping("param").ToDictionary(kv => kv.Key.AssertString("").Value, kv => kv.Value) : null, "stage-template-root");
} catch(Exception ex) when (!(ex is TemplateValidationException)) {
throw new TemplateValidationException($"{GitHub.DistributedTask.ObjectTemplating.Tokens.TemplateTokenExtensions.GetAssertPrefix(mstep[0].Key)}{ex.Message}");
}
await ParseStages(context.ChildContext(file, path), stages, (from e in file where e.Key.AssertString("").Value == "stages" select e.Value).First().AssertSequence(""));
} else {
stages.Add(await new Stage().Parse(context, mstep));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,8 @@ internal static string GetAssertPrefix(TemplateToken value) {
if(value?.FileId != null) {
builder.Add($"FileId: {value.FileId}");
}
if(value?.Line != null) {
builder.Add($"Line: {value.Line}");
}
if(value?.Column != null) {
builder.Add($"Column: {value.Column}");
if(value?.Line != null && value?.Column != null) {
builder.Add(TemplateStrings.LineColumn(value?.Line, value?.Column) + ":");
}
return String.Join(" ", builder) + " ";
}
Expand Down
105 changes: 103 additions & 2 deletions src/azure-pipelines-vscode-ext/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This is a minimal Azure Pipelines Extension, the first vscode Extension which ca

### Remote Template References

The `azure-pipelines.repositories` settings maps the external Repositories to local or remote folders.
The `azure-pipelines-vscode-ext.repositories` settings maps the external Repositories to local or remote folders.

Syntax `[<owner>/]<repo>@<ref>=<uri>` per line. `<uri>` can be formed like `file:///<folder>` (raw file paths are not supported (yet?)), `vscode-vfs://github/<owner>/<repository>` and `vscode-vfs://azurerepos/<owner>/<project>/<repository>`

Expand All @@ -26,9 +26,96 @@ _Once this extension has been activated by any command, you can validate your pi

This command tries to evaluate your current open Azure Pipeline including templates and show the result in a new document, which you can save or validate via the official api.

### Azure Pipelines Linter Task

You can configure parameters, variables, repositories per task. You can define multiple tasks with different parameters and variables or filenames to catch errors on changing template files as early as possible.

`.vscode/tasks.json`
```jsonc
{
"version": "2.0.0",
"tasks": [
{
"type": "azure-pipelines-vscode-ext",
"label": "test",
"program": "${workspaceFolder}/azure-pipeline.yml",
"repositories": {
"myrepo@windows": "file:///C:/AzurePipelines/myrepo",
"myrepo@unix": "file:///AzurePipelines/myrepo",
"myrepo@github": "vscode-vfs://github/AzurePipelines/myrepo", // Only default branch, url doesn't accept readable ref
"myrepo@azure": "vscode-vfs://azurerepos/AzurePipelines/myrepo/myrepo" // Only default branch, url doesn't accept readable ref
},
"parameters": {
"booleanparam": true,
"numberparam": 12,
"stringparam": "Hello World",
"objectparam": {
"booleanparam": true,
"numberparam": 12,
"stringparam": "Hello World",
},
"arrayparam": [
true,
12,
"Hello World"
]
},
"variables": {
"system.debug": "true"
},
"preview": true, // Show a preview of the expanded yaml
"watch": true // Watch for yaml file changes
},
{
"type": "azure-pipelines-vscode-ext",
"label": "test2",
"program": "${workspaceFolder}/azure-pipeline.yml",
"repositories": {
"myrepo@windows": "file:///C:/AzurePipelines/myrepo",
"myrepo@unix": "file:///AzurePipelines/myrepo",
"myrepo@github": "vscode-vfs://github/AzurePipelines/myrepo", // Only default branch, url doesn't accept readable ref
"myrepo@azure": "vscode-vfs://azurerepos/AzurePipelines/myrepo/myrepo" // Only default branch, url doesn't accept readable ref
},
"parameters": {
"booleanparam": true,
"numberparam": 12,
"stringparam": "Hello World",
"objectparam": {
"booleanparam": true,
"numberparam": 12,
"stringparam": "Hello World",
},
"arrayparam": [
true,
12,
"Hello World"
]
},
"variables": {
"system.debug": "true"
},
"watch": true // Watch for yaml file changes
}
]
}
```
Sample Pipeline which dumps the parameters object (legacy parameters syntax)
```yaml
parameters:
booleanparam:
numberparam:
stringparam:
objectparam:
arrayparam:
steps:
- script: echo '${{ converttojson(parameters) }}'
- script: echo '${{ converttojson(variables) }}'
```

### Azure Pipelines Debug Adapter

Sample Debugging configuration
`.vscode/launch.json`
```jsonc
{
"type": "azure-pipelines-vscode-ext",
Expand Down Expand Up @@ -129,7 +216,11 @@ stages:

[Azure Pipelines Tools](https://marketplace.visualstudio.com/items?itemName=christopherhx.azure-pipelines-vscode-ext)

## Running the Extension
## Contributing

I'm happy to review Pull Requests to this repository, including Documentation / Readme updates or suggesting a new icon for the vscode extension.

### Running the Dev Extension

```sh
npm install
Expand All @@ -141,6 +232,16 @@ npm run build

## Changelog

### v0.0.10

- Added azure-pipelines-vscode-ext custom task type
- provide some per file diagonstics to vscode
- Not all errors have attached file locations, those are not available yet as diagnostics
- replace fileid in errors with filename
- remove stacktrace from errors
- renamed settings and commands to use the extension id as prefix
- added simple extension icon to replace the default placeholder

### v0.0.9

- Fix an error of non boolean if results
Expand Down
4 changes: 2 additions & 2 deletions src/azure-pipelines-vscode-ext/ext-core/Interop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ public static partial class Interop {
[JSImport("readFile", "extension.js")]
internal static partial Task<string> ReadFile(JSObject handle, string repositoryAndRef, string name);
[JSImport("message", "extension.js")]
internal static partial Task Message(int type, string message);
internal static partial Task Message(JSObject handle, int type, string message);
[JSImport("sleep", "extension.js")]
internal static partial Task Sleep(int time);
[JSImport("log", "extension.js")]
internal static partial void Log(int type, string message);
internal static partial void Log(JSObject handle, int type, string message);
[JSImport("requestRequiredParameter", "extension.js")]
internal static partial Task<string> RequestRequiredParameter(JSObject handle, string name);
[JSImport("error", "extension.js")]
Expand Down
42 changes: 26 additions & 16 deletions src/azure-pipelines-vscode-ext/ext-core/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,34 +26,40 @@ public async Task<string> ReadFile(string repositoryAndRef, string path)
}

public class TraceWriter : GitHub.DistributedTask.ObjectTemplating.ITraceWriter {
private JSObject handle;

public TraceWriter(JSObject handle) {
this.handle = handle;
}

public void Error(string format, params object[] args)
{
if(args?.Length == 1 && args[0] is Exception ex) {
Interop.Log(5, string.Format("{0} {1}", format, ex.Message));
Interop.Log(handle, 5, string.Format("{0} {1}", format, ex.Message));
return;
}
try {
Interop.Log(5, args?.Length > 0 ? string.Format(format, args) : format);
Interop.Log(handle, 5, args?.Length > 0 ? string.Format(format, args) : format);
} catch {
Interop.Log(5, format);
Interop.Log(handle, 5, format);
}
}

public void Info(string format, params object[] args)
{
try {
Interop.Log(3, args?.Length > 0 ? string.Format(format, args) : format);
Interop.Log(handle, 3, args?.Length > 0 ? string.Format(format, args) : format);
} catch {
Interop.Log(3, format);
Interop.Log(handle, 3, format);
}
}

public void Verbose(string format, params object[] args)
{
try {
Interop.Log(2, args?.Length > 0 ? string.Format(format, args) : format);
Interop.Log(handle, 2, args?.Length > 0 ? string.Format(format, args) : format);
} catch {
Interop.Log(2, format);
Interop.Log(handle, 2, format);
}
}
}
Expand All @@ -68,14 +74,14 @@ public IDictionary<string, string> GetVariablesForEnvironment(string name = null

[MethodImpl(MethodImplOptions.NoInlining)]
public static async Task<string> ExpandCurrentPipeline(JSObject handle, string currentFileName, string variables, string parameters, bool returnErrorContent) {
var context = new Runner.Server.Azure.Devops.Context {
FileProvider = new MyFileProvider(handle),
TraceWriter = new TraceWriter(handle),
Flags = GitHub.DistributedTask.Expressions2.ExpressionFlags.DTExpressionsV1 | GitHub.DistributedTask.Expressions2.ExpressionFlags.ExtendedDirectives,
RequiredParametersProvider = new RequiredParametersProvider(handle),
VariablesProvider = new VariablesProvider { Variables = JsonConvert.DeserializeObject<Dictionary<string, string>>(variables) }
};
try {
var context = new Runner.Server.Azure.Devops.Context {
FileProvider = new MyFileProvider(handle),
TraceWriter = new TraceWriter(),
Flags = GitHub.DistributedTask.Expressions2.ExpressionFlags.DTExpressionsV1 | GitHub.DistributedTask.Expressions2.ExpressionFlags.ExtendedDirectives,
RequiredParametersProvider = new RequiredParametersProvider(handle),
VariablesProvider = new VariablesProvider { Variables = JsonConvert.DeserializeObject<Dictionary<string, string>>(variables) }
};
Dictionary<string, TemplateToken> cparameters = new Dictionary<string, TemplateToken>();
foreach(var kv in JsonConvert.DeserializeObject<Dictionary<string, string>>(parameters)) {
cparameters[kv.Key] = AzurePipelinesUtils.ConvertStringToTemplateToken(kv.Value);
Expand All @@ -84,10 +90,14 @@ public static async Task<string> ExpandCurrentPipeline(JSObject handle, string c
var pipeline = await new Runner.Server.Azure.Devops.Pipeline().Parse(context.ChildContext(template, currentFileName), template);
return pipeline.ToYaml();
} catch(Exception ex) {
var fileIdReplacer = new System.Text.RegularExpressions.Regex("FileId: (\\d+)");
var errorContent = fileIdReplacer.Replace(ex.Message, match => {
return $"File: {context.FileTable[int.Parse(match.Groups[1].Value) - 1]}";
});
if(returnErrorContent) {
await Interop.Error(handle, ex.ToString());
await Interop.Error(handle, errorContent);
} else {
await Interop.Message(2, ex.ToString());
await Interop.Message(handle, 2, errorContent);
}
return null;
}
Expand Down
Loading