| name | tag-changelog |
| description | Create a draft GitHub tag/release and rewrite autogenerated notes into frontend/backend sections grouped by workstream. Use when given a tag or draft release link and asked to organize, group, categorize, or clean up release notes or a changelog. |
| disable-model-invocation | true |
Tag changelog
Turns GitHub's flat autogenerated ## What's Changed list into notes grouped by
area (frontend / backend / CI) and, inside each area, by single body of
work. One bullet per issue, binary, or campaign. PR numbers go at the end of
that line. Never one bullet per PR when several PRs are the same fix or the
same binary bump.
Hard rule: never publish
Creating a draft release is required. Publishing is forbidden.
Never gh release edit --draft=false. Never drop draft: true on a PATCH.
If the release is already published, stop and tell the user instead of editing.
Allowed:
gh release create TAG --draft --generate-notes --title TAG
That creates a draft release. The git tag does not exist until publish.
Then rewrite the body. The release name is the tag, nothing else.
Workflow
- [ ] 1. Create or resolve the draft release and read its body
- [ ] 2. Parse PRs (number, title, author)
- [ ] 3. Resolve files changed for PRs whose area is not obvious
- [ ] 4. Group by workstream, issue, or binary; split by area
- [ ] 5. Render the template
- [ ] 6. Save back as draft, keeping draft: true
1. Create or resolve the draft release
If the user named a tag that has no release yet, create it as a draft and let
GitHub generate the PR list:
gh release create TAG --repo OWNER/REPO --draft --generate-notes --title TAG
Pass --target COMMITISH when the user named a branch or SHA. Then read the
body from the new release.
If a draft already exists, do not create another. Untagged drafts have no git
ref, so gh release view <tag> fails. Find them by listing releases and
matching html_url:
gh api repos/OWNER/REPO/releases --jq '.[] | select(.draft) | {id, name, tag_name, html_url}'
gh api repos/OWNER/REPO/releases/RELEASE_ID --jq '.body' > /tmp/draft_body.md
Keep RELEASE_ID. Step 6 needs it. After gh release create, take the id from
the select(.draft) listing above. releases/tags/TAG 404s on drafts.
2. Parse PRs
Each generated line looks like:
* [1.4] core: services: log_zipper: Keep all inputs in one gzip stream by @joaoantoniocardoso in https://github.com/bluerobotics/BlueOS/pull/4182
Extract number, title, author. No API call needed for authorship.
Title cleanup
- Strip release-branch prefixes:
[1.4], [master], [backport/1.4].
- Strip the leading path prefix when the group heading already says it
(
core: tools: nginx: Fix gzip_static 404s -> nginx: Fix gzip_static 404s
under a group that is already about nginx). Keep the service name when it
disambiguates within a mixed group.
- Otherwise keep the author's wording. Do not rewrite titles into your own prose.
- Pure ASCII only:
-> not an arrow glyph, - not an em dash, " not smart
quotes.
Attribution
Drop by @user in <url> everywhere in the changelog bullets. Bare #NNNN
already links the PR. Outside-org PRs still go in Frontend / Backend / CI
like everyone else. Do not take them out of the groups.
Check org membership for every author:
gh api orgs/OWNER/members/LOGIN --silent && echo member || echo outside
Collect outside authors in first-seen order. After **Full Changelog**, add
one thanks line. Each name is @user plus that author's PR numbers as
markdown links. Those same #NNNN also stay in the changelog bullets.
Omit the thanks line when there are no outside contributors:
Thanks [@Safe-bet](https://github.com/Safe-bet) for [#4186](https://github.com/bluerobotics/BlueOS/pull/4186) [#4184](https://github.com/bluerobotics/BlueOS/pull/4184) [#4185](https://github.com/bluerobotics/BlueOS/pull/4185) and [@Michael-F-Bryan](https://github.com/Michael-F-Bryan) for [#4209](https://github.com/bluerobotics/BlueOS/pull/4209).
Use the real repo in those URLs. Several people: Thanks @a for [#N](url) and @b for [#M](url). One person: drop the and.
3. Resolve ambiguous PRs
Titles like "Allow websockets for extensions" or "refresh ethernet interfaces
immediately after write actions" do not say which side they touch. Fetch files
for those only, in one batch (this is slow serially -- expect a minute for ~40
PRs, so run it in the background and keep working):
for n in 4020 3926 4130; do
gh api "repos/OWNER/REPO/pulls/$n/files" --jq "\"#$n \" + ([.[].filename] | join(\", \"))"
done
Area mapping (BlueOS paths; adapt to the repo at hand)
| Paths | Area |
|---|
core/frontend/** | Frontend |
core/services/**, core/libs/**, core/tools/**, core/start-blueos-core, core/run-service.sh, core/Dockerfile | Backend |
.github/**, .hooks/**, *.md, root configs | CI / hooks / docs / misc |
A PR touching both sides is cross-cutting: list it under both Frontend and
Backend, under whichever group fits that side. Do not invent a third section for
it.
4. Group by workstream, issue, or binary
Look for the campaign behind the PRs, not the service they live in. Several PRs
that are the same change go on one bullet, with every PR number at the end
of that line.
Collapse onto one line when:
- Same issue. Two or more PRs that fix one bug, including a follow-up,
revert+fix, or backport of that fix.
- Same binary. Version bumps or rebuilds of one tool or artifact
(
mavlink-camera-manager, linux2rest, cockpit) even if they span
several PRs.
- Same author + same repeated title shape. Twelve
"Add to venv2 updated deps" PRs by one author are one bullet:
"Python env migration (venv2, uv, pydantic) #3615 #4015 #4074". Fold in the
PRs that only make sense as part of it (
Remove old venv, Drop (already unused) poetry, Use uv sync --locked, coverage-threshold tweaks).
- Same symptom class across services: "Ping robustness", "Settings races",
"Cut polling and duplicate requests", "MAVLink and websocket lifecycle".
- Version bumps of external tools collapse into "Tool updates" only when
they are not already one-binary groups.
Order groups within a section by size, largest campaign first. Single-PR groups
are fine when the work is self-contained. Sweep true leftovers into one trailing
bullet named Misc (or a descriptive multi-topic bullet). Name a group after
the work, not after a directory.
5. Render
Three ### sections. One bullet per group. PR refs only at the end of the
line, space-separated, never as sub-bullets. A single PR still sits at the end.
Several PRs for one issue or one binary share that line:
### Frontend
- Cut polling and duplicate requests #4027 #4026 #4049
- MAVLink and websocket lifecycle #4044 #4134
### Backend
- Python env migration (venv2, uv, pydantic) #3615 #4015 #4074
- Ping robustness #4036 #4051
### CI / hooks / docs / misc
- `claude-pr-review`: Fix post comment action #4080
- README #4158
**Full Changelog**: https://github.com/OWNER/REPO/compare/PREV...CURR
Thanks [@Safe-bet](https://github.com/Safe-bet) for [#4186](https://github.com/OWNER/REPO/pull/4186).
Formatting rules, all load-bearing:
- PR numbers are bare
#4027, never in backticks. GitHub only autolinks
bare refs. Put them at the end of the bullet, after the title.
Never by @user on a changelog bullet.
- If a bullet has more than one PR, list every number on that same line.
- Backticks are for code identifiers, paths, and env vars only.
- No nested sub-bullets for PRs.
- Drop the
## What's Changed heading; keep the trailing **Full Changelog**
line verbatim.
- After Full Changelog, the thanks line (outside contributors only). Their PR
numbers are markdown links
[#N](https://github.com/OWNER/REPO/pull/N), not
bare refs. The same PRs stay in the changelog as bare #NNNN. Omit the
thanks line when nobody is outside the org.
- The last section is still one bullet per leftover item, with
#NNNN at the
end. Collapse CI PRs that are the same workflow fix onto one line.
- Omit a section entirely when no PR lands in it. Do not emit an empty heading.
- Every PR from the input appears in a changelog bullet. Outside PRs also
appear on the thanks line. Count before saving.
6. Save back as draft
A PATCH that omits tag_name wipes it on an untagged draft, turning the
tag into untagged-<hash>. Always read the current metadata and merge the body
into it rather than sending body alone:
gh api repos/OWNER/REPO/releases/RELEASE_ID \
--jq '{tag_name, name, target_commitish, prerelease, draft: true}' > /tmp/meta.json
jq --rawfile body /tmp/new_notes.md '. + {body: $body}' /tmp/meta.json \
| gh api --method PATCH repos/OWNER/REPO/releases/RELEASE_ID --input - \
--jq '{draft, tag_name, name, html_url}'
Confirm the response still shows "draft": true and the original tag_name.
The untagged-<hash> slug in html_url changes on every draft edit. That is
normal; the numeric release id is the stable handle. Report the fresh URL.