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
13 changes: 13 additions & 0 deletions .github/docs/trigger-azdo-pipeline-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,19 @@ Automated CI-fix PR validation is implemented separately by
revalidation, dedupe reads, queue POSTs, retries, and reconciliation. The
ten-minute job timeout therefore preserves a real three-minute reserve for
summaries and an explicit failed outcome instead of a hard cancellation;
- uses exact-file, non-cone sparse checkout of the standalone dispatcher script
at the trusted `github.sha`, keeping the pinned checkout action, shallow fetch,
and disabled credential persistence. This avoids materializing unrelated
repository files; it does not guarantee checkout finishes within the deadline
or establish production checkout performance;
- keeps the validated caller budget separate from the effective remaining
budget. If preparation consumes the deadline, startup fails explicitly with
`[dispatcher-budget-exhausted]`, records the preparation/deadline stage in the
job summary, and states that no queue POST was attempted before any PR
discovery, authentication, HTTP request, or retry sleep. An expired deadline
is a failure even when no eligible work exists; it is never restarted after
checkout. Checkout or startup exceeding the hard job timeout can still cancel
the job before the script can report this diagnostic;
- caps every HTTP timeout and retry sleep to the remaining shared budget. If it
expires, no new requests start, completed results remain visible, all
unprocessed PR/pipeline work is marked failed, and an ambiguous one-time POST
Expand Down
125 changes: 123 additions & 2 deletions .github/scripts/Queue-CiFixAzdoValidation.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,18 @@ Describe 'Test-CiFixPrFingerprint' {
}

