Engineering

From conventional commits to a changelog

5 min read updated

Conventional commits give a changelog three things for free: the type of each change, the part of the system it touched, and whether it breaks anything. They give it nothing else. Wording, grouping and selection, which are the changelog, are left entirely open, and a pipeline that pretends otherwise ships a formatted git log.

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 does the convention specify?

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.

Commit gives youChangelog needsWho supplies the gap
feat / fix / choreAdded / Fixed / internalA mapping, automatic
(scope)A grouping the reader recognisesA person, once per scope
! or BREAKING CHANGE:Who breaks, by when, and what to doA person, every time
The description, written for a reviewerThe outcome, written for a customerA person, every entry
One commitOne change, which may be many commitsSquash rules, or a person

The marker tells the tooling; it does not tell the caller, which is the subject of how to deprecate an API and what is a breaking change. 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 do conventional commits stop?

They stop at the sentence. Everything the convention captures is metadata about a change; the change itself is still described in a reviewer’s vocabulary.

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. Who actually owns a changelog entry covers who should be doing that wording, grouping and selection, since the convention itself has no opinion on it.

How do you generate a changelog from conventional commits?

In two layers, and the second one has to be mandatory.

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. Land it in the Unreleased section that Keep a Changelog asks for.

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. Which steps belong to the machine and which to the person is the whole of changelog automation.

Cutting the release is also the moment a git tag, a release, and this changelog entry either line up or start drifting apart; git tags, releases, and your changelog covers how to keep the three in sync.

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 covers what exists.

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.

FAQ

Do conventional commits generate a changelog automatically? They generate a draft automatically: typed, scoped, linked entries. The wording for a customer, the grouping and the decision about what to leave out still need a person, and a pipeline that skips that step publishes commit messages.

Which conventional commit types appear in a changelog? feat and fix always, as Added and Fixed. perf usually, as Changed. chore, docs, refactor, test, build and ci are internal by default and appear only if a person promotes one.

How do conventional commits mark a breaking change? A ! after the type or scope (feat(api)!: ...), or a BREAKING CHANGE: footer in the commit body. Both are lost if a squash merge keeps only the PR title.

Do you need conventional commits to automate a changelog? No. PR labels, PR templates and issue links carry the same metadata for teams that merge by pull request. Conventional commits are the cheapest option when the unit of change is the commit.


The technical claims in this article have not been independently reviewed. If something here is wrong, tell us and we will correct it.

Related on changeloop: Changelog generator, Changelog tools compared

changeloop
The team building a closed-loop changelog. Your users ask, your team ships, the person who asked gets told.