* feat(release): reconcile-changelog tool + version-anchored fragment aggregation
`npm run release:reconcile` (scripts/release/reconcile-changelog.mjs) turns the
v3.8.51 reconciliation pass (#12971) into a repeatable Phase 0a step:
- folds `changelog.d/` fragments under `## [<version>]` — never under the first
matching heading — and credits each one with the PR of the commit that ADDED
it (`git log --diff-filter=A`), because the filename prefix is not reliable
(issue numbers, closed/recreated PRs, literal `#PR_NUMBER`); `--carrier N`
marks a PR that only back-filled fragments for other people's PRs
- drops fragments whose text already ships in another version section
(phantom fragments re-added by branches cut before the previous aggregation)
- covers a commit only when its OWN PR is a primary ref of a bullet (a
`/pull/N` link, the trailing `(#N)`, or an explicit `(#N …)` group) so an
incidental mention cannot hide a PR's own bullet; generates
`**type(scope):** subject (#PR) — thanks @author` for the rest, rolls
Dependabot bumps into one line, documents direct pushes by hash
- `--credit N=handle` carries the closed-PR / co-author / deleted-PR audit
- keeps pre-existing section bullets verbatim (changelog-integrity compares
bullet lines), never touches `[Unreleased]` or older sections
- opens the section with "📊 Release by the numbers" + "🏆 Top 25" (mailmap +
merged-PR login), the v3.8.50 format
`aggregate-changelog.mjs` gains the same anchoring: `insertBullets(text,
bullets, version)` searches the heading inside `## [version]` only (with
`[Unreleased]` still carrying `### ✨ New Features`, every feature fragment was
landing there); `aggregate()` reads the version from package.json.
Tests: tests/unit/reconcile-changelog.test.ts (helpers + an end-to-end
reconcile fixture) and two new cases in tests/unit/changelog-fragments.test.ts.
* fix(release): escape every regex metacharacter before building the mention regex
CodeQL js/incomplete-sanitization on reconcile-changelog.mjs: the handle was only
escaping '-' before being interpolated into a RegExp. Use a full escapeRegExp
helper instead; handles are [A-Za-z0-9_-] in practice, so behaviour is unchanged
for real input and the addCredit tests still pass.
* feat(release): changelog.d/ fragments — kill the CHANGELOG-eat merge-storm cascade
Every PR used to edit the same top lines of CHANGELOG.md (its bullet), so in a
merge-storm each merge conflicted every sibling (CHANGELOG-eat / DIRTY cascade),
forcing a re-sync push + full CI re-run per PR per merge — O(N^2) CI runs.
A PR now adds ONE new file under changelog.d/{features|fixes|maintenance}/ with its
bullet; two PRs never touch the same file. scripts/release/aggregate-changelog.mjs
(npm run changelog:aggregate) folds fragments into the living section and deletes
them at release reconciliation. check:changelog-integrity (already wired in the
merge-integrity CI job — zero workflow change) now also validates fragment
well-formedness. This PR dogfoods the convention: its own entry is a fragment.
* chore(changelog): fragment filename matches PR number (#6783)