| File: AnalysisInvocation.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.Text.Json; namespace AnalyzeCiFailure; /// <summary>Owns invocation output directories, temporary scratch, captured facts and diagnostic finalization.</summary> internal sealed class AnalysisInvocation : IDisposable { internal string RootDirectory { get; } = Directory.CreateTempSubdirectory("aspire-ci-invocation-").FullName; internal AnalysisScratchWorkspace Scratch { get; } internal EvidencePaths Evidence { get; } internal EvidencePaths Replay { get; } internal InvocationCapture Capture { get; } = new(); internal AnalysisProgress Progress { get; } = new(); internal string DiagnosticsDirectory => Path.Combine(RootDirectory, "diagnostics"); internal AnalysisInvocation() { Scratch = new AnalysisScratchWorkspace(Path.Combine(RootDirectory, "scratch")); Evidence = new EvidencePaths(Path.Combine(RootDirectory, "evidence")); Replay = new EvidencePaths(Path.Combine(RootDirectory, "replay")); Progress.Diagnostics.EnableProgress(DiagnosticsDirectory); } internal string RecoveryDirectory(string stage) => Path.Combine(RootDirectory, "recovery", stage); internal async Task WritePathsAsync(string stage, string? githubOutput) { var lines = $"workspace_dir={RootDirectory}\n" + $"evidence_dir={Evidence.RootDirectory}\nreplay_dir={Replay.RootDirectory}\nrecovery_dir={RecoveryDirectory(stage)}\n" + $"diagnostics_dir={DiagnosticsDirectory}\n"; // Write paths before work begins, including failed/cancelled acquisition. // Upload steps consume runner-local paths; only artifacts cross job boundaries. if (lines.Contains('\r') || RootDirectory.Contains('\n')) { throw new ValidationException("Invocation paths must not contain newlines."); } Console.Write(lines); if (githubOutput is not null) { try { await File.AppendAllTextAsync(githubOutput, lines, CancellationToken.None); } catch (Exception error) when (error is IOException or UnauthorizedAccessException) { Capture.Failure = error; Progress.RecordProblem(AnalysisProblemCategory.Fatal, "path-output", error.Message); await WriteDiagnosticsBestEffortAsync("failed"); throw; } } } internal Task WriteDiagnosticsAsync(string status) => Progress.Diagnostics.WriteAsync(DiagnosticsDirectory, Progress, status, Capture.Failure); internal async Task WriteDiagnosticsBestEffortAsync(string status) { try { await WriteDiagnosticsAsync(status); } catch (Exception error) when (error is IOException or UnauthorizedAccessException or System.Text.Json.JsonException) { Progress.RecordProblem(AnalysisProblemCategory.Fatal, "diagnostics", error.Message); Console.Error.WriteLine($"::error::Unable to retain analysis diagnostics: {CiFailureJson.Display(DiagnosticTextSanitizer.Sanitize(error.Message, limit: 500))}"); } } public void Dispose() => Scratch.Dispose(); internal async Task WriteRecoveryBestEffortAsync(string stage, string status) { try { await RecoveryCaptureWriter.WriteAsync(RecoveryDirectory(stage), Progress, status, Capture, Scratch); } catch (Exception error) when (error is IOException or UnauthorizedAccessException or ValidationException or JsonException) { Progress.RecordProblem(AnalysisProblemCategory.Fatal, "recovery", error.Message); Console.Error.WriteLine($"::error::Unable to retain analysis recovery: {CiFailureJson.Display(error.Message)}"); } } }