fix(release): pin conventionalcommits preset and backfill lost release notes - #292
Merged
Merged
Conversation
…ble line Every release since 2026.7.0-next.1 (2026-07-06) shipped with empty release notes. The CHANGELOG.md sections, the GitHub Release bodies and the `chore(release):` commit bodies all contain nothing but the version heading. .releaserc.cjs asks release-notes-generator for `preset: "conventionalcommits"`, but the preset package was never declared here — it was only reachable because @commitlint/config-conventional@21 depends on it and npm hoisted its copy to the root of node_modules. Renovate's "dev dependencies (non-major)" group bumped that transitive copy from 9.3.1 to 10.2.0 in d9d5458, and preset v10 switched to the @conventional-changelog/writer@2 API, returning `template` and `commitPartial` as JS functions. release-notes-generator@14 (latest) still renders with the Handlebars-based conventional-changelog-writer@8, which reads `options.mainTemplate` and registers partials through Handlebars. So the main template silently fell back to writer@8's default (no `### <group>` headings) and every commit partial rendered as "". Only `headerPartial` happened to still work, which is why the heading survived and the failure was silent instead of loud. commit-analyzer was unaffected, because its loader reads `loadedConfig.parser`, which v10 does provide — so releases kept happening, just empty. Declare the preset as an exact devDependency on 9.3.1. npm then places 9.3.1 at the root, where release-notes-generator resolves it, and gives @commitlint/config-conventional its own nested 10.x; both consumers get a compatible copy and commitlint is unchanged. Alternatives considered, and why not: upgrading semantic-release (14.1.1 is latest and still on writer@8 — there is no upstream fix yet); hand-writing `writerOpts` in .releaserc.cjs (duplicates preset internals and drifts); switching to the `angular` preset (changes heading levels and type/section mapping, making the existing CHANGELOG.md internally inconsistent). Two guards against recurrence: - renovate.json holds the package below 10 with the reason inline, so the weekly dev-dependency group cannot reintroduce the break. - tests/unit/release/release-notes.test.ts renders synthetic feat/fix commits through the real .releaserc.cjs plugin options and asserts on the grouped sections, bullets and issue links. It fails on next today and fails again if the preset is forced back to 10.x, so a future resolution drift is caught in CI rather than in a shipped release. Reading the options out of .releaserc.cjs (instead of restating them) means the test also follows any config change. Because that test now imports @semantic-release/release-notes-generator for real, it comes off knip's ignoreDependencies; the preset itself takes its place there, since nothing imports it by specifier.
Seven releases (2026.7.0-next.1 through 2026.7.0) shipped with nothing but a version heading, and the preset pin cannot repair what was already published. Landing the regenerated bodies means committing CHANGELOG.md, which guard-changelog-history rejects outright. Rather than add label or permission plumbing, exempt exactly one commit subject — `chore(release): backfill changelog notes` — and only when that commit changes nothing but CHANGELOG.md and only adds lines to it. The guard walks the matching commits individually instead of reading `--name-status` in bulk, so a mixed commit is still reported, and the error messages name the exemption and the additions-only rule so both are discoverable from a failing run. The extra-file and deletion checks report independently rather than short-circuiting, so a commit that violates both gets both errors in one run instead of one per push. Every other CHANGELOG.md edit stays blocked. The commit list is captured into a variable rather than fed to the loop's here-string directly. Command substitution inside a here-string discards the substituted command's exit status, so `set -e` never fires: if `origin/$GITHUB_BASE_REF` is not resolvable in the runner's clone, git writes "fatal: bad revision" to stderr, the loop reads a single empty line, and the step reports "No CHANGELOG.md history violations — OK" — failing open exactly when it cannot see the history it is supposed to police. Verified against both a resolvable and a bogus base ref. The generator script that produced the backfill is deliberately not committed. It is a one-time repair for a defect the pin now prevents, so a permanent `scripts/` entry and npm script would be dead weight that still has to be maintained, type-checked and kept out of knip's way. CONTRIBUTING.md records the exemption and every property the guard enforces — reserved subject, CHANGELOG.md only, additions only — plus the generated-not-hand-written requirement, which is the part worth keeping. Alternative considered: dropping the exemption entirely and requiring an admin merge for backfills. Rejected because the repair is rare but real, and an enforced narrow path leaves a better audit trail than a bypassed check.
Restores the seven release bodies that semantic-release dropped between 2026.7.0-next.1 and 2026.7.0, when the conventionalcommits preset resolved to a version the notes generator's writer could not render. Generated, not hand-edited: for each empty section the notes were re-rendered from the commit range in the heading's own compare link, using the real .releaserc.cjs plugin options, and only the body was spliced in. Every heading keeps its original version, date and compare link, so the diff is 52 insertions and zero deletions. This subject is the one guard-changelog-history exempts, so this commit contains CHANGELOG.md and nothing else.
✅ knip — no dead codeNo unused files, exports, types, or dependencies detected. |
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.
What does this PR do?
Every release since
2026.7.0-next.1shipped with empty notes:.releaserc.cjsrequests theconventionalcommitspreset by bare specifier, and Renovate bumped the (undeclared, hoisted) transitive copy to v10, which uses the@conventional-changelog/writer@2API that release-notes-generator's Handlebars writer@8 cannot render — the heading survived, every section and bullet rendered empty, and the release still succeeded. This declares the preset as an exact9.3.1devDependency, holds it below 10 inrenovate.json, adds a regression test that renders synthetic commits through the real.releaserc.cjsplugin options, and backfills the seven damagedCHANGELOG.mdsections. Becauseguard-changelog-historyblocks any PR that touchesCHANGELOG.md, the guard now exempts exactly one reserved subject (chore(release): backfill changelog notes) and only when that commit is CHANGELOG.md-only and additions-only;CONTRIBUTING.mddocuments the exemption.Type of change
Checklist
npm run check:cipasses (lint + format)npx tsc --noEmitpasses (type check)npm testpasses (unit tests)Testing
Full AGENTS.md checklist on this branch:
npm run generate,npm run check:ci(200 files, clean),npx tsc --noEmit(clean),npm test(67 files / 938 tests passing),npm run build,npm run knip(no findings). The newtests/unit/release/release-notes.test.tsfails onnexttoday and fails again if the preset is forced back to 10.x. The guard's failure-open path was verified against both a resolvable and a bogus base ref.Notes for reviewers
Risk is concentrated in
guard-changelog-history: it now walks matching commits individually instead of reading--name-statusin bulk, and captures the commit list into a variable rather than a here-string so an unresolvable base ref fails loudly instead of silently passing. EveryCHANGELOG.mdedit outside the reserved subject stays blocked. TheCHANGELOG.mdcommit is generated, not hand-edited — headings, dates and compare links are untouched, so the diff is 52 insertions and zero deletions. GitHub Release bodies are damaged the same way and need a separategh apipatch by a maintainer; the one-off generator script is deliberately not committed.