Repository navigation
ci: guard main so a PR into it brings nothing not already on v2/main - #5120
Conversation
Adds scripts/verify-main-source.mjs and the main-source.yml workflow (pull_request_target into main), which fails unless the PR's head commit is on origin/v2/main. GitHub rulesets cannot restrict a PR's source branch, so this check, made required on main, is what enforces the milestone-merge-only rule in AGENTS.md. Closes #5119 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
|
There was a problem hiding this comment.
🟡 Changes recommended
Workflow regression coverage and documentation and rollout inaccuracies remain unresolved.
3 open findings
What changed in this PR
Adds CI enforcement ensuring PRs into main contain only commits already present on v2/main.
Changes:
- Adds the source-verification script and tests.
- Adds a protected
pull_request_targetworkflow. - Documents the release-branch policy and guard.
| File | Description |
|---|---|
.github/workflows/main-source.yml |
Runs the source guard for PRs into main. |
scripts/verify-main-source.mjs |
Verifies PR-head ancestry and reports violations. |
scripts/verify-main-source.test.mjs |
Tests argument handling and Git histories. |
docs/quality-gate.md |
Documents the new CI-only check. |
AGENTS.md |
Records repository and release rules. |
.claude/skills/release/SKILL.md |
Updates milestone-merge instructions. |
🧠 Review effort: Balanced
…te exception Addresses Copilot round 1 on #5120. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
|
Copilot round 1: 3 findings, all addressed.
Gate green on 9422c3f. Requesting round 2. |
…pure merge Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
|
Copilot round 2: clean (no new findings). The one item it still lists as open is round 1's workflow-tests thread, already fixed in 9422c3f. Since then, c9c6e0e adds the admin escape hatch for a release that cannot be a pure merge ( |
…der one exit trap Addresses Copilot round 3 on #5120. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
|
Copilot round 3: 1 new finding (escape hatch could leave the ruleset disabled if interrupted), fixed in 8002268. Disable, merge and re-enable now run under one exit trap, verified in bash and zsh against a fake Gate green on 8002268. Requesting round 4. |
There was a problem hiding this comment.
🟢 Approval recommended
The implementation matches #5119, fails closed, and has comprehensive automated and live-probe coverage.
0 open findings
2 resolved since last review
🧠 Review effort: Balanced
…erge The guard (modelcontextprotocol#5119, PR modelcontextprotocol#5120 on v2/main) runs on pull_request_target, which loads the workflow and script from main. Bring the same three files here byte-for-byte, so the check can run on, and be required for, PRs into main before the next milestone merge carries them. Part of modelcontextprotocol#5119 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
|
Copilot round 4: clean (0 open findings, both earlier threads resolved). Review loop done; ready for a maintainer merge. |


Closes #5119
Description
Adds a guard so that a pull request into
maincan bring nothing that is not already onv2/main.scripts/verify-main-source.mjs(+ tests): passes when the PR's head commit isorigin/v2/mainor an ancestor of it, and fails otherwise, listing each commit the head carries thatv2/maindoes not. The check is on the commit rather than the branch name, because thereleaseskill opens the merge PR fromv2/chore/<N>-release-<milestone>, a branch pushed fromorigin/v2/main. It accepts an ancestor becausev2/mainkeeps moving while the merge PR is open..github/workflows/main-source.yml: the Source is v2/main job, onpull_request_targetintomain. It runsmain's copy of the workflow and of the script, so a PR cannot edit the guard into passing. It checks outmainand only fetches the PR head as git objects; none of the PR's code runs. Read-only token, no secrets.AGENTS.md,docs/quality-gate.mdand thereleaseskill describe the guard and the merge-commit-only rule onmain. The ruleset has no bypass actors. A release that cannot be a pure merge (a release branch like 1.0.0's Release v1.0.0: merge v2/release/1.0.0 into main #5090, or a hotfix) goes through only when a repo admin disables the ruleset for that one merge and re-enables it straight away. Thereleaseskill has the recipe under "A release that cannot be a pure merge". The guard is not alocal:gatestage, since no local push targetsmain. That exception is stated next to thepack:verifyone.Server Details
None (repository-wide): CI, a root script, docs.
Motivation and Context
GitHub rulesets can require PRs and approvals on
main, but cannot restrict where a PR comes from, and #4939 and #5081 merged commits of their own intomain. See #5119.Rollout, which runs alongside this PR:
main: milestone merges only(id 24866866, no bypass actors), now limitsmainto merge commits.pull_request_targetrunsmain's copy, these three files also go tomainin a one-off bootstrap PR (cherry-picked byte-for-byte, so the next milestone merge adds nothing new for them).mainprobe it live: one from a branch atorigin/v2/main, which should pass, and one from a branch with a commit of its own, which should fail. Both are closed unmerged.How Has This Been Tested?
No client-observable surface, so here are targeted probes:
node --test scripts/verify-main-source.test.mjs: 9 pass. The cases build throwaway repos shaped like the release flow: a release branch pushed from v2 passes; a head v2 has moved past passes; a branch with its own commit fails and names it; a branch cut frommainfails; v2 merged into another branch fails.--head origin/v2/main→is on origin/v2/main; nothing else comes with it.--head origin/v2/main~5→ passes.--head HEAD(this branch) →carries 1 commit that origin/v2/main does not.--head 5abed86c(chore: bring the contribution policy to main ahead of the v2.0.0 merge #4939's merge intomain) →carries 5 commits that origin/v2/main does not, listing them. That is the case this guard exists to stop.npm run local:gate: exit 0.enforcement). Disabling a protection is a human admin's act, so it was not run from the agent session. It should be checked by hand the first time it is needed, or once now while the ruleset holds only the merge-method rule.v2/mainitself against force-push and deletion, since the guard trusts its history).pull_request_targetintomainonly, read-only, a checkout ofmainwith noref, the PR head fetched as objects only, no install or execution after that, no secrets, and the event's head SHA passed to the guard. Mutation probe: addingref: ${{ github.event.pull_request.head.sha }}to the checkout fails exactly the checkout test.pull_request_targetloads the workflow frommain, so the bootstrap PR (step 2) cannot run it. The first live runs were the step 3 probes, after ci: bring the main-source guard to main (bootstrap for #5119) #5122 merged (bbb701e7):origin/v2/main→ pass:9245b93c… is on origin/v2/main; nothing else comes with it.main→ fail:094c1af0… carries 11 commits that origin/v2/main does not(the probe plusmain's own merge commits).pull_request_targetunder the GitHub Actions app (id 15368), and both were closed unmerged.Breaking Changes
None for users. For maintainers: any PR into
mainthat is not a purev2/mainmerge will fail once the check is required, andmainoffers only "Create a merge commit".Types of changes
Checklist
pull_request_targetwith no PR code checked out or run, read-only token🤖 Generated with Claude Code