Skip to content

chore(#546): replace CHANGELOG prepend-anchor with changelog.d fragments - #547

Merged
debugmcpdev merged 1 commit into
mainfrom
chore/546-changelog-fragments
Aug 28, 2026
Merged

chore(#546): replace CHANGELOG prepend-anchor with changelog.d fragments#547
debugmcpdev merged 1 commit into
mainfrom
chore/546-changelog-fragments

Conversation

@debugmcpdev

Copy link
Copy Markdown
Collaborator

📋 Description

Every PR added its changelog entry by prepending to the same line under ### Fixed in
CHANGELOG.md. Any two PRs open at once therefore edited that line, and the second to merge always
conflicted — 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.md was the only
conflicted 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 same
path, so the conflict is structurally impossible.

Closes #546

🔄 Type of Change

  • 🧹 Chore / tooling
  • 📚 Documentation update
  • 🧪 Test improvement

What's here

scripts/changelog-fragments.mjs — three modes:

Mode Purpose
--check (pnpm changelog:check) Validate pending fragment names, categories, and bodies
--collate (pnpm changelog:collate) Fold fragments into [Unreleased], then delete them — the release step
--ci The gate below

Collation 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 ### Fixed block found in #462. Released sections are
never touched.

The CI gate runs in the existing Lint job and fails a PR that touches src/, packages/, or
tools/ without adding a fragment. Two deliberate carve-outs:

  • Test paths are exempt automatically. Tests live under packages/, so a package's test-only
    change would otherwise trip the gate.
  • The no-changelog label 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: 0 so the gate can diff against the PR base.

Release wiringrelease:dry-run now fails if any fragment is left uncollated, so an entry
cannot 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

✅ Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my change is effective
  • New and existing unit tests pass locally with my changes

🧪 Testing

24 new unit tests in tests/unit/changelog/, covering filename parsing (valid, slugged, unknown
category, 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 readFragments
against a real temp directory.

pnpm run lint                 # clean
npx vitest run --project unit # 234 files, 4325 tests passed
pnpm run changelog:check      # ok  546.added.md  (added)

All three gate paths exercised directly:

CHANGED_FILES=$'src/server.ts'exit 1, names src/server.ts
CHANGED_FILES=$'src/server.ts\nchangelog.d/546.added.md'exit 0, "fragment is present"
PR_LABELS='no-changelog'exit 0, "label is applied"

Test configuration: Windows x64, Node.js 24.14.1, pnpm 10.33.0

📝 Note for reviewers

pnpm check:all-personal-paths fails on a local working tree because it scans gitignored
artifacts (.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.

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

codecov Bot commented Aug 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@debugmcpdev
debugmcpdev merged commit 002a49d into main Aug 28, 2026
10 checks passed
@debugmcpdev
debugmcpdev deleted the chore/546-changelog-fragments branch August 28, 2026 14:43
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.

chore(changelog): every PR prepends to the same anchor, so concurrent PRs always conflict — adopt changelog.d/ fragments

2 participants