Skip to content

fix(ci): merge driver no longer reverts release version bumps - #2958

Merged
maaaathis merged 3 commits into
mainfrom
claude/zen-spence-6a9b7b
Aug 27, 2026
Merged

fix(ci): merge driver no longer reverts release version bumps#2958
maaaathis merged 3 commits into
mainfrom
claude/zen-spence-6a9b7b

Conversation

@mfal

@mfal mfal commented Aug 27, 2026

Copy link
Copy Markdown
Member

What & why

The merge=package-json driver silently reverted release version bumps whenever
main was merged into a branch off it.

The driver (.github/scripts/merge-package-json.cjs) resolved the version
field to ours and 3-way-merged everything else. That is correct for the
forward-merge cascade it was written for (main → next, next → major line,
ADR 0004 §3), where the higher line must keep its own version. It is wrong in
the far more common direction — main merged into a branch off it — where
"ours" is the fork point, so merging a release commit put the manifest back on
the version the branch forked at.

It is silent by construction. The driver only runs when both sides changed
the file, it emits no conflict markers, and the package's CHANGELOG.md merges
cleanly and keeps the new entry — so the manifest and the changelog end up
disagreeing with nothing to show for it.

Reproduced twice on #2942, which edits packages/codemods/package.json: merging
the 1.0.1 release left that package at 1.0.0 (fixed in 57fe92817), merging
1.0.2 left it at 1.0.1 (fixed in ac6a5963d). Both times every other package
was on the new version and only the edited one lagged.

The blast radius is every developer, not just CI: prepare runs
init-merge-drivers.cjs on install, and git config is per-repository, not
per-branch
— so the driver runs on every merge anyone performs here. For a
published package a stale version in package.json is worse than cosmetic:
lerna publish from-package ships what the manifest says.

The fix

Resolve to the higher semver, not to "ours". A version only ever moves
forward on any line, so the higher side is the right answer in every direction:

Merge Higher side Resolves to
mainnext X.(Y+1).0-next.N > X.Y.Z ours
next → major line the major line's own prerelease ours
main → feature main's release bump theirs
main → next-based the next line ours

The cascade behaviour is unchanged — whenever the driver actually runs there,
next is the higher side, so it still wins and the empty-forward-merge property
(ADR 0004 §6/§7) holds. The comparison is dependency-free and implements semver
prerelease precedence (1.1.0-next.0 > 1.0.99 is the property the cascade rests
on); unparseable values sort below everything, so a workspace:* can never win.

The fix reaches every existing checkout without a reinstall: git config
stores the driver's script path, not its contents.

A version-consistency guard (version-consistency-guard.mjs + lib + tests)
runs in the required main job of test.yml, on every PR regardless of base.
Under fixed versioning every package matched by the lerna.json globs must carry
lerna.json's version; a manifest left behind by a merge is otherwise invisible
until the wrong version reaches npm. It reads the globs from lerna.json rather
than hardcoding packages/*.

Options weighed and not taken

  • Register the driver only in forward-merge.yml (both workflows already do
    git config themselves). git -c merge.package-json.driver=… does work
    per-command, so sync:resolve could scope it — but local registration is
    load-bearing beyond the cascade: /prepare-release depends on it for its
    Step-2 guard probe and its promotion merge, and a manual cascade merge would
    face 35 conflicts instead of one. It also would not reach existing checkouts
    until the next install. Scoping treats the symptom (the driver runs where it
    was not designed to) rather than the cause (it was not correct there).
  • The guard alone. It catches the mismatch but leaves the bad merge to be
    fixed by hand each time — which is exactly what already happened twice. Worth
    having as a net, not as the fix.

Notes for a reviewer

  • **/CHANGELOG.md merge=ours is left as it is. It is the same blunt
    repo-wide instrument with no "higher wins" equivalent, but it only fires when
    both sides changed a generated changelog, which on an ordinary branch never
    happens; the one direction where it is wrong (the promotion) is already
    compensated for by /prepare-release. Documented as a caution in
    .gitattributes instead of changed. Happy to close that hole too if you want
    it closed here.
  • Conflict-marker labels changed from next (ours) / main (theirs) to
    neutral ours / theirs. git tells a driver nothing about the branches, and
    the driver is no longer direction-specific, so the old labels were a guess.
  • ADR 0004 gets a dated amendment plus a §3 rewrite, and a Consequences bullet
    now states plainly that registering a driver locally makes it a repo-wide merge
    rule that must be correct in both directions.

Verification

node --test .github/scripts/*.test.mjs38 pass, 0 fail. Three of the new
tests drive real git merge calls through the real driver — the pure comparison
helpers alone could never have caught this bug.

The reported scenario was also replayed against a real clone of this repo (branch
edits packages/codemods/package.json, then merges a synthetic 1.0.3 release
commit):

  • before: merges silently, codemods stuck at 1.0.2, the new guard exits 1
    and names the file;
  • after: merges cleanly, all 16 packages at 1.0.3, the branch's own
    dependency change intact, guard passes.

prettier --check across the repo and eslint .github/scripts/ are clean.

Checklist

  • PR title is a Conventional Commit and matches the base branch above
  • pnpm lint is clean and pnpm affected:test passes (browser tests if
    behavior changed)
  • Generated code is committed (git diff is empty after the relevant
    build:* targets) — n/a, no generators touched
  • User-facing strings added to both de-DE and en-US locale files —
    n/a, no UI strings
  • Docs updated if a public API changed; intentional visual changes get
    updated snapshots / the update-screenshots label — ADR 0004 and
    CONTRIBUTE.md updated; no visual change

🤖 Generated with Claude Code

`merge=package-json` resolved the `version` field to "ours", which is right
for the forward-merge cascade it was written for (ADR 0004 §3) but wrong in
the far more common direction: `main` merged INTO a branch off it. There
"ours" is the fork point, so merging a release commit put the manifest back
on the version the branch forked at.

It is silent. The driver only runs when both sides changed the file, it
emits no conflict markers, and the package's CHANGELOG.md merges cleanly and
keeps the new entry — so the manifest and the changelog disagree with
nothing to show for it. Reproduced twice on #2942, once per release.

The blast radius is every developer, not just CI: `prepare` registers the
driver in every local git config, and git config is per-repository, not
per-branch.

Resolve to the HIGHER semver instead. A version only ever moves forward on
any line, so the higher side is correct in every direction:

  main -> next        next's X.(Y+1).0-next.N > main's X.Y.Z   -> ours
  main -> feature     main's release bump     > the fork point -> theirs
  next -> feature     next's newer prerelease > the fork point -> theirs
  main -> next-based  the next line           > main's stable  -> ours

For the cascade this is not a behaviour change: whenever the driver runs
there, `next` is the higher side, so it still wins and the empty-
forward-merge property (ADR 0004 §6/§7) holds. The fix reaches existing
checkouts without a reinstall — git config stores the script's path, not
its contents.

Back it up with a version-consistency guard in the required `test.yml` job:
under fixed versioning every package matched by the `lerna.json` globs must
carry `lerna.json`'s version, and a manifest left behind is otherwise
invisible until the wrong version reaches npm.

Tests drive real `git merge` calls through the real driver — the pure
comparison helpers alone could not have caught this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mfal
mfal requested a review from a team August 27, 2026 07:19
@maaaathis
maaaathis requested a lite review from Copilot August 27, 2026 07:20
@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for ./packages/components/

Status Category Percentage Covered / Total
🔵 Lines 76% 627 / 825
🔵 Statements 75.91% 643 / 847
🔵 Functions 77.95% 145 / 186
🔵 Branches 66.66% 298 / 447
File CoverageNo changed files found.
Generated in workflow #6360 for commit 7b9b6f3 by the Vitest Coverage Report Action

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes the repo-wide merge=package-json merge driver so it no longer silently reverts package.json/lerna.json version bumps when merging main into feature branches, and adds a CI guard to detect any version drift between lerna.json and Lerna-managed package manifests.

Changes:

  • Update the package-json merge driver to resolve version to the higher semver (direction-agnostic), while still 3-way-merging all other fields.
  • Add a version-consistency guard (lib + CLI wrapper) and run it in the required test.yml job.
  • Document the repo-wide implications of locally registered merge drivers (ADR + contribute docs + .gitattributes/init script notes) and add end-to-end driver tests.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
docs/adr/0004-forward-merge-main-into-next.md Updates ADR 0004 to reflect repo-wide merge driver behavior and the “higher semver wins” rule; documents the new guard.
CONTRIBUTE.md Notes that merge drivers affect local merges and documents the version consistency invariant and CI enforcement.
.github/workflows/test.yml Adds a CI step to run the new version-consistency guard on every PR.
.github/scripts/version-consistency-lib.test.mjs Adds unit tests for glob parsing, package-dir classification, and mismatch collection.
.github/scripts/version-consistency-lib.mjs Introduces pure helper functions to compute version mismatches against lerna.json.
.github/scripts/version-consistency-guard.mjs Adds a guard script that enumerates managed package manifests and fails CI on version drift.
.github/scripts/merge-package-json.test.mjs Adds unit + end-to-end git merge tests to validate the driver’s behavior in both merge directions.
.github/scripts/merge-package-json.cjs Changes driver logic to pick the higher semver and exports helper functions for tests.
.github/scripts/init-merge-drivers.cjs Documents that locally registered drivers become repo-wide merge rules and must be correct in all directions.
.gitattributes Clarifies merge-driver behavior and ensures lerna.json is also handled by the same merge driver.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/scripts/version-consistency-guard.mjs
`git ls-files "*package.json"` reads as root-only, and was reported as such
in review. It is not: a pathspec wildcard matches across `/` (fnmatch without
FNM_PATHNAME), so it finds all 20 manifests at any depth. No behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Preview Deployment

Preview environments are ready:

Type URL
docs pr-2958.docs.review.flow-components.de
storybook pr-2958.storybook.review.flow-components.de

Images:

  • docs: ghcr.io/mittwald/flow/docs:pr-2958
  • storybook: ghcr.io/mittwald/flow/storybook:pr-2958

# Conflicts:
#	docs/adr/0004-forward-merge-main-into-next.md
@maaaathis
maaaathis merged commit 5624853 into main Aug 27, 2026
11 checks passed
@maaaathis
maaaathis deleted the claude/zen-spence-6a9b7b branch August 27, 2026 13:13
mfal added a commit that referenced this pull request Aug 27, 2026
Conflict in `.github/workflows/test.yml`: #2958 inserted the version-consistency
guard step directly above the step this branch renamed. Kept both — the new
guard step, followed by "Run tests" instead of "Run unit tests", which no longer
tells the truth now that `pnpm affected:test` also runs `test:links`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants