| File: AnalysisRerunner.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.Net; using System.Text.Json; using System.Text.Json.Nodes; using Aspire.TestTools; using static AnalyzeCiFailure.CiFailureJson; namespace AnalyzeCiFailure; /// <summary>Validates bound proposals and fresh GitHub state before optionally rerunning failed jobs.</summary> internal static class AnalysisRerunner { internal static async Task<int> RunAsync( string directory, EvidencePaths paths, string repository, string analysisDirectory, GitHubApiClient api, CancellationToken ct) { var outputPath = Path.GetFullPath(RequiredEnvironment("GH_AW_AGENT_OUTPUT", "for rerun authorization"), directory); var output = TransientJson.ReadUtf8<WorkflowAgentOutput>(await File.ReadAllBytesAsync(outputPath, ct)); if (output.Items.ValueKind != JsonValueKind.Array) { throw new ValidationException("Agent output must contain an items array."); } var requests = new List<JsonElement>(); foreach (var item in output.Items.EnumerateArray()) { if (item.ValueKind != JsonValueKind.Object || !item.TryGetProperty("type", out var type) || type.ValueKind != JsonValueKind.String) { throw new ValidationException("Every agent output item must be an object with a string type."); } if (type.GetString() == "rerun_failed_jobs") { requests.Add(item); } } if (requests.Count == 0) { ct.ThrowIfCancellationRequested(); Console.WriteLine("::notice::No rerun_failed_jobs items in agent output."); return 0; } if (requests.Count != 1) { throw new ValidationException("Exactly one rerun_failed_jobs request is permitted."); } var request = TransientJson.Read<WorkflowRerunRequest>(requests[0].GetRawText()); ct.ThrowIfCancellationRequested(); var analysis = EvidencePackageStore.ReadAndValidate(paths, analysisDirectory, repository); ct.ThrowIfCancellationRequested(); var bundle = analysis.Bundle; var expectedPr = bundle.SubjectPr?.Number.ToString(CultureInfo.InvariantCulture) ?? ""; if (request.RunId != bundle.Run.Id || (request.PrNumbers ?? "") != expectedPr || analysis.Report.Verdict != AnalysisVerdict.TransientInfrastructure || analysis.Report.Tests.Length != 0) { throw new ValidationException("Rerun request does not match the validated transient infrastructure analysis."); } var id = bundle.Run.Id.ToString(CultureInfo.InvariantCulture); if (!string.Equals(Environment.GetEnvironmentVariable("ENABLE_RERUN"), "true", StringComparison.OrdinalIgnoreCase)) { ct.ThrowIfCancellationRequested(); Console.WriteLine($"::notice::Dry-run mode: would have rerun failed jobs for {bundle.Run.Url} attempt={bundle.Run.Attempt}."); return 0; } var github = new AnalysisGitHubClient(api, ct); if (bundle.Run.Scope == RunScope.PullRequest) { if (bundle.SubjectPr is null) { Console.WriteLine($"::notice::No unambiguous subject PR for {bundle.Run.Url}; skipping rerun."); return 0; } try { if (!await new AnalysisPublicationQueries(github).IsPullRequestOpenAndUnlockedAsync(repository, expectedPr)) { Console.WriteLine($"::notice::PR #{expectedPr} is closed or locked; skipping rerun for {bundle.Run.Url}."); return 0; } } catch (ValidationException ex) { Console.WriteLine($"::warning::Cannot check PR #{expectedPr}; skipping rerun for {bundle.Run.Url}: " + Display(DiagnosticTextSanitizer.Sanitize(ex.Message, limit: 500))); return 0; } } JsonNode? current; try { current = await github.GetAsync($"repos/{repository}/actions/runs/{id}"); } catch (Exception ex) when (ex is ValidationException or JsonException) { throw new ValidationException($"Unable to read current run {bundle.Run.Url} attempt={bundle.Run.Attempt}: " + Display(DiagnosticTextSanitizer.Sanitize(ex.Message, limit: 500))); } if (current is not JsonObject run || !GitHubJson.TryReadId(run["id"], out var currentId) || currentId != bundle.Run.Id || run["run_attempt"] is not JsonValue attempt || !attempt.TryGetValue<int>(out var currentAttempt) || currentAttempt <= 0) { throw new ValidationException($"Invalid current workflow run metadata for {bundle.Run.Url}."); } if (currentAttempt != bundle.Run.Attempt) { Console.WriteLine($"::warning::Run attempt advanced for {bundle.Run.Url}; skipping stale rerun request."); return 0; } ct.ThrowIfCancellationRequested(); try { await github.SendAsync( HttpMethod.Post, $"repos/{repository}/actions/runs/{id}/rerun-failed-jobs", body: null, HttpStatusCode.Created); } catch (ValidationException ex) { throw new ValidationException($"Unable to request rerun for {bundle.Run.Url} attempt={bundle.Run.Attempt}: " + Display(DiagnosticTextSanitizer.Sanitize(ex.Message, limit: 500))); } Console.WriteLine($"::notice::Requested rerun of failed jobs for {bundle.Run.Url} attempt={bundle.Run.Attempt}."); return 0; } private static string RequiredEnvironment(string name, string purpose) => Environment.GetEnvironmentVariable(name) is { Length: > 0 } value ? value : throw new ValidationException($"{name} is required {purpose}."); }