# Draft Release Notes

> Draft the GitHub release notes for a RomM release, stable or alpha/beta prerelease, from the diff since the previous stable tag, in the house format the published RomM releases use. Use when asked to "draft the 5.3.0 notes", "write the release notes for the beta", "what goes in the 5.3.0-alpha.1 notes", or to refresh a draft after more PRs land. Produces callouts, Highlights, Minor changes, Fixes, Other changes, API changes and New Contributors, and never publishes.

- Skill: `rommapp/draft-release-notes` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add rommapp/draft-release-notes`
- Raw SKILL.md: https://api.skillmd.com/api/skills/rommapp/draft-release-notes/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: rommapp (https://skillmd.com/u/rommapp)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/rommapp/draft-release-notes

---


# Drafting release notes

Turn a tag range into the notes RomM actually publishes, for a stable release or
for an alpha/beta prerelease. The GitHub autogenerated list is raw material, not
the deliverable: the published notes reorganize it into sections, lift the few
changes that matter into prose Highlights, and add the callouts an upgrader needs
before they pull the image.

Output a markdown file and print it. **Never** run `gh release create` or
`gh release edit`. Publishing is the maintainer's call.

---

## 1. Pick the shape

Two documents exist, and the tag decides which one you are writing.

| Tag                                                 | Document                              |
| --------------------------------------------------- | ------------------------------------- |
| `X.Y.Z`, `X.Y.Z-alpha.1`, `X.Y.Z-beta.1`            | the full treatment, sections 3 onward |
| `X.Y.Z-alpha.2` and later, `X.Y.Z-beta.2` and later | flat `## What's Changed`              |
| a stable patch with no `feat:` work                 | flat `## What's Changed`              |

This is what the history does without exception: every `.1` prerelease and every
feature-carrying stable gets Highlights and partitioned bullets, while every
later prerelease in the same line is a plain list of what landed since. The flat
shape is just the autogenerated body with `## New Contributors` and the compare
link kept, so for those tags steps 4 through 6 do not apply.

## 2. Resolve the range

`$ARGUMENTS` is the tag being drafted. If it is empty, infer it: take the last
stable tag, bump minor for a range containing `feat:` work or patch otherwise,
and say which you picked so it can be corrected.

The base is **the stable tag below the release line**, for a stable release and a
prerelease alike, so a whole line accumulates against one base. `5.1.0-alpha.1`,
`5.1.0-beta.1` and `5.1.0` all compare against `5.0.0`, which makes each document
a superset of the one before it rather than a delta on top of it.

```bash
TAG=5.3.0-alpha.1                       # what you are drafting, stable or prerelease
LINE=${TAG%%-*}                         # 5.3.0, the stable version of this line
git fetch origin master --tags

# the stable tag immediately below $LINE. Numeric tags only, since the legacy
# v-prefixed ones sort above them, and sort -V rather than --sort=v:refname
BASE=$(git tag --list '[0-9]*' | grep -vE -- '-(alpha|beta|rc)' | grep -vFx "$LINE" \
  | { cat; echo "$LINE"; } | sort -V | grep -B1 -Fx "$LINE" | head -1)

# an already-tagged prerelease is drafted against its tag, not against master,
# or the draft picks up everything merged since it shipped
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then HEAD_REF="$TAG"; else HEAD_REF=origin/master; fi

RANGE="$BASE..$HEAD_REF"
git rev-list --count $RANGE
git diff --stat $RANGE | tail -20
```

Sanity-check the pair before going further. `$BASE` should equal the tag in the
previous release's compare link. If `$TAG` is a prerelease and `$LINE` is already
tagged stable, the line has shipped and `$TAG` is wrong: stop and confirm the
version.

## 3. Pull the raw material

The autogenerated list is the spine. Its **order is merge order, keep it** when
you partition lines into sections, and keep each line's `by @user in <url>` tail
byte for byte so attribution and first-time-contributor detection stay correct.

```bash
gh api repos/rommapp/romm/releases/generate-notes \
  -f tag_name="$TAG" -f previous_tag_name="$BASE" --jq '.body' > /tmp/raw-notes.md
```

Then run the sweeps that find what the PR titles do not say. They are an index,
not an answer: a `@protected_route(...)` or `_get_env(...)` call wrapped across
lines matches only on the line that actually changed, so a moved path or a new
default can surface as a bare `+@protected_route(`. Read the full hunks of every
file a sweep touches before writing the tables.

```bash
# new or changed env vars
git diff $RANGE -- backend/config/__init__.py | grep -E '^[+-].*_get_env'
# new, moved or removed routes
git diff $RANGE -- backend/endpoints/ | grep -E '^[+-].*@(protected_route|router\.)'
# response-schema changes the frontend types are generated from
git diff --stat $RANGE -- backend/endpoints/responses/ backend/handler/scan_handler.py
# migrations, which imply an irreversible upgrade step
git diff --name-status $RANGE -- backend/alembic/versions/
# config.yml surface
git diff $RANGE -- backend/config/config_manager.py | grep -E '^[+-].*(class |: )' | head -40
```

```bash
# the sweeps only point at files, so read what actually changed inside them
git diff $RANGE -- backend/endpoints/ backend/config/
```

For each candidate Highlight, read the PR itself rather than paraphrasing its
title: `gh pr view <n> --json title,body,author`. The prose in the notes is
usually a compression of the PR description.

## 4. Assemble the document

Follow `template.md` in this skill directory for the exact skeleton. Sections in
this order, all optional except the last two:

