Skip to content

fix(changelog): write CHANGELOG.md and backfill the history - #12

Merged
GSTJ merged 1 commit into
masterfrom
fix/changelog-write-and-backfill
Jul 28, 2026
Merged

fix(changelog): write CHANGELOG.md and backfill the history#12
GSTJ merged 1 commit into
masterfrom
fix/changelog-write-and-backfill

Conversation

@GSTJ

@GSTJ GSTJ commented Jul 28, 2026

Copy link
Copy Markdown
Owner

This repo has shipped 9 releases and has no CHANGELOG.md. release-it's conventional-changelog plugin had no infile, so it generated a changelog for each release body and threw it away. Two of those bodies came out empty while the range under them contained a breaking change.

Infile

infile: "CHANGELOG.md" plus a backfill of the 9 existing releases, generated from the commits. The plugin prepends, so once the file exists the history below the newest section is never rewritten.

The config moves from the release-it key in package.json to .release-it.mjs, because the type list now lives in tools/changelog-preset.mjs and JSON can't import it. tools/changelog-check.mjs reads the same list.

Sections

files publishes src and lib side by side, so the split is whether a type can change what npm hands a consumer:

renders why
feat, fix, perf, revert the stock set
build bob's targets and tsconfig.build.json decide what lands in lib/
refactor rewrites src/ and lib/, both published
chore dependency and config moves ship
docs README.md is inside the tarball
hidden why
ci .github/ is not in files
style oxfmt touches published src/, whitespace only
test !**/__tests__ is excluded from files

The four new ones are effect: "changelog". They render without entering the bump count, so a release of nothing but build: commits is still a patch. Only feat, fix, perf and revert can raise a version.

Backfill

Where the git history and a published release body disagree, the file follows the history.

0.2.2 and 0.3.1 each shipped a feat!: and each published a body with no sections at all. 0.2.2 went out as a patch off a breaking change. Both now carry their ⚠ BREAKING CHANGES heading. No tag was moved and no release body was edited.

0.4.0 and 1.0.0 were written by hand and say more than the commits do (the peer version table, the new-architecture steps, the return-value change on magicToast.show()). Their generated sections carry a link to the release notes.

Backfilled changelog vs the published bodies

Check

pnpm run changelog:check builds a throwaway repo with 12 commits, one per type, 3 of them breaking, renders it through the real preset and asserts on what comes back. ci is hidden and the ci!: commit's breaking note still has to surface.

The check carries its own copy of the policy, so the two disagreeing is a failure. Derived from the preset, every assertion would be a tautology: hide a section and the check quietly redefines what it expected. The negative control in the image is what that failure looks like.

The bump is asserted too: feat! major, feat minor, fix patch, and build/refactor/chore/docs together no release at all.

Wired into Branch Checkup as extra-command.

Preset check with its negative control

Release

npm latest is 1.0.0 and the 16 commits since the tag are tooling. Verified by rehearsing on a throwaway clone with publish, push and the GitHub release all off: 1.0.0 -> 1.0.1, the new section prepends, and git diff shows zero lines removed from everything below it.

The annotated tag now carries the same section as the changelog and the release body. git's default tag cleanup strips every line starting with #. That was eating all the headings. tagArgs pins --cleanup=verbatim.

Release rehearsal

release-it generated a changelog for every release body and then dropped it:
the conventional-changelog plugin had no `infile`, so nine releases went out
and the repo still has no CHANGELOG.md. Adds the infile, backfills the file
from git, and moves the config out of package.json so the type list has one
home.

The preset renders what can reach the tarball. `files` publishes `src` and
`lib` side by side, so `build`, `refactor`, `chore` and `docs` all change
what a consumer installs and now show up; `ci`, `style` and `test` cannot and
stay hidden. The four new ones are `effect: "changelog"`, so they render
without entering the bump count and a pile of `build:` commits is still a
patch.

The backfill follows the commits, not the release bodies. 0.2.2 and 0.3.1
each shipped a `feat!:` and each published empty notes; those breaking
changes are in the file now. No tag was moved, and 0.4.0 and 1.0.0 keep a
pointer to their hand-written upgrade guides.

The annotated tag gets the same section as the changelog and the GitHub
release. git's default tag cleanup was eating every `#` heading out of it, so
`tagArgs` pins `--cleanup=verbatim`.

tools/changelog-check.mjs states the policy longhand and asserts the type
list and the rendered output both match it, so hiding a section fails instead
of quietly redefining what was expected. It also checks the bump: feat! is a
major, feat a minor, fix a patch, and build/refactor/chore/docs together are
no release at all. Three of its twelve synthetic commits are breaking,
including a `ci!:` whose note has to surface off a hidden type. Wired into
Branch Checkup.
@GSTJ
GSTJ merged commit da02bd7 into master Jul 28, 2026
2 checks passed
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