release
Cuts a versioned release: pick the next semantic version, write proper release notes, tag the commit, and publish the GitHub release.
This sits next to /gh-bootstrap and /gh and answers a different question. /gh-bootstrap wires the release plumbing once (.github/release.yml, the tag-driven workflow). /gh is the raw gh release command reference. This skill is the ceremony you run each time you ship: decide the bump, draft the notes, tag, publish.
Where this skill fits
| Concern | Skill |
|---|---|
| One-time release-notes config + tag-driven workflow | /gh-bootstrap |
Raw per-command gh release create / view / upload |
/gh |
| Deciding the version, drafting notes, tagging, publishing | /release (this skill) |
| Local commit before any of this | /plate |
If the repo has a tag-driven release workflow (from /gh-bootstrap), pushing the tag is enough — the workflow publishes the release. This skill still decides the version and (optionally) drafts curated notes; it just stops after the tag push and reports that the workflow takes over. Otherwise it publishes the release itself with gh release create.
Protocol
1. Pre-flight
Refuse to release from a dirty or diverged state — a release tag is permanent and public.
git fetch --tags origin
BRANCH=$(git branch --show-current)
DEFAULT=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name)
git diff-index --quiet HEAD || { echo "uncommitted changes — commit or stash first"; exit 1; }
[ "$BRANCH" = "$DEFAULT" ] || echo "WARNING: not on $DEFAULT (on $BRANCH) — releases normally tag the default branch"
read -r AHEAD BEHIND < <(git rev-list --left-right --count "origin/$DEFAULT...HEAD")
[ "$BEHIND" = 0 ] || { echo "HEAD is $BEHIND commits behind origin/$DEFAULT — pull/rebase before tagging"; exit 1; }
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
Confirm with the user:
- The commit to tag (default: tip of the default branch).
- The last release tag (
$LAST_TAG) — the baseline for the version bump and the notes range. If the repo has no tags yet, this is the first release.
Stop and tell the user — don't tag — if the tree is dirty, or HEAD is behind origin/$DEFAULT.
2. Decide the version
Read references/semver.md for the full decision rules. The short version: scan the Conventional Commits since the last tag and map the highest-impact change to a bump.
git log "${LAST_TAG:+$LAST_TAG..}HEAD" --no-merges --pretty='%s'
| Highest-impact change since last tag | Bump (≥ 1.0.0) | Bump (0.x pre-1.0) |
|---|---|---|
feat!: / fix!: / BREAKING CHANGE: footer |
major | minor |
feat: |
minor | patch |
fix: / perf: |
patch | patch |
only docs / chore / refactor / test / ci |
patch (or skip — ask) | patch |
Match the repo's existing tag scheme — if $LAST_TAG is v1.2.3, the next is v1.3.0, not 1.3.0. Detect and reuse the v prefix (or its absence). Detect prerelease identifiers (-rc.1, -beta.2) and keep the user's convention.
Propose the computed version and the one-line reason ("3 feats, no breaking → minor → v1.3.0"). Let the user confirm or override. Never tag a version the user hasn't seen.
3. Draft the release notes
Read references/release-notes.md for the three strategies and how to choose. Summary:
- Auto-generated (
gh release create --generate-notes) — best when the repo merges via PRs and has.github/release.yml. GitHub groups merged PRs into the configured category buckets by label. Cheapest, and complete as long as every change landed as a labelled PR. - Hand-curated — best when there's no
.github/release.yml, when commits (not PRs) are the unit of change, or when the release needs editorial highlights. Group by Conventional Commit type; lead with a Highlights section and, for any breaking change, an Upgrade notes section with the concrete migration step.assets/release-notes-template.mdis the starting structure. - Hybrid — generate, then hand-edit the top to add highlights and upgrade notes. The most common choice for a release that matters.
When curating, derive the entries from the commit log, not from imagination — every bullet must trace to a real commit. Link PRs/commits where the audience would want the detail.
4. Update CHANGELOG.md (if the repo keeps one)
If CHANGELOG.md exists, keep it in sync. Most repos follow Keep a Changelog: move the ## [Unreleased] entries under a new ## [VERSION] - YYYY-MM-DD heading, leave a fresh empty Unreleased, and update the comparison links at the bottom. If there's no changelog and the user doesn't want one, skip this — don't introduce a changelog they didn't ask for.
5. Tag and push
Annotated tag (carries a message, author, and date — lightweight tags don't, and git describe treats them differently). Tag the agreed commit, then push only the tag.
VERSION=v1.3.0
git tag -a "$VERSION" -m "$VERSION" # add -s to GPG-sign if the repo signs tags
git push origin "$VERSION"
If a tag-driven release workflow exists (.github/workflows/release.yml), stop here — the workflow publishes the release on tag push. Report the workflow run URL and skip step 6.
6. Publish the GitHub release
Only when there's no tag-driven workflow doing it for you.
# Auto-generated notes:
gh release create "$VERSION" --title "$VERSION" --generate-notes --latest
# Curated notes from a file:
gh release create "$VERSION" --title "$VERSION" --notes-file NOTES.md --latest
# Prerelease (rc/beta/alpha) — do NOT mark --latest:
gh release create "$VERSION" --title "$VERSION" --notes-file NOTES.md --prerelease
# Draft for review before going public:
gh release create "$VERSION" --title "$VERSION" --notes-file NOTES.md --draft
Attach build artifacts by listing them after the tag (gh release create "$VERSION" ... dist/*.tar.gz) when the project ships binaries.
7. Verify and report
Read the published state back — don't claim success from the create command's exit code alone.
gh release view "$VERSION" --json tagName,isLatest,isDraft,isPrerelease,url --jq .
git ls-remote --tags origin "$VERSION"
Report to the user: the version and why, the notes strategy used, the release URL, and whether it's draft/prerelease/latest. Call out anything skipped (e.g. "workflow is publishing — release not yet visible") and why.
What this skill is NOT for
- Scaffolding
.github/release.ymlor the release workflow — that's/gh-bootstrap(run once, first). - Looking up raw
gh releaseflags outside a release ceremony — that's/gh. - Committing or pushing branch code — that's
/plate. - Publishing to a package registry (npm / crates.io / PyPI). This skill tags and creates the GitHub release; registry publishing is the project's own
just/CI step. - Per-package versioning in a monorepo with independent release lines — out of scope; this skill cuts one tag for one version line. Say so if the repo is a multi-package monorepo.
Gotchas
- Never re-point an existing tag.
git tag -f+git push -fto move a published tag breaks every consumer who already fetched it. If you tagged the wrong commit, cut a new patch version instead. - Auto-generated notes only see merged PRs. Direct pushes to the default branch don't appear. A repo without PR-based merges (no branch protection) should curate notes, not rely on
--generate-notes. --generate-notesneeds.github/release.ymlto group nicely; without it you get a flat "What's Changed" list. If the grouping looks wrong, the categories/labels inrelease.ymlare the lever — fix them via/gh-bootstrap, not here.- Prereleases must not be
--latest. Marking an-rcbuild as latest makes it the default download and the target ofreleases/latest. Use--prereleaseand omit--latest. - Pre-1.0 semver is different. Under
0.x, breaking changes bump the minor, not the major. Seereferences/semver.md. - Tag must be an ancestor of the default branch. Tagging a feature-branch commit ships unreviewed code. Confirm the commit is on (or merged into) the default branch before tagging.
- Signed tags — if the repo signs releases (
git config tag.gpgSign trueor a project convention), usegit tag -s; an unsigned tag on a signed-release repo fails verification downstream.