Release Notes From the Diff
Turns what actually changed into something a user can read — grounded in the commits, not in what you meant to build.
When to reach for it
At release time, when the choice is between pasting a commit log nobody reads and writing a summary nobody can verify.
What changes
- Every line traces back to a change in the range, so nothing gets announced that did not actually ship.
- Entries are written as what the reader can now do or no longer suffers, rather than as the name of the module you touched.
- Anything that needs action — new config, a changed default, a breaking call, a migration — is lifted into its own section at the top with the action spelled out.
- Changes with no user-visible effect are grouped or dropped instead of padding the list until people stop reading it.
- You get a list of changes you could not classify, which is usually exactly where an unintended behaviour change is hiding.
Pairs with
- Demo Then ShipProves the feature works in the actual running app, not only in the tests written for it.
- Feature Flag ExitShip behind a flag with the removal already written down, so the flag does not quietly become a permanent fork in the code.
- Migration SafetyChange a schema or move data without ever creating a minute where the running code and the database disagree.
Release Notes From the Diff
Write the notes from what shipped, not from what was planned. The range of commits is the only source that cannot be mistaken about it.
1. Get the actual range
Take the exact boundaries — the last released version and the thing going out now — and list the commits and files between them. If you cannot name the last released version, find it before writing a word.
2. Sort every change into four buckets
- Action required — the reader has to do something: set config, update a call, accept a new default, migrate data
- New — something they can do now that they could not before
- Fixed — a symptom they may have hit that is now gone
- Internal — invisible to them: refactors, tests, tooling, most dependency bumps
3. Rewrite the visible ones from the reader's side
The subject of each line is the user or their data, never the code:
- Not "refactored the export queue" but "exports over ten thousand rows now finish instead of timing out"
- Not "fixed null check in the invoice mapper" but "invoices with no billing address can be downloaded again"
One sentence each. No class names, no service names, no ticket number standing in for a description.
4. Put the action-required items first
Each one states the action rather than the change: what to set, what to update, by when, and what happens if they do nothing.
5. Check every line against the code
For each line, point at the change that justifies it. If you cannot, delete the line. A note about something that did not ship costs more trust than a missing note ever will.
6. Report what you could not classify
List the changes whose user impact you could not work out, and ask someone who knows. That list is where a behaviour change nobody intended usually turns up.
Rules
- Do not write notes from ticket titles. Tickets record intent; diffs record what happened, and the two drift.
- Do not describe a fix by its cause. The reader never saw the null pointer, they saw the download fail.
- Do not merge unrelated fixes into one vague line to keep the list short. Vague lines are read as "nothing changed".
- Do not list every dependency bump. Mention one only when it changes a requirement, a default, or closes a known vulnerability.
- Do not announce anything still behind a flag as available. It ships when the flag is on, not when the code merges.