Describe 'trusted workflow configuration' {
It 'pins checkout to the reviewed v7.0.1 commit in the id-token job' {
It 'pins checkout to the reviewed v7.0.1 commit in the id-token job with <LineEnding> line endings' -ForEach @(
@{ LineEnding = 'LF'; NewLine = "`n" }
@{ LineEnding = 'CRLF'; NewLine = "`r`n" }
) {
$workflow = Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot '../workflows/ci-fix-azdo-validation.yml')
$workflow = $workflow -replace '\r?\n', $NewLine

$workflow | Should -Match 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7\.0\.1'
$workflow | Should -Match 'ref: \$\{\{ github\.sha \}\}'
$workflow | Should -Not -Match 'github\.event\.pull_request\.base\.sha'
$workflow | Should -Match 'fetch-depth: 1'
$workflow | Should -Match '(?m)^\s+sparse-checkout: \|\r?\n\s+/\.github/scripts/Queue-CiFixAzdoValidation\.ps1\r?\n\s+sparse-checkout-cone-mode: false\r?$'
$workflow | Should -Match 'persist-credentials: false'
$workflow | Should -Match 'id-token: write'
$workflow | Should -Match 'timeout-minutes: 10'
Expand Down Expand Up @@ -880,7 +886,7 @@ Describe 'Invoke-AzdoPipelineQueue retry safety' {
$script:TransientHttpStatusCodes = @(408, 429, 500, 502, 503, 504)
$script:MaxHttpAttempts = 4
$script:RetryBaseDelaySeconds = 2
$script:DispatcherBudgetSeconds = 480
$script:EffectiveDispatcherBudgetSeconds = 480
$script:DispatcherStopwatch = [System.Diagnostics.Stopwatch]::StartNew()
$script:DispatcherBudgetPrefix = '[dispatcher-budget-exhausted]'
foreach ($definition in $FunctionDefinitions) {
Expand Down Expand Up @@ -1517,6 +1523,121 @@ Describe 'event payload validation' {
}
}

Describe 'entrypoint dispatcher deadline' {
BeforeEach {
$script:oldStepSummary = $env:GITHUB_STEP_SUMMARY
$summaryPath = Join-Path $TestDrive "deadline-summary-$([Guid]::NewGuid().ToString('N')).md"
$env:GITHUB_STEP_SUMMARY = $summaryPath
$eventPath = Join-Path $TestDrive 'deadline-event.json'
$fixturePath = Join-Path $TestDrive 'deadline-fixture.json'
New-TestEvent | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $eventPath
New-TestPullRequestFixture -PullRequests @((New-TestPullRequest)) |
ConvertTo-Json -Depth 20 |
Set-Content -LiteralPath $fixturePath

Mock Invoke-RestMethod { throw 'HTTP, OIDC, and Azure requests must not run.' }
Mock Invoke-WebRequest { throw 'HTTP requests must not run.' }
Mock Start-Sleep { throw 'Sleep must not run.' }
}

AfterEach {
$env:GITHUB_STEP_SUMMARY = $script:oldStepSummary
}

It 'fails explicitly before external work when preparation exhausted the deadline (<Mode>)' -ForEach @(
@{ Mode = 'live discovery'; UseFixture = $false; EmptyFixture = $false }
@{ Mode = 'eligible offline fixture'; UseFixture = $true; EmptyFixture = $false }
@{ Mode = 'no eligible offline work'; UseFixture = $true; EmptyFixture = $true }
) {
if ($EmptyFixture) {
[pscustomobject]@{ pullRequests = @() } |
ConvertTo-Json -Depth 10 |
Set-Content -LiteralPath $fixturePath
}
$parameters = @{
EventPath = $eventPath
Repository = 'dotnet/maui'
EventName = 'pull_request_target'
DispatcherBudgetSeconds = 420
DispatcherDeadlineUnixSeconds = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() - 60
}
if ($UseFixture) {
$parameters.PullRequestsFixturePath = $fixturePath
$parameters.DryRun = $true
}

{ & $scriptPath @parameters } |
Should -Throw '*dispatcher-budget-exhausted*preparation*No queue POST was attempted.*'

$summary = Get-Content -Raw -LiteralPath $summaryPath
$summary | Should -Match '\[dispatcher-budget-exhausted\]'
$summary | Should -Match 'Stage: preparation / dispatcher deadline'
$summary | Should -Match 'Result: failed'
$summary | Should -Match 'before PR discovery or authentication'
$summary | Should -Match 'No queue POST was attempted\.'
Should -Invoke Invoke-RestMethod -Times 0 -Exactly
Should -Invoke Invoke-WebRequest -Times 0 -Exactly
Should -Invoke Start-Sleep -Times 0 -Exactly
}

It 'returns a nonzero process exit with the preparation diagnostic for an expired deadline' {
$output = & pwsh -NoLogo -NoProfile -File $scriptPath `
-EventPath $eventPath `
-Repository dotnet/maui `
-EventName pull_request_target `
-PullRequestsFixturePath $fixturePath `
-DispatcherBudgetSeconds 420 `
-DispatcherDeadlineUnixSeconds ([DateTimeOffset]::UtcNow.ToUnixTimeSeconds() - 60) `
-DryRun 2>&1

$LASTEXITCODE | Should -Not -Be 0
$diagnostic = $output -join [Environment]::NewLine
$diagnostic | Should -Match '\[dispatcher-budget-exhausted\]'
$diagnostic | Should -Match 'preparation'
$diagnostic | Should -Not -Match 'variable cannot be validated'
(Get-Content -Raw -LiteralPath $summaryPath) |
Should -Match 'No queue POST was attempted\.'
}

It 'retains caller validation that rejects a zero budget before writing a startup summary' {
{
& $scriptPath `
-EventPath $eventPath `
-Repository dotnet/maui `
-EventName pull_request_target `
-DispatcherBudgetSeconds 0 `
-DispatcherDeadlineUnixSeconds ([DateTimeOffset]::UtcNow.ToUnixTimeSeconds() - 60)
} | Should -Throw "*'DispatcherBudgetSeconds'*"

Test-Path -LiteralPath $summaryPath | Should -BeFalse
Should -Invoke Invoke-RestMethod -Times 0 -Exactly
Should -Invoke Invoke-WebRequest -Times 0 -Exactly
Should -Invoke Start-Sleep -Times 0 -Exactly
}

It 'reaches verified offline payload generation with a valid future deadline' {
$output = & $scriptPath `
-EventPath $eventPath `
-Repository dotnet/maui `
-EventName pull_request_target `
-PullRequestsFixturePath $fixturePath `
-DispatcherBudgetSeconds 420 `
-DispatcherDeadlineUnixSeconds ([DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + 120) `
-DryRun

$results = @($output -join [Environment]::NewLine | ConvertFrom-Json -Depth 20)
$results.Count | Should -Be 3
@($results.Outcome | Select-Object -Unique) | Should -Be @('dry-run')
@($results.DefinitionId | Sort-Object) | Should -Be @(302, 313, 314)
@($results.Request.sourceVersion | Select-Object -Unique) |
Should -Be @('2222222222222222222222222222222222222222')
(Get-Content -Raw -LiteralPath $summaryPath) | Should -Match 'dotnet/maui#123'
Should -Invoke Invoke-RestMethod -Times 0 -Exactly
Should -Invoke Invoke-WebRequest -Times 0 -Exactly
Should -Invoke Start-Sleep -Times 0 -Exactly
}
}

Describe 'entrypoint verification failure routing' {
BeforeEach {
$script:oldStepSummary = $env:GITHUB_STEP_SUMMARY
Expand Down
19 changes: 17 additions & 2 deletions .github/scripts/Queue-CiFixAzdoValidation.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ $secondsUntilDeadline = if ($DispatcherDeadlineUnixSeconds -gt 0) {
else {
$DispatcherBudgetSeconds
}
$script:DispatcherBudgetSeconds = [Math]::Max(
$script:EffectiveDispatcherBudgetSeconds = [Math]::Max(
0,
[Math]::Min($DispatcherBudgetSeconds, $secondsUntilDeadline))
$script:DispatcherStopwatch = [System.Diagnostics.Stopwatch]::StartNew()
Expand All @@ -36,7 +36,7 @@ function Get-DispatcherElapsedSeconds {
}

function Get-DispatcherRemainingSeconds {
return [Math]::Max(0, $script:DispatcherBudgetSeconds - (Get-DispatcherElapsedSeconds))
return [Math]::Max(0, $script:EffectiveDispatcherBudgetSeconds - (Get-DispatcherElapsedSeconds))
}

function Test-IsDispatcherBudgetException {
Expand Down Expand Up @@ -1001,6 +1001,21 @@ function Invoke-CiFixQueueWork {
return $workResults.ToArray()
}

if ([Math]::Floor((Get-DispatcherRemainingSeconds)) -lt 1) {
$preparationFailure = "$($script:DispatcherBudgetPrefix) Dispatcher deadline exhausted during preparation (trusted checkout / PowerShell startup), before PR discovery or authentication. No queue POST was attempted."
if (-not [string]::IsNullOrWhiteSpace($env:GITHUB_STEP_SUMMARY)) {
$lines = @(
'## Automated CI-fix Azure DevOps validation',
'',
'- Stage: preparation / dispatcher deadline',
'- Result: failed',
"- Details: $preparationFailure"
)
Add-Content -LiteralPath $env:GITHUB_STEP_SUMMARY -Value ($lines -join [Environment]::NewLine)
}
throw $preparationFailure
}

if ([string]::IsNullOrWhiteSpace($EventPath) -or -not (Test-Path -LiteralPath $EventPath -PathType Leaf)) {
throw 'GITHUB_EVENT_PATH must identify a supported GitHub event payload file.'
}
Expand Down
28 changes: 28 additions & 0 deletions .github/scripts/Register-CiFixSafeOutputExpectation.Tests.ps1
Original file line number Diff line number Diff line change
@@ -1,6 +1,34 @@
#!/usr/bin/env pwsh
#Requires -Modules Pester

Describe 'CI-fixer safe-output helper availability' {
It 'keeps <Workflow> environment guidance consistent with the existing helper allowlist' -ForEach @(
@{ Workflow = 'ci-status-fix.md' }
@{ Workflow = 'ci-status-fix-net11.md' }
) {
$workflowPath = Join-Path (Split-Path $PSScriptRoot) "workflows/$Workflow"
$source = Get-Content -Raw -LiteralPath $workflowPath
$bashAllowlist = [regex]::Match($source, '(?m)^ bash: \[(?<commands>.*)\]\r?$')
$environment = [regex]::Match(
$source,
'(?ms)^## Environment constraints\r?\n(?<guidance>.*?)(?=^## )')

$bashAllowlist.Success | Should -BeTrue
$bashAllowlist.Groups['commands'].Value | Should -Match '"pwsh"'
$bashAllowlist.Groups['commands'].Value | Should -Not -Match '"(?:gh|python)"'
$environment.Success | Should -BeTrue
$guidance = $environment.Groups['guidance'].Value
$guidance | Should -Not -Match 'no\s+`pwsh`'
$guidance | Should -Match '`pwsh` is available'
$guidance | Should -Match 'Register-CiFixSafeOutputExpectation\.ps1'
$guidance | Should -Match 'Test-CiFixTransport\.ps1'
$guidance | Should -Match 'Hard Rule 11'
$guidance | Should -Match 'Step 5\.6'
$guidance | Should -Match 'no `gh`, no `python`'
$guidance | Should -Match 'Use `curl` \+ `jq` for all API calls'
}
}

Describe 'Register-CiFixSafeOutputExpectation' {
BeforeEach {
$script:outputDirectory = Join-Path $TestDrive "expectations-$([Guid]::NewGuid().ToString('N'))"
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci-fix-azdo-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ jobs:
ref: ${{ github.sha }}
fetch-depth: 1
persist-credentials: false
sparse-checkout: |
/.github/scripts/Queue-CiFixAzdoValidation.ps1
sparse-checkout-cone-mode: false

- name: Queue Azure DevOps PR validation
shell: pwsh
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-status-fix-net11.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions .github/workflows/ci-status-fix-net11.md
Original file line number Diff line number Diff line change
Expand Up @@ -2205,8 +2205,13 @@ These look like permission errors but are physical:
- OData `$top` must be encoded as `%24top` in URLs.
- Each bash call runs in a fresh subshell. Persist state to
`/tmp/gh-aw/agent/<file>`.
- Bash allowlist per frontmatter `tools.bash`: no `gh`, no `pwsh`, no
`python`. Use `curl` + `jq` for all API calls.
- Bash allowlist per frontmatter `tools.bash`: no `gh`, no `python`.
Use `curl` + `jq` for all API calls.
- `pwsh` is available for deterministic safe-output expectation registration
(`.github/scripts/Register-CiFixSafeOutputExpectation.ps1`) and transport
validation/registration (`.github/scripts/Test-CiFixTransport.ps1`). Run these
helpers as required by Hard Rule 11 and Step 5.6; never bypass their fail-closed
registration or transport checks.

## Output discipline

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-status-fix.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading