diff --git a/.vscode/launch.json b/.vscode/launch.json index 946eaf9..fff1c21 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,9 +11,15 @@ "preLaunchTask": "build", "program": "${workspaceFolder}/runfo/bin/Debug/netcoreapp3.1/runfo.dll", "args": [ - "builds", + "search-helix", "-before", - "2020/4/29" + "2020/4/29", + "-c", + "10", + "-d", + "runtime", + "-v", + "after 60000ms waiting for remote process" ], "cwd": "${workspaceFolder}/runfo", "console": "internalConsole", diff --git a/DevOps.Functions/Functions.cs b/DevOps.Functions/Functions.cs index ac1cadf..3410bab 100644 --- a/DevOps.Functions/Functions.cs +++ b/DevOps.Functions/Functions.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable + using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; @@ -12,12 +13,16 @@ using DevOps.Util.Triage; using Octokit; using DevOps.Util; +using System; namespace DevOps.Functions { public class BuildCompleteMessage { - public string ProjectId { get; set; } + public string? ProjectId { get; set; } + + public string? ProjectName { get; set; } + public int BuildNumber { get; set; } } @@ -42,8 +47,8 @@ public Functions(DevOpsServer server, GitHubClient gitHubClient, TriageContext c [FunctionName("build")] public async Task OnBuild( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, - ILogger logger, - [Queue("build-complete", Connection = "AzureWebJobsStorage")] IAsyncCollector queueCollector) + [Queue("build-complete", Connection = "AzureWebJobsStorage")] IAsyncCollector queueCollector, + ILogger logger) { string requestBody = await new StreamReader(req.Body).ReadToEndAsync().ConfigureAwait(false); logger.LogInformation(requestBody); @@ -60,17 +65,48 @@ public Functions(DevOpsServer server, GitHubClient gitHubClient, TriageContext c } [FunctionName("triage-build")] - public async Task OnBuildComplete( + public async Task TriageBuildAsync( [QueueTrigger("build-complete", Connection = "AzureWebJobsStorage")] string message, ILogger logger) { var buildCompleteMessage = JsonConvert.DeserializeObject(message); - var projectName = await Server.ConvertProjectIdToNameAsync(buildCompleteMessage.ProjectId); + var projectName = buildCompleteMessage.ProjectName; + if (projectName is null) + { + var projectId = buildCompleteMessage.ProjectId; + if (projectId is null) + { + logger.LogError("Both project name and id are null"); + return; + } + + projectName = await Server.ConvertProjectIdToNameAsync(projectId); + } logger.LogInformation($"Triaging build {projectName} {buildCompleteMessage.BuildNumber}"); var util = new AutoTriageUtil(Server, Context, logger); - await util.TriageAsync(projectName, buildCompleteMessage.BuildNumber); + await util.TriageBuildAsync(projectName, buildCompleteMessage.BuildNumber); + } + + [FunctionName("triage-query")] + public async Task TriageQueryAsync( + [QueueTrigger("triage-query", Connection = "AzureWebJobsStorage")] string message, + [Queue("build-complete", Connection = "AzureWebJobsStorage")] IAsyncCollector triageQueue, + ILogger logger) + { + logger.LogInformation($"Triaging query: {message}"); + var queryUtil = new DotNetQueryUtil(Server); + foreach (var build in await queryUtil.ListBuildsAsync(message)) + { + var key = build.GetBuildKey(); + var buildCompleteMessage = new BuildCompleteMessage() + { + ProjectName = key.Project, + BuildNumber = key.Number, + }; + await triageQueue.AddAsync(buildCompleteMessage); + } } // [FunctionName("issues-update")] diff --git a/DevOps.Util.DotNet/HelixLogInfo.cs b/DevOps.Util.DotNet/HelixLogInfo.cs index 45f5466..be9b18c 100644 --- a/DevOps.Util.DotNet/HelixLogInfo.cs +++ b/DevOps.Util.DotNet/HelixLogInfo.cs @@ -6,7 +6,7 @@ namespace DevOps.Util.DotNet { public enum HelixLogKind { - RunClientUri, + RunClient, Console, @@ -41,7 +41,7 @@ public sealed class HelixLogInfo public string? GetUri(HelixLogKind kind) => kind switch { - HelixLogKind.RunClientUri => RunClientUri, + HelixLogKind.RunClient => RunClientUri, HelixLogKind.Console => ConsoleUri, HelixLogKind.CoreDump => CoreDumpUri, HelixLogKind.TestResults => TestResultsUri, diff --git a/DevOps.Util.DotNet/ReportBuilder.cs b/DevOps.Util.DotNet/ReportBuilder.cs index e9594e3..2e0e94d 100644 --- a/DevOps.Util.DotNet/ReportBuilder.cs +++ b/DevOps.Util.DotNet/ReportBuilder.cs @@ -56,12 +56,11 @@ public sealed class ReportBuilder builder.Append($"|[{definitionName}]({definitionUri})"); } - var kind = "Rolling"; - if (buildInfo.PullRequestKey.HasValue) - { - kind = $"PR {buildInfo.PullRequestKey.Value.PullRequestUri}"; - } - builder.AppendLine($"|[{buildInfo.Number}]({buildInfo.BuildUri})|{kind}|{result.JobName}|"); + builder.Append("|"); + AppendBuildLink(builder, buildInfo); + builder.Append("|"); + AppendBuildKind(builder, buildInfo); + builder.AppendLine($"|{result.JobName}|"); } else { @@ -84,5 +83,118 @@ public sealed class ReportBuilder return builder.ToString(); } + + public string BuildSearchHelix( + IEnumerable<(BuildInfo BuildInfo, HelixLogInfo HelixLogInfo)> results, + HelixLogKind[] kinds, + bool markdown, + string? footer = null) + { + var builder = new StringBuilder(); + if (markdown) + { + builder.AppendLine(MarkdownReportStart); + builder.Append("|Build|Kind|"); + + var header = "|---|---|"; + foreach (var kind in kinds) + { + var columnName = GetTitleName(kind); + builder.Append($"{columnName}|"); + header += "---|"; + } + builder.AppendLine(); + builder.AppendLine(header); + + foreach (var tuple in results) + { + var buildInfo = tuple.BuildInfo; + var helixLogInfo = tuple.HelixLogInfo; + builder.Append("|"); + AppendBuildLink(builder, buildInfo); + builder.Append("|"); + AppendBuildKind(builder, buildInfo); + builder.Append("|"); + foreach (var kind in kinds) + { + var uri = helixLogInfo.GetUri(kind); + if (uri is null) + { + builder.Append("|"); + continue; + } + + var name = GetValueName(kind); + builder.Append($"[{name}]({uri})|"); + } + builder.AppendLine(); + } + + AppendFooter(); + builder.AppendLine(MarkdownReportEnd); + } + else + { + foreach (var tuple in results) + { + var buildInfo = tuple.BuildInfo; + var helixLogInfo = tuple.HelixLogInfo; + + builder.AppendLine(buildInfo.BuildUri); + foreach (var kind in kinds) + { + var name = GetTitleName(kind); + var uri = helixLogInfo.GetUri(kind); + builder.AppendLine($" {name} - {uri}"); + } + } + AppendFooter(); + } + + return builder.ToString(); + + void AppendFooter() + { + if (footer is object) + { + builder.AppendLine(); + builder.AppendLine(footer); + } + } + + static string GetTitleName(HelixLogKind kind) => kind switch + { + HelixLogKind.Console => "Console", + HelixLogKind.CoreDump => "Core Dump", + HelixLogKind.RunClient => "Run Client", + HelixLogKind.TestResults => "Test Results", + _ => throw new InvalidOperationException($"Invalid kind {kind}") + }; + + static string GetValueName(HelixLogKind kind) => kind switch + { + HelixLogKind.Console => "console.log", + HelixLogKind.CoreDump => "core dump", + HelixLogKind.RunClient => "runclient.py", + HelixLogKind.TestResults => "test results", + _ => throw new InvalidOperationException($"Invalid kind {kind}"), + }; + } + + private static void AppendBuildLink(StringBuilder builder, BuildInfo buildInfo) + { + builder.Append($"[{buildInfo.Number}]({buildInfo.BuildUri})"); + } + + private static void AppendBuildKind(StringBuilder builder, BuildInfo buildInfo) + { + var kind = "Rolling"; + if (buildInfo.PullRequestKey is GitHubPullRequestKey prKey) + { + kind = $"[PR {prKey.Number}]({prKey.PullRequestUri})"; + } + + builder.Append(kind); + } } } \ No newline at end of file diff --git a/DevOps.Util.Triage/AutoTriageUtil.cs b/DevOps.Util.Triage/AutoTriageUtil.cs index 957b4ce..bce9a6f 100644 --- a/DevOps.Util.Triage/AutoTriageUtil.cs +++ b/DevOps.Util.Triage/AutoTriageUtil.cs @@ -94,23 +94,23 @@ public void EnsureTriageIssues() }; } - public async Task TriageAsync(string projectName, int buildNumber) + public async Task TriageBuildAsync(string projectName, int buildNumber) { var build = await Server.GetBuildAsync(projectName, buildNumber).ConfigureAwait(false); - await TriageAsync(build).ConfigureAwait(false); + await TriageBuildAsync(build).ConfigureAwait(false); } - public async Task TriageAsync(string buildQuery) + public async Task TriageQueryAsync(string buildQuery) { foreach (var build in await QueryUtil.ListBuildsAsync(buildQuery)) { - await TriageAsync(build).ConfigureAwait(false); + await TriageBuildAsync(build).ConfigureAwait(false); } } // TODO: need overload that takes builds and groups up the issue and PR updates // or maybe just make that a separate operation from triage - public async Task TriageAsync(Build build) + public async Task TriageBuildAsync(Build build) { var buildInfo = build.GetBuildInfo(); var modelBuild = TriageContextUtil.EnsureBuild(buildInfo); diff --git a/DevOps.Util.Triage/BuildTriageUtil.cs b/DevOps.Util.Triage/BuildTriageUtil.cs index dd1b588..70027d9 100644 --- a/DevOps.Util.Triage/BuildTriageUtil.cs +++ b/DevOps.Util.Triage/BuildTriageUtil.cs @@ -93,7 +93,7 @@ internal async Task TriageAsync() await DoSearchTimelineAsync(issue); break; case SearchKind.SearchHelixRunClient: - await DoSearchHelixAsync(issue, HelixLogKind.RunClientUri); + await DoSearchHelixAsync(issue, HelixLogKind.RunClient); break; case SearchKind.SearchHelixConsole: await DoSearchHelixAsync(issue, HelixLogKind.Console); diff --git a/DevOps.Util/DevOpsServer.cs b/DevOps.Util/DevOpsServer.cs index 553f9e5..62ec5bf 100644 --- a/DevOps.Util/DevOpsServer.cs +++ b/DevOps.Util/DevOpsServer.cs @@ -211,7 +211,7 @@ public async Task GetArtifactAsync(string project, int buildId, s } /// - /// The project in a server can be expressed as an IDE or a name. This method will convert the + /// The project in a server can be expressed as an ID or a name. This method will convert the /// ID form, typically a GUID, into a friendly name. /// public async Task ConvertProjectIdToNameAsync(string id) @@ -225,6 +225,17 @@ public async Task ConvertProjectIdToNameAsync(string id) return definitions[0].Project.Name; } + public async Task ConvertProjectNameToIdAsync(string name) + { + var definitions = await ListDefinitionsAsync(name, top: 1); + if (definitions.Count == 0) + { + throw new InvalidOperationException(); + } + + return definitions[0].Project.Id; + } + public Task DownloadArtifactAsync(string project, int buildId, string artifactName) => WithMemoryStream(s => DownloadArtifactAsync(project, buildId, artifactName, s)); diff --git a/runfo/Program.cs b/runfo/Program.cs index eb06154..84c925d 100644 --- a/runfo/Program.cs +++ b/runfo/Program.cs @@ -49,6 +49,7 @@ internal static async Task Main(string[] args) catch (Exception ex) { Console.WriteLine(ex.Message); + Console.WriteLine(ex.StackTrace); return ExitFailure; } diff --git a/runfo/RuntimeInfo.cs b/runfo/RuntimeInfo.cs index db1b2f1..19e8ca4 100644 --- a/runfo/RuntimeInfo.cs +++ b/runfo/RuntimeInfo.cs @@ -11,6 +11,7 @@ using Mono.Options; using static RuntimeInfoUtil; using static DevOps.Util.DotNet.OptionSetUtil; +using System.Text; internal sealed partial class RuntimeInfo { @@ -103,9 +104,11 @@ internal async Task CollectCache() internal async Task PrintSearchHelix(IEnumerable args) { string text = null; + bool markdown = false; var optionSet = new BuildSearchOptionSet() { { "v|value=", "text to search for", t => text = t }, + { "m|markdown", "print output in markdown", m => markdown = m is object }, }; ParseAll(optionSet, args); @@ -119,34 +122,19 @@ internal async Task PrintSearchHelix(IEnumerable args) var textRegex = new Regex(text, RegexOptions.Compiled | RegexOptions.IgnoreCase); var collection = await QueryUtil.ListBuildsAsync(optionSet); - var found = collection + var foundRaw = collection .AsParallel() .Select(async b => await SearchBuild(Server, QueryUtil, textRegex, b)); - var badLogList = new List(); - - Console.WriteLine("|Build|Kind|Console Log|"); - Console.WriteLine("|---|---|---|"); - foreach (var task in found) - { - var (build, helixLogInfo, list) = await task; - badLogList.AddRange(list); - if (helixLogInfo is null) - { - continue; - } - - var kind = "Rolling"; - if (DevOpsUtil.TryGetPullRequestKey(build, out var pullRequestKey)) - { - kind = $"PR {pullRequestKey.PullRequestUri}"; - } - Console.WriteLine($"|[{build.Id}]({DevOpsUtil.GetBuildUri(build)})|{kind}|[console.log]({helixLogInfo.ConsoleUri})|"); - } - - foreach (var line in badLogList) - { - Console.WriteLine(line); - } + var found = await RuntimeInfoUtil.ToListAsync(foundRaw); + var badLogBuilder = new StringBuilder(); + found.ForEach(x => x.BadLogs.ForEach(l => badLogBuilder.AppendLine(l))); + Console.WriteLine(ReportBuilder.BuildSearchHelix( + found + .Select(x => (x.Build.GetBuildInfo(), x.LogInfo)) + .Where(x => x.LogInfo is object), + new[] { HelixLogKind.Console, HelixLogKind.CoreDump }, + markdown: markdown, + badLogBuilder.ToString())); return ExitSuccess; @@ -157,12 +145,12 @@ internal async Task PrintSearchHelix(IEnumerable args) Build build) { var badLogList = new List(); - var workItems = await queryUtil - .ListHelixWorkItemsAsync(build, DotNetUtil.FailedTestOutcomes) - .ConfigureAwait(false); - foreach (var workItem in workItems) + try { - try + var workItems = await queryUtil + .ListHelixWorkItemsAsync(build, DotNetUtil.FailedTestOutcomes) + .ConfigureAwait(false); + foreach (var workItem in workItems) { var logInfo = await HelixUtil.GetHelixLogInfoAsync(server, workItem); if (logInfo.ConsoleUri is object) @@ -177,10 +165,10 @@ internal async Task PrintSearchHelix(IEnumerable args) } } } - catch - { - badLogList.Add($"Unable to download helix logs for {build.Id} {workItem.HelixInfo.JobId}"); - } + } + catch (Exception ex) + { + badLogList.Add($"Unable to search helix logs for {build.Id}: {ex.Message}"); } return (build, null, badLogList); @@ -339,7 +327,7 @@ internal async Task PrintSearchBuildLogs(IEnumerable args) return (tuple.IsMatch, TimelineRecord: r, tuple.Line); }); - var list = await RuntimeInfoUtil.ToList(all); + var list = await RuntimeInfoUtil.ToListAsync(all); if (trace) { Console.WriteLine(DevOpsUtil.GetBuildUri(build)); @@ -480,7 +468,7 @@ private async Task> GetHel return (helixLogInfo, consoleText); }); - return await RuntimeInfoUtil.ToList(logs); + return await RuntimeInfoUtil.ToListAsync(logs); } internal void PrintBuildDefinitions() @@ -1041,7 +1029,7 @@ private async Task> GetHelixLogs(BuildTestInfoCollec var helixLogInfo = await GetHelixLogInfoAsync(result.HelixWorkItem.Value); return (result.Build, helixLogInfo); }); - var list = await RuntimeInfoUtil.ToList(query); + var list = await RuntimeInfoUtil.ToListAsync(query); return list; } diff --git a/runfo/RuntimeInfoUtil.cs b/runfo/RuntimeInfoUtil.cs index ed5bf6f..a8b7ac6 100644 --- a/runfo/RuntimeInfoUtil.cs +++ b/runfo/RuntimeInfoUtil.cs @@ -25,7 +25,7 @@ internal static class RuntimeInfoUtil return f - s; } - internal static async Task> ToList(IEnumerable> e) + internal static async Task> ToListAsync(IEnumerable> e) { await Task.WhenAll(e); return e.Select(x => x.Result).ToList(); diff --git a/triage/Program.cs b/triage/Program.cs index f254d90..bca3ba7 100644 --- a/triage/Program.cs +++ b/triage/Program.cs @@ -145,10 +145,10 @@ async Task RunList() async Task RunRebuild() { autoTriageUtil.EnsureTriageIssues(); - await autoTriageUtil.TriageAsync("-d runtime -c 1000 -pr"); - await autoTriageUtil.TriageAsync("-d aspnet -c 1000 -pr"); - await autoTriageUtil.TriageAsync("-d runtime-official -c 100 -pr"); - await autoTriageUtil.TriageAsync("-d aspnet-official -c 100 -pr"); + await autoTriageUtil.TriageQueryAsync("-d runtime -c 1000 -pr"); + await autoTriageUtil.TriageQueryAsync("-d aspnet -c 1000 -pr"); + await autoTriageUtil.TriageQueryAsync("-d runtime-official -c 100 -pr"); + await autoTriageUtil.TriageQueryAsync("-d aspnet-official -c 100 -pr"); } async Task RunIssues() @@ -166,7 +166,7 @@ async Task RunScratch() //await autoTriageUtil.Triage("-d runtime -c 100 -pr"); // await gitHubUtil.UpdateGithubIssues autoTriageUtil.EnsureTriageIssues(); - await autoTriageUtil.TriageAsync("-d runtime -c 400 -pr"); + await autoTriageUtil.TriageQueryAsync("-d runtime -c 400 -pr"); } static (DevOpsServer Server, IGitHubClient githubClient, TriageContext Context) Create(ref List args)