Drafting release notes

SkillDocs & knowledge

Lets your agent draft GitHub release notes for a RomM release from the changes since the last stable tag.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Drafting release notes skill

About this capability

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 not

What this skill tells your AI

The instructions your AI receives, as published by rommapp/romm in .claude/skills/draft-release-notes/SKILL.md and read by ahel’s review.

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.

TagDocument
X.Y.Z, X.Y.Z-alpha.1, X.Y.Z-beta.1the full treatment, sections 3 onward
X.Y.Z-alpha.2 and later, X.Y.Z-beta.2 and laterflat ## What's Changed
a stable patch with no feat: workflat ## 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.

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.

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.

# 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
# 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:

SectionContents
Callouts> [!WARNING] upgrade actions, > [!NOTE] env vars and API changes
## Highlights### Title + prose per headline feature
## Minor changesevery feat: PR not already a Highlight
## Fixesevery fix: PR
## Other changesperf:, chore:, docs:, refactor:, revert:, build(deps):
API changestable, inline for an alpha, in <details> once it gets long
## New Contributorsverbatim 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:

# 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.

Signals

GitHub stars
13k
Forks
735
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
draft-release-notes-rommapp
Source
github.com/rommapp/romm