Repository navigation
ci: per-PR plugin version bumps force repeated base merges on every PR at the current merge rate #6006
Description
Problem
Every PR that changes a plugin bumps plugins/<name>/.claude-plugin/plugin.json version and prepends a CHANGELOG.md heading. scripts/check-changelog-parity.sh --check-bump origin/main (.github/workflows/ci.yml, step "Verify a version bump updates the plugin's CHANGELOG.md") requires the PR's version to be above main's, and "Fail when behind base on overlapping paths" (scripts/check-stale-base-overlap.sh) fails a PR that is behind main on a path it also changed. plugin.json and CHANGELOG.md are the paths every PR on a plugin shares, so each PR that lands on the same plugin forces every other open PR on it to merge main, renumber, rewrite its CHANGELOG heading, and re-run CI.
This was raised before as #464 and #1746. Both closed as not planned (#1746: "Re-open with a concrete acceptance brief if a consumer needs the work"). The merge rate has grown since, and this issue is that brief.
Evidence
Measured 2026-10-03.
- Merge rate:
gh pr list --state merged --base main --search "merged:>=2026-10-02"returns 84 PRs merged between 2026-10-02T01:29Z and 2026-10-03T04:51Z, about 3 per hour. - Of the 40 most recently merged PRs, 33 change at least one
plugin.json. Together they carry 112 "Merge ... main" commits; 25 have two or more and 9 have four or more. Median time open is 2.3 h (p90 5.9 h). - Worst cases: #5935 14 base merges in 6.1 h; #5902 9 in 8 h (44 manifests); #5920 6 in 3.8 h; #5992 3 in 1.2 h.
- Not every base merge is a version collision; some are stale-base overlaps on other files. The version and CHANGELOG lines are the one overlap every plugin PR has.
- A PR that conflicts with main gets no
pull_requestruns at all ("Workflows will not run onpull_requestactivity if the pull request has a merge conflict", GitHub docs). Observed on feat(effort): phase effort advice, event-log content opt-in and typed OTEL columns, with the upstream record #5992 head090bfb49f:gh run list --branch feat/spending-your-effort-pr2shows no run for that SHA andmergeablereadUNKNOWN. A collision therefore costs a silent stall as well as a re-run.
Command used for the churn numbers:
gh pr list --repo melodic-software/claude-code-plugins --state merged --base main \
--search "merged:>=2026-10-02" --limit 40 --json number,createdAt,mergedAt,files,commits \
--jq '.[]|[.number,
([.commits[]|select(.messageHeadline|test("^Merge (remote-tracking )?branch .*main"))]|length),
([.files[].path|select(test("^plugins/[^/]+/\\.claude-plugin/plugin\\.json$"))]|length)]|@tsv'Impact
- Each base merge re-runs
ciandtest-windowsand needs a fresh human or agent turn to renumber and rewrite a CHANGELOG heading. - The merge queue now being adopted (feat: adopt GitHub async merge, merge queue, stacked PRs and the new App token format #5935, melodic-software/github-iac#583) does not remove this. Two PRs that both take plugin X from 1.2.0 to 1.2.1 each pass their own PR checks; the second fails to build its merge group on the textual conflict and is ejected, and its author renumbers as today.
Proposed fix options
- CHANGELOG fragments plus release-time bump. A PR adds
plugins/<name>/changes/<pr-or-slug>.mdwith a bump level (patch/minor/major) and its entry text, and never editsversionorCHANGELOG.md. A release workflow (scheduled or on push to main) folds pending fragments intoCHANGELOG.md, bumpsversion, and opens one release PR. This is the changesets model. New files never conflict. Cost: a release workflow, a fragment-format check in place of--check-bump, and a release PR that must itself passci-statusunder thesigningand squash-only rulesets. - Drop
versionfromplugin.json. The plugin loading reference says that with noversionin the manifest or marketplace entry, a relative-path plugin in a Git-hosted marketplace takes the commit SHA of its directory, and "To let users track commits instead, leaveversionout of both the manifest and the entry." No plugin here declares a versioneddependenciesconstraint (grep -A4 '"dependencies"' plugins/*/.claude-plugin/plugin.jsonfinds none) andmarketplace.jsoncarries no versions. CHANGELOG entries would still conflict on top-insert unless they also become fragments, and every main commit becomes an update for every user of that plugin. - Keep per-PR bumps, automate the renumber. A bot or babysit step re-merges main and renumbers on conflict. Fewest repo changes, but it keeps the re-runs and only hides the turn cost.
Recommendation: option 1. It removes the shared lines rather than automating around them, keeps semver for users, and is the only option that stays conflict-free inside a merge queue. Basis: the churn numbers above, and the queue behaviour described under Impact (judgment, from GitHub's documented merge-group build on the combined head).
Acceptance criteria
- A PR that changes a plugin edits neither
versioninplugin.jsonnorCHANGELOG.mddirectly. - Two open PRs on the same plugin can both merge with no base merge between them caused by version or CHANGELOG lines.
--check-bumpand the version-monotonicity checks (check-changelog-parity.sh --check-bump has no version-monotonicity check: regressions and equal versions pass silently #2056, check-changelog-parity: --check-bump compares against the fork point, so a version already taken on main passes #4325) are replaced by a check that every plugin change carries a fragment, and released versions stay monotonic.- Over a week after rollout, the share of merged plugin PRs with two or more base merges falls from 25 of 40 (measured above), re-measured with the same command.
Activity
kyle-sexton commentedon Oct 3, 2026
Another data point from 2026-10-03, same cause:
plugin.json/CHANGELOG.mdfiles the moment fix: clear claude.ai marketplace sync warnings and gate them in the validator #5902 merged, because fix: clear claude.ai marketplace sync warnings and gate them in the validator #5902 had bumped most of the same plugins. None of the conflicts touch the PR's actual change; all are version lines and changelog headings.This supports moving the bump out of the PR (computed at merge or release time from per-PR fragments or commit metadata) so concurrent PRs on the same plugin never touch the same lines.
kyle-sexton commentedon Oct 3, 2026
The accepted fix for this is ADR 0048 (merged in #6015): each PR adds a changelog fragment instead of bumping
plugin.jsonandCHANGELOG.md, and a bot-maintained release PR does the bumps. This issue now tracks carrying it out.Phases (see the ADR for each phase's rollback):
workflow_dispatchonlyplanning,claude-ops,source-control,disk-hygiene); turn on the scheduleSettled decisions (ADR, Resolved decisions):
workflow_dispatchuntil the phase 2 pilot proves the workflow's merge step.bump: none.The merge queue on
main(ruleset 24422263) went live on 2026-10-03. Its first two merges (#6094, #6105) went through without a CHANGELOG ejection.kyle-sexton commentedon Oct 3, 2026
More evidence for this issue, measured 2026-10-03 over a wider window than the body's 40 PRs.
Last 100 merged PRs (#5875, merged 2026-10-02T19:01Z, through #6121, merged 2026-10-03T20:09Z), with the same headline regex as the body (
^Merge (remote-tracking )?branch .*main), commits and files read over REST:plugins/*/.claude-plugin/plugin.jsonThe regex misses differently worded merges such as
Merge origin/main into ..., so these counts are a lower bound.PRs from today's CI work that went through repeated merge-main rounds:
plugin.json. Its merges came fromci.ymloverlaps with concurrent CI PRs, so the stale-base overlap gate produces the same churn on shared infrastructure files too.The silent stall happened again. While each of these PRs was in conflict, no
pull_requestworkflow ran, and nothing showed that CI was waiting until someone merged main. That matches the GitHub docs quote in the body.These numbers give the acceptance criterion's "share of merged plugin PRs with two or more base merges" a 100-PR baseline: 49 of 80 (61%). Re-measure with the same window size after phase 4 of ADR 0048.
kyle-sexton commentedon Oct 4, 2026
This was generated by AI during triage.
Routing: human-gated. Work class: structural (C4). Priority: high.
Why: the design decision is settled. ADR 0048 is on main with
Status: accepted, and the 2026-10-03 comment turns this issue into its five-phase rollout. What is left is not delegable as one item:Priority is high because the current per-PR bump serializes concurrent PRs on the same plugin (the 100-PR baseline in the comments: 49 of 80 plugin PRs needed two or more base merges).
Suggested next step: break the rollout into one item per phase with
/work-items:decompose, so the parts that need no human (phase 1 tooling, with no plugin opted in) can be routed separately from the GitHub App setup and the pilot. Nostatus: needs-decision: nothing about the design is open, and the blocker is the capability and class.Finding: the title still describes the problem, not the work. Something like "ci: carry out ADR 0048 (changelog fragments and a bot-maintained release PR)" would match what the issue now tracks.
Brief (phase-level contract, from the ADR):
versionin its manifest nor itsCHANGELOG.md; a bot-maintained release PR applies the bumps.versionfrom manifests (option 2) beyond the version-omission probe the ADR requires before phase 2.kyle-sexton commentedon Oct 4, 2026
Version-omission probe (ADR 0048, Resolved decisions 5) run 2026-10-04 on Claude Code 2.1.289: both plugins update, so the ADR stands and phase 2 is not blocked by the probe.
alpha,beta) and noversionanywhere, tested as a local directory and as a Git-hosted repo over smart HTTP. A commit that changed onlyalphamoved both to the new HEAD SHA (claude plugin update betareportedupdated from 4498f2af7489 to b02d5c93a41calthoughbeta's last directory commit was5b27beb), and the cache gained a full copy of each per HEAD.version: a content change with no bump was not delivered (already at the latest version (1.0.0)); bumping onlyalphato 1.1.0 updated onlyalpha.versionwould refresh every relative-path plugin for every user on every merge to main, which is the ADR's reason for rejecting that option. Explicit per-plugin versions remain required.versionis "The commit SHA of the installed directory". In practice that is the marketplace repo's HEAD, not the last commit that touched the plugin directory.Phase 1 tooling (empty opt-in list) is in progress as a draft PR. Phase 2 needs the dedicated release GitHub App.
kyle-sexton commentedon Oct 4, 2026
Phase 1 is in draft #6328: tooling only, with an empty opt-in list. An independent review found the logic matches the ADR. These Gates-table items are outside phase 1's rollout list and must land before the phase 2 pilot:
check-changelog-parity.sh --check-bump(lines 93-96): count a consumed fragment as the change. Today a release PR touches only CHANGELOG, the version field and.changes/, so it fails BUMP WITHOUT CHANGE. That would keep the pilot'sci-statusred.--check-requiredcurrently exempts CHANGELOG and version-onlyplugin.jsonedits for every PR. In a scratch test, a non-release PR that bumped a listed plugin by hand and added no fragment passed.--checkand--check-requiredinto the changelog job, and--check-releaseinto the release PR andmerge_group.release-plugins.shorders fragments by commit, and feat(scripts): add changelog-fragment tooling with no plugin opted in #6328 now refuses to run in a shallow clone.fix(scripts): harden fragment creator and release ordering from review
kyle-sexton commentedon Oct 4, 2026
More evidence, 2026-10-04 (Pocock upstream sync batch):
CHANGELOG.mdand/orplugin.jsonat the first base merge: fix(source-control): make the PR body gate agree with CI on code-only sections #6270, fix(review): dispatch only named read-only agents so review subagents cannot recurse #6307, fix(work-items): keep won't-do blockers blocking and ignore foreign PRs as in-flight work #6310 (twice in one ready pass), fix(planning): drop the one-seam absolutism and sweep the wayfind map after each resolution #6295 and fix(wizard): set the shell var in write_env, keep a symlinked .env, survive TERM=dumb #6275. fix(wizard): set the shell var in write_env, keep a symlinked .env, survive TERM=dumb #6275 conflicted again onplugins/wizard/CHANGELOG.mdafter itsci-statushad already passed.pull_requestCI run. fix(work-items): keep won't-do blockers blocking and ignore foreign PRs as in-flight work #6310's lint fix heade5461e409had zero workflow runs until it was re-merged.gh pr merge --squash --auto --match-head-commit <sha>so GitHub merges the momentci-statuspasses.fix(scripts): catch edited consumed fragments and stage release writes
kyle-sexton commentedon Oct 4, 2026
Phase 1 merged in #6328 (2026-10-04T20:43Z): fragment tooling with an empty opt-in list. Added to the phase 2 prerequisites: a release run interrupted after it writes the manifests but before it deletes the fragments would release those fragments again on a rerun. The script refuses a duplicate CHANGELOG entry and says to run
git checkout -- ., but the release workflow should run from a clean checkout of main each time so the case never arises.fix(release): close the verifier's gaps in fragment-mode Dependabot a…
fix(source-control): convert-bump-to-fragment covers both-bumped merg…
fix(release): exact --carriers-of hint and accurate copied-install notes
fix(release): remove a partial fragment set on interrupt
kyle-sexton commentedon Oct 13, 2026
Decision recorded. The owner approved applying independently verified recommendations (high-risk set, reviewed) on 2026-10-13.
Basis: scripts/fragment-plugins.txt vs plugins/ listing (missing the three named); docs/adr/0048-...md:286-288; release PRs chore(release): release 1 plugins #7114-chore(release): release 26 plugins #7238 merged 2026-10-11
Tracks ADR 0048 (changelog fragments plus a bot release PR). Phases 1 to 4 landed (#6328, #6414, #6543, #6661) and release PRs merge regularly; three plugins are not in fragment mode and phase 5 (retire the per-PR path) plus the acceptance re-measure remain.
Verified by an independent fresh-context check (confirmed, risk high): gh issue view 6006 (OPEN, 8 owner comments; ADR 0048 accepted as the fix); comm of plugins/ vs scripts/fragment-plugins.txt on origin/main: developer-experience, display-redaction, user-experience missing (87 of 90); plugin creation commits: #6668 and #6669 on 2026-10-09, #7221 on 2026-10-11 (after or around #6661 merged 2026-10-09T13:33Z), so assumption holds; origin/main docs/adr/0048-...md:280-288 (phase 5 definition); origin/main .github/workflows/pr-require-checks.yml:1636 --check-bump; origin/main .github/workflows/pr-bump-plugin-version.yml:8-12
Admission to an agent lane stays with the work-class rules and owner ratification.
docs: leave AGENTS.md to its own pull request
docs(adr): mark the removed bump workflow in ADR 0048's gate table