Release Notes
Goal
Take the commits in a release range (current tag → previous tag),
write human-readable notes that answer "what changed and why does
it matter?", and update the existing GitHub release body with
those notes. The release itself is created by the publish workflow
(GoReleaser or similar); this skill does not create releases — it
only edits an existing one's body.
The output is read by humans deciding whether to sync, product
owners reviewing what changed, and downstream-repo owners deciding
whether the release requires action on their side. The notes are
NOT a raw commit log.
Output Artefacts
- A release notes document in canonical form, written to
/tmp/release-notes.md and applied to the GitHub release via
gh release edit <tag> --notes-file.
- Optionally, a
## ⚠️ Required Action section at the top, when
the release range contains newly-added migration docs.
- A printed one-line summary at the end of the run with the
release URL and the section counts.
The skill's three valid terminal outputs:
A. Notes published. Commits were collected, categorised, and
the GitHub release body was updated. Migration callout was
emitted if applicable.
B. No notable commits. The range contained only merge commits,
automated chores, and version bumps. The skill emits a one-line
notes body ("Internal release — no human-visible changes since
") and updates the release.
C. Release missing. The named tag does not have a corresponding
GitHub release. The skill exits cleanly without creating one — the
publish workflow's responsibility, not this skill's. Emit a clear
diagnosis.
Definitions
skills/definitions/error-handling.md — severity taxonomy for
INVALID_TAG, RELEASE_MISSING, GIT_RANGE_FAILED,
RELEASE_EDIT_FAILED.
skills/definitions/step-skip-rule.md — articulation-as-enforcement.
The "no migration docs added" branch in step 3 is a natural
no-op, not a skip violation.
Dependencies
This skill makes no calls to other skills. It uses git and the
gh CLI directly. No apply-label, set-issue-status,
post-issue-comment, etc.
Steps
The step-skip rule applies. The migration-detection branch in
step 3 is naturally no-op when no migration docs were added; not
a skip.
Inputs. The skill receives two inputs from the caller (a
release workflow):
tag (string, required) — the version tag for the release being
published, e.g. v1.2.3.
repo (string, optional) — the repo to operate on. If omitted,
resolve via gh repo view --json nameWithOwner -q .nameWithOwner.
Hold as <tag> and <repo>.
Section A — Range and commits
Validate the tag exists. Confirm the tag is real:
git tag -l "$tag"
- Empty result → raise
INVALID_TAG (ERROR); exit. The caller
gave a tag that doesn't exist locally.
- Otherwise → continue.
Determine the previous tag. List tags by version, exclude
the current one, take the head:
PREV_TAG=$(git tag --sort=-version:refname | grep -v "^${tag}\$" | head -1)
PREV_TAG empty → this is the first release; the range is
the full history. Note that in step 4's summary line.
PREV_TAG set → the range is ${PREV_TAG}..${tag}.
Detect added migration docs. Scan the release range for
newly-added files matching concepts/migration-*.md or
docs/migration-*.md (case-insensitive on migration to catch
MIGRATION-*.md):
if [ -n "${PREV_TAG}" ]; then
git diff --name-status --diff-filter=A "${PREV_TAG}..${tag}" -- \
'concepts/migration-*.md' 'docs/migration-*.md' \
'docs/MIGRATION-*.md'
else
git log --name-status --diff-filter=A --pretty=format: -- \
'concepts/migration-*.md' 'docs/migration-*.md' \
'docs/MIGRATION-*.md' | sort -u
fi
--diff-filter=A restricts to ADDED files, so edits to an
existing migration doc do not re-emit the callout in every
subsequent release. Hold the result as <added-migrations>.
For each entry, read its first-line # … heading as the title,
for use in the callout in step 5.
Collect commits in the range. Subject and short hash only;
the body (and any Reuse: trailer) is not consumed:
if [ -n "${PREV_TAG}" ]; then
git log --pretty=format:"%s (%h)" "${PREV_TAG}..${tag}"
else
git log --pretty=format:"%s (%h)" "${tag}"
fi
On non-zero exit → raise GIT_RANGE_FAILED (ERROR); exit.
Hold the list as <commits>.
Section B — Categorise and write
Categorise each commit. Conventional-commits prefix decides
the section. Both type: and type(scope): forms are accepted.
| Prefix |
Section |
feat: / feat(...): |
Features |
fix: / fix(...): |
Fixes |
docs: / docs(...): |
Documentation |
chore: / chore(...):, ci:, refactor:, test:, style:, perf:, build: |
Chores (only if notable) |
Merge pull request ... |
Omit |
| Auto-bump / sync / no-prefix junk commits |
Omit |
Specific automated-commit patterns to omit (release notes
are for human-visible changes; these are tooling noise):
Merge pull request #... and Merge branch ...
chore: update TEMPLATE_VERSION ...
chore: update AGENTIC_FRAMEWORK_VERSION ...
chore: sync ... (any sync commit from the framework mount
or a downstream mirror operation)
chore: bump ... (version-bump-only commits)
chore: archive recovery log for #... (the dev-session's
end-of-session bookkeeping commit; not human-visible)
chore: recovery checkpoint — ... (the dev-session's
mid-task breadcrumb commits; not human-visible)
- Commits with no conventional-commits prefix and no clear
human content (e.g.
WIP, fixup, temp)
When in doubt, omit. A release-note bullet for tooling noise
adds nothing for the human reader.
For each retained commit, write a one-sentence, present-tense
bullet that answers "what changed and why it matters" — NOT
"what was the commit message". Examples:
- Commit:
feat: add foreground-recovery skill (#42) →
Bullet: Adds the foreground-recovery skill so humans can interactively diagnose and clear stuck pipeline state.
- Commit:
fix: drop scheduled stage; rename to ready-to-implement →
Bullet: Renames the Requirement-lifecycle stage from "scheduled" to "ready-to-implement" across the Go CLI, the project template, and the workflow YAML.
The agent paraphrases for the human; it does NOT echo the
commit subject verbatim. Read changed files for ambiguous
commits if needed.
Hold per-section bullet lists as <features>, <fixes>,
<docs>, <chores>.
Decide if this is a no-notable-changes release. If every
<commits> entry was filtered (only merges / auto-bumps /
junk), and <added-migrations> is empty → Output B path:
Compose the notes file. Build /tmp/release-notes.md in
the canonical shape:
<One-sentence summary of what this release delivers overall.>
## ⚠️ Required Action ← only when <added-migrations> is non-empty
- <One bullet per added migration doc, linking it at the release tag>
## Features
- <bullet>
- ...
## Fixes
- <bullet>
## Documentation
- <bullet>
## Chores
- <bullet>
Rules:
- The one-sentence summary at the top is mandatory; it is the
human's TL;DR.
- Omit any section whose bullet list is empty.
- The migration-callout placement is fixed: immediately after
the summary, before
## Features. Never fold a migration
link into ## Documentation — it is a required-action
signal, not a doc improvement.
- The release tag and a title MUST NOT appear in the body —
GitHub renders those separately.
- Each migration bullet links at the release tag, e.g.
https://github.com/<repo>/blob/<tag>/<path>.
- Each bullet is one sentence, present tense: "Adds...",
"Renames...", "Removes...", "Fixes...", "Documents...".
Write the composed body to /tmp/release-notes.md. Use the
Write tool — never shell echo/heredoc, since the content
may contain backticks, dollar signs, etc.
Section C — Publish
Confirm the release exists. Before editing:
gh release view "$tag" --repo "$repo" --json url --jq .url
- Non-zero exit / "release not found" → Output C. Surface a
clear diagnosis ("Release
<tag> does not exist on
<repo>. The publish workflow must create the release first;
this skill only edits existing ones.") and exit cleanly with
a RELEASE_MISSING (WARN).
- Otherwise → continue. Hold the URL as
<release-url>.
Update the release body.
gh release edit "$tag" --repo "$repo" \
--notes-file /tmp/release-notes.md
Verifies the edit by re-querying the release body:
gh release view "$tag" --repo "$repo" --json body --jq .body
The returned body must equal what was written. On mismatch or
non-zero exit → raise RELEASE_EDIT_FAILED (ERROR); the
notes file is on disk for the human to apply manually.
Print the summary line. One-line stdout for the workflow
log:
Release notes published: <release-url>
Features: <N> Fixes: <N> Docs: <N> Chores: <N> Migrations: <N>
For Output B (no notable changes):
Release notes published (internal release): <release-url>
For Output C (no release):
Release notes NOT published — release <tag> does not exist on
<repo>. The notes file at /tmp/release-notes.md was generated;
apply manually with `gh release edit <tag> --notes-file ...`
once the release is created.
Error Handling
INVALID_TAG from step 1 (the tag does not exist locally) →
severity ERROR; propagate. Caller bug — they passed a tag
that wasn't created.
GIT_RANGE_FAILED from step 4 (git log non-zero) → severity
ERROR; propagate. Surface the underlying error; the human
investigates.
RELEASE_MISSING from step 8 (no GitHub release for the tag) →
severity WARN. Output C. Notes file remains on disk for manual
application; this is recoverable.
RELEASE_EDIT_FAILED from step 9 (gh release edit failed or
verification mismatch) → severity ERROR. The local notes file
is preserved; surface the manual-apply command in the exit
message so the human can finish.
- All other errors: propagate (default).
Source: eddiecarpenter/gh-agentic — distributed by TomeVault.
1---2name: release-notes-113description: Generates human-readable, well-structured release notes from the git commit history between the current tag and the previous one, then updates the existing GitHub release body with the AI-written notes. Categorises commits as Features / Fixes / Documentation / Chores; detects newly-added migration docs and prepends a `## ⚠️ Required Action` callout linking each one. Use when a release workflow fires on a version tag pushed to main and the release body needs structured notes. Use even when the caller doesn't say "release notes" — phrases like "generate release notes for v1.2.3", "update the release body", "publish notes for the latest tag" should trigger this skill.4---56# Release Notes78## Goal910Take the commits in a release range (current tag → previous tag),11write human-readable notes that answer "what changed and why does12it matter?", and update the existing GitHub release body with13those notes. The release itself is created by the publish workflow14(GoReleaser or similar); this skill does not create releases — it15only edits an existing one's body.1617The output is read by humans deciding whether to sync, product18owners reviewing what changed, and downstream-repo owners deciding19whether the release requires action on their side. The notes are20NOT a raw commit log.2122## Output Artefacts2324- A release notes document in canonical form, written to25 `/tmp/release-notes.md` and applied to the GitHub release via26 `gh release edit <tag> --notes-file`.27- Optionally, a `## ⚠️ Required Action` section at the top, when28 the release range contains newly-added migration docs.29- A printed one-line summary at the end of the run with the30 release URL and the section counts.3132The skill's three valid terminal outputs:3334**A. Notes published.** Commits were collected, categorised, and35the GitHub release body was updated. Migration callout was36emitted if applicable.3738**B. No notable commits.** The range contained only merge commits,39automated chores, and version bumps. The skill emits a one-line40notes body ("Internal release — no human-visible changes since41<prev>") and updates the release.4243**C. Release missing.** The named tag does not have a corresponding44GitHub release. The skill exits cleanly without creating one — the45publish workflow's responsibility, not this skill's. Emit a clear46diagnosis.4748## Definitions4950- `skills/definitions/error-handling.md` — severity taxonomy for51 `INVALID_TAG`, `RELEASE_MISSING`, `GIT_RANGE_FAILED`,52 `RELEASE_EDIT_FAILED`.53- `skills/definitions/step-skip-rule.md` — articulation-as-enforcement.54 The "no migration docs added" branch in step 3 is a natural55 no-op, not a skip violation.5657## Dependencies5859This skill makes no calls to other skills. It uses `git` and the60`gh` CLI directly. No `apply-label`, `set-issue-status`,61`post-issue-comment`, etc.6263## Steps6465The **step-skip rule** applies. The migration-detection branch in66step 3 is naturally no-op when no migration docs were added; not67a skip.6869**Inputs.** The skill receives two inputs from the caller (a70release workflow):7172- `tag` (string, required) — the version tag for the release being73 published, e.g. `v1.2.3`.74- `repo` (string, optional) — the repo to operate on. If omitted,75 resolve via `gh repo view --json nameWithOwner -q .nameWithOwner`.7677Hold as `<tag>` and `<repo>`.7879---8081### Section A — Range and commits82831. **Validate the tag exists.** Confirm the tag is real:8485 ```bash86 git tag -l "$tag"87 ```8889 - Empty result → raise `INVALID_TAG` (`ERROR`); exit. The caller90 gave a tag that doesn't exist locally.91 - Otherwise → continue.92932. **Determine the previous tag.** List tags by version, exclude94 the current one, take the head:9596 ```bash97 PREV_TAG=$(git tag --sort=-version:refname | grep -v "^${tag}\$" | head -1)98 ```99100 - `PREV_TAG` empty → this is the first release; the range is101 the full history. Note that in step 4's summary line.102 - `PREV_TAG` set → the range is `${PREV_TAG}..${tag}`.1031043. **Detect added migration docs.** Scan the release range for105 newly-added files matching `concepts/migration-*.md` or106 `docs/migration-*.md` (case-insensitive on `migration` to catch107 `MIGRATION-*.md`):108109 ```bash110 if [ -n "${PREV_TAG}" ]; then111 git diff --name-status --diff-filter=A "${PREV_TAG}..${tag}" -- \112 'concepts/migration-*.md' 'docs/migration-*.md' \113 'docs/MIGRATION-*.md'114 else115 git log --name-status --diff-filter=A --pretty=format: -- \116 'concepts/migration-*.md' 'docs/migration-*.md' \117 'docs/MIGRATION-*.md' | sort -u118 fi119 ```120121 `--diff-filter=A` restricts to ADDED files, so edits to an122 existing migration doc do not re-emit the callout in every123 subsequent release. Hold the result as `<added-migrations>`.124125 For each entry, read its first-line `# …` heading as the title,126 for use in the callout in step 5.1271284. **Collect commits in the range.** Subject and short hash only;129 the body (and any `Reuse:` trailer) is not consumed:130131 ```bash132 if [ -n "${PREV_TAG}" ]; then133 git log --pretty=format:"%s (%h)" "${PREV_TAG}..${tag}"134 else135 git log --pretty=format:"%s (%h)" "${tag}"136 fi137 ```138139 On non-zero exit → raise `GIT_RANGE_FAILED` (`ERROR`); exit.140141 Hold the list as `<commits>`.142143---144145### Section B — Categorise and write1461475. **Categorise each commit.** Conventional-commits prefix decides148 the section. Both `type:` and `type(scope):` forms are accepted.149150 | Prefix | Section |151 |---|---|152 | `feat:` / `feat(...):` | Features |153 | `fix:` / `fix(...):` | Fixes |154 | `docs:` / `docs(...):` | Documentation |155 | `chore:` / `chore(...):`, `ci:`, `refactor:`, `test:`, `style:`, `perf:`, `build:` | Chores (only if notable) |156 | `Merge pull request ...` | Omit |157 | Auto-bump / sync / no-prefix junk commits | Omit |158159 **Specific automated-commit patterns to omit** (release notes160 are for human-visible changes; these are tooling noise):161162 - `Merge pull request #...` and `Merge branch ...`163 - `chore: update TEMPLATE_VERSION ...`164 - `chore: update AGENTIC_FRAMEWORK_VERSION ...`165 - `chore: sync ...` (any sync commit from the framework mount166 or a downstream mirror operation)167 - `chore: bump ...` (version-bump-only commits)168 - `chore: archive recovery log for #...` (the dev-session's169 end-of-session bookkeeping commit; not human-visible)170 - `chore: recovery checkpoint — ...` (the dev-session's171 mid-task breadcrumb commits; not human-visible)172 - Commits with no conventional-commits prefix and no clear173 human content (e.g. `WIP`, `fixup`, `temp`)174175 When in doubt, omit. A release-note bullet for tooling noise176 adds nothing for the human reader.177178 For each retained commit, write a one-sentence, present-tense179 bullet that answers "what changed and why it matters" — NOT180 "what was the commit message". Examples:181182 - Commit: `feat: add foreground-recovery skill (#42)` →183 Bullet: `Adds the foreground-recovery skill so humans can184 interactively diagnose and clear stuck pipeline state.`185 - Commit: `fix: drop scheduled stage; rename to ready-to-implement` →186 Bullet: `Renames the Requirement-lifecycle stage from187 "scheduled" to "ready-to-implement" across the Go CLI, the188 project template, and the workflow YAML.`189190 The agent paraphrases for the human; it does NOT echo the191 commit subject verbatim. Read changed files for ambiguous192 commits if needed.193194 Hold per-section bullet lists as `<features>`, `<fixes>`,195 `<docs>`, `<chores>`.1961976. **Decide if this is a no-notable-changes release.** If every198 `<commits>` entry was filtered (only merges / auto-bumps /199 junk), and `<added-migrations>` is empty → Output B path:200201 - Compose a one-line body:202 ```203 Internal release — no human-visible changes since <PREV_TAG>.204 ```205 - Skip to step 8.2062077. **Compose the notes file.** Build `/tmp/release-notes.md` in208 the canonical shape:209210 ```markdown211 <One-sentence summary of what this release delivers overall.>212213 ## ⚠️ Required Action ← only when <added-migrations> is non-empty214 - <One bullet per added migration doc, linking it at the release tag>215216 ## Features217 - <bullet>218 - ...219220 ## Fixes221 - <bullet>222223 ## Documentation224 - <bullet>225226 ## Chores227 - <bullet>228 ```229230 **Rules:**231 - The one-sentence summary at the top is mandatory; it is the232 human's TL;DR.233 - Omit any section whose bullet list is empty.234 - The migration-callout placement is fixed: immediately after235 the summary, before `## Features`. Never fold a migration236 link into `## Documentation` — it is a required-action237 signal, not a doc improvement.238 - The release tag and a title MUST NOT appear in the body —239 GitHub renders those separately.240 - Each migration bullet links at the release tag, e.g.241 `https://github.com/<repo>/blob/<tag>/<path>`.242 - Each bullet is one sentence, present tense: "Adds...",243 "Renames...", "Removes...", "Fixes...", "Documents...".244245 Write the composed body to `/tmp/release-notes.md`. Use the246 `Write` tool — never shell `echo`/heredoc, since the content247 may contain backticks, dollar signs, etc.248249---250251### Section C — Publish2522538. **Confirm the release exists.** Before editing:254255 ```bash256 gh release view "$tag" --repo "$repo" --json url --jq .url257 ```258259 - Non-zero exit / "release not found" → Output C. Surface a260 clear diagnosis ("Release `<tag>` does not exist on261 `<repo>`. The publish workflow must create the release first;262 this skill only edits existing ones.") and exit cleanly with263 a `RELEASE_MISSING` (`WARN`).264 - Otherwise → continue. Hold the URL as `<release-url>`.2652669. **Update the release body.**267268 ```bash269 gh release edit "$tag" --repo "$repo" \270 --notes-file /tmp/release-notes.md271 ```272273 Verifies the edit by re-querying the release body:274275 ```bash276 gh release view "$tag" --repo "$repo" --json body --jq .body277 ```278279 The returned body must equal what was written. On mismatch or280 non-zero exit → raise `RELEASE_EDIT_FAILED` (`ERROR`); the281 notes file is on disk for the human to apply manually.28228310. **Print the summary line.** One-line stdout for the workflow284 log:285286 ```287 Release notes published: <release-url>288 Features: <N> Fixes: <N> Docs: <N> Chores: <N> Migrations: <N>289 ```290291 For Output B (no notable changes):292 ```293 Release notes published (internal release): <release-url>294 ```295296 For Output C (no release):297 ```298 Release notes NOT published — release <tag> does not exist on299 <repo>. The notes file at /tmp/release-notes.md was generated;300 apply manually with `gh release edit <tag> --notes-file ...`301 once the release is created.302 ```303304## Error Handling305306- `INVALID_TAG` from step 1 (the tag does not exist locally) →307 severity `ERROR`; propagate. Caller bug — they passed a tag308 that wasn't created.309- `GIT_RANGE_FAILED` from step 4 (`git log` non-zero) → severity310 `ERROR`; propagate. Surface the underlying error; the human311 investigates.312- `RELEASE_MISSING` from step 8 (no GitHub release for the tag) →313 severity `WARN`. Output C. Notes file remains on disk for manual314 application; this is recoverable.315- `RELEASE_EDIT_FAILED` from step 9 (`gh release edit` failed or316 verification mismatch) → severity `ERROR`. The local notes file317 is preserved; surface the manual-apply command in the exit318 message so the human can finish.319- All other errors: propagate (default).320321---322> Source: [eddiecarpenter/gh-agentic](https://github.com/eddiecarpenter/gh-agentic) — distributed by [TomeVault](https://tomevault.io).323<!-- tomevault:4.0:skill_md:2026-05-22 -->