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
4 changes: 3 additions & 1 deletion .github/CI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 5 additions & 4 deletions docs/adr/0005-pr-check-moves-to-circleci-windows.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
29 changes: 22 additions & 7 deletions scripts/circleci-pr-common.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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/<base>/pr-<N>-<sha> 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(
Expand All @@ -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."
}
}

Expand Down
21 changes: 11 additions & 10 deletions scripts/circleci-pr-gates.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
13 changes: 13 additions & 0 deletions scripts/circleci-pr.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down