Why commit messages are the source of truth¶
changelog is opinionated about one thing: commit messages follow the
Conventional Commits specification. That
convention is what lets it turn a flat list of commits into a structured, categorised
changelog with no manual curation, no fragment files to collect, and nothing for a
contributor to remember beyond the message they were already writing.
This page is the reasoning. The exact mapping — every type, every category, every dropped case — is in commit classification.
The trade you are making¶
A changelog has to come from somewhere, and there are three places it can come from:
- A hand-written file. Accurate, and forgotten. It rots the first busy week.
- A fragment per change, collected at release time. Reliable, but it adds a file to every merge request and a reviewer who has to notice when it is missing.
- The commit messages you already write. Free at the point of use, and only as good as the discipline behind them.
changelog takes the third, which means the discipline is not optional. A repository
where half the commits say "fix stuff" produces a changelog that says "fix stuff". Nothing
in the module can improve on its input, and it does not try to.
What you get in exchange is that the changelog cannot drift from the history. There is no second artefact to keep in step, because the history is the artefact.
Why unreadable messages are dropped rather than reported¶
A commit whose subject is not a Conventional Commit produces no entry, silently. There is no warning and no count of what was skipped.
The alternative — failing generation — would let an unrelated typo in a commit message break a release. And a warning nobody reads is not better than silence. The right place to catch a malformed message is a commit hook or a CI check, where the author can still amend it; by the time a changelog is generated the commit is immutable and shipped.
WithIncludeAll exists for the case where the history predates the convention and you
would rather have imperfect entries than none: it keeps unparseable subjects verbatim
under Other. It is a migration aid, not a mode to run permanently.
Why test and ci commits are dropped¶
test and ci describe changes to the project's own scaffolding, not to the thing being
released. Nobody upgrading a dependency needs to know that its pipeline moved to a new
runner image.
They are the only two types dropped, and the line is drawn at "did this change what a user
gets". docs, refactor, chore and build all survive under Other, because each of
them can change what a user gets — a documentation fix changes what they read, a refactor
can change behaviour by accident, a build change can change what they can install it on.
A test or ci commit marked breaking is kept regardless. If a change is significant
enough to break someone, the type it was filed under stops mattering.
Why a breaking change is detected two ways¶
Both the ! marker and the BREAKING CHANGE: footer are honoured because the
specification defines both, and projects use them differently: ! for the terse case
where the subject says enough, the footer where the change needs a paragraph. Recognising
only one would silently mis-classify half the repositories out there.
The hyphenated BREAKING-CHANGE: spelling is an accepted synonym in the specification, so
it is accepted here too — precisely the sort of detail a reader gets wrong once and never
thinks about again.
Breaking classification overrides the type. A fix with a breaking footer is a breaking
change first and a fix second, because that is the ordering a reader needs: the thing that
will stop their build has to be the thing they see first. FormatSummary puts breaking
changes above everything else under a warning banner for the same reason.
Why the categories are so few¶
Five categories — breaking, feature, fix, performance, other — against eleven recognised commit types. The collapse is intentional.
Categories exist to help someone decide whether to upgrade, and that decision has a small
number of shapes: will this break me, does it give me something I want, does it fix
something that hurt me, is it faster, is it none of the above. Splitting Other
into docs, refactor, chore and style would make the changelog longer and the
decision no easier.
The categories are also the stable interface between the two halves of the module. A changelog this module generated can be parsed back and queried by category, and that symmetry only holds because the category set is small, fixed, and expressed in both directions.
Why semver ordering, not tag order or dates¶
Tags are validated and ordered with golang.org/x/mod/semver. Lexical ordering would put
v1.10.0 before v1.9.0; tag-creation order is scrambled by any repository whose history
was imported or re-tagged; commit dates are worse still, for the reasons in how commits
are attributed to releases.
Semver is also why non-semver tags are ignored rather than sorted somehow: there is no
defined order to put nightly in, and inventing one produces a changelog that looks
correct and is not.