Uh oh!
There was an error while loading. Please reload this page.
fix(ci): the ADR merge gate requires an APPROVED review, from anyone - #8172
Merged
Conversation
The gate matched the approving account against a hard-coded `MAINTAINER_APPROVERS` list. That proxy became unsatisfiable once cloud sessions began authoring PRs under the maintainer's own account: GitHub forbids self-approval, so a required context was permanently red on exactly the PRs the human was driving, while an AI seat holding the same credential could still satisfy it. Per the maintainer ruling of 2026-08-12, the account list is retired rather than widened: the gate now passes when the PR's latest state-setting review is APPROVED, whoever submitted it. The revocation semantics are unchanged -- a later CHANGES_REQUESTED or DISMISSED still turns it red, and COMMENTED/PENDING still set nothing. - `MAINTAINER_APPROVERS` and `isMaintainer` are removed; `latestMaintainerReviewState` becomes `latestReviewState`, folding over every reviewer; `approvalsFromNonMaintainers` becomes `approverLogins`, a pure diagnostic that no longer asserts a distinction the gate does not draw. - Both file headers are rewritten rather than patched: they now state what the gate guarantees (someone approved, currently) and what it no longer does (that the approver is the maintainer, that a human merged). - The job name `ADR maintainer approval` is deliberately unchanged -- it is the required-context string in the `main` ruleset.
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
hotlong
marked this pull request as ready for review
August 12, 2026 19:14
This was referenced Aug 12, 2026
Uh oh!
There was an error while loading. Please reload this page.
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.
Fixes#8161
The ADR merge gate matched the approving account against a hard-coded
MAINTAINER_APPROVERSlist. That proxy became unsatisfiable once cloud sessions began authoring PRs under the maintainer's own account: GitHub forbids self-approval, so a required context was permanently red on exactly the PRs the human was driving — while an AI seat holding the same credential could still satisfy it.The ruling this implements
Maintainer, 2026-08-12, verbatim (quoted untranslated):
So the account list is retired, not widened: there is no list any more, and deliberately no configurable replacement (a repo/org variable is writable by the same shared identities the original constant refused, which is why the old header ruled that surface out — that reasoning still holds).
This supersedes the earlier "give the agent fleet its own GitHub account" ruling recorded on the card; that is an org-side action, now decoupled from the gate and not a prerequisite for this change.
What changed
MAINTAINER_APPROVERSandisMaintainerare deleted.latestMaintainerReviewStatebecomeslatestReviewState, same fold, minus the account filter. The revocation semantics are unchanged:APPROVED/CHANGES_REQUESTED/DISMISSEDset the standing, the last one wins,COMMENTEDandPENDINGset nothing. The pass condition is the current standing, never "an APPROVED review has ever existed" — an unrevokable approval would be a one-way door.decide()'s verdict kinds:maintainer-approved→approved,missing-maintainer-approval→missing-approval.docs/adr/**path-prefix match with no size exemption, the zero-API-lookup clean path, the loud-failure discipline (an unfetchable or unreadable review list is exit 1, never exit 0), themerge_groupresolution and its fail-closed over-approximation, and the missing-input behaviour.approvalsFromNonMaintainers— kept, renamed, re-purposedIts name asserted a maintainer/non-maintainer distinction the gate no longer draws, so leaving it as-is was not an option. It is not deleted either: it is now
approverLogins(reviews), listing every login that has submitted an approving review, and it earns its place on the RED path, where the confusing case is "someone approved, yet the check is red". The verdict fieldstrangerApprovalsbecomesapprovals, and the failure message now explains the supersession instead of scolding the approver:Deleting it would have removed the only signal that distinguishes "nobody looked at this" from "an approval was revoked" — two very different reds.
Cross-reviewer semantics — a new surface, resolved in the strict direction
With the account filter gone the fold runs over all reviewers, so two cases exist that could not arise before. Both are pinned in the self-test:
CHANGES_REQUESTEDdoes revoke);The first is the reason for folding globally rather than per-reviewer: "a later CHANGES_REQUESTED still revokes an earlier APPROVED" holds unconditionally, which is the fail-closed reading.
Headers rewritten, not patched
Both headers argued at length for the account allowlist. They now carry a two-clause table stating what the gate guarantees (a
docs/adr/**diff cannot reach a mergeable state with no current approving review) and what it no longer guarantees (that the approver is the maintainer; that a human performed the merge). #6741's 「维护者自己确认」/「人工合并」 survive as convention, and the header says so rather than letting code and comment drift apart again.Under the new rule, any account with review rights on this repository — including an AI seat — satisfies this gate. That is the maintainer's accepted trade, made explicitly.
It also makes the gap recorded in #8012 materially sharper: this gate blocks merging but does not block arming auto-merge, and the trigger for that armed merge moves from "the maintainer approves" to "anyone approves" — an armed ADR PR can now be merged unattended by the first approval that lands. That hardening is out of scope here and is tracked at #8012; this PR does not change arming behaviour. (#8012 remains open.)
Measurements — the PM's hypotheses, including the falsified ones
A — passing exits. Confirmed, with one correction:
decide()has twook: truesites, not one.no-adr-diff(the zero-lookup clean path) andapproved.reportVerdicthas exactly tworeturn 0sites, both guarded byverdict.ok. Every route — gate mode,--pr,--files-json/--reviews-json, andmerge_group— reaches its verdict throughdecide();merge_grouponly affects PR resolution and the diff base, never the pass condition. Verified structurally and by driving the replay CLI (approved ⇒ exit 0; approved-then-revoked ⇒ exit 1; malformed review list ⇒ loud exit 1).B — self-test. 27 assertions before, 36 after. Nine depended on the allowlist and were rewritten rather than re-pinned; the four "bot approvals do not count" style assertions inverted. Reverse verification (account filter restored on the committed file, direction predicted before running): exactly the 9 predicted assertions went red, by name — the four per-seat approvals, the unknown-account approval, the revocation pin, the cross-reviewer clearing pin, and both historical replays with an approval. The new pins assert something. Both directions the card required are pinned: an approval from a non-maintainer account passes, and a later
CHANGES_REQUESTEDstill turns it red.C — re-trigger. Confirmed against the workflow:
pull_request_review: [submitted, edited, dismissed]already re-runs the gate on any approval, andpermissions: pull-requests: readcovers listing reviews from any account. No extra wiring, permission, or token scope is needed — noted in the trigger comment.D — required-context registry: partly falsified. It does not merely name the check.
scripts/check-required-contexts.mjs:196carries prose restating the old rule ("only the maintainer own-account approval may land adocs/adr/**merge"), which this change makes false. That one string is updated; thecontextstring itself is untouched. The field is documentation-only — nothing in the check reads it — so this is a self-consistency fix, not a behavioural one.E — display name: confirmed, and left alone.
ADR maintainer approvalis both the required-context string in themainruleset (#7022) and a fixture incheck-required-contexts's own self-test, which pins that renaming the job goes red. Renaming here alone would leave the ruleset waiting for a context that never reports and hang the merge queue until the 60-minute timeout. The word "maintainer" now over-claims; a comment on the job says so and says why it stays. Correcting it needs an org-settings action first, so it belongs in a follow-up card, not here.Verification
The gate list was re-derived against the actual changed paths with
node scripts/pm/dispatch-gates.mjs; it addedcheck:node-version,check:shard-attestation,check:workflow-status-functions,check:changeset-gate-self-testsandcheck-changeset-no-majorbeyond the families the dispatch named. All run and green.Changeset: route 2,
skip-changeset. The diff touches onlyscripts/**and.github/workflows/**— no released package, so this PR declares no release of its own.Merge ownership
⛔ Not merged, not enqueued, no auto-merge armed by this session — this changes the gate that constrains AI merges, so landing it is the maintainer's.
Generated by Claude Code