| File: PrCommentRenderer.cs | Web Access |
| Project: src\tools\AnalyzeCiFailure\AnalyzeCiFailure.csproj (AnalyzeCiFailure) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using static AnalyzeCiFailure.CiFailureJson; namespace AnalyzeCiFailure; /// <summary>Renders validated failure decisions as a pull-request analysis comment.</summary> internal static class PrCommentRenderer { internal static string Render(AnalysisReport analysis, IEnumerable<WorkflowJob> collectedJobs, string runUrl) { if (ValidationRules.UsesAggregatePublication(analysis.Causes.Length)) { return "<!-- analyze-ci-failure -->\n[automated] CI analysis identified " + analysis.Causes.Length.ToString(System.Globalization.CultureInfo.InvariantCulture) + $" independent causes. They are tracked together in one aggregate issue.\n\n[Source CI run]({runUrl})\n"; } var jobs = collectedJobs.Select(job => job with { Name = DiagnosticTextSanitizer.Sanitize(job.Name!, limit: 500) }).ToArray(); var classified = analysis.Jobs.ToDictionary(job => job.Id?.GetRawText() ?? "null"); var jobList = string.Join( '\n', jobs.Select(job => { var proposal = classified[job.Id?.GetRawText() ?? "null"]; var classification = proposal.Classification is { } value ? EnumText(value) : ""; return $"- {MarkdownFormatting.CodeSpan(job.Name!)} — {MarkdownFormatting.CodeSpan(DiagnosticTextSanitizer.Sanitize(proposal.Reason ?? "", limit: 500))} ({classification})"; })); var tests = analysis.Tests.Where(test => test.Classification == TestClassification.Flaky).Select(test => { var job = jobs.FirstOrDefault(j => j.Name == DiagnosticTextSanitizer.Sanitize(test.Job ?? "", limit: 500)); var text = $"- {MarkdownFormatting.CodeSpan(DiagnosticTextSanitizer.Sanitize(test.Name ?? "", limit: 500))}" + (job is null ? "" : " in job " + MarkdownFormatting.CodeSpan(job.Name!)) + "\n - **Error**:\n\n" + MarkdownFormatting.Indent(DiagnosticTextSanitizer.Sanitize(test.Error ?? "", limit: 1000, multiline: true), spaces: 8) + "\n"; var stack = DiagnosticTextSanitizer.Sanitize(test.StackTrace ?? "", limit: 2000, multiline: true); if (stack.Length > 0) { text += " - **Stack Trace** (first frames):\n\n" + MarkdownFormatting.Indent(string.Join('\n', stack.Split('\n').Take(5)), spaces: 8) + "\n"; } foreach (var (value, heading) in new[] { (test.StandardOutput, "Standard Output"), (test.StandardError, "Standard Error") }) { var diagnostic = DiagnosticTextSanitizer.Sanitize(value ?? "", limit: 4000, multiline: true); if (diagnostic.Length > 0) { text += $" - **{heading}** (sensitive values redacted):\n\n" + MarkdownFormatting.Indent(diagnostic, spaces: 8) + "\n"; } } return text + " - **Why likely flaky**: " + MarkdownFormatting.CodeSpan(DiagnosticTextSanitizer.Sanitize(test.Reason ?? "", limit: 500)); }); var testList = string.Join('\n', tests); var testSection = testList.Length == 0 ? "" : "\n\n**Suspected flaky test(s):**\n" + testList; var body = analysis.Verdict switch { AnalysisVerdict.TransientInfrastructure => MarkdownTemplate.Render( "CommentTransientInfrastructure", ("FAILED_JOBS", jobList), ("RUN_URL", runUrl)), AnalysisVerdict.FlakyTest => MarkdownTemplate.Render( "CommentFlakyTests", ("FAILED_JOBS", jobList), ("FLAKY_TEST_SECTION", testSection), ("RUN_URL", runUrl)), AnalysisVerdict.CodeIssue => MarkdownTemplate.Render("CommentCodeIssue", ("FAILED_JOBS", jobList)), _ => MarkdownTemplate.Render( "CommentMixedFailures", ("FAILED_JOBS", jobList), ("FLAKY_TEST_SECTION", testSection)), }; // Preserve the final blank line in the published comment contract. return "<!-- analyze-ci-failure -->\n" + body + "\n"; } }