Skip to content

fix(release): pin conventionalcommits preset and backfill lost release notes - #292

Merged
iamfj merged 3 commits into
nextfrom
conductor/iamfj-fix/changelog-generation-backfill
Aug 10, 2026
Merged

fix(release): pin conventionalcommits preset and backfill lost release notes#292
iamfj merged 3 commits into
nextfrom
conductor/iamfj-fix/changelog-generation-backfill

Conversation

@iamfj

@iamfj iamfj commented Aug 10, 2026

Copy link
Copy Markdown
Member

What does this PR do?

Every release since 2026.7.0-next.1 shipped with empty notes: .releaserc.cjs requests the conventionalcommits preset by bare specifier, and Renovate bumped the (undeclared, hoisted) transitive copy to v10, which uses the @conventional-changelog/writer@2 API 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 exact 9.3.1 devDependency, holds it below 10 in renovate.json, adds a regression test that renders synthetic commits through the real .releaserc.cjs plugin options, and backfills the seven damaged CHANGELOG.md sections. Because guard-changelog-history blocks any PR that touches CHANGELOG.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.md documents the exemption.

Type of change

  • Bug fix
  • New feature
  • Refactor (no behavior change)
  • Documentation
  • Tests
  • Build / CI

Checklist

  • npm run check:ci passes (lint + format)
  • npx tsc --noEmit passes (type check)
  • npm test passes (unit tests)
  • New code has tests (happy path + primary error case)
  • Commit messages follow Conventional Commits

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 new tests/unit/release/release-notes.test.ts fails on next today 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-status in 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. Every CHANGELOG.md edit outside the reserved subject stays blocked. The CHANGELOG.md commit 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 separate gh api patch by a maintainer; the one-off generator script is deliberately not committed.

iamfj added 3 commits August 9, 2026 11:37
…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.
@github-actions

Copy link
Copy Markdown

✅ knip — no dead code

No unused files, exports, types, or dependencies detected.

@iamfj
iamfj merged commit 6295ed9 into next Aug 10, 2026
12 checks passed
@iamfj
iamfj deleted the conductor/iamfj-fix/changelog-generation-backfill branch August 10, 2026 10:01
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.

1 participant