From conventional commits to a changelog
feat(exports): add CSV column selection
fix(auth): reject expired refresh tokens
chore(deps): bump node-pg to 8.11
Three commits in the Conventional Commits format. From these, a machine can tell you that one is a feature, one is a fix, one is housekeeping, and which part of the system each touched. That is genuinely useful, and it is the entire promise of the convention: a commit history that can be read by something other than a person.
The mistake is thinking that gets you a changelog. It gets you the raw material.
What the convention actually specifies
A type, an optional scope, and a description: type(scope): description. Types are conventionally
feat, fix, chore, docs, refactor, test, perf, build, ci. Two things mark a
breaking change: a ! before the colon, or a BREAKING CHANGE: footer. Tooling keys off
feat and fix for minor and patch version bumps, and off the breaking marker for a major.
It is a small spec and it is worth following even if you never generate anything from it, because it forces one decision per commit: is this a change users see, or not.
Where it stops
Commit messages are written for reviewers. fix(auth): reject expired refresh tokens is
correct and tells a customer nothing. The reader of a changelog wants “you will be signed out when
a session really has expired, instead of seeing intermittent 401s”.
Scopes are internal. exports, auth, ingest are module names. They are stable, which makes
them good for grouping, and meaningless to anyone outside the codebase.
One change is often several commits. A feature merged over eleven commits produces eleven entries, ten of which are noise, and squashing to hide that loses the review history.
chore is a bin, not a category. Dependency bumps, CI changes and renames all land there, and
some of them matter to users while most do not.
So: the convention gives you type, scope and breaking status for free, and leaves wording, grouping and selection entirely open. Those three are the changelog.
The two-layer shape that works
Layer one, automatic. On merge, derive a draft entry from the commit: type mapped to a
changelog type (feat to Added, fix to Fixed, a breaking marker to Changed plus a flag), scope
kept as metadata rather than as text, link to the PR.
Layer two, human, and required. Before a release goes out, every draft entry either gets a one-line rewrite in the user’s vocabulary, or gets marked internal and dropped from the public view. This is the step people try to skip, and skipping it is what produces changelogs that read like a diff.
The important design detail is that layer two is not optional in the pipeline. If a release can be cut with unedited drafts, it will be, on the week when everyone is busy.
Three traps
Squash merges eat the footers. If your platform squashes with the PR title as the message, the
BREAKING CHANGE: footer from a commit inside that branch disappears, and your tooling silently
stops seeing the breaking change. Check what your squash template actually keeps.
Revert commits produce phantom entries. A fix that is reverted the next day generates an
entry for something that never shipped, unless the derivation reconciles reverts. Most tools do
not.
Version bump and changelog get out of order. If the version is computed from commits and the changelog is written by hand afterwards, they drift within about two releases. Compute both from the same pass or accept that one of them is wrong.
If you want the mechanical part without a pipeline
Our changelog generator does the derivation step in the browser: paste commits, get grouped, typed entries out. It is deliberately deterministic and entirely client side, so the commits you paste never leave your machine, which matters when the messages are from a private repo. It does the collection half honestly and makes no attempt at layer two, because layer two is a judgement call and a tool that fakes it produces exactly the changelog this article is arguing against.
For the pipeline version, changelog tools has the landscape.
The summary
Conventional commits answer “what kind of change is this” reliably and cheaply. They do not answer “what should we tell people”, and no amount of tooling on top of the commit message will, because the information was never in the commit message. Budget for the rewrite.
The technical claims in this article have not been independently reviewed. If something here is wrong, tell us and we will correct it.