Skip to content

feat: add script to auto-resolve CHANGELOG.md merge conflicts - #9994

Open
Mrtenz wants to merge 10 commits into
mainfrom
mrtenz/changelog-merge-conflicts
Open

feat: add script to auto-resolve CHANGELOG.md merge conflicts#9994
Mrtenz wants to merge 10 commits into
mainfrom
mrtenz/changelog-merge-conflicts

Conversation

@Mrtenz

@Mrtenz Mrtenz commented Aug 27, 2026

Copy link
Copy Markdown
Member

Explanation

Every consumer-facing change requires a packages/*/CHANGELOG.md entry under ## [Unreleased], and since many PRs land against main around the same time, that section conflicts constantly — usually because two branches added different bullet points to the same category, not because of any real disagreement about content. Resolving these by hand is repetitive and error-prone (easy to accidentally drop an entry).

This adds a yarn changelog:merge script that finds packages/*/CHANGELOG.md files with unresolved Git conflicts and computes the union of entries from both sides, using @metamask/auto-changelog's parsing/stringification (parseChangelog/Changelog) rather than hand-rolling Markdown parsing, so category ordering (Added, Changed, Deprecated, Removed, Fixed, Security) and formatting stay correct.

Merge behaviour:

  • Reads both sides of the conflict via git show :2:<path> (ours) and git show :3:<path> (theirs) — no manual conflict-marker parsing.
  • Deduplicates entries by PR number (falling back to description) so an entry added by both sides isn't duplicated.
  • Inserts new **BREAKING:** entries below any existing breaking entries in a category, and other new entries at the end.
  • Inserts a release version that exists on only one side into its correct descending-SemVer position.
  • Formats the result with oxfmt, matching the convention used by scripts/update-changelog.sh.
  • Files it can't confidently merge (e.g. a structurally invalid side) are skipped and left with their conflict markers intact, and the script exits non-zero so it's clear something needs manual attention.

References

  • Related to docs/processes/updating-changelogs.md

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

Note

Low Risk
Developer-only merge helper and tests; it only rewrites conflicted package changelogs in the working tree and does not affect runtime or release automation unless someone runs it manually.

Overview
Adds yarn changelog:merge, a maintainer tool that finds unresolved conflicts in packages/*/CHANGELOG.md during a merge/rebase and writes a merged file without touching conflict markers in unmergeable cases.

Conflict sides are loaded from Git stages (:2 ours, :3 theirs) and merged with @metamask/auto-changelog (parseChangelog + oxfmt) so formatting matches existing changelog tooling. mergeChangelogs unions [Unreleased] and per-release entries, dedupes by PR number + description, keeps ours ordering for shared bullets, places new BREAKING lines after existing breaking entries, and inserts release sections in descending SemVer order when only one side has them. Package metadata comes from each package’s package.json (with a fallback to the ours stage if package.json is conflicted).

The CLI logs resolved/skipped paths, suggests yarn changelog:validate, and exits 1 when any file is skipped; extensive Jest coverage covers merge edge cases and the entrypoint.

Reviewed by Cursor Bugbot for commit a4d18c6. Bugbot is set up for automated code reviews on this repo. Configure here.

Mrtenz added 2 commits August 27, 2026 22:46
Every consumer-facing change requires an Unreleased changelog entry,
so packages/*/CHANGELOG.md conflicts constantly when multiple PRs land
around the same time, even though there's usually no real disagreement
about content. This adds `yarn changelog:merge` to automatically
resolve those conflicts by taking the union of entries added on each
side, using @metamask/auto-changelog's parsing/stringification so
category ordering and formatting stay correct. Files it can't
confidently merge are left with their conflict markers intact for
manual resolution.
Extract package.json reading into its own function instead of
mutating a local variable, derive the merged entry count from array
length deltas instead of a manual counter, replace insertion-index
loops with findIndex, narrow exported types to only what's used
outside the module, and use @metamask/utils#getErrorMessage instead
of a hand-rolled instanceof check.
@Mrtenz
Mrtenz marked this pull request as ready for review August 28, 2026 08:15
@Mrtenz
Mrtenz deployed to default-branch August 28, 2026 08:15 — with GitHub Actions Active
@cursor
cursor Bot requested review from cryptodev-2s and mcmire August 28, 2026 08:25
Comment thread scripts/lib/changelog-conflicts.ts
Comment thread scripts/lib/changelog-conflicts.ts
Mrtenz added 2 commits August 28, 2026 10:57
Keying merged entries by PR number alone meant a PR that legitimately
adds multiple distinct changelog bullets would have all but the first
treated as duplicates and silently dropped. Key on the PR number and
description together instead, and add a regression test.
During a rebase, "theirs" is the commit being replayed, which can
already contain an entry that also exists in "ours" (the branch being
rebased onto). Since "ours" is usually the side more likely to already
overlap with "theirs", using it as the merge base keeps shared entries
in their existing position and only appends genuinely new entries from
"theirs", instead of the reverse producing a confusing reordering.
Comment thread scripts/merge-changelog-conflicts.ts Outdated
Co-authored-by: Frederik Bolding <frederik.bolding@gmail.com>

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit cd4046c. Configure here.

Comment thread scripts/merge-changelog-conflicts.ts
Comment thread scripts/lib/changelog-conflicts.ts Outdated
Comment thread scripts/lib/changelog-conflicts.ts
Comment thread scripts/lib/changelog-conflicts.ts
Comment thread scripts/lib/changelog-conflicts.ts
repoUrl: string;
tagPrefix: string;
}): Promise<{ content: string; mergedEntryCount: number }> {
// `ours` is used as the base to mutate and stringify. During a Git merge,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I find the ours/theirs versus base/ìncomingnaming differences across the file a bit confusing.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, good point. ours/theirs is Git terminology, but base/incoming may be easier to understand. I can rename everything to the latter if you're good with that.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sounds good 👍

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hmm, unsure about renaming everything, since "base" in Git means something different than "ours" so that could potentially be confusing too. Right now it uses "ours" and "theirs" in places where dealing with Git, and "base" and "incoming" in tool-agnostic merge logic.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah, I'm not quite sure either. But the current naming is confusing as well IMO

Comment thread scripts/lib/changelog-conflicts.ts Outdated
);
await execa('git', ['add', changelogPath], { cwd: ROOT_WORKSPACE });

resolved.push({ path: changelogPath, mergedEntryCount });

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should the returned paths be normalized?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It returns the path as used by Git. Do you think it should return OS-style paths instead? That would break when using Git on Windows with these paths.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hmm, I was thinking these would get logged to the terminal and might not be clickable for example if they were in the wrong format. Unsure how Windows deals with that actually

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, good point. I'm not able to test it (don't have a Windows dev machine), but can just add normalisation and see if anyone runs into issues. 😅

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks like it works either way, probably not worth the trouble then

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Just pushed. Should I revert then?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Now that you've done it, I guess we can just keep it 😄

Mrtenz added 2 commits August 28, 2026 14:40
Writing the merged result without running `git add` lets the user
review the resolution (and the state of any other conflicted files)
before deciding what to stage, rather than silently adding to the
index on their behalf.
changelogPath stays forward-slash internally (git commands and
path.posix calls need that form), but the path surfaced in the
resolved/skipped results is what gets printed to the terminal, where
a Windows-style backslash path is more likely to be recognized as a
clickable link.
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.

2 participants