ci: stop cancelling in-progress required runs on main - #2610
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…, newest pending survives)
|
Review (agent half of the ADR-0066 gate; Codex credits exhausted, SC-9). One fresh-context reviewer (read-only, Opus 5) at exact head a9f03a9: verdict SHIP, no CRITICAL/HIGH. This PR is CI-control ( Confirmed: the diff is the concurrency block plus its comment (3 insertions, 1 deletion) and one new contract test; no job, Findings and dispositions:
Merge gate: maintainer review (SC-10) plus hosted Workflow Lint, |
Summary
ci-required.ymlcancelled its own in-progress runs onrefs/heads/main, so during a merge wave eachmerge killed the previous merge's push run and no landed commit ended up with a completed required run.
This keeps cancellation for PR refs and turns it off on the default branch:
A two-line comment above the block records why. The group key is unchanged, so main runs queue behind each
other on one ref rather than running in parallel. Nothing else in the workflow changed: no job,
needs,permissions,usesor trigger edits, and no other workflow,ci/policy.v1.jsonorscripts/ci/dev-up*was touched. A new
scripts/ci/smart-ci/required-concurrency.test.mjslocks the contract in place.Root cause
The concurrency block was declared with an unconditional
cancel-in-progress: trueand no branchcondition. GitHub applies that to every event the workflow accepts, including
pushonmain. Because thegroup key is
${{ github.workflow }}-${{ github.ref }}, every push tomainlands in the same group as theprevious push to
main, and the newer run cancels the older one. On a PR ref this is the intendedbehaviour (a new head supersedes the old one). On
mainthere is no superseding: each merge commit is adistinct artifact that needs its own evidence.
Evidence from the issue, six consecutive cancelled
mainruns on 2026-09-04:The last completed green
mainrun before that sequence was 33886539482 at 61e94f6, 15:21Z. The samepattern repeated at 01:12Z on 2026-09-05.
Each merged PR head carried its own green required run, so the merges themselves were gated. What is
missing is the post-merge proof of the combined tip, which the readiness view's clause 4, the SC-4
observation window and the CI-03 landed-commit verifier (#2327) all read.
Verification
Run from the worktree at
.worktrees/codex-2582-main-run-no-cancel.Red first, against the unmodified workflow (test committed before the fix, dbfe5c6):
3 pass, 1 fail. The failing assertion:
After the workflow change (a9f03a9):
node --test scripts/ci/smart-ci/required-concurrency.test.mjs— 4 tests, 4 pass, 0 fail.node --test scripts/ci/smart-ci/*.test.mjs— 95 tests, 95 pass, 0 fail (91 before this branch, plusthe 4 new ones).
smart-ci-self-test.ymlline 41 already runs exactly this glob, so the new file iswired with no workflow edit; confirmed by grepping
.github/workflowsfor it.node scripts/check-docs-governance.mjs— "Docs governance check passed."git diff --check— clean.yaml.safe_load): theconcurrencymapping reads back as{"group": "${{ github.workflow }}-${{ github.ref }}", "cancel-in-progress": "${{ github.ref != 'refs/heads/main' }}"}and the job count is still 13.
Not verified
actionlintis not installed on this machine, so no local workflow lint ran. Hosted Workflow Lint isthe proof for the YAML.
Self-Test and
ci-requiredat the exact head are the R4 proof and were not observed at the time ofwriting; the local runs above are additive.
mainrun now queues instead of being cancelled, can only be observedafter this merges, on the next merge wave. Nothing local can exercise the GitHub concurrency engine.
dotnetand novitestor Playwright run applies.Risk notes
github.refon apull_requestevent isrefs/pull/<n>/merge, so the expression evaluates totrueand a new push to a PR still cancels theolder run exactly as before.
merge_grouprefs are likewise unaffected.main: runs queue. A wave of N merges now costs N Linux required runs instead of onesurviving run, and the last one finishes some minutes after the wave ends rather than being killed.
move to local runners, so the marginal cost here is the Linux legs. Queued
mainruns are the price ofhaving one completed run per landed commit, which is what the landed-commit verifier needs.
${{ github.workflow }}-${{ github.sha }}onmain.That would let every merge's run start immediately and finish sooner, but it runs the whole wave in
parallel and multiplies peak runner usage. Queueing keeps peak concurrency at one per ref, which the
issue records as the preferred trade.
cancel-in-progress: trueand deleting the test file.Closes #2582