From dcd4e28f17ee0c619b8284cadfc2d1f0e57b4fc1 Mon Sep 17 00:00:00 2001 From: Colin Neilens Date: Fri, 30 Oct 2026 18:20:35 -0700 Subject: [PATCH] Parallelize Windows validation runner Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens --- .github/workflows/windows-hardening.yml | 7 +- .github/workflows/windows-port-validation.yml | 79 ++++++++---- .../Invoke-ValidationRunnerContract.ps1 | 21 ++++ .../windows/Tests/WindowsCiTopology.Tests.ps1 | 115 ++++++++++++++++++ 4 files changed, 196 insertions(+), 26 deletions(-) create mode 100644 Tools/windows/Invoke-ValidationRunnerContract.ps1 create mode 100644 Tools/windows/Tests/WindowsCiTopology.Tests.ps1 diff --git a/.github/workflows/windows-hardening.yml b/.github/workflows/windows-hardening.yml index 487c13dc..79bb4d86 100644 --- a/.github/workflows/windows-hardening.yml +++ b/.github/workflows/windows-hardening.yml @@ -70,9 +70,12 @@ jobs: shell: pwsh run: ./Tools/windows/validate.ps1 -Task hardening - name: Verify runner contract - if: env.HARDENING_REQUIRED == 'true' + # Pull requests run this source contract in the windows-spikes aggregate + # once with pinned Swift. Keep one fallback for this workflow's independent + # scheduled and manual runs. + if: env.HARDENING_REQUIRED == 'true' && github.event_name != 'pull_request' && matrix.os == 'windows-2022' shell: pwsh - run: ./Tools/windows/Tests/ValidationRunner.Tests.ps1 + run: ./Tools/windows/Invoke-ValidationRunnerContract.ps1 - name: Resolve capture contract failure diagnostics root id: capture_temp if: failure() diff --git a/.github/workflows/windows-port-validation.yml b/.github/workflows/windows-port-validation.yml index 88742648..74983bc8 100644 --- a/.github/workflows/windows-port-validation.yml +++ b/.github/workflows/windows-port-validation.yml @@ -21,7 +21,8 @@ env: # on every Windows-relevant pull request: windows-shell and packaging run in # windows-shell.yml (required check `windows-shell`), and hardening runs in # windows-hardening.yml (required `Deterministic hardening (...)` checks). The -# remaining tasks are split across two parallel parts behind the required +# remaining tasks and the source-only runner contract are split across three +# parallel parts behind the required # `windows-spikes` aggregate job. jobs: changes: @@ -58,15 +59,16 @@ jobs: shell: pwsh run: ./Tools/windows/validate.ps1 -Task privacy - # The Swift spikes, production package tests and clean-runtime smoke, and the - # swift-format gate. The runner contract also runs here, where Swift exists, - # so its unformatted-source negative control exercises the real formatter. - spikes-swift: - name: windows-spikes Swift + # ValidationRunner.Tests.ps1 is source-only but deliberately uses the real + # pinned Swift formatter for its negative control. Run it once on Windows + # 2022 in parallel instead of repeating it inside spikes-swift and both + # deterministic hardening legs. Windows 2025 remains covered by hardening. + validation-runner: + name: Validation runner contract needs: changes if: ${{ !cancelled() && (github.event_name != 'pull_request' || needs.changes.result != 'success' || needs.changes.outputs.windows == 'true') }} runs-on: windows-2022 - timeout-minutes: 45 + timeout-minutes: 20 steps: - name: Check out repository uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 @@ -80,24 +82,11 @@ jobs: swift-build: 6.3.3-RELEASE cache: true - - name: Restore pinned Zig toolchains - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - timeout-minutes: 10 - with: - path: | - .ci-tools/zig-0.15.2 - .ci-tools/zig-0.16.0 - key: windows-zig-v1-${{ hashFiles('Tools/windows/bootstrap.ps1') }} - - - name: Bootstrap exact Windows dependencies - shell: pwsh - timeout-minutes: 30 - run: ./Tools/windows/bootstrap.ps1 -ToolRoot .ci-tools -ProviderRoot .ci-providers - - name: Verify validation runner contract shell: pwsh timeout-minutes: 15 - run: ./Tools/windows/Tests/ValidationRunner.Tests.ps1 + run: ./Tools/windows/Invoke-ValidationRunnerContract.ps1 + - name: Resolve capture contract failure diagnostics root id: capture_temp if: failure() @@ -120,7 +109,7 @@ jobs: if: ${{ failure() && steps.capture_temp.conclusion == 'success' }} uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: - name: ui-capture-spikes-swift-${{ github.run_attempt }} + name: ui-capture-validation-runner-${{ github.run_attempt }} path: | ${{ steps.capture_temp.outputs.root }}\gc-ui-capture-*\suite\**\*.json ${{ steps.capture_temp.outputs.root }}\gc-ui-capture-*\suite\**\*.jsonl @@ -139,6 +128,48 @@ jobs: if-no-files-found: warn retention-days: 7 + # The Swift spikes, production package tests and clean-runtime smoke, and the + # swift-format gate. + spikes-swift: + name: windows-spikes Swift + needs: changes + if: ${{ !cancelled() && (github.event_name != 'pull_request' || needs.changes.result != 'success' || needs.changes.outputs.windows == 'true') }} + runs-on: windows-2022 + timeout-minutes: 45 + steps: + - name: Check out repository + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + timeout-minutes: 5 + + # Keep the source topology check in this job so the existing runner + # contract verifies its timeout before the expensive Swift/provider work. + - name: Verify validation runner contract topology + shell: pwsh + timeout-minutes: 15 + run: ./Tools/windows/Tests/WindowsCiTopology.Tests.ps1 + + - name: Install pinned Swift toolchain + uses: compnerd/gha-setup-swift@397094e75494a93fa8d81db0268dbc8f5d6cf7c6 # v0.4.1 + timeout-minutes: 10 + with: + swift-version: swift-6.3.3-release + swift-build: 6.3.3-RELEASE + cache: true + + - name: Restore pinned Zig toolchains + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + timeout-minutes: 10 + with: + path: | + .ci-tools/zig-0.15.2 + .ci-tools/zig-0.16.0 + key: windows-zig-v1-${{ hashFiles('Tools/windows/bootstrap.ps1') }} + + - name: Bootstrap exact Windows dependencies + shell: pwsh + timeout-minutes: 30 + run: ./Tools/windows/bootstrap.ps1 -ToolRoot .ci-tools -ProviderRoot .ci-providers + - name: Run Swift Windows port validation shell: pwsh timeout-minutes: 35 @@ -210,7 +241,7 @@ jobs: retention-days: 7 windows-spikes: - needs: [changes, spikes-swift, spikes-other] + needs: [changes, validation-runner, spikes-swift, spikes-other] if: ${{ !cancelled() }} runs-on: ubuntu-latest timeout-minutes: 10 diff --git a/Tools/windows/Invoke-ValidationRunnerContract.ps1 b/Tools/windows/Invoke-ValidationRunnerContract.ps1 new file mode 100644 index 00000000..38e68be2 --- /dev/null +++ b/Tools/windows/Invoke-ValidationRunnerContract.ps1 @@ -0,0 +1,21 @@ +[CmdletBinding()] +param() + +$ErrorActionPreference = "Stop" +$runner = Join-Path $PSScriptRoot "Tests\ValidationRunner.Tests.ps1" +$pwsh = (Get-Process -Id $PID).Path +$passMarkers = 0 +& $pwsh -NoProfile -File $runner 2>&1 | ForEach-Object { + $line = "$_" + if ($line -ceq "ValidationRunner.Tests.ps1: PASS") { $script:passMarkers++ } + Write-Host $line +} +$exitCode = $LASTEXITCODE +if ($exitCode -ne 0) { + throw "Validation runner contract failed with exit code $exitCode" +} +if ($passMarkers -ne 1) { + throw "Validation runner contract did not report exactly one PASS marker" +} +Write-Host "VALIDATION_RUNNER_EXECUTED=1" +exit 0 diff --git a/Tools/windows/Tests/WindowsCiTopology.Tests.ps1 b/Tools/windows/Tests/WindowsCiTopology.Tests.ps1 new file mode 100644 index 00000000..048b9e3f --- /dev/null +++ b/Tools/windows/Tests/WindowsCiTopology.Tests.ps1 @@ -0,0 +1,115 @@ +[CmdletBinding()] +param() + +$ErrorActionPreference = "Stop" +$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..\..") +$portWorkflow = Get-Content ` + (Join-Path $repoRoot ".github\workflows\windows-port-validation.yml") -Raw +$hardeningWorkflow = Get-Content ` + (Join-Path $repoRoot ".github\workflows\windows-hardening.yml") -Raw +$checks = 0 + +function Assert-Contract([object] $Condition, [string] $Message) { + $values = @($Condition) + if ($values.Count -ne 1 -or -not [bool] $values[0]) { + throw "RED: $Message" + } + $script:checks++ +} + +function Get-WorkflowJobs([string] $Workflow) { + $body = $Workflow.Replace("`r`n", "`n") + $jobsStart = [regex]::Match($body, '(?m)^jobs:\s*$') + if (-not $jobsStart.Success) { throw "Workflow has no jobs mapping" } + $body = $body.Substring($jobsStart.Index + $jobsStart.Length) + $heads = @([regex]::Matches($body, '(?m)^ ([A-Za-z0-9_-]+):\s*$')) + $jobs = [ordered]@{} + for ($index = 0; $index -lt $heads.Count; $index++) { + $end = if ($index + 1 -lt $heads.Count) { $heads[$index + 1].Index } else { $body.Length } + $jobs[$heads[$index].Groups[1].Value] = + $body.Substring($heads[$index].Index, $end - $heads[$index].Index) + } + $jobs +} + +$portJobs = Get-WorkflowJobs $portWorkflow +$hardeningJobs = Get-WorkflowJobs $hardeningWorkflow + +Assert-Contract $portJobs.Contains("validation-runner") ` + "Windows port CI needs a dedicated validation-runner job" +$runnerJob = $portJobs["validation-runner"] +Assert-Contract ($runnerJob -match '(?m)^ needs: changes\s*$' -and + $runnerJob -match "needs\.changes\.outputs\.windows == 'true'") ` + "the validation-runner job must use the fail-closed Windows path gate" +Assert-Contract ($runnerJob -match '(?m)^ runs-on: windows-2022\s*$' -and + $runnerJob -notmatch '(?m)^\s+(strategy|matrix):') ` + "the validation-runner job must execute exactly once on Windows 2022" +Assert-Contract ($runnerJob -match + 'compnerd/gha-setup-swift@397094e75494a93fa8d81db0268dbc8f5d6cf7c6' -and + $runnerJob -match 'swift-version: swift-6\.3\.3-release' -and + $runnerJob -match 'swift-build: 6\.3\.3-RELEASE') ` + "the validation-runner job must install the pinned Swift formatter" +Assert-Contract ([regex]::Matches( + $runnerJob, + '(?m)^\s+run: \./Tools/windows/Invoke-ValidationRunnerContract\.ps1\s*$' + ).Count -eq 1) ` + "the dedicated job must run the counted validation runner contract exactly once" +Assert-Contract ($runnerJob -notmatch 'bootstrap\.ps1|\.ci-providers|validate\.ps1|continue-on-error') ` + "the source-only validation runner job must not bootstrap providers or hide failures" + +$spikesSwift = $portJobs["spikes-swift"] +Assert-Contract ($spikesSwift -notmatch + 'ValidationRunner\.Tests\.ps1|Invoke-ValidationRunnerContract\.ps1') ` + "the sequential Swift job must not repeat the validation runner contract" +Assert-Contract ([regex]::Matches( + $spikesSwift, + '(?m)^\s+run: \./Tools/windows/Tests/WindowsCiTopology\.Tests\.ps1\s*$' + ).Count -eq 1) ` + "the Swift job must verify runner scheduling before expensive setup" +Assert-Contract ([regex]::Matches( + $portWorkflow, + '(?m)^\s+run: \./Tools/windows/Invoke-ValidationRunnerContract\.ps1\s*$' + ).Count -eq 1) ` + "Windows port CI must declare exactly one validation runner step" + +$aggregate = $portJobs["windows-spikes"] +Assert-Contract ($aggregate -match + '(?m)^ needs: \[changes, validation-runner, spikes-swift, spikes-other\]\s*$' -and + $aggregate -match + 'Assert-CiPartResults\.ps1 -NeedsJson \$env:NEEDS_JSON -Required \$env:WINDOWS_REQUIRED') ` + "the required windows-spikes aggregate must fail closed over the dedicated runner job" + +$hardeningRunnerStep = [regex]::Match( + $hardeningJobs["deterministic"], + '(?ms)^ - name: Verify runner contract\s+.*?(?=^ - |\z)' +) +$hardeningRunnerIf = [regex]::Match( + $hardeningRunnerStep.Value, + '(?m)^ if: ([^\r\n]+)\s*$' +) +Assert-Contract ($hardeningRunnerStep.Success -and $hardeningRunnerIf.Success -and + $hardeningRunnerStep.Value -match + '(?m)^ run: \./Tools/windows/Invoke-ValidationRunnerContract\.ps1\s*$' -and + $hardeningRunnerIf.Groups[1].Value -match "env\.HARDENING_REQUIRED == 'true'" -and + $hardeningRunnerIf.Groups[1].Value -match "github\.event_name != 'pull_request'" -and + $hardeningRunnerIf.Groups[1].Value -match "matrix\.os == 'windows-2022'") ` + "hardening must keep one scheduled/manual fallback without duplicating the PR contract" +Assert-Contract ([regex]::Matches( + $hardeningWorkflow, + '(?m)^\s+run: \./Tools/windows/Invoke-ValidationRunnerContract\.ps1\s*$' + ).Count -eq 1) ` + "hardening must declare exactly one non-PR validation runner fallback" + +$wrapper = Get-Content ` + (Join-Path $repoRoot "Tools\windows\Invoke-ValidationRunnerContract.ps1") -Raw +Assert-Contract ([regex]::Matches( + $wrapper, + '(?m)^& \$pwsh -NoProfile -File \$runner 2>&1 \| ForEach-Object \{\s*$' + ).Count -eq 1 -and + $wrapper -match '\$exitCode -ne 0' -and + $wrapper -match '\$passMarkers -ne 1' -and + $wrapper -match 'VALIDATION_RUNNER_EXECUTED=1') ` + "the validation runner wrapper must execute once, fail closed, and report a positive count" + +Write-Host "Windows CI topology: $checks checks, 0 failures" +exit 0