Turn a changelog, a commit list, or a shipped items list into user-facing release notes: group changes by user impact, put breaking changes and known issues first, write ordered upgrade steps, and check every claimed item against what actually shipped. Use when a release is ready and the notes a user reads still need to be written or checked.
name: release_notes_writing description: "Turn a changelog, a commit list, or a shipped items list into user-facing release notes: group changes by user impact, put breaking changes and known issues first, write ordered upgrade steps, and check every claimed item against what actually shipped. Use when a release is ready and the notes a user reads still need to be written or checked." metadata: { "includeInPrompt": true }
Release Notes Writing
Purpose
Take what shipped in a release and write the notes a user reads before and after updating. The notes group changes by what a user can do, notice, or needs to fix, they put breaking changes and known issues first, and they carry upgrade steps in the order a user has to do them. Every entry traces to an item that actually shipped. Wording stays plain and exact about scope: partial rollouts, flags, and limits are stated, not smoothed over.
Scope:
- changelog-discipline covers the developer-facing CHANGELOG.md contract: entry format, versioning rules, and how the changelog is kept during development. This skill may read a changelog as a source, but it does not own that contract.
- release-checklist and release-management cover running the release itself: gates, versioning, staged rollout, and machine-generated changelogs, with at most a short upgrade-safety note. This skill starts after that work and covers only the hand-written, user-facing notes: grouping by user impact and checking every entry against the shipped list.
- This skill starts from what shipped and writes the notes a user reads. Its inputs are a changelog, a commit list, or a shipped items list. Its output is one release notes document in the Markdown shape defined in Workflow step 2.
- This skill does not cover deciding what ships, writing store listings or announcement posts, or setting the version number. If the shipped items list cannot be produced, stop and report that gap. Notes written without a shipped list cannot be checked for honesty, and unchecked notes are not delivered.
Verification note: the checker script in the Operating Rules section uses only the Python standard library. It was completed during review on 2026-10-20 (the draft arrived with the script body missing) and run the same day under Python 3.12.3 against fixtures built into the script. The shipped items fixture holds 5 item slugs. The clean fixture passed with sections_found 6, entries_checked 5, effect_entries_checked 6, and total_issues 0, exit code 0. The faulty fixture failed with total_issues 4, exit code 1: section_issues 1, hype_issues 1, missing_effect_issues 1, and unshipped_issues 1, with header_issues 0 and required_missing 0, and the four issue lines at the line numbers quoted in the Operating Rules section. What was NOT verified: no store or platform release notes format, length limit, or character limit is asserted anywhere in this skill, because none was checked against a platform source. All format guidance here is platform-neutral and applies to a plain Markdown notes document. Any destination with its own format rules needs those rules checked separately before the notes are pasted in.
Workflow
- Gather the source and build the shipped items list.
- Collect every source that describes the release: the changelog entries for the version, the commit list since the last release, and any release record that names what was included. Note the version and the release date from the release record. The notes header in step 2 uses those two values and no others.
- Build a shipped items list from the sources: one line per shipped change, named with a short slug in lowercase words joined by hyphens, for example dark-mode or login-timeout-fix. Save the list as a plain text file. This file is the honesty baseline for the whole task and the second input to the checker in step 8.
- Exclude anything that did not ship. A change that was reverted before release, merged after the release cut, or held back for a later version stays off the list and out of the notes, even when a changelog draft or an early announcement already named it.
- Mark each listed item that ships turned off, behind a flag, or to a subset of accounts. The mark travels with the item into its entry in step 5, so the entry can state the limit in the same sentence as the change.
- When sources disagree about whether an item shipped, treat the item as unshipped until the release record or the shipped build settles it. An item left out by mistake can be added in a later notes revision. An item claimed by mistake tells users to look for something they do not have.
- Set the header, the section skeleton, and the length budget.
- Open the notes with the header line: # Release X.Y.Z - YYYY-MM-DD, with the version and date from step 1. The checker requires this exact shape on the first line.
- Lay out the sections in this fixed order: Breaking changes, Known issues, New, Improved, Fixed, Upgrade steps. Breaking changes and Known issues are required in every set of notes. Write the single word None as the body of either section when there is nothing to report. The other four sections may be omitted when they would be empty.
- Give every bullet entry in Breaking changes, Known issues, New, Improved, and Fixed an effect line: the marker Effect: followed by what the user sees, gains, loses, or has to do, in plain words. Upgrade steps are numbered steps and carry no effect marker.
- Tag every bullet entry in New, Improved, and Fixed with its shipped item slug in brackets at the start of the entry, for example - [dark-mode] followed by the entry text. The tag is what the checker matches against the shipped items list.
- Fix the length budget before drafting entries. The default budget is one entry per shipped item, one or two sentences per entry, and no background sections. Internal history, design reasons, and team credit lines do not go in user notes. If a destination sets a shorter budget, cut entries by lowest user impact first, in step 7, and never cut a breaking change, a known issue, or an upgrade step to fit.
- Group entries by user impact.
- Sort each shipped item into the section that matches what the user experiences: New for a capability the user did not have, Improved for a capability that works better or faster, Fixed for a defect that no longer occurs. Sort by the user task the item changes, never by the team that built it, the component it touched, or the order commits landed.
- Merge items that land as one user-visible change. Three commits behind one visible change get one entry with one effect line. Users read effects, and a per-commit entry list repeats the same effect in fragments.
- Split an item that carries two separate user effects into two entries with the same slug only when each effect stands on its own. Each entry still needs its own effect line.
- Keep internal changes with no user-visible effect out of the notes. A refactor, a build change, or a dependency bump with no effect a user can see or feel earns no entry. If such a change alters behavior in any way a user could notice, it has an effect, and it is written as that effect.
- Write breaking changes and known issues first.
- Write the Breaking changes section before all other sections, in document order and in drafting order. A breaking change is any shipped change that stops an existing user workflow, file, setting, integration, or habit from working as it did.
- State each breaking change as three parts in one entry: what changed, the effect line naming what stops working or behaves differently, and the action the user needs to take. Add a workaround line when a workaround exists in the shipped build. When no workaround exists, write that plainly in the entry rather than leaving the gap silent.
- Write the Known issues section second. A known issue is a defect present in this release that the team knows about and did not fix in this release. State what triggers it, the effect line naming what the user sees, and a workaround line when one exists.
- Hold both sections to shipped facts. A breaking change from an earlier release does not belong here unless this release changes it again. A known issue that was fixed in this release moves to Fixed and leaves this section.
- Write the New, Improved, and Fixed entries in user terms.
- Name each change by the user task, screen, or object it touches, in words a user would use to find it. Internal component names, project names, and ticket numbers do not appear in entry text.
- Write the effect line as a concrete outcome: what the user can do now, what they will see, or what stops happening. An entry whose effect line only repeats the change in other words has no effect line yet and stays in draft.
- State flags and rollout scope inside the entry. An item that ships turned off reads as shipped turned off, with the setting named where the user turns it on. An item that ships behind a flag reads as available only where the flag is on. An item rolling out to a subset of accounts names the subset as the release record states it. Claiming a fix or a feature as generally available when it ships disabled or partial is a false claim, and the checker honesty pass in step 8 cannot catch it, because the slug is on the shipped list. The wording is the check.
- State limits plainly in the same entry that claims the change: caps, counts, sizes, supported formats, or account types, as the source gives them. A limit stated in the entry is a fact the user can plan on. A limit left out reads as no limit.
- Keep Fixed entries honest about reach. A fix for a defect that only occurred under a stated condition names the condition. A fix that covers part of a defect says which part, and the rest goes in Known issues if users can still hit it.
- Write the upgrade steps in order.
- Write upgrade steps as a numbered list in the exact order a user performs them: preparation first, including any backup step the release needs, then the update action, then any restart, migration, sign-in, or confirmation step the release requires after install.
- Call out any irreversible step in its own words inside the step: what cannot be reversed, and what the user should do before reaching it. A step that converts data, removes a fallback, or blocks return to the prior version always gets this callout.
- Include a final confirmation step that tells the user how to tell the upgrade worked: the screen, setting, or version display to check. Steps that end at install leave the user guessing whether the new version is running.
- Match the steps to the shipped build. If a step depends on a menu path or a setting name, take the name from the shipped build or its release record, not from a draft or a plan. A wrong path in upgrade steps fails at the moment the user has the least room to recover.
- Run the honest wording pass and cut to budget.
- Read every entry once for wording alone. Cut praise, filler, and claims about quality that no shipped fact supports. The banned hype list is enforced by the checker for New and Fixed entries and is named in Operating Rule 7. The same standard applies by hand to every other section.
- Check each entry against its mark from step 1: turned off, flag, subset, limit. If the mark is missing from the entry text, add it or cut the claim down to what the entry can support.
- Cut to the budget from step 2 in this order: background first, duplicate effects next, lowest impact Improved entries next. Breaking changes, known issues, and upgrade steps are never cut for length. When a cut removes the only entry for a shipped item, the item simply has no entry. Silence about a shipped item is a length choice. A claim about an unshipped item is a defect.
- Read the full draft once as a user who has not seen the changelog. Any sentence that needs internal knowledge to parse gets rewritten in user terms or cut.
- Run the checker, fix what it flags, and run it again.
- Save the script from the Operating Rules section as release_check.py. Save the shipped items list from step 1 as shipped.txt and the draft as a Markdown file. Run: python3 release_check.py --make-fixture once to confirm the script works, then run: python3 release_check.py shipped.txt notes.md with the real file names.
- Treat every issue line as a draft defect, not a script opinion. An out_of_order line means sections moved out of the step 2 order. An unshipped_claim line means the entry tag is missing or names an item that is not on the shipped list: fix the tag, prove the item shipped and add it to the list with its source, or cut the entry. A hype_word line means the entry text carries a banned word: rewrite the entry in plain terms. A missing_effect line means the entry needs its Effect: line written.
- On the built-in faulty fixture the checker reports 4 issues, one in each of those four kinds, and total_issues 4 with result FAIL. On the clean fixture it reports total_issues 0 with result PASS. A real draft ships only on a PASS run against its own shipped list.
- Keep the final checker output with the delivered notes. The output is the record that the delivered file, not an earlier draft, passed.
- Do the final read against the source.
- Read the notes top to bottom and confirm the order still serves the user: header, breaking changes, known issues, the change sections, upgrade steps last.
- Confirm version and date in the header match the release record exactly. A notes file carrying a prior version header over new content is a known failure shape, and the checker cannot catch it, because the header shape is valid either way.
- Confirm each breaking change action and each upgrade step against the shipped build one last time. These are the lines a user acts on directly, and they get the last check.
Output Contract
Deliver one release notes document plus its check record, in this order:
- The notes document itself, in the Markdown shape from Workflow step 2: a # Release X.Y.Z - YYYY-MM-DD header line, then the sections Breaking changes, Known issues, New, Improved, Fixed, and Upgrade steps in that order, with optional sections omitted only when empty and the two required sections present, with None as the body when there is nothing to report.
- Entry record: every New, Improved, and Fixed entry carries its shipped item tag, and every bullet entry in Breaking changes, Known issues, New, Improved, and Fixed carries an Effect: line. On the worked clean fixture in the Operating Rules section, entries_checked is 5 against a shipped list of 5 items, and effect_entries_checked is 6.
- The shipped items list used for the check, saved as shipped.txt, one slug per line, holding only items that shipped in this release.
- Check record: the final release_check.py output for the delivered notes. A delivered set of notes shows sections_found 6 when all six sections are present, required_missing 0, hype_issues 0, missing_effect_issues 0, unshipped_issues 0, total_issues 0, and result PASS. The faulty fixture, kept as the contrast case, shows section_issues 1, hype_issues 1, missing_effect_issues 1, unshipped_issues 1, and total_issues 4 with result FAIL.
Operating Rules
- The shipped list governs the notes. An item that is not on the shipped items list gets no entry, whatever any draft, plan, or announcement says. Disputes about the list are settled against the release record or the shipped build before drafting continues.
- Breaking changes and known issues go first, always. Their section order is fixed, they are written before the change sections, and they are never cut, shortened past their effect and action, or moved for length or tone.
- Every entry states a user effect. The Effect: line names what the user sees, gains, loses, or must do. An entry that can only describe internal work has no user effect and does not ship in the notes.
- Group by user impact only. Team names, component names, commit order, and ticket order never set the grouping or the order of entries.
- Flags, subsets, and limits are part of the claim. An entry that claims a change without its shipped state, turned off, behind a flag, subset only, or capped, overstates the release and is rewritten before delivery.
- Irreversible upgrade steps are called out in the step itself. The callout names what cannot be reversed and the preparation the user should finish first.
- No hype words in entries. The banned list in the checker code is: game-changer, revolutionary, supercharge, blazing-fast, seamless, cutting-edge, and world-class. The checker flags these words in New and Fixed entries. The same words, and any praise with no shipped fact behind it, are cut from every other section by hand.
- Version and date come from the release record only. The header is copied from the record, never typed from memory and never carried over from the prior release notes file.
- Use the checker script below on every draft before delivery. Save it as release_check.py, run it as its header describes, fix every issue it reports, and keep its final output in the check record. Report counts from its output, not from a hand count.
Release notes check (release_check.py)
Run: python3 release_check.py --make-fixture writes the worked example files shipped.txt, notes_good.md, and notes_bad.md in the current folder. Run: python3 release_check.py shipped.txt notes_bad.md checks the faulty fixture. Run: python3 release_check.py shipped.txt notes_good.md checks the clean fixture. For real notes, replace the file names with the shipped list and the draft from Workflow step 8.
Expected output for the faulty fixture run (exit code 1):
text
issue: line 12: out_of_order: section out of order: New
issue: line 14: unshipped_claim: item not in shipped list: voice-notes
issue: line 17: hype_word: entry in Fixed contains banned hype word: revolutionary
issue: line 18: missing_effect: entry in Fixed has no Effect: marker
sections_found: 6
required_missing: 0
entries_checked: 5
effect_entries_checked: 6
header_issues: 0
section_issues: 1
hype_issues: 1
missing_effect_issues: 1
unshipped_issues: 1
total_issues: 4
result: FAIL
Expected output for the clean fixture run (exit code 0):
text
sections_found: 6
required_missing: 0
entries_checked: 5
effect_entries_checked: 6
header_issues: 0
section_issues: 0
hype_issues: 0
missing_effect_issues: 0
unshipped_issues: 0
total_issues: 0
result: PASS
The faulty fixture is built to trip four checks once each: the Improved section is placed before the New section, so New is out of order. One New entry is tagged voice-notes, which is not one of the 5 slugs in the shipped fixture. One Fixed entry carries a banned hype word. One Fixed entry has no Effect: marker. The clean fixture carries the same 5 shipped items, all six sections in order, and no faults, and it passes. The shipped fixture holds the slugs dark-mode, export-csv, faster-search, login-timeout-fix, and crash-on-empty-title-fix.
release_check.py (Python 3, standard library only). Save this code as release_check.py:
python
#!/usr/bin/env python3
"""Check user-facing release notes against a shipped items list.
Usage:
python3 release_check.py --make-fixture
python3 release_check.py SHIPPED_FILE NOTES_FILE
--make-fixture writes shipped.txt, notes_good.md, and notes_bad.md
in the current folder. The shipped file holds one item slug per
line. The notes file is a Markdown document whose first line has
the shape: # Release X.Y.Z - YYYY-MM-DD
Sections are level-2 headings, expected in this order:
Breaking changes, Known issues, New, Improved, Fixed,
Upgrade steps. Breaking changes and Known issues are required.
Bullet entries in Breaking changes, Known issues, New, Improved,
and Fixed are checked for an Effect: marker. Bullet entries in
New, Improved, and Fixed are checked for a shipped item tag in
brackets at the start of the entry, matched against the shipped
list. Entries in New and Fixed are also checked against the
banned hype word list below.
Output prints one line per issue found, then summary counts, then
result: PASS or FAIL. Exit code is 0 on PASS, 1 on FAIL, and 2
on a usage or file error. Standard library only.
"""
import re
import sys
SECTION_ORDER = [
"Breaking changes",
"Known issues",
"New",
"Improved",
"Fixed",
"Upgrade steps",
]
REQUIRED_SECTIONS = ["Breaking changes", "Known issues"]
EFFECT_SECTIONS = [
"Breaking changes",
"Known issues",
"New",
"Improved",
"Fixed",
]
TAGGED_SECTIONS = ["New", "Improved", "Fixed"]
HYPE_SECTIONS = ["New", "Fixed"]
HYPE_WORDS = [
"game-changer",
"revolutionary",
"supercharge",
"blazing-fast",
"seamless",
"cutting-edge",
"world-class",
]
HEADER_RE = re.compile(r"^# Release \d+\.\d+\.\d+ - \d{4}-\d{2}-\d{2}\s*$")
TAG_RE = re.compile(r"^-\s+\[([^\]]+)\]")
SHIPPED_FIXTURE = (
"dark-mode\n"
"export-csv\n"
"faster-search\n"
"login-timeout-fix\n"
"crash-on-empty-title-fix\n"
)
NOTES_GOOD = """# Release 2.4.0 - 2026-10-19
## Breaking changes
- The old export format is removed. Effect: exports saved in the old format no longer open, and the action is to re-export in the new format.
## Known issues
None
## New
- [dark-mode] Dark mode is available in settings. Effect: the app can be used in a dark theme.
- [export-csv] Reports can be exported as CSV files. Effect: report data can be opened in a spreadsheet.
## Improved
- [faster-search] Search returns results in fewer steps. Effect: results appear sooner on large accounts.
## Fixed
- [login-timeout-fix] Login no longer times out on slow connections. Effect: sign-in completes instead of failing.
- [crash-on-empty-title-fix] Saving an item with an empty title no longer stops the app. Effect: the save is refused with a message instead of a crash.
## Upgrade steps
1. Back up your data from the settings screen.
2. Install the update.
3. Open settings and confirm the version reads 2.4.0.
"""
NOTES_BAD = """# Release 2.4.0 - 2026-10-19
## Breaking changes
- The old export format is removed. Effect: exports saved in the old format no longer open, and the action is to re-export in the new format.
## Known issues
None
## Improved
- [faster-search] Search returns results in fewer steps. Effect: results appear sooner on large accounts.
## New
- [dark-mode] Dark mode is available in settings. Effect: the app can be used in a dark theme.
- [voice-notes] Voice notes can be recorded in items. Effect: notes can be dictated instead of typed.
## Fixed
- [crash-on-empty-title-fix] A revolutionary fix stops the crash on empty titles. Effect: the save is refused with a message instead of a crash.
- [login-timeout-fix] Login no longer times out on slow connections.
## Upgrade steps
1. Back up your data from the settings screen.
2. Install the update.
3. Open settings and confirm the version reads 2.4.0.
"""
def check(shipped, text):
lines = text.splitlines()
issues = []
counts = {
"header_issues": 0,
"section_issues": 0,
"hype_issues": 0,
"missing_effect_issues": 0,
"unshipped_issues": 0,
}
if not lines or not HEADER_RE.match(lines[0]):
counts["header_issues"] += 1
issues.append("issue: line 1: header: first line is not a release header")
sections_found = []
max_order = -1
current = None
entries_checked = 0
effect_entries_checked = 0
for idx, line in enumerate(lines, start=1):
stripped = line.strip()
if stripped.startswith("## "):
name = stripped[3:].strip()
if name in SECTION_ORDER:
sections_found.append(name)
order = SECTION_ORDER.index(name)
if order < max_order:
counts["section_issues"] += 1
issues.append(
f"issue: line {idx}: out_of_order: section out of order: {name}"
)
else:
max_order = order
current = name
else:
current = None
continue
if current in EFFECT_SECTIONS and stripped.startswith("- "):
effect_entries_checked += 1
if current in TAGGED_SECTIONS:
entries_checked += 1
m = TAG_RE.match(stripped)
slug = m.group(1).strip() if m else None
if slug is None or slug not in shipped:
counts["unshipped_issues"] += 1
label = slug if slug else "missing-tag"
issues.append(
f"issue: line {idx}: unshipped_claim: item not in shipped list: {label}"
)
if current in HYPE_SECTIONS:
low = stripped.lower()
for word in HYPE_WORDS:
if word in low:
counts["hype_issues"] += 1
issues.append(
f"issue: line {idx}: hype_word: entry in {current} contains banned hype word: {word}"
)
break
if "Effect:" not in stripped:
counts["missing_effect_issues"] += 1
issues.append(
f"issue: line {idx}: missing_effect: entry in {current} has no Effect: marker"
)
required_missing = sum(1 for s in REQUIRED_SECTIONS if s not in sections_found)
total = sum(counts.values()) + required_missing
for issue in issues:
print(issue)
print(f"sections_found: {len(sections_found)}")
print(f"required_missing: {required_missing}")
print(f"entries_checked: {entries_checked}")
print(f"effect_entries_checked: {effect_entries_checked}")
print(f"header_issues: {counts['header_issues']}")
print(f"section_issues: {counts['section_issues']}")
print(f"hype_issues: {counts['hype_issues']}")
print(f"missing_effect_issues: {counts['missing_effect_issues']}")
print(f"unshipped_issues: {counts['unshipped_issues']}")
print(f"total_issues: {total}")
print(f"result: {'PASS' if total == 0 else 'FAIL'}")
return 0 if total == 0 else 1
def make_fixture():
with open("shipped.txt", "w", encoding="utf-8") as fh:
fh.write(SHIPPED_FIXTURE)
with open("notes_good.md", "w", encoding="utf-8") as fh:
fh.write(NOTES_GOOD)
with open("notes_bad.md", "w", encoding="utf-8") as fh:
fh.write(NOTES_BAD)
print("wrote shipped.txt, notes_good.md, notes_bad.md")
return 0
def main():
args = sys.argv[1:]
if args == ["--make-fixture"]:
return make_fixture()
if len(args) != 2:
print("usage: release_check.py SHIPPED_FILE NOTES_FILE")
print(" release_check.py --make-fixture")
return 2
try:
with open(args[0], encoding="utf-8") as fh:
shipped = [ln.strip() for ln in fh if ln.strip()]
with open(args[1], encoding="utf-8") as fh:
text = fh.read()
except OSError as exc:
print(f"error: {exc}")
return 2
return check(shipped, text)
if __name__ == "__main__":
raise SystemExit(main())
No ratings yet — be the first to rate it.