| File: Workflows\AspireCiWorkflowConventions.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.Nodes; using System.Text.RegularExpressions; using static AnalyzeCiFailure.CiFailureJson; namespace AnalyzeCiFailure; /// <summary>Recognizes Aspire CI jobs and their required, optional and interrupted-execution result artifacts.</summary> internal sealed partial class AspireCiWorkflowConventions : IGitHubWorkflowConventions { internal const string ResultsUploadStep = "Upload logs, and test results"; internal const string ExtensionTestStep = "Run extension E2E tests"; internal const string FinalResultsJob = "Final Results"; public string WorkflowPath => ".github/workflows/ci.yml"; public bool IsAggregationJob(JsonObject job) { if (Text(job["name"]) == FinalResultsJob) { return true; } var firstFailure = (job["steps"] as JsonArray ?? []) .FirstOrDefault(step => Text(Field(step, "conclusion")) is "failure" or "cancelled" or "timed_out"); return firstFailure is not null && AggregationStepNameRegex().IsMatch(Text(Field(firstFailure, "name")) ?? ""); } public IReadOnlyList<TestArtifactExpectation> DescribeTestArtifacts(JsonObject job, int runAttempt) { var name = Text(job["name"]); if (name is null) { return []; } var steps = job["steps"] as JsonArray ?? []; var required = steps.Any(step => Text(Field(step, "name")) == ResultsUploadStep) || RequiredTestJobNameRegex().IsMatch(name); var extension = ExtensionTestJobNameRegex().IsMatch(name); if (!required && !extension) { return []; } // Reusable workflow jobs appear as "... / Infrastructure (ubuntu-latest)". // Their logs artifacts are "logs-Infrastructure-ubuntu-latest"; extension // artifacts instead contain a runtime ID, shard and explicit attempt. var match = (required ? TestArtifactJobNameRegex() : ExtensionArtifactJobNameRegex()).Match(name); if (!match.Success) { throw new ValidationException("failed test job name does not match the artifact naming contract: " + Display(DiagnosticTextSanitizer.Sanitize(name, limit: 500))); } if (required) { return [new( name, $"logs-{match.Groups["short"].Value}-{match.Groups["runner"].Value}", TestArtifactFormat.Trx, TestEvidencePolicy.Required)]; } var interrupted = !steps.Any(step => Text(Field(step, "conclusion")) == "failure") || steps.Any(step => Text(Field(step, "status")) == "in_progress") || steps.Any(step => Text(Field(step, "name")) == ExtensionTestStep && Text(Field(step, "conclusion")) is "cancelled" or "timed_out"); // Extension diagnostics are optional after setup failure or completed tests, // but runner loss/cancelled tests must not be interpreted as zero failures. return [new( name, $"extension-e2e-diagnostics-{(match.Groups["os"].Value == "Windows" ? "win-x64" : "linux-x64")}-{match.Groups["shard"].Value}-attempt{runAttempt}", TestArtifactFormat.Mocha, interrupted ? TestEvidencePolicy.RequiredForInterruptedExecution : TestEvidencePolicy.OptionalDiagnostics)]; } [GeneratedRegex(@"^Tests / (No-package tests|Package tests - (Linux|Windows|macOS)|CLI archive tests)( \(| / )")] private static partial Regex RequiredTestJobNameRegex(); [GeneratedRegex(@"^(Fail if|Check ).*(depend|failed)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] private static partial Regex AggregationStepNameRegex(); [GeneratedRegex(@"(^| / )VS Code extension E2E( \(|$)")] private static partial Regex ExtensionTestJobNameRegex(); [GeneratedRegex(@"(^| / )(?<short>[^/]+) \((?<runner>[^()]*)\)\z")] private static partial Regex TestArtifactJobNameRegex(); [GeneratedRegex(@"(^| / )VS Code extension E2E \((?<os>Windows|Linux), (?<shard>[^()]+)\)\z")] private static partial Regex ExtensionArtifactJobNameRegex(); }