Uh oh!
There was an error while loading. Please reload this page.
fix(ci): the ADR gate goes red while auto-merge is armed on a docs/adr/** PR - #8302
Merged
Merged
Conversation
…r/** PR Arming auto-merge is not merging, so no gate fired on it: an AI seat armed one on a live docs/adr/** PR at 11:15Z on 2026-08-12 while this gate was red. The next approving review would have turned the check green and GitHub would have merged the PR unattended, with every check reporting success — 「人工合并」 bypassed while nothing looked wrong. The gate now reads the PR's live auto-merge state and is red while it is armed, whether or not the PR is approved. The approved case is the only one in which the unattended merge actually happens; a rule firing only on the unapproved case would change no verdict, since that state is already red for the missing approval. Not a deadlock: anyone can disable auto-merge, and the new auto_merge_disabled trigger re-runs the gate so it clears itself. Refs #8012
Mutation testing found the looser assertion passed on a mutant that deleted the Object.hasOwn guard: undefined falls through to the not-an-object branch and throws a message that also contains 'auto_merge'. Refs #8012
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
This was referenced Aug 13, 2026
os-zhuang
marked this pull request as ready for review
August 13, 2026 04:28
This was referenced Aug 13, 2026
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #8012 — the maintainer-approved half (b), gate hardening. See "What this PR deliberately does not do" for the halves left open.
The gap, and why the obvious rule does not close it
Arming auto-merge is not merging, so no gate fired on it. An AI seat armed one on a live
docs/adr/**PR at 11:15Z on 2026-08-12 while this gate was red. Nothing merged — a human disarmed it two minutes later — but the state left behind was one review away from an unattended merge with every check reporting success.docs/adr/**PR without a qualifying approval". That predicate cannot change any verdict:The armed-and-unapproved state is already red for the missing approval, so a rule that fires only there rejects nothing that was not rejected already. And the defect materialises one event later — at the approval, which is exactly when the literal predicate stops being true. So the clause here fires on the arming whether or not the PR is approved; the approved case is the only one in which the unattended merge actually happens.
This is strictly stricter in every direction — no threshold relaxed, no bypass added, no path by which a PR's own author, or any account, satisfies the gate by identity. The gate still reads state only, never actors.
What landed
scripts/check-adr-merge-approval.mjs— pass condition gains clause (2): auto-merge must not be armed. Both clauses are evaluated so a red names every reason at once.armingFrom()reads the state from the liveGET /repos/{owner}/{repo}/pulls/{n}. The event payload is deliberately not trusted: GitHub does not documentauto_mergeas a member of the pull_request object carried bypull_request_review, and at least one projection in use here (pull_request_read) drops the field entirely. A payload with noauto_mergekey is refused rather than read as disarmed — absent and null are different facts, and only null means disarmed.getArmingis mandatory ondecide(): a caller that omits it throws. A forgotten argument must not coast on a default that reads as "auto-merge is off"..github/workflows/adr-merge-approval.yml—pull_requestgainsauto_merge_enabled(arming is otherwise a silent state change this workflow never observes) andauto_merge_disabled(the zero-friction exit). The three default activity types are restated because naming anytypes:replaces the default set. Jobname:unchanged;permissionsunchanged —pull-requests: readalready covers the PR read.Measured, not assumed
Auto-merge state is reachable from where the gate runs.
GET /pulls/{n}carriesauto_mergein both states, measured against this repo on 2026-08-13:nullon #7960, and a populated object on #6208 (enabled_by: os-zhuang,merge_method: merge) — that real capture is the self-test fixture, not a hand-written imitation.The disarm/re-arm wrinkle.
decide()keeps no state between runs, so the sequence green → re-arm → red → disarm → green is judged fresh each time; there is no earlier green for a re-arm to inherit. Pinned asarming-is-read-fresh-so-rearming-is-red-again. GitHub additionally refuses arming on a PR that can be merged immediately ("The option to enable auto-merge is shown only on pull requests that cannot be merged immediately", docs), so the fully-green case cannot be armed at all.The window this does NOT close, stated rather than assumed. A PR that is approved, green here, and still waiting on some other required check can be armed in that window;
auto_merge_enabledre-runs this gate, which then goes red, but if the other check goes green first the merge fires before the red lands. The race needs the arming to be the last blocking action. It is not closed here — closing it belongs to the ruleset side (option 2), which no CI job can perform. Recorded in the file header rather than left for a reader to discover.Not a deadlock (#8161's lesson). Every red is cleared by an action any account can take — disable auto-merge — and disarming re-runs the gate, which then clears itself with no push, no re-review, no admin.
Not judged on
merge_groupbuilds, on purpose: by then the PR passed this gate at the PR level, where auto-merge waits, so a red there adds no safety and only evicts — and if "merge when ready" setsauto_mergeas part of enqueueing, judging it there would make ADR PRs permanently unqueueable. The verdict records that the question was not asked (judged: false,armed: null) rather than quietly answering it "no".The self-test is an instrument, not decoration
66 assertions (was 35). Per this family's standing lesson — a suite that passes first try is a signal to prove it is not empty — the new clause was mutation-tested, six distinct breaks, each caught by named assertions:
armed-and-approved-is-red,armed-clause-changes-a-verdict-that-would-otherwise-be-green,the-7960-incident-state-replays-red, +4auto_mergekey defaults to disarmeda-missing-auto_merge-key-is-diagnosed-as-a-projection-not-as-a-bad-valuegetArmingoptional, defaulting to not-judgedomitting-getArming-throws,omitting-getArming-throws-on-a-clean-diff-toocaptured-armed-pull-reads-as-armed, +4armed: falsenot-judged-arming-is-null-never-falseauto_merge, which the generic fallback error also does, so it pinned nothing. The safety property held either way; what the dedicated guard buys is the operator being told which situation they are in. The assertion now pins that diagnosis, and M3 fails as it should. Without mutation testing this suite would have shipped with one empty assertion in it.armed-clause-changes-a-verdict-that-would-otherwise-be-greenis a permanent emptiness proof: two runs over identical reviews differing only in the arming bit must land on opposite verdicts.Also replayed end-to-end over real captured payloads from open ADR PR #8291 — identical files and approval, differing only in the arming object: green when disarmed, red when armed.
What this PR deliberately does not do
ADR maintainer approvalis unsatisfiable on any PR the maintainer authored — GitHub forbids self-approval, so the gate is permanently red exactly when the human IS driving #8161 is not addressed here; it remains open and is referenced only as the source of the current predicate.Landing
skip-changeset: scripts + workflow only, no package changes, releases nothing.Verification
All green. Gate list re-derived against the actual changed paths via
scripts/pm/dispatch-gates.mjs— no delta from the dispatch.Generated by Claude Code