Generating release notes from commit trailers Jump to heading
There are two familiar ways to produce release notes. Generate them from commit subjects, and you get a list written for developers: βrefactor: extract rounding helperβ is not something a customer needs. Write them by hand at release time, and someone spends an afternoon reconstructing what changed from pull request titles. Commit trailers offer a middle path. The author of each change writes one user-facing sentence in a Changelog: trailer β at the moment they understand the change best β and release tooling collects those lines between two tags with a single git log command. Commits with nothing user-facing simply omit the trailer. This page sets up the convention, the collection script and the checks that keep it reliable, within commit message hooks and templates.
When to use this approach Jump to heading
- Release notes are written by hand from pull request titles, and it takes hours.
- Generated changelogs from commit subjects are too technical for your users.
- You want breaking changes and security fixes called out consistently.
- Trailers are already part of your workflow, for example from adding trailers automatically with a commit-msg hook.
Step 1 β Define the trailers Jump to heading
Trailers are Key: value lines at the end of a commit message. Pick a small set with clear meaning.
Add per-account export scheduling
Exports can now run on a schedule configured per account. The scheduler
reuses the existing job queue, so no new infrastructure is needed.
Changelog: Schedule CSV exports per account from the export settings page.
Changelog-Type: added
Fixes: PAY-812 Remove the legacy v1 export endpoint
Breaking: The /v1/exports endpoint has been removed; use /v2/exports instead.
Changelog-Type: removed Step 2 β Collect trailers between two tags Jump to heading
git log can print specific trailers directly with the %(trailers) placeholder, so collecting them needs no parsing of whole messages.
prev=v2.3.0; next=v2.4.0
git log --no-merges --format='%(trailers:key=Changelog-Type,valueonly,separator=%x2C)%x09%(trailers:key=Changelog,valueonly,separator=%x20)' \
"$prev..$next" | awk -F'\t' '$2 != ""' With squash merging, each pull request is one commit, so this gives one line per user-visible change. With preserved commits, put the trailer on one commit per pull request, or on the merge commit, and adjust --no-merges accordingly.
# Verification: count user-facing changes in the range
git log --no-merges --format='%(trailers:key=Changelog,valueonly)' v2.3.0..v2.4.0 | grep -c . Step 3 β Assemble the notes by type Jump to heading
Group the lines by type and put breaking changes and security fixes first, where readers look.
#!/bin/sh
# scripts/release-notes.sh <from-tag> <to-tag>
set -eu
range="$1..$2"
section() { # section <title> <git log args...>
title=$1; shift
out=$(git log --no-merges "$@" "$range" | sed '/^$/d' | sed 's/^/- /')
[ -n "$out" ] && printf '### %s\n\n%s\n\n' "$title" "$out"
}
printf '## %s\n\n' "$2"
section "Breaking changes" --format='%(trailers:key=Breaking,valueonly)'
section "Security" --format='%(trailers:key=Security,valueonly)'
for t in added changed fixed removed; do
title=$(printf '%s' "$t" | awk '{print toupper(substr($0,1,1)) substr($0,2)}')
section "$title" --format='%(trailers:key=Changelog,valueonly)' \
--grep="^Changelog-Type: $t" --extended-regexp
done Step 4 β Make sure trailers get written Jump to heading
The convention works only if authors write the trailer when it applies. Two checks help: a template that reminds them, and a CI check that asks β not demands β when a pull request touches user-facing code without one.
# CI: warn when user-facing paths changed but no Changelog trailer exists
base=$(git merge-base "origin/$BASE_REF" HEAD)
if git diff --name-only "$base" HEAD | grep -qE '^(src/api|src/ui)/'; then
git log --format='%(trailers:key=Changelog,valueonly)' "$base..HEAD" | grep -q . ||
echo "::warning::User-facing code changed β add a 'Changelog:' trailer, or label the PR no-changelog"
fi A warning is the right strength: a hard failure pushes people to write empty trailers. The template approach is in generating a commit message template per branch.
Step 5 β Keep trailers through squash merges Jump to heading
Squash merging builds a new message on the forge, which may drop trailers from the original commits. Put the trailers in the pull request description, which most forges use as the squash body, or check after merging that they survived.
# After merge: did the trailers reach main?
git log -1 --format='%(trailers)' origin/main The squash message itself is covered in writing a good squash commit message.
Step 6 β Publish the notes with the release Jump to heading
The assembled notes are most useful attached to the release itself, where users look. Generate them in the release job, let a person review them in a draft release, and publish once approved.
sh scripts/release-notes.sh "$PREV_TAG" "$NEW_TAG" > notes.md
gh release create "$NEW_TAG" --draft --notes-file notes.md --title "$NEW_TAG"
# after review in the forge UI:
gh release edit "$NEW_TAG" --draft=false Keeping the release as a draft until someone has read the notes is a small safeguard: generated lines are only as good as the trailers, and a reviewer catches the odd sentence written for developers rather than users. Store the final notes in the repository too β in the changelog file or a docs/releases/ directory β so they survive a change of forge.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
How is this different from Conventional Commits changelogs? Jump to heading
Conventional Commits use the subjectβs type and text, written for developers. Trailers carry a separate, user-facing sentence. You can combine them: the type from the subject, the text from the trailer.
Can trailers be added after the fact? Jump to heading
Not to commits already on main without rewriting history. Use git notes for a missed line, or add it to the release notes by hand during review.
Does git interpret-trailers help? Jump to heading
Yes, for adding or normalising trailers in hooks and scripts: git interpret-trailers --trailer "Changelog: β¦" appends correctly even when other trailers exist.
Related Jump to heading
- Commit Message Hooks & Templates β the parent topic.
- Automating Changelog Generation with semantic-release β the subject-based alternative.
- Changelog Fragments Instead of One Changelog β the file-based alternative.
- Recording Backports with cherry-pick -x β trailers that make backports traceable.