Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/windows-hardening.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
79 changes: 55 additions & 24 deletions .github/workflows/windows-port-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions Tools/windows/Invoke-ValidationRunnerContract.ps1
Original file line number Diff line number Diff line change
@@ -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
115 changes: 115 additions & 0 deletions Tools/windows/Tests/WindowsCiTopology.Tests.ps1
Original file line number Diff line number Diff line change
@@ -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
Loading