Skip to content

Commit

Permalink
port azure pipelines file selection to language server via ai
Browse files Browse the repository at this point in the history
* no longer all yaml files are interpreted as ado pipeline
* collapse more than two codelenses into a prompt
  • Loading branch information
ChristopherHX committed Dec 1, 2024
1 parent a10aca8 commit 1bfa20d
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 19 deletions.
5 changes: 4 additions & 1 deletion src/Runner.Language.Server/AutoCompleter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ private static TemplateContext CreateTemplateContext(GitHub.DistributedTask.Obje

public async Task<CompletionList> Handle(CompletionParams request, CancellationToken cancellationToken)
{
var content = data.Content[request.TextDocument.Uri];
string content;
if(!data.Content.TryGetValue(request.TextDocument.Uri, out content)) {
return new CompletionList();
}
var currentFileName = "t.yml";
var files = new Dictionary<string, string>() { { currentFileName, content } };

Expand Down
43 changes: 33 additions & 10 deletions src/Runner.Language.Server/CodeLensProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using GitHub.DistributedTask.Pipelines.ObjectTemplating;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities;
using OmniSharp.Extensions.LanguageServer.Protocol.Document;
Expand All @@ -29,7 +30,7 @@ public CodeLensProvider(SharedData data) {
public CodeLensRegistrationOptions GetRegistrationOptions(CodeLensCapability capability, ClientCapabilities clientCapabilities)
{
return new CodeLensRegistrationOptions {
DocumentSelector = new TextDocumentSelector(new TextDocumentFilter() { Language = "yaml" }, new TextDocumentFilter() { Language = "azure-pipelines" }),
DocumentSelector = new TextDocumentSelector(new TextDocumentFilter() { Language = "yaml", Pattern = "**/.github/workflows/*.{yml,yaml}" }),
};
}

Expand Down Expand Up @@ -167,7 +168,10 @@ string GetDefaultDisplaySuffix(IEnumerable<string> item) {

public async Task<CodeLensContainer?> Handle(CodeLensParams request, CancellationToken cancellationToken)
{
var content = data.Content[request.TextDocument.Uri];
string content;
if(!data.Content.TryGetValue(request.TextDocument.Uri, out content)) {
return null;
}
var currentFileName = "t.yml";
var files = new Dictionary<string, string>() { { currentFileName, content } };

Expand Down Expand Up @@ -354,15 +358,25 @@ string GetDefaultDisplaySuffix(IEnumerable<string> item) {
}

if(rawstrategy != null) {
JArray allMatrices = new JArray();
Action<string, Dictionary<string, TemplateToken>> addAction = (suffix, item) => {
var json = JsonConvert.SerializeObject(item.ToDictionary(kv => kv.Key, kv => kv.Value.ToContextData().ToJToken()));

codeLens.Add(new CodeLens { Command = new Command { Name = "runner.server.runjob", Title = $"{jobs[i].Key}{suffix}", Arguments = new Newtonsoft.Json.Linq.JArray(request.TextDocument.Uri.ToString(), $"{jobs[i].Key}({json})", new Newtonsoft.Json.Linq.JArray(events.ToArray())) },
Range = new OmniSharp.Extensions.LanguageServer.Protocol.Models.Range(
new OmniSharp.Extensions.LanguageServer.Protocol.Models.Position(rawstrategy.Line.Value - 1, rawstrategy.Column.Value - 1),
new OmniSharp.Extensions.LanguageServer.Protocol.Models.Position(rawstrategy.Line.Value - 1, rawstrategy.Column.Value - 1)
)
});
var matrixEntries = item.ToDictionary(kv => kv.Key, kv => kv.Value.ToContextData().ToJToken());
var json = JsonConvert.SerializeObject(matrixEntries);

if(allMatrices.Count < 2) {
codeLens.Add(new CodeLens { Command = new Command { Name = "runner.server.runjob", Title = $"{jobs[i].Key}{suffix}", Arguments = new Newtonsoft.Json.Linq.JArray(request.TextDocument.Uri.ToString(), $"{jobs[i].Key}({json})", new Newtonsoft.Json.Linq.JArray(events.ToArray())) },
Range = new OmniSharp.Extensions.LanguageServer.Protocol.Models.Range(
new OmniSharp.Extensions.LanguageServer.Protocol.Models.Position(rawstrategy.Line.Value - 1, rawstrategy.Column.Value - 1),
new OmniSharp.Extensions.LanguageServer.Protocol.Models.Position(rawstrategy.Line.Value - 1, rawstrategy.Column.Value - 1)
)
});
}
var entry = new JObject();
entry["name"] = $"{jobs[i].Key}{suffix}";
entry["jobId"] = $"{jobs[i].Key}";
entry["jobIdLong"] = $"{jobs[i].Key}({json})";
entry["matrix"] = JsonConvert.DeserializeObject<JObject>(json);
allMatrices.Add(entry);
};

if(keys.Count != 0 || includematrix.Count == 0) {
Expand All @@ -373,6 +387,15 @@ string GetDefaultDisplaySuffix(IEnumerable<string> item) {
foreach (var item in includematrix) {
addAction(GetDefaultDisplaySuffix(from displayitem in item.SelectMany(it => it.Value.Traverse(true)) where !(displayitem is SequenceToken || displayitem is MappingToken) select displayitem.ToString()), item);
}

if(allMatrices.Count >= 2) {
codeLens.Add(new CodeLens { Command = new Command { Name = "runner.server.runjob", Title = "More", Arguments = new Newtonsoft.Json.Linq.JArray(request.TextDocument.Uri.ToString(), allMatrices, new Newtonsoft.Json.Linq.JArray(events.ToArray())) },
Range = new OmniSharp.Extensions.LanguageServer.Protocol.Models.Range(
new OmniSharp.Extensions.LanguageServer.Protocol.Models.Position(rawstrategy.Line.Value - 1, rawstrategy.Column.Value - 1),
new OmniSharp.Extensions.LanguageServer.Protocol.Models.Position(rawstrategy.Line.Value - 1, rawstrategy.Column.Value - 1)
)
});
}
}
}
return new CodeLensContainer(codeLens);
Expand Down
5 changes: 4 additions & 1 deletion src/Runner.Language.Server/HoverProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ private static TemplateContext CreateTemplateContext(GitHub.DistributedTask.Obje

public async Task<Hover?> Handle(HoverParams request, CancellationToken cancellationToken)
{
var content = data.Content[request.TextDocument.Uri];
string content;
if(!data.Content.TryGetValue(request.TextDocument.Uri, out content)) {
return null;
}
var currentFileName = "t.yml";
var files = new Dictionary<string, string>() { { currentFileName, content } };

Expand Down
3 changes: 2 additions & 1 deletion src/Runner.Language.Server/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@
}
}
Interop.Output.Reader.AdvanceTo(result.Buffer.End);
// await Interop.SendOutputMessageAsync(result.Buffer.ToArray());
}
}).ConfigureAwait(false);
#endif
Expand All @@ -90,7 +89,9 @@
.WithHandler<WorkspaceFolderListener>()
.WithHandler<HoverProvider>()
.WithHandler<SemanticTokenHandler>()
#if !WASM
.WithHandler<CodeLensProvider>()
#endif
.WithServices(x => x.AddLogging(b => b.SetMinimumLevel(LogLevel.Trace)))
.WithServices(
services =>
Expand Down
5 changes: 4 additions & 1 deletion src/Runner.Language.Server/SemanticTokenHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ private static TemplateContext CreateTemplateContext(GitHub.DistributedTask.Obj

public async Task<OmniSharp.Extensions.LanguageServer.Protocol.Models.SemanticTokens?> Handle(SemanticTokensParams request, CancellationToken cancellationToken)
{
var content = data.Content[request.TextDocument.Uri];
string content;
if(!data.Content.TryGetValue(request.TextDocument.Uri, out content)) {
return null;
}
var currentFileName = "t.yml";
var files = new Dictionary<string, string>() { { currentFileName, content } };

Expand Down
95 changes: 91 additions & 4 deletions src/Runner.Language.Server/TextDocumentSyncHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
using OmniSharp.Extensions.LanguageServer.Protocol.Server.Capabilities;
using YamlDotNet.RepresentationModel;

using Runner.Server.Azure.Devops;

Expand All @@ -31,17 +32,103 @@ public override TextDocumentAttributes GetTextDocumentAttributes(DocumentUri uri
return new TextDocumentAttributes(uri, "yaml");
}

private async Task<bool> ShouldHandle(TextDocumentIdentifier doc, string content, string? langID) {

Check warning on line 35 in src/Runner.Language.Server/TextDocumentSyncHelper.cs

View workflow job for this annotation

GitHub Actions / deploy

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
var known = langID == "azure-pipelines" || doc.Uri.Path.Contains("/.github/workflows/") || doc.Uri.Path.EndsWith("/action.yml") || doc.Uri.Path.EndsWith("/azure-pipeline.yml");

if(known) {
return true;
}
try {
var input = new StringReader(content);
var yamlStream = new YamlStream();
yamlStream.Load(input);
var rootNode = (YamlMappingNode)yamlStream.Documents[0].RootNode;
var isPipeline = CheckIsPipeline(rootNode);
return isPipeline != null;
} catch {

}

return false;

static bool CheckAllIsIn(YamlMappingNode obj, string[] allowed)
{
foreach (var k in obj.Children.Keys)
{
if (!allowed.Contains(k.ToString()))
{
return false;
}
}
return true;
}

static YamlMappingNode CheckIsPipeline(YamlMappingNode obj)
{
try
{
var hasPipelineProperties =
(obj.Children.ContainsKey("trigger") || obj.Children.ContainsKey("pr") ||
(obj.Children.ContainsKey("resources") &&
(obj["resources"] is YamlMappingNode resources &&
(resources.Children.ContainsKey("builds") ||
resources.Children.ContainsKey("containers") ||
resources.Children.ContainsKey("pipelines") ||
resources.Children.ContainsKey("repositories") ||
resources.Children.ContainsKey("webhooks") ||
resources.Children.ContainsKey("packages")))) ||
obj.Children.ContainsKey("schedules") ||
obj.Children.ContainsKey("lockBehavior") ||
obj.Children.ContainsKey("variables") ||
obj.Children.ContainsKey("parameters")) &&
(obj.Children.ContainsKey("stages") ||
obj.Children.ContainsKey("jobs") ||
obj.Children.ContainsKey("steps"))
|| obj.Children.ContainsKey("extends") && obj["extends"] is YamlMappingNode extends && extends.Children.ContainsKey("template")
|| obj.Children.ContainsKey("steps") && ((YamlSequenceNode)obj["steps"]).Children.Any(x => x is YamlMappingNode step &&
(step.Children.ContainsKey("task") || step.Children.ContainsKey("script") ||
step.Children.ContainsKey("bash") || step.Children.ContainsKey("pwsh") ||
step.Children.ContainsKey("powershell") || step.Children.ContainsKey("template")))
|| obj.Children.ContainsKey("jobs") && ((YamlSequenceNode)obj["jobs"]).Children.Any(x => x is YamlMappingNode job &&
(job.Children.ContainsKey("job") || job.Children.ContainsKey("deployment") ||
job.Children.ContainsKey("template")))
|| obj.Children.ContainsKey("stages") && ((YamlSequenceNode)obj["stages"]).Children.Any(x => x is YamlMappingNode stage &&
(stage.Children.ContainsKey("stage") || stage.Children.ContainsKey("template")))
|| obj.Children.ContainsKey("variables") && ((YamlSequenceNode)obj["variables"]).Children.Any(x => x is YamlMappingNode variable &&
(variable.Children.ContainsKey("name") && variable.Children.ContainsKey("value") ||
variable.Children.ContainsKey("group") || variable.Children.ContainsKey("template")))
|| obj.Children.ContainsKey("variables") && CheckAllIsIn(obj, new[] { "parameters", "variables" });

return hasPipelineProperties ? obj : null;

Check warning on line 102 in src/Runner.Language.Server/TextDocumentSyncHelper.cs

View workflow job for this annotation

GitHub Actions / deploy

Possible null reference return.
}
catch
{
return null;

Check warning on line 106 in src/Runner.Language.Server/TextDocumentSyncHelper.cs

View workflow job for this annotation

GitHub Actions / deploy

Possible null reference return.
}
}
}

public override async Task<Unit> Handle(DidOpenTextDocumentParams request, CancellationToken cancellationToken)
{
this.data.Content[request.TextDocument.Uri] = request.TextDocument.Text;
await ValidateSyntaxAsync(request.TextDocument.Uri);
if(await ShouldHandle(request.TextDocument, request.TextDocument.Text, request.TextDocument.LanguageId)) {
this.data.Content[request.TextDocument.Uri] = request.TextDocument.Text;
await ValidateSyntaxAsync(request.TextDocument.Uri);
} else {
this.data.Content.Remove(request.TextDocument.Uri);
SendDiagnostics(request.TextDocument.Uri, new List<string>());
}
return Unit.Value;
}

public override async Task<Unit> Handle(DidChangeTextDocumentParams request, CancellationToken cancellationToken)
{
this.data.Content[request.TextDocument.Uri] = request.ContentChanges.FirstOrDefault()?.Text ?? "";
await ValidateSyntaxAsync(request.TextDocument.Uri);
if(await ShouldHandle(request.TextDocument, request.ContentChanges.FirstOrDefault()?.Text, null)) {

Check warning on line 125 in src/Runner.Language.Server/TextDocumentSyncHelper.cs

View workflow job for this annotation

GitHub Actions / deploy

Possible null reference argument for parameter 'content' in 'Task<bool> TextDocumentSyncHelper.ShouldHandle(TextDocumentIdentifier doc, string content, string? langID)'.
this.data.Content[request.TextDocument.Uri] = request.ContentChanges.FirstOrDefault()?.Text ?? "";
await ValidateSyntaxAsync(request.TextDocument.Uri);
} else {
this.data.Content.Remove(request.TextDocument.Uri);
SendDiagnostics(request.TextDocument.Uri, new List<string>());
}
return Unit.Value;
}

Expand Down
15 changes: 14 additions & 1 deletion src/runner-server-vscode/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { commands, window, ExtensionContext, Uri, ViewColumn, env, workspace, languages, TreeItem, TextEditor, WebviewPanel } from 'vscode';
import { commands, window, ExtensionContext, Uri, ViewColumn, env, workspace, languages, TreeItem, TextEditor, WebviewPanel, QuickPickItem } from 'vscode';
import { LanguageClient, TransportKind } from 'vscode-languageclient/node';
import { join } from 'path';
import { ChildProcessWithoutNullStreams, spawn } from 'child_process';
Expand Down Expand Up @@ -232,6 +232,19 @@ function activate(context : ExtensionContext) {
});
});
commands.registerCommand("runner.server.runjob", async (workflow, job, events) => {
if(typeof job === 'object') {
var jobs : QuickPickItem[] = [];
for(var j of job) {
jobs.push({
label: j.name,
detail: j.jobIdLong
});
}
job = (await window.showQuickPick(jobs, { canPickMany: false, title: "Select matrix job entry" }))?.detail;
if(!job) {
throw new Error("No job selected");
}
}
console.log(`runner.server.runjob {workflow}.{job}`)
var sel : string = events.length === 1 ? events : await window.showQuickPick(events, { canPickMany: false })
var args = [ join(context.extensionPath, 'native', 'Runner.Client.dll'), '--event', sel || 'push', '-W', Uri.parse(workflow).fsPath, '-j', job ];
Expand Down

0 comments on commit 1bfa20d

Please sign in to comment.