Appearance
Commit subjects and release notes
Release notes are not written by hand. tools/changelog reads the commit subjects between two release tags and renders them twice: as the body of the GitHub Release, and as a version section at the top of CHANGELOG.md. A commit subject is therefore text an operator reads, and the rules below decide whether it reaches them.
Which subjects reach the notes
A subject has to be a Conventional Commit subject: a lowercase type, an optional scope in one pair of parentheses, an optional !, a colon, a space and a description. The type picks the section:
| Type | Section |
|---|---|
feat | Added |
refactor, perf | Changed |
fix | Fixed |
docs | Documentation |
Sections appear in that order, and the entries of a section in commit order, oldest first. Every other type (chore, test, ci, build, plan, style and the rest) is dropped, and so is every subject that does not parse as a Conventional Commit: free-form subjects such as Fix failing CI checks, subjects that name several types such as test,web: …, merge commits, and Dependabot's double scope chore(deps)(deps): Bump …. A range in which nothing is kept renders as No user-facing changes.
How an entry renders
The description is kept verbatim. A scope becomes a prefix, and the breaking marker becomes a suffix:
| Subject | Entry |
|---|---|
feat(api): add widget listing | - api: add widget listing |
fix: handle empty page token | - handle empty page token |
refactor(store)!: rename widget table | - store: rename widget table (breaking) |
Only the subject counts. A BREAKING CHANGE footer in the commit body is not read, so mark a breaking change with ! in the subject.
Write every subject for a reader outside the project:
- A subject is rendered text. It must not carry internal traceability identifiers or
(#N)issue and pull request references, because a reader of the release notes can open neither. - A change a user notices, including the deprecation of a
/v1operation (see API versioning and compatibility), needs a type the notes keep, usuallyfeatorfix.
The first release
The range of a version starts at the newest release tag below it. A release tag is v followed by a SemVer core, such as v0.2.0, so a pre-release tag like v1.0.0-rc.1 never starts a range. The first release has no earlier tag: its notes list no commits and point at the "Before the first release" section of the changelog instead. That section keeps the hand-curated history of everything shipped before the first tag and receives no new entries.
Render the notes
make release-notes prints the Release body:
shell
make release-notes # previous release tag..HEAD
make release-notes TO=v0.2.0 # previous release tag of v0.2.0..v0.2.0
make release-notes FROM=v0.1.0 TO=v0.2.0make changelog inserts the section for a version above the newest section of CHANGELOG.md:
shell
make changelog RELEASE_VERSION=v0.2.0 # dated today, in UTC
make changelog RELEASE_VERSION=v0.2.0 RELEASE_DATE=2026-01-02 TO=v0.2.0It refuses a changelog that already has a section for the version, and one that still carries an ## [Unreleased] heading. CHANGELOG_REPO points both targets at another repository and CHANGELOG_FILE at another changelog; the defaults are this checkout and its CHANGELOG.md. Do not edit a generated section by hand.
make release renders the Release body with the same generator, and the releases runbook walks through cutting a release, from the changelog pull request to the tag.