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'