| File: GitHubActionsIntegration.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 System.Globalization; using System.Text; using static AnalyzeCiFailure.CiFailureJson; namespace AnalyzeCiFailure; /// <summary>Writes GitHub Actions outputs and summaries and interprets completion job results.</summary> internal static class GitHubActionsIntegration { internal static async Task WriteOutputsAsync(string path, AnalysisOutcome outcome, CancellationToken ct) { var outputs = new (string Name, string? Value)[] { ("run_id", outcome.RunId?.ToString(CultureInfo.InvariantCulture)), ("run_attempt", outcome.RunAttempt?.ToString(CultureInfo.InvariantCulture)), ("head_sha", outcome.HeadSha), ("run_url", outcome.RunUrl), ("run_scope", outcome.RunScope is { } scope ? EnumText(scope) : null), ("pr_numbers", outcome.RunScope is null ? null : outcome.SubjectPrNumber?.ToString(CultureInfo.InvariantCulture) ?? ""), ("has_work", outcome.HasRelevantFailures is { } hasWork ? hasWork ? "true" : "false" : null), ("analysis_ready", outcome.Status == "ready" ? "true" : "false"), ("collection_status", outcome.Status), }; var batch = new StringBuilder(); foreach (var (name, value) in outputs) { if (value is null) { continue; } if (value.Contains('\n') || value.Contains('\r') || value.Contains('\0')) { throw new ValidationException($"Invalid workflow output value for {name}"); } batch.Append(name).Append('=').Append(value).Append('\n'); } ct.ThrowIfCancellationRequested(); if (batch.Length > 0) { // GitHub file commands use UTF-8 "name=value" lines. Validate the entire // batch before opening the runner-owned file; do not emit a partial batch // when a later value contains a newline. // https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#setting-an-output-parameter await File.AppendAllTextAsync(path, batch.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), ct); } ct.ThrowIfCancellationRequested(); } internal static void EnsureExpectedCompletion(string? hasRelevantFailures, string? publicationResult, string? collectionStatus, string? collectionResult) { if (hasRelevantFailures is not (null or "" or "true" or "false")) { throw new ValidationException("HAS_WORK must be true, false, or empty."); } if (hasRelevantFailures == "true" && collectionStatus == "degraded" && collectionResult == "success") { Console.WriteLine("::notice::Collection completed with a limited-evidence report; complete diagnosis and publication were not authorized."); return; } if (hasRelevantFailures == "true" && publicationResult != "success") { throw new ValidationException("CI failure analysis did not reach an accepted terminal outcome " + $"(collection status: {Display(DiagnosticTextSanitizer.Sanitize(collectionStatus ?? "", limit: 100))}; " + $"collection result: {Display(DiagnosticTextSanitizer.Sanitize(collectionResult ?? "", limit: 100))}; " + $"publication result: {Display(DiagnosticTextSanitizer.Sanitize(publicationResult ?? "", limit: 100))}). Check the collection and publication logs."); } } internal static async Task AppendStepSummaryAsync(string markdown, CancellationToken ct) { if (Environment.GetEnvironmentVariable("GITHUB_STEP_SUMMARY") is { Length: > 0 } summary) { await File.AppendAllTextAsync(summary, markdown, ct); } } }