diff --git a/.github/CI.md b/.github/CI.md index a32d408077..bb11aa685c 100644 --- a/.github/CI.md +++ b/.github/CI.md @@ -36,7 +36,9 @@ node --test .github/scripts/interaction-guard.test.mjs CircleCI's GitHub App trigger and auto-cancel settings live outside the repository. Keep PR, default-branch, and budget rules there; do not add a -second tag trigger. +second tag trigger. Merge-queue builds (`gh-readonly-queue/...` pushes) need +the "Pushes to merge queues" trigger turned on there; the scope gate runs +them as full checks, never as docs-only skips. ## Fork pull requests diff --git a/docs/adr/0005-pr-check-moves-to-circleci-windows.md b/docs/adr/0005-pr-check-moves-to-circleci-windows.md index 8c8e6468ab..92283e2bcc 100644 --- a/docs/adr/0005-pr-check-moves-to-circleci-windows.md +++ b/docs/adr/0005-pr-check-moves-to-circleci-windows.md @@ -29,8 +29,9 @@ Move the hosted PR/push gate to CircleCI as a new `pr-check` job in build`, plus the interaction-guard script tests). The script's default (no-parameter) local behavior is unchanged. - `scripts/circleci-pr-gates.ps1` owns the three skip decisions that used - to live inline in the config (budget `off`, non-PR/non-main branch - pushes, docs-only PR diffs); each true skip calls `circleci-agent step + to live inline in the config (budget `off`, non-PR branch pushes other + than main/master and `gh-readonly-queue/` merge-queue pushes, docs-only PR + diffs); each true skip calls `circleci-agent step halt`. - `scripts/run-circleci-pr-check.ps1` owns toolchain provisioning with official checksum verification: `rustup-init.exe` is downloaded from @@ -69,8 +70,8 @@ Move the hosted PR/push gate to CircleCI as a new `pr-check` job in still PR-associated, the base SHA is resolved from the public GitHub pulls API using the documented `CIRCLE_PROJECT_USERNAME`/`CIRCLE_PROJECT_REPONAME` variables and fetched with `--depth=1`. Docs-only evaluation applies only - to PR-associated pipelines; every `main`/`master` push runs the full - checks. Any resolution, fetch, or diff failure on a PR pipeline fails + to PR-associated pipelines; every `main`/`master` push and every + `gh-readonly-queue/` merge-queue push runs the full checks. Any resolution, fetch, or diff failure on a PR pipeline fails open: it exits only the gate step and therefore continues the job into the checks; the gate never silently skips on an unknown base. - Budget gating stays honest and coarse: the job reads `CI_BUDGET_MODE` as a diff --git a/scripts/circleci-pr-common.ps1 b/scripts/circleci-pr-common.ps1 index daaeda5574..3e0c141584 100644 --- a/scripts/circleci-pr-common.ps1 +++ b/scripts/circleci-pr-common.ps1 @@ -24,11 +24,25 @@ function Test-DocsOnlyDiff { return ($codeFiles.Count -eq 0) } +<# +.SYNOPSIS + True for pushes that always run the full checks and never take the + docs-only skip: main/master, and GitHub merge-queue builds, which push to + gh-readonly-queue//pr-- and carry no PR URL. +#> +function Test-MainLikePushBranch { + param([AllowEmptyString()][string]$Branch) + + if ($Branch -in @('main', 'master')) { return $true } + return ($Branch -match '^gh-readonly-queue/') +} + <# .SYNOPSIS Gates 1 and 2 of the hosted pr-check: budget emergency stop and branch/PR scope. Gate 3 (docs-only) needs a diff and lives in - scripts\circleci-pr-gates.ps1; main/master pushes never reach it. + scripts\circleci-pr-gates.ps1; main/master and merge-queue pushes never + reach it. #> function Get-TriggerGateDecision { param( @@ -46,16 +60,17 @@ function Get-TriggerGateDecision { } } - # Gate 2 - scope: PR pipelines and main/master pushes run the checks; - # every other branch push skips. CircleCI delivers same-repo PR builds - # as branch pipelines, so PR association comes from the compile-time - # GitHub App pipeline value pipeline.event.context.github.pr_url. + # Gate 2 - scope: PR pipelines, main/master pushes and merge-queue pushes + # (gh-readonly-queue/...) run the checks; every other branch push skips. + # CircleCI delivers same-repo PR builds as branch pipelines, so PR + # association comes from the compile-time GitHub App pipeline value + # pipeline.event.context.github.pr_url. $isPr = -not [string]::IsNullOrWhiteSpace($PrUrl) - $isMainPush = $Branch -in @('main', 'master') + $isMainPush = Test-MainLikePushBranch -Branch $Branch if (-not $isPr -and -not $isMainPush) { return [pscustomobject]@{ Skip = $true - Reason = "Branch push to '$Branch' (not a PR, not main/master): hosted pr-check skips." + Reason = "Branch push to '$Branch' (not a PR, not main/master, not a merge-queue branch): hosted pr-check skips." } } diff --git a/scripts/circleci-pr-gates.ps1 b/scripts/circleci-pr-gates.ps1 index bd229fe1d2..7c2d19a0ee 100644 --- a/scripts/circleci-pr-gates.ps1 +++ b/scripts/circleci-pr-gates.ps1 @@ -9,14 +9,15 @@ 1. Budget gate - CI_BUDGET_MODE=off is the emergency stop (unset/empty = normal). - 2. Scope gate - PR pipelines and main/master pushes run the checks; - any other branch push skips. + 2. Scope gate - PR pipelines, main/master pushes and merge-queue + pushes (gh-readonly-queue/...) run the checks; any + other branch push skips. 3. Docs-only gate - PRs whose diff touches only docs/**, **/*.md, CONTEXT.md, and .github/CI.md skip. - Each true skip calls `circleci-agent step halt`. main/master pushes never - reach the docs-only gate, so they can never be skipped by a multi-commit - docs-only diff. Unknown bases fail open (the checks run). + Each true skip calls `circleci-agent step halt`. main/master and + merge-queue pushes never reach the docs-only gate, so they can never be + skipped by a multi-commit docs-only diff. Unknown bases fail open (the checks run). Pure decision logic lives in scripts\circleci-pr-common.ps1 and is exercised by scripts\circleci-pr.tests.ps1 without CircleCI. @@ -83,13 +84,13 @@ Write-Host "CI_BUDGET_MODE is '$BudgetMode': hosted pr-check passes budget gate. Write-Host $trigger.Reason # Gate 3 - docs-only: mirror paths-ignore (docs/**, **/*.md, CONTEXT.md, -# .github/CI.md). Applies to PR pipelines only; main/master pushes always -# run the checks, so a multi-commit docs-only history can never suppress -# them. When the base cannot be determined the gate fails open. -$isMainPush = $Branch -in @('main', 'master') +# .github/CI.md). Applies to PR pipelines only; main/master and merge-queue +# pushes always run the checks, so a multi-commit docs-only history can never +# suppress them. When the base cannot be determined the gate fails open. +$isMainPush = Test-MainLikePushBranch -Branch $Branch $base = $PrBaseSha if ($isMainPush) { - Write-Host "Push to '$Branch' runs the full checks (docs-only skip never applies to main/master)." + Write-Host "Push to '$Branch' runs the full checks (docs-only skip never applies to main, master or merge-queue pushes)." } elseif ([string]::IsNullOrWhiteSpace($base)) { # PR association without a populated event value (e.g. api trigger): # resolve the base from the public GitHub pulls API. diff --git a/scripts/circleci-pr.tests.ps1 b/scripts/circleci-pr.tests.ps1 index a776beda5d..85794fee54 100644 --- a/scripts/circleci-pr.tests.ps1 +++ b/scripts/circleci-pr.tests.ps1 @@ -89,6 +89,19 @@ Assert-True ($decision.Reason -match 'codex/topic') 'topic branch skip reason' $decision = Get-TriggerGateDecision -BudgetMode 'off' -Branch 'main' -PrUrl '' Assert-True $decision.Skip 'budget off wins over main push' +Write-Host '==> Merge-queue pushes (Test-MainLikePushBranch, Get-TriggerGateDecision)' +Assert-True (Test-MainLikePushBranch -Branch 'main') 'main is main-like' +Assert-True (Test-MainLikePushBranch -Branch 'master') 'master is main-like' +Assert-True (Test-MainLikePushBranch -Branch 'gh-readonly-queue/main/pr-805-abc') 'queue branch is main-like' +Assert-True (-not (Test-MainLikePushBranch -Branch 'feature/gh-readonly-queue/main/pr-1')) 'queue prefix must start the branch name' +Assert-True (-not (Test-MainLikePushBranch -Branch 'codex/topic')) 'topic branch is not main-like' +$decision = Get-TriggerGateDecision -BudgetMode '' -Branch 'gh-readonly-queue/main/pr-805-abc' -PrUrl '' +Assert-True (-not $decision.Skip) 'merge-queue push runs (budget normal, no PR URL)' +$decision = Get-TriggerGateDecision -BudgetMode 'normal' -Branch 'feature/gh-readonly-queue/main/pr-1' -PrUrl '' +Assert-True $decision.Skip 'branch containing the queue prefix mid-name still skips' +$decision = Get-TriggerGateDecision -BudgetMode 'off' -Branch 'gh-readonly-queue/main/pr-805-abc' -PrUrl '' +Assert-True $decision.Skip 'budget off wins over merge-queue push' + Write-Host '==> Hosted Node provisioning guard' $prRunnerText = Get-Content -Raw -LiteralPath (Join-Path $scriptRoot 'run-circleci-pr-check.ps1') Assert-True ($prRunnerText -match '\$installedNodeVersion\s*=\s*\$imageNodeVersion') 'Node status is initialized when the hosted image already has the required major'