From ff4692b21cee935210ddb1abbf21c3b8c83a4fd9 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Fri, 18 Sep 2026 13:55:02 -0700 Subject: [PATCH] fix(ci): select latest test-result artifacts by creation time --- .github/scripts/download-test-results.ps1 | 165 ++++++++++ .github/scripts/test-test-results.ps1 | 374 ++++++++++++++++++++++ .github/workflows/test-results.yml | 19 +- .github/workflows/test-test-results.yml | 37 +++ 4 files changed, 589 insertions(+), 6 deletions(-) create mode 100644 .github/scripts/download-test-results.ps1 create mode 100644 .github/scripts/test-test-results.ps1 create mode 100644 .github/workflows/test-test-results.yml diff --git a/.github/scripts/download-test-results.ps1 b/.github/scripts/download-test-results.ps1 new file mode 100644 index 00000000000..3ac380866b9 --- /dev/null +++ b/.github/scripts/download-test-results.ps1 @@ -0,0 +1,165 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string] $ResultsPath, + + [Parameter(Mandatory)] + [ValidatePattern('^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$')] + [string] $Repository, + + [Parameter(Mandatory)] + [ValidateRange(1, [long]::MaxValue)] + [long] $RunId, + + [Parameter(Mandatory)] + [ValidateRange(1, [int]::MaxValue)] + [int] $RunAttempt, + + [Parameter(Mandatory)] + [ValidatePattern('^[0-9a-f]{40}$')] + [string] $Sha +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +if (-not $env:GH_TOKEN) { + throw 'GH_TOKEN is required to download test results.' +} + +function Get-GitHubJson { + param( + [string] $Endpoint, + [switch] $Paginate + ) + + $arguments = @('api', $Endpoint, '-H', 'X-GitHub-Api-Version: 2022-11-28') + if ($Paginate) { + $arguments += '--paginate', '--slurp' + } + + $json = & gh @arguments + if ($LASTEXITCODE -ne 0) { + throw "Unable to read GitHub API endpoint '$Endpoint'." + } + + return ($json | ConvertFrom-Json) +} + +function Assert-SourceRun { + $run = Get-GitHubJson "repos/$Repository/actions/runs/$RunId" + if ($run.id -ne $RunId -or $run.head_sha -cne $Sha -or + $run.run_attempt -ne $RunAttempt -or $run.status -ne 'completed') { + throw "Source run $RunId is not completed attempt $RunAttempt for commit $Sha." + } +} + +Assert-SourceRun +$pages = @(Get-GitHubJson "repos/$Repository/actions/runs/$RunId/artifacts?per_page=100" -Paginate) +if ($pages.Count -eq 0) { + throw "No artifact listing was returned for run $RunId." +} + +$artifacts = @($pages | ForEach-Object { $_.artifacts }) +$totalCount = $pages[0].total_count +if ($totalCount -ne $artifacts.Count -or @($pages | Where-Object { $_.total_count -ne $totalCount }).Count -gt 0) { + throw "Artifact listing for run $RunId changed or is incomplete. Retry after uploads finish." +} + +$seenIds = [Collections.Generic.HashSet[long]]::new() +$partitions = [Collections.Generic.Dictionary[string, object]]::new([StringComparer]::Ordinal) +foreach ($artifact in $artifacts) { + if ($artifact.id -le 0 -or -not $seenIds.Add($artifact.id)) { + throw "Artifact listing contains an invalid or repeated id '$($artifact.id)'." + } + if (-not $artifact.name.StartsWith('test_output_', [StringComparison]::Ordinal)) { + continue + } + if ($artifact.name -cnotmatch '^test_output_[A-Za-z0-9_.-]+$') { + throw "Invalid test result artifact name '$($artifact.name)'." + } + if ($null -eq $artifact.workflow_run -or $artifact.workflow_run.id -ne $RunId -or $artifact.workflow_run.head_sha -cne $Sha) { + throw "Test result artifact $($artifact.id) does not belong to run $RunId for commit $Sha." + } + + $createdAt = [DateTimeOffset]::MinValue + if ($artifact.created_at -is [DateTime]) { + $createdAt = [DateTimeOffset] $artifact.created_at + } elseif (-not $artifact.created_at -or -not [DateTimeOffset]::TryParse( + [string] $artifact.created_at, + [Globalization.CultureInfo]::InvariantCulture, + [Globalization.DateTimeStyles]::AssumeUniversal, + [ref] $createdAt + )) { + throw "Test result artifact $($artifact.id) has an invalid created_at." + } + + if (-not $partitions.ContainsKey($artifact.name)) { + $partitions[$artifact.name] = [Collections.Generic.List[object]]::new() + } + $partitions[$artifact.name].Add([pscustomobject]@{ Artifact = $artifact; CreatedAt = $createdAt }) +} + +if ($partitions.Count -eq 0) { + throw "No test result artifacts were found for run $RunId." +} + +# The artifacts API has creation times, but no attempt field. IDs are not chronological. +# Select each partition independently so failed-only reruns retain the other partitions. +$selected = @( + foreach ($name in @($partitions.Keys | Sort-Object)) { + $candidates = @($partitions[$name] | Sort-Object CreatedAt -Descending) + if ($candidates.Count -gt 1 -and $candidates[0].CreatedAt -eq $candidates[1].CreatedAt) { + throw "Test result artifact '$name' has ambiguous latest creation times." + } + + $artifact = $candidates[0].Artifact + if ($artifact.expired -isnot [bool] -or $artifact.expired) { + throw "Latest test result artifact '$name' ($($artifact.id)) is expired or has invalid expiry metadata." + } + $artifact + } +) + +$resolvedResultsPath = [IO.Path]::GetFullPath($ResultsPath) +if (Test-Path -LiteralPath $resolvedResultsPath) { + throw "Test results destination '$resolvedResultsPath' already exists. Use a fresh directory." +} +[void] (New-Item -ItemType Directory -Path $resolvedResultsPath) + +$headers = @{ + Accept = 'application/vnd.github+json' + Authorization = "Bearer $env:GH_TOKEN" + 'User-Agent' = 'dotnet-orleans-test-reporter' + 'X-GitHub-Api-Version' = '2022-11-28' +} +foreach ($artifact in $selected) { + Write-Host "Downloading $($artifact.name) (ID: $($artifact.id), created_at: $($artifact.created_at))" + $archive = [IO.Path]::GetTempFileName() + try { + Invoke-WebRequest ` + -Uri "https://api.github.com/repos/$Repository/actions/artifacts/$($artifact.id)/zip" ` + -Headers $headers ` + -OutFile $archive + + $digest = $artifact.PSObject.Properties['digest'] + if ($digest -and $digest.Value) { + if ($digest.Value -cnotmatch '^sha256:[0-9a-f]{64}$' -or + "sha256:$((Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant())" -cne $digest.Value) { + throw "Test result artifact $($artifact.id) failed SHA-256 digest validation." + } + } else { + Write-Warning "Test result artifact $($artifact.id) has no API digest to verify." + } + + $destination = Join-Path $resolvedResultsPath $artifact.name + [IO.Compression.ZipFile]::ExtractToDirectory($archive, $destination) + if (@(Get-ChildItem -LiteralPath $destination -Recurse -File -Filter '*.trx').Count -eq 0) { + throw "Latest test result artifact '$($artifact.name)' ($($artifact.id)) contains no TRX files." + } + } finally { + Remove-Item -LiteralPath $archive -Force + } +} + +Assert-SourceRun diff --git a/.github/scripts/test-test-results.ps1 b/.github/scripts/test-test-results.ps1 new file mode 100644 index 00000000000..978e891052f --- /dev/null +++ b/.github/scripts/test-test-results.ps1 @@ -0,0 +1,374 @@ +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$downloadScript = Join-Path $PSScriptRoot 'download-test-results.ps1' +$publishScript = Join-Path $PSScriptRoot 'publish-test-results.ps1' +$temporaryRoot = Join-Path ([IO.Path]::GetTempPath()) "orleans-test-results-tests-$([guid]::NewGuid())" +$testsRun = 0 +$savedToken = $env:GH_TOKEN +$savedSummary = $env:GITHUB_STEP_SUMMARY +$sha = '0123456789abcdef0123456789abcdef01234567' + +function Assert-Equal { + param($Expected, $Actual) + if ($Expected -cne $Actual) { + throw "Expected '$Expected', actual '$Actual'." + } +} + +function Assert-Matches { + param([string] $Value, [string] $Pattern) + if ($Value -notmatch $Pattern) { + throw "Expected content matching '$Pattern', actual '$Value'." + } +} + +function Assert-Throws { + param([scriptblock] $Action, [string] $Pattern) + try { + & $Action + } catch { + Assert-Matches $_.Exception.Message $Pattern + return + } + throw "Expected error matching '$Pattern'." +} + +function New-Artifact { + param( + [long] $Id, + [string] $Name = 'test_output_Functional_windows_net10.0', + [string] $CreatedAt = '2026-09-18T20:14:10Z', + [string] $Outcome = 'Passed', + [string] $EntryName = 'TestResults/result.trx' + ) + + $archivePath = Join-Path $state.Root "$Id.zip" + $archive = [IO.Compression.ZipFile]::Open($archivePath, [IO.Compression.ZipArchiveMode]::Create) + try { + $entry = $archive.CreateEntry($EntryName) + $writer = [IO.StreamWriter]::new($entry.Open()) + try { + $writer.Write(@" + + + +"@) + } finally { + $writer.Dispose() + } + } finally { + $archive.Dispose() + } + $state.Archives[$Id] = $archivePath + return [pscustomobject]@{ + id = $Id + name = $Name + created_at = $CreatedAt + expired = $false + digest = "sha256:$((Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash.ToLowerInvariant())" + workflow_run = @{ id = 42; head_sha = $sha } + } +} + +function Set-Artifacts { + param([object[]] $Artifacts) + $state.Pages = @(@{ total_count = $Artifacts.Count; artifacts = $Artifacts }) +} + +function gh { + $global:LASTEXITCODE = 0 + Assert-Equal 'api' $args[0] + switch ($args[1]) { + 'repos/dotnet/orleans/actions/runs/42' { + $state.RunReads++ + $attempt = if ($state.RunReads -gt 1) { $state.FinalAttempt } else { $state.InitialAttempt } + return (@{ id = 42; head_sha = $sha; run_attempt = $attempt; status = 'completed' } | ConvertTo-Json) + } + 'repos/dotnet/orleans/actions/runs/42/artifacts?per_page=100' { + Assert-Equal $true ($args -contains '--paginate') + Assert-Equal $true ($args -contains '--slurp') + $state.ListReads++ + $global:LASTEXITCODE = $state.ListExitCode + return (ConvertTo-Json -InputObject $state.Pages -Depth 10) + } + default { throw "Unexpected GitHub API call: $args" } + } +} + +function Invoke-WebRequest { + param([string] $Uri, [hashtable] $Headers, [string] $OutFile) + $match = [regex]::Match($Uri, '^https://api.github.com/repos/dotnet/orleans/actions/artifacts/([0-9]+)/zip$') + Assert-Equal $true $match.Success + $id = [long] $match.Groups[1].Value + Assert-Equal 'Bearer test-token' $Headers.Authorization + $state.Downloads.Add($id) + if ($state.DownloadError) { + throw $state.DownloadError + } + Copy-Item -LiteralPath $state.Archives[$id] -Destination $OutFile +} + +function Invoke-Download { + & $downloadScript -ResultsPath $state.Results -Repository dotnet/orleans -RunId 42 -RunAttempt 2 -Sha $sha +} + +function Get-Report { + return ((& $publishScript -ResultsPath $state.Results -Repository dotnet/orleans -Sha $sha -DryRun) -join "`n") +} + +function Test-Case { + param([string] $Name, [scriptblock] $Action) + $caseRoot = Join-Path $temporaryRoot $Name + [void] (New-Item -ItemType Directory -Path $caseRoot) + $script:state = @{ + Root = $caseRoot + Results = Join-Path $caseRoot 'results' + Archives = @{} + Downloads = [Collections.Generic.List[long]]::new() + DownloadError = '' + InitialAttempt = 2 + FinalAttempt = 2 + RunReads = 0 + ListReads = 0 + ListExitCode = 0 + Pages = @() + } + & $Action + $script:testsRun++ + Write-Output "PASS $Name" +} + +try { + $env:GH_TOKEN = 'test-token' + $env:GITHUB_STEP_SUMMARY = $null + + Test-Case 'NewerLowerIdReplacesStaleFailure' { + $old = New-Artifact -Id 10566260879 -Outcome Failed -EntryName 'TestResults/old.trx' + $new = New-Artifact -Id 10566159940 -CreatedAt '2026-09-18T20:38:46Z' + Set-Artifacts @($old, $new) + Invoke-Download + Assert-Equal '10566159940' ($state.Downloads -join ',') + Assert-Equal $false (Test-Path (Join-Path $state.Results "$($old.name)/TestResults/old.trx")) + Assert-Matches (Get-Report) 'Conclusion: success' + Assert-Matches (Get-Report) '\| 1 \| 0 \| 0 \| 1 \| 1 \|' + } + + Test-Case 'DuplicateNamesAreOrderIndependent' { + $old = New-Artifact -Id 99 + $new = New-Artifact -Id 10 -CreatedAt '2026-09-18T20:38:46Z' + foreach ($artifacts in @(@($old, $new), @($new, $old))) { + $state.Results = Join-Path $state.Root ([guid]::NewGuid()) + Set-Artifacts $artifacts + Invoke-Download + } + Assert-Equal '10,10' ($state.Downloads -join ',') + } + + Test-Case 'FailedOnlyRerunRetainsUntouchedPartitions' { + $old = New-Artifact -Id 99 -Outcome Failed + $retained = New-Artifact -Id 98 -Name 'test_output_BVT_linux_net8.0' + $new = New-Artifact -Id 10 -CreatedAt '2026-09-18T20:38:46Z' + Set-Artifacts @($old, $retained, $new) + Invoke-Download + Assert-Equal '98,10' ($state.Downloads -join ',') + $report = Get-Report + Assert-Matches $report '\| BVT_linux_net8.0 \| 1 \| 1 \| 0 \| 0 \|' + Assert-Matches $report '\| Functional_windows_net10.0 \| 1 \| 1 \| 0 \| 0 \|' + Assert-Matches $report '\| 2 \| 0 \| 0 \| 2 \| 2 \|' + Assert-Matches $report 'Conclusion: success' + } + + Test-Case 'AllPagesParticipateInSelection' { + $firstPage = @(New-Artifact -Id 199 -Outcome Failed) + $firstPage += @(1..99 | ForEach-Object { @{ id = $_; name = "build_log_$_" } }) + $new = New-Artifact -Id 110 -CreatedAt '2026-09-18T20:38:46Z' + $state.Pages = @( + @{ total_count = 101; artifacts = $firstPage }, + @{ total_count = 101; artifacts = @($new) } + ) + Invoke-Download + Assert-Equal 1 $state.ListReads + Assert-Equal '110' ($state.Downloads -join ',') + Assert-Matches (Get-Report) 'Conclusion: success' + } + + Test-Case 'GenuineLatestFailureIsReported' { + $old = New-Artifact -Id 99 + $new = New-Artifact -Id 10 -CreatedAt '2026-09-18T20:38:46Z' -Outcome Failed + Set-Artifacts @($old, $new) + Invoke-Download + Assert-Equal '10' ($state.Downloads -join ',') + $report = Get-Report + Assert-Matches $report 'Conclusion: failure' + Assert-Matches $report 'Test10 \[Failed\]' + Assert-Matches $report '\| 0 \| 1 \| 0 \| 1 \| 1 \|' + } + + Test-Case 'AmbiguousLatestTimesFail' { + Set-Artifacts @((New-Artifact -Id 99), (New-Artifact -Id 10)) + Assert-Throws { Invoke-Download } 'ambiguous latest creation times' + Assert-Equal 0 $state.Downloads.Count + } + + Test-Case 'EquivalentOffsetsAreAmbiguous' { + Set-Artifacts @( + (New-Artifact -Id 99), + (New-Artifact -Id 10 -CreatedAt '2026-09-18T13:14:10-07:00') + ) + Assert-Throws { Invoke-Download } 'ambiguous latest creation times' + } + + Test-Case 'MissingOrInvalidCreationTimeFails' { + $artifact = New-Artifact -Id 10 + foreach ($value in @($null, 'invalid')) { + $artifact.created_at = $value + Set-Artifacts @($artifact) + Assert-Throws { Invoke-Download } 'invalid created_at' + } + } + + Test-Case 'ExpiredLatestDoesNotFallBack' { + $old = New-Artifact -Id 99 + $new = New-Artifact -Id 10 -CreatedAt '2026-09-18T20:38:46Z' + $new.expired = $true + Set-Artifacts @($old, $new) + Assert-Throws { Invoke-Download } 'Latest test result artifact.*expired' + Assert-Equal 0 $state.Downloads.Count + } + + Test-Case 'ExpiredSupersededArtifactIsIgnored' { + $old = New-Artifact -Id 99 + $old.expired = $true + $new = New-Artifact -Id 10 -CreatedAt '2026-09-18T20:38:46Z' + Set-Artifacts @($old, $new) + Invoke-Download + Assert-Equal '10' ($state.Downloads -join ',') + } + + Test-Case 'UnavailableSelectedArtifactFails' { + $old = New-Artifact -Id 99 + $new = New-Artifact -Id 10 -CreatedAt '2026-09-18T20:38:46Z' + Set-Artifacts @($old, $new) + foreach ($status in @(404, 410)) { + $state.Results = Join-Path $state.Root "$status" + $state.DownloadError = "HTTP $status" + Assert-Throws { Invoke-Download } "HTTP $status" + } + Assert-Equal '10,10' ($state.Downloads -join ',') + } + + Test-Case 'IncompleteOrChangingListingFails' { + $artifact = New-Artifact -Id 10 + $state.Pages = @(@{ total_count = 2; artifacts = @($artifact) }) + Assert-Throws { Invoke-Download } 'changed or is incomplete' + $state.Pages += @{ total_count = 1; artifacts = @($artifact) } + Assert-Throws { Invoke-Download } 'changed or is incomplete' + Assert-Equal 0 $state.Downloads.Count + } + + Test-Case 'RepeatedIdsAcrossPagesFail' { + $artifact = New-Artifact -Id 10 + $state.Pages = @( + @{ total_count = 2; artifacts = @($artifact) }, + @{ total_count = 2; artifacts = @($artifact) } + ) + Assert-Throws { Invoke-Download } 'invalid or repeated id' + } + + Test-Case 'ListingFailureStopsDownload' { + Set-Artifacts @((New-Artifact -Id 10)) + $state.ListExitCode = 1 + Assert-Throws { Invoke-Download } 'Unable to read GitHub API endpoint' + Assert-Equal 0 $state.Downloads.Count + } + + Test-Case 'NoMatchingArtifactsFail' { + Set-Artifacts @() + Assert-Throws { Invoke-Download } 'No test result artifacts' + Set-Artifacts @(@{ id = 1; name = 'coverage_test_output_Functional' }) + Assert-Throws { Invoke-Download } 'No test result artifacts' + } + + Test-Case 'InvalidArtifactProvenanceFails' { + $artifact = New-Artifact -Id 10 + foreach ($provenance in @( + @{ id = 41; head_sha = $sha }, + @{ id = 42; head_sha = ('a' * 40) }, + $null + )) { + $artifact.workflow_run = $provenance + Set-Artifacts @($artifact) + Assert-Throws { Invoke-Download } 'does not belong to run 42' + } + Assert-Equal 0 $state.Downloads.Count + } + + Test-Case 'SupersededRunAttemptFails' { + Set-Artifacts @((New-Artifact -Id 10)) + $state.InitialAttempt = 3 + Assert-Throws { Invoke-Download } 'not completed attempt 2' + Assert-Equal 0 $state.ListReads + } + + Test-Case 'RunAttemptChangingDuringDownloadFails' { + Set-Artifacts @((New-Artifact -Id 10)) + $state.FinalAttempt = 3 + Assert-Throws { Invoke-Download } 'not completed attempt 2' + Assert-Equal 2 $state.RunReads + } + + Test-Case 'DigestMismatchFails' { + $artifact = New-Artifact -Id 10 + $artifact.digest = 'sha256:' + ('0' * 64) + Set-Artifacts @($artifact) + Assert-Throws { Invoke-Download } 'failed SHA-256 digest validation' + Assert-Equal 0 @(Get-ChildItem -LiteralPath $state.Results -Recurse -File).Count + } + + Test-Case 'OptionalApiDigestIsSupported' { + $artifact = New-Artifact -Id 10 + $artifact.PSObject.Properties.Remove('digest') + Set-Artifacts @($artifact) + Invoke-Download + Assert-Matches (Get-Report) 'Conclusion: success' + } + + Test-Case 'EmptyLatestResultsFail' { + $old = New-Artifact -Id 99 + $new = New-Artifact -Id 10 -CreatedAt '2026-09-18T20:38:46Z' -EntryName 'logs/output.log' + Set-Artifacts @($old, $new) + Assert-Throws { Invoke-Download } 'contains no TRX files' + Assert-Equal '10' ($state.Downloads -join ',') + } + + Test-Case 'ExistingResultsDirectoryFails' { + Set-Artifacts @((New-Artifact -Id 10)) + [void] (New-Item -ItemType Directory -Path $state.Results) + Assert-Throws { Invoke-Download } 'already exists' + Assert-Equal 0 $state.Downloads.Count + } + + Test-Case 'InvalidArtifactNameFails' { + Set-Artifacts @((New-Artifact -Id 10 -Name 'test_output_../../outside')) + Assert-Throws { Invoke-Download } 'Invalid test result artifact name' + Assert-Equal 0 $state.Downloads.Count + } + + Test-Case 'ArchiveTraversalFails' { + Set-Artifacts @((New-Artifact -Id 10 -EntryName '../outside.trx')) + Assert-Throws { Invoke-Download } 'outside the specified destination' + Assert-Equal $false (Test-Path -LiteralPath (Join-Path $state.Results 'outside.trx')) + } + + Write-Output "$testsRun test result tests passed." +} finally { + $env:GH_TOKEN = $savedToken + $env:GITHUB_STEP_SUMMARY = $savedSummary + if (Test-Path -LiteralPath $temporaryRoot) { + Remove-Item -LiteralPath $temporaryRoot -Recurse -Force + } +} diff --git a/.github/workflows/test-results.yml b/.github/workflows/test-results.yml index 94ab7a409da..4b1031a630c 100644 --- a/.github/workflows/test-results.yml +++ b/.github/workflows/test-results.yml @@ -27,12 +27,19 @@ jobs: uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Download test results - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: test_output_* - path: test-results - run-id: ${{ github.event.workflow_run.id }} - github-token: ${{ github.token }} + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + TEST_COMMIT: ${{ github.event.workflow_run.head_sha }} + TEST_RUN_ID: ${{ github.event.workflow_run.id }} + TEST_RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }} + run: > + ./.github/scripts/download-test-results.ps1 + -ResultsPath test-results + -Repository ${{ github.repository }} + -RunId $env:TEST_RUN_ID + -RunAttempt $env:TEST_RUN_ATTEMPT + -Sha $env:TEST_COMMIT - name: Publish test results shell: pwsh diff --git a/.github/workflows/test-test-results.yml b/.github/workflows/test-test-results.yml new file mode 100644 index 00000000000..aa5faffca18 --- /dev/null +++ b/.github/workflows/test-test-results.yml @@ -0,0 +1,37 @@ +name: Test results reporting +on: + workflow_dispatch: + pull_request: + branches: + - main + paths: + - .github/scripts/download-test-results.ps1 + - .github/scripts/publish-test-results.ps1 + - .github/scripts/test-test-results.ps1 + - .github/workflows/test-results.yml + - .github/workflows/test-test-results.yml + push: + branches: + - main + paths: + - .github/scripts/download-test-results.ps1 + - .github/scripts/publish-test-results.ps1 + - .github/scripts/test-test-results.ps1 + - .github/workflows/test-results.yml + - .github/workflows/test-test-results.yml +permissions: + contents: read +jobs: + selection: + name: Test result selection (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 5 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + steps: + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + - name: Test artifact selection and reporting + shell: pwsh + run: ./.github/scripts/test-test-results.ps1