| Section               | Contents                                                             |
| --------------------- | -------------------------------------------------------------------- |
| Callouts              | `> [!WARNING]` upgrade actions, `> [!NOTE]` env vars and API changes |
| `## Highlights`       | `### Title` + prose per headline feature                             |
| `## Minor changes`    | every `feat:` PR not already a Highlight                             |
| `## Fixes`            | every `fix:` PR                                                      |
| `## Other changes`    | `perf:`, `chore:`, `docs:`, `refactor:`, `revert:`, `build(deps):`   |
| API changes           | table, inline for an alpha, in `<details>` once it gets long         |
| `## New Contributors` | verbatim from the autogenerated body                                 |
| `**Full Changelog**`  | verbatim from the autogenerated body                                 |

Which of the two optional tables you carry depends on the line, not on whether
the tag is stable. See section 6.

**A patch release skips all of it**, stable or prerelease. Under roughly eight
PRs with no `feat:`, publish a single flat `## What's Changed` list plus the
changelog link, the way `4.9.1-beta.1` and `4.9.1` both did. Do not manufacture
Highlights for a bugfix release.

## 5. Section rules

**Highlights.** Three to eight for a minor line, zero for a patch line. A change
earns one only if it changes what a user can do or how they run RomM: a new
subsystem, a new metadata source, a new UI surface, a new export format. Format:

- `### Sentence case name of the thing`, no trailing punctuation.
- One or two short paragraphs, second person, present tense. Say what it does,
  then the one thing the reader has to know to use it (the config key, the
  setting, the caveat). Name real keys and paths in backticks.
- End the first paragraph with a space and the bare PR reference, `#3211`. Bare
  `#NNNN` in
  prose, full URLs only in the bullet lists.
- Add a fenced `yaml` block when a config key drives the feature.
- Leave a `<!-- screenshot: ... -->` placeholder for anything visual. Do not
  invent `user-images.githubusercontent.com` URLs; the maintainer pastes real
  ones.

**Bullet sections.** One line per PR, copied from the autogenerated body. Fix up
only the title, in place: give it a conventional-commit prefix and scope if it
lacks one (`Skip DMCA-locked grids when scraping SteamGridDB` becomes
`fix: Skip DMCA-locked grids when scraping SteamGridDB`), and lowercase a shouty
one. Never drop a PR, never merge two into one line, never reorder across
sections. Promoting a PR to a Highlight **removes** its bullet: every published
release does this, so `5.1.0-beta.1` lists neither #3211 nor #3854 under Minor
changes, and #3155 left the `4.9.0-alpha.1` bullets once the beta gave it a
Highlight. Promote or list, never both.

`build(deps):` and `dependabot[bot]` lines always land in Other changes, never
in Fixes, even when the bump closes a CVE. Call the CVE out in a callout instead
if it matters.

**Callouts.** A `> [!WARNING]` is for something the upgrader must do, and it goes
at the very top, above Highlights: a renamed `config.yml` key, a DB privilege
the migration needs, a proxy cache to purge. Write the migration action, not the
rationale. A `> [!NOTE]` carries the env var table and the API change table. A
`> [!IMPORTANT]` marks a feature shipping ahead of its documentation.

Env vars go in a table with `variable | default | description`; API changes in a
table with `Change | Description` for edits, and `Method | Path | Description`
for a new endpoint group. Prefix a breaking entry with `⚠️`.

## 6. Prerelease versus stable

The three documents on a line are the same document, regenerated over a range
that keeps growing against a fixed base. Each one starts as its predecessor, not
as a blank page, so open the previous one's body and edit that:

```bash
# the release before $TAG on this line: its alpha.1, or its last full beta
gh release view 5.1.0-beta.1 --json body --jq .body
```

- **alpha.1** opens the line. Put the env var and API tables in one `> [!NOTE]`
  block at the top, where the third-party app developers reading the alpha will
  see them first.
- **beta.1** is the alpha's notes plus everything merged since. Carry the alpha's
  Highlights across verbatim unless the feature changed, add the new ones, and
  fold in any warning that alpha feedback produced. Once the API table runs past
  roughly fifteen rows, move it to the bottom inside
  `<details><summary><h2>API changes</h2></summary>`, since by the beta most
  readers are upgraders rather than integrators.
- **the stable release** is the last full-treatment prerelease's notes plus
  everything merged during the beta. Two things change:

  **Re-curate the Highlights.** They are not carried over untouched. Going from
  `5.0.0-beta.1` to `5.0.0`, four were added (CRT mode, server-side ROM patching,
  the permission system, the music player), three were dropped, and three were
  renamed, "A brand-new frontend" becoming "UI overhaul". Pick what the finished
  release is about, not what the beta was about.

  **Drop the tables a prerelease already published.** The API table is for the
  developers tracking the line, and they read it in the alpha. `5.1.0`, `5.0.0`,
  `4.9.0` and `4.8.0` all dropped theirs; `5.2.0` kept one only because its line
  shipped no prerelease that carried it. Keep the env var table when it grew
  during the beta, as `5.1.0` did going from three variables to seven, since a
  reader upgrading straight from the last stable has seen none of them.

## 7. Before handing it over

- Every PR in `/tmp/raw-notes.md` is accounted for exactly once, either as a
  Highlight or as a bullet, never both and never dropped.
- Every `#NNNN` in the Highlights resolves to a PR in the range.
- Every new env var found in step 3 is documented, and every route change too.
- A migration in the range means you have checked whether it needs a warning.
- The `**Full Changelog**` compare link ends in the tag you are drafting. Editing
  a prerelease's body into a stable one makes this easy to miss: `4.9.0` shipped
  pointing at `4.8.1...4.9.0-beta.1`.
- No em-dashes, no invented screenshot URLs, no marketing claims the diff does
  not support.

Print the draft and say what you could not determine (which screenshots are
needed, which version you inferred), rather than guessing.

