Basic

Writing a changelog people can act on

A changelog is not a commit list. It is a record of what a consumer must do differently after this release. The format that survives is: one section per release, one line per change, and the line names the action.

What you will see

At least 1. Every consumer-visible interface change needs that word so a migration guide can be generated from it.

About 1 min of reading

Try It

Write a real changelog block for a container release

1

Pick the last release

docker images --format '{{.Tag}}' | grep -E '^v' | head -1

Before you run it, say out loud what it will print. Then open the answer.

What it prints
The most recent numbered tag you built.
2

List what changed since

git log --oneline $(git log --format='%H' --grep='v1.4.1' -1 2>/dev/null || echo HEAD^)..HEAD

Before you run it, say out loud what it will print. Then open the answer.

What it prints
The commits since the previous release tag. If you have no tags yet, use the last two commits.
3

Write the block

printf '## v1.4.2 - %s\n\n### Fixed\n- Pagination cursor no longer skips the last item\n\n### Breaking\n- `run()` returns a dict, not a list\n' "$(date +%Y-%m-%d)" | tee -a CHANGELOG.md

Before you run it, say out loud what it will print. Then open the answer.

What it prints
A block appended to CHANGELOG.md with a dated header and two action lines.
4

Check it is actionable

grep -c 'Breaking' CHANGELOG.md

Before you run it, say out loud what it will print. Then open the answer.

What it prints
At least 1. Every consumer-visible interface change needs that word so a migration guide can be generated from it.
If the release has no breaking change, zero is correct. Do not add a Breaking line for an internal refactor.

Data Flow

From commits to a release note
1 Collect the commits since the last release
↓
2 Drop the ones with no consumer-visible effect
↓
3 Group the rest under Added / Changed / Fixed / Removed
↓
4 Add a Breaking line for anything that needs a code change
Why this works
Where the file lives

At the repo root, named CHANGELOG.md, newest release first. Anyone who opens the repo to check what version they are on should not have to search a wiki or a release page.

What not to write

Do not write 'improved performance' without a number, and do not write internal names that consumers cannot grep. Every line should be checkable against the code a user has.

Examples

The header carries the version

## v1.4.2 - 2026-10-07 at the top of each block. A reader can find the release they are on and read everything below it to learn what changed since.

Lines name actions

Added, Changed, Fixed, Removed. A line that says 'Fixed off-by-one in pagination' tells a reader to re-test pagination. A line that says 'Refactored internals' tells them nothing.

Breaking changes get their own line

If a caller must change code, say so explicitly: 'Breaking: run() now returns a dict, not a list.' This is the line a migration guide is built from.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← Tags, layers, and the cache
Previous Lesson
Tags, layers, and the cache
Next Lesson
Pre-releases and release candidates
Pre-releases and release candidates →