chore(#546): replace CHANGELOG prepend-anchor with changelog.d fragments - #547
Merged
Conversation
Every PR prepended its entry to the same line under `### Fixed`, so any two concurrent PRs edited that line and the second to merge always conflicted regardless of what it actually changed. In the 2026-08-28 merge round two of five PRs needed hand-resolution and CHANGELOG.md was the only conflicted source file in both. Each PR now adds its own `changelog.d/<issue>.<category>.md`, so two PRs never write the same path and the conflict is structurally impossible. - `scripts/changelog-fragments.mjs` — fragment parsing, collation, CI gate. `--check` validates pending fragments, `--collate` folds them into `[Unreleased]` and deletes them, `--ci` enforces the gate. - Collation inserts at the top of each category (matching the hand-written newest-first order) and creates missing headings in Keep a Changelog order, so the section cannot grow the duplicate `### Fixed` block found in #462. Released sections are never touched. - CI gate in the Lint job fails a PR touching `src/`, `packages/`, or `tools/` with no fragment. Test-only paths are exempt automatically — tests live under `packages/`, so a package's test-only change would otherwise trip it. The `no-changelog` label opts a genuine no-op PR out. The lint checkout moves to `fetch-depth: 0` so the gate can diff against the PR base. - `release:dry-run` fails on uncollated fragments, so entries cannot miss the release notes; the release checklist gains the collate step ahead of the reconcile check, which becomes a verification rather than a catch-up. Existing `[Unreleased]` entries are deliberately left in place: the collator has to merge into a non-empty section anyway, and rewriting ~40 hand-written entries would risk more than it gains. Closes #546 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q11TpacTZ2uSPeM8mES9Yq
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
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.
📋 Description
Every PR added its changelog entry by prepending to the same line under
### FixedinCHANGELOG.md. Any two PRs open at once therefore edited that line, and the second to merge alwaysconflicted — regardless of how unrelated the actual changes were.
In the 2026-08-28 merge round (#540, #542, #543, #544, #545), whose code changes were almost
entirely disjoint, two of five PRs still needed hand-resolution, and
CHANGELOG.mdwas the onlyconflicted source file in both. #544 and #545 escaped by luck of merge order.
Each PR now adds its own
changelog.d/<issue-number>.<category>.md. Two PRs never write the samepath, so the conflict is structurally impossible.
Closes #546
🔄 Type of Change
What's here
scripts/changelog-fragments.mjs— three modes:--check(pnpm changelog:check)--collate(pnpm changelog:collate)[Unreleased], then delete them — the release step--ciCollation inserts each entry at the top of its category, matching the newest-first order the
section has always been written in, and creates a missing heading in Keep a Changelog order — so the
section structurally cannot grow the duplicate
### Fixedblock found in #462. Released sections arenever touched.
The CI gate runs in the existing Lint job and fails a PR that touches
src/,packages/, ortools/without adding a fragment. Two deliberate carve-outs:packages/, so a package's test-onlychange would otherwise trip the gate.
no-changeloglabel opts out a PR that genuinely needs no entry (pure refactor,CI-internal change). The label has been created on the repo.
The lint checkout moves to
fetch-depth: 0so the gate can diff against the PR base.Release wiring —
release:dry-runnow fails if any fragment is left uncollated, so an entrycannot silently miss the release notes. The release checklist gains the collate step ahead of the
reconcile check, which turns that check from a catch-up into a verification.
Deliberate scope decisions
[Unreleased]entries are left in place. The collator has to merge into a non-emptysection anyway, and rewriting ~40 hand-written entries would risk more than it gains. The
release-checklist note records that pre-chore(changelog): every PR prepends to the same anchor, so concurrent PRs always conflict — adopt changelog.d/ fragments #546 entries aren't covered by the gate.
scripts/is not a gated root. Only the three roots specified in chore(changelog): every PR prepends to the same anchor, so concurrent PRs always conflict — adopt changelog.d/ fragments #546 are. Worth revisiting —scripts/mem-bench.mjsdid earn a changelog entry once — but widening it was out of scope here.A consequence worth naming: this PR touches no gated root, so it does not itself trip the gate.
The satisfied and exempt paths are covered by unit tests and were exercised manually (below).
✅ Checklist
🧪 Testing
24 new unit tests in
tests/unit/changelog/, covering filename parsing (valid, slugged, unknowncategory, missing number, missing category), collation into a non-empty
[Unreleased](insertion order, heading creation in category order, released sections untouched, empty-input
no-op, missing-section error), the gate's path matching and label handling, and
readFragmentsagainst a real temp directory.
All three gate paths exercised directly:
Test configuration: Windows x64, Node.js 24.14.1, pnpm 10.33.0
📝 Note for reviewers
pnpm check:all-personal-pathsfails on a local working tree because it scans gitignoredartifacts (
.osoji/,examples/dotnet/obj/, vendored CodeLLDB Python). None of those are tracked,and the Lint job runs on a clean checkout with
SKIP_ADAPTER_VENDOR, which is why it passes in CI.No file in this PR was flagged.