ci(governance): machine-enforce the ADR merge prohibition — docs/adr/** requires the maintainer's own approval (#6741) - #6802
Draft
os-project-manager wants to merge 1 commit into
Draft
Conversation
…the GitHub side A PR whose diff touches docs/adr/** is no longer mergeable without an APPROVED review from the maintainer's own account (hotlong, matched by account id). Prose did not propagate: two AI-seat merges of docs/adr/** PRs landed within an hour of the ruling (#6671, #6732) — both are pinned RED forever as replay fixtures in the gate's --self-test. - scripts/check-adr-merge-approval.mjs: the gate. No path filter on the trigger (objectui#3523); zero API lookups on a clean diff; merge_group PR resolution from the queue ref with subject/API fallbacks; every missing input fails loud (#4690, #4928). Approver set is a hard-coded constant on purpose — repo variables are writable by the shared agent identities that are the root cause. - .github/workflows/adr-merge-approval.yml: standalone workflow (pull_request + pull_request_review + merge_group), reports on every PR. - .github/CODEOWNERS: routes docs/adr/ and the enforcement chain itself to the maintainer, for the ruleset half only he can flip. Fixes #6785 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
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 free
to 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 #6785
Machine enforcement of the #6741 ruling (maintainer, verbatim, untranslated):
Prose enforcement was measured insufficient the day the ruling landed: filed 13:52Z on 2026-08-08, and within the following hour two different AI-operated seats merged docs/adr/** PRs — #6671 at 14:23:32Z (os-zhuang) and #6732 at 14:38:56Z (os-project-manager, while the PR was in draft state). Neither was the maintainer; both carried zero reviews of any kind. Declared = enforced applies to governance exactly as it applies to metadata, so the prohibition now lives on the GitHub side.
What this PR lands (Half 1 — in-repo, self-serve)
scripts/check-adr-merge-approval.mjs--self-test+ replay modes (--pr N,--files-json/--reviews-json).github/workflows/adr-merge-approval.ymlpull_request+pull_request_review+merge_group, deliberately nopathsfilter.github/CODEOWNERSdocs/adr/— and the enforcement chain itself — to @hotlongpackage.jsoncheck:adr-merge-approvalscript entry (local convenience; the workflow calls node directly, no install)Decision rule: a diff that does not touch docs/adr/** passes with zero API lookups (structurally — the review fetch is a lazy thunk the clean path never invokes). A diff that touches it passes only with an APPROVED review from the maintainer's own account.
Design points, each with its receipt:
pull_request_reviewtrigger = the zero-friction green path: an approval does not firepull_request, so without it the red check would sit red after the maintainer approves until re-run by hand. With it, the approval itself re-runs the gate to green. No extra step for the maintainer.merge_group: the PR is resolved from the queue ref (thegh-readonly-queue/.../pr-N-shaspelling embeds the number), falling back to the head commit subject (merge and squash spellings), then to the commit's associated PRs via the API. Unresolvable is a loud failure, never a skip. The diff is narrowed to the PR's own slice of the group when the head commit provably belongs to the ref-named PR; otherwise the whole-group diff is used — a fail-closed over-approximation, argued in the script header.os-*,claude,yinlianghui), and those identities granting themselves approval rights is exactly the failure this gate closes. Matching is by numeric account id (login squats do not count).hotlong= 50353452 is verified, not assumed: 2,153 commits on main are authored as50353452+hotlong@users.noreply.github.com, GitHub's id+login noreply form (the MCP collaborator listing does not show org owners, so the git history is the confirmation source).filterjob 一旦失败,Test Core / Build Core / Dogfood 会全部 skipped 而分支保护判为通过 —— 隐式 success() 今天已第三次咬人 #4928 filter contract): unresolvable diff base, failedgit diff, unresolvable PR on a queue build, unfetchable or non-array review list — all exit 1 with the reason, never a silent pass.Review-state semantics: the maintainer's latest state-setting review decides (APPROVED / CHANGES_REQUESTED / DISMISSED; a later COMMENTED does not revoke, a later CHANGES_REQUESTED or a dismissal does).
Acceptance evidence
Historical replay — real API inputs, direction predicted RED before running. File lists and review lists for #6671 and #6732 were captured from the live GitHub API on 2026-08-08 (
pulls/N/files,pulls/N/reviews; both review lists really are empty) and replayed through the shipped decision path (--files-json/--reviews-json):Both violations are also pinned as inline fixtures in
--self-test, RED forever, alongside their inversion (same file lists + a maintainer-approval fixture → GREEN), which pins that the red is about the missing approval, not about ADR diffs being unmergeable per se.Red/green paths, direction predicted before each run — all confirmed:
--self-test: 27 assertions over the realdecide()/ review-state / PR-resolution functions, no imitations.Self-referential smoke: this PR touches
.github/,scripts/and onepackage.jsonline — the gate run in gate mode on this branch reports GREEN via the not-an-ADR-diff path with zero lookups, and the workflow on this PR is the live demonstration that the check creates a run and reports on a non-ADR PR.Local verification (under the shared verify lock):
pnpm lintgreen; all 32 family gates of lint.yml's ESLint job green (includingcheck:workflow-status-functions,check:node-version,check:adr-anchors,check:nul-bytesover the new files) plus the TypeScript job's root-level gates. Nothing underpackages/changes, so the workspace build/typecheck surface is untouched by this diff; CI's own run on this PR is the authoritative pass.Half 2 — repo settings, for @hotlong to apply by hand
This PR deliberately changes no repository settings (the point of #6785 is that governance actions belong to you). Two switches, both under Settings:
A. Make the new check required (binds the CI half):
main(or "New branch ruleset", target branchmain).ADR maintainer approval(the job name; it appears in the picker once this PR's workflow has run at least once) → add it.mainrule → "Require status checks to pass before merging" → addADR maintainer approval.)B. Require code-owner review on the routed paths (binds the CODEOWNERS half):
docs/adr/and the three enforcement files, this adds a required review from you on exactly those paths and changes nothing anywhere else.C. Optional hardening, recommended: in the same pull-request rule, tick "Dismiss stale pull request approvals when new commits are pushed". The CI gate accepts your standing APPROVED review; dismissal-on-push closes the window where content changes after your approval while the approval stands.
Your own flow stays exactly two clicks: approve, merge. The gate goes green on your approval automatically (the
pull_request_reviewtrigger re-runs it).Scope guards honoured
docs/adr/**; the red-path demonstration used a throwaway synthetic repo in the session scratchpad.packages/changes, nothing is published or released by this PR —skip-changesetapplied.🤖 Generated with Claude Code
https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn
Generated by Claude Code