GitHub release
Release a single-package GitHub repository by reading tags and public diffs, selecting the next SemVer version, updating CHANGELOG.md, creating a release branch, and opening a pull request with gh. Keep steps 1 through 4 read-only; write only after the version is confirmed.
When to invoke
- "Cut a new GitHub release."
- "Bump the version and generate a changelog."
- "Create a release branch and PR."
- "Let's ship a new version."
- "Publish a new version from this repository."
Prerequisites and context
- Requires
gh authenticated with gh auth status and a working git checkout inside a GitHub repository.
- Verify
gh repo view --json nameWithOwner succeeds before release work.
- Start from a clean working tree unless the user explicitly directs how to handle local changes.
- Ask for the public-facing source directory once and store it as
PUBLIC_PATH; examples include src/, lib/, pkg/, or cmd/; if empty, use ..
- Exclude
tests/, test/, spec/, __tests__/, docs/, *.lock, *-lock.json, *.sum, generated files with a do-not-edit header, and build artifacts from public API classification.
Procedure
Ensure main is current:
git checkout main
git pull origin main
Fetch and identify the latest version tag from git tags, not gh release list:
git fetch --tags
PREV_TAG=$(git tag --sort=-version:refname | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+' | head -1)
echo "Latest tag: $PREV_TAG"
git ls-remote --tags origin | grep "refs/tags/$PREV_TAG$"
PREV_SHA=$(git rev-list -n 1 "$PREV_TAG" 2>/dev/null || git rev-list --max-parents=0 HEAD)
PREV_TAG must preserve the tag spelling exactly, for example v1.4.2, while arithmetic strips a leading v. If no tag exists, set PREV_TAG to (none), set PREV_SHA to git rev-list --max-parents=0 HEAD, default NEXT_VERSION to 1.0.0, and skip SemVer arithmetic. If the tag is local-only or orphaned, warn before continuing.
Analyze the code diff as the primary signal:
git diff "$PREV_SHA"..HEAD -- "$PUBLIC_PATH" \
':(exclude)tests/' ':(exclude)test/' ':(exclude)spec/' \
':(exclude)__tests__/' ':(exclude)docs/' \
':(exclude)*.lock' ':(exclude)*-lock.json' ':(exclude)*.sum'
If the diff is huge, triage with git diff "$PREV_SHA"..HEAD --stat -- "$PUBLIC_PATH" and then focus on public interface files such as index.*, api.*, exports.*, public.*, mod.*, and __init__.*.
Read commit intent as the secondary signal:
git log "$PREV_SHA"..HEAD --oneline --no-merges
Use the bundled references/commit-classification.md only to interpret messages such as feat: new API, fix: typo, chore: refactor, or a one-line security fix whose intent is not self-explanatory from code; prefer the code diff when signals conflict.
Determine the highest SemVer bump and present the proposed NEXT_VERSION with evidence. Ask for confirmation before writing. Compute from MAJOR.MINOR.PATCH and format as vMAJOR.MINOR.PATCH; highest precedence wins: MAJOR > MINOR > PATCH.
Create and push the release branch only after confirmation:
git checkout -b release/vX.Y.Z
git push -u origin release/vX.Y.Z
Update or create CHANGELOG.md in Keep a Changelog format using today's YYYY-MM-DD date, show the proposed section to the user, then write it. Add or update the comparison link: https://github.com/OWNER/REPO/compare/vPREV...vNEXT.
Commit and push:
git add CHANGELOG.md
git commit -m "chore: release vX.Y.Z"
git push origin release/vX.Y.Z
Open the release PR. Always use --body-file, not inline --body; inline \n can render literally in PowerShell. Create release_pr_body.md or an equivalent repo-local scratch file, then run:
gh pr create --base main --head release/vX.Y.Z --title "Release vX.Y.Z" --body-file release_pr_body.md
Hand off tagging after merge:
git tag vX.Y.Z <merge-commit-sha>
git push origin vX.Y.Z
SemVer classification
| Evidence from public diff |
Bump |
Changelog section |
| Removed symbols, changed signatures, or breaking behavior changes |
MAJOR |
Removed or Changed with breaking note |
New exported symbols or user-visible features such as NewClient or WithTimeout in src/client.go |
MINOR |
Added |
| Bug/logic fix, off-by-one correction, performance improvement, security fix, docs, chore only |
PATCH |
Fixed or Security; omit purely internal work |
| No commits since last tag |
none |
Report nothing to release |
When a commit says fix: typo but the diff removes a public method, classify as MAJOR. When a commit says feat: new API but only private internals changed, classify as PATCH. Document every conflict in the changelog review.
Treat HTTP API surface changes as public when the library exposes handlers, middleware, generated clients, or route contracts. Keep PATH changes only when they alter public CLI behavior.
Changelog rules
Insert directly below # Changelog:
## [X.Y.Z] - YYYY-MM-DD
### Added
- ...
### Changed
- ...
### Deprecated
- ...
### Removed
- ...
### Fixed
- ...
### Security
- ...
Omit empty headings. Write plain English from the user's perspective, not raw commit messages. Map a new exported symbol to Added, a breaking removal to Removed, a breaking existing API change to Changed, a bug or perf fix to Fixed, and a security fix to Security.
PowerShell equivalents
Use PowerShell-safe forms on Windows:
git fetch --tags
$prevTag = git tag --sort='-version:refname' | Select-String '^[vV]?\d+\.\d+\.\d+' | Select-Object -First 1 -ExpandProperty Line
if ($prevTag) { $prevSha = git rev-list -n 1 $prevTag } else { $prevSha = git rev-list --max-parents=0 HEAD }
git diff "$($prevSha)..HEAD" -- $publicPath ':(exclude)tests/' ':(exclude)test/' ':(exclude)spec/' ':(exclude)__tests__/' ':(exclude)docs/' ':(exclude)*.lock' ':(exclude)*-lock.json' ':(exclude)*.sum'
For the PR body, use a here-string, write with Out-File -FilePath release_pr_body.md -Encoding utf8 -NoNewline, and pass --body-file release_pr_body.md. If gh usage appears unexpectedly, check Get-Command gh, gh --version, git fetch --tags, and git diff --name-only $prevSha..HEAD -- src/.
Progressive disclosure and bundled resources
Read bundled references only when the release decision needs deeper rules:
references/semver-rules.md: extended SemVer edge cases for MAJOR, MINOR, and PATCH.
references/commit-classification.md: commit-message heuristics for secondary signal classification.
Troubleshooting
| Situation |
Resolution |
gh auth status fails |
Stop and tell the user to run gh auth login. |
| Not inside a git repo |
Stop and tell the user to cd into the repository. |
| Working tree is dirty |
Warn and ask whether to stash, commit, or abort. |
| No commits since last tag |
Tell the user there is nothing to release. |
| Tag exists locally but not remotely |
Warn that the tag appears local-only and ask whether to push or continue. |
| Tag points to no commit |
Use git rev-list --max-parents=0 HEAD as the fallback diff base and warn. |
Diff is empty for PUBLIC_PATH but commits exist |
Warn that changes may be internal and ask whether to proceed. |
git push fails |
Report the error verbatim and suggest checking protected branch rules. |
Gotchas
- Do not use
gh release list as source of truth: releases are optional; tags are authoritative.
- Do not create
release/vX.Y.Z before version confirmation: branch names must match the final version.
- Do not use inline
--body for multiline PR text: use --body-file so markdown line breaks survive Bash and PowerShell.
- Do not classify from commits alone: code diff is primary; commit log is context.
- IMPORTANT defaults: represent no previous tag as
(none), default the first release to 1.0.0, and use git tag only after the release PR merges.
Output template
## GitHub release result
**Status:** PR opened | ready for PR | blocked
**Previous tag:** `<PREV_TAG>`
**Previous SHA:** `<PREV_SHA>`
**Next version:** `<NEXT_VERSION>`
**Public path:** `<PUBLIC_PATH>`
### Classification
| Evidence | Bump impact | Changelog entry |
| --- | --- | --- |
| `<file or commit evidence>` | `MAJOR | MINOR | PATCH` | `<entry or omitted>` |
### Commands run
- `gh auth status`
- `gh repo view --json nameWithOwner`
- `git status`
- `git fetch --tags`
- `git diff <PREV_SHA>..HEAD -- <PUBLIC_PATH>`
- `git log <PREV_SHA>..HEAD --oneline --no-merges`
- `git checkout -b release/vX.Y.Z`
- `git push -u origin release/vX.Y.Z`
- `gh pr create --base main --head release/vX.Y.Z --title "Release vX.Y.Z" --body-file release_pr_body.md`
### Handoff
After merge, create the tag:
`git tag vX.Y.Z <merge-commit-sha>`
`git push origin vX.Y.Z`
Quality gate
References
1---2name: github-release3description: Run an end-to-end GitHub library release workflow with git and gh: inspect tags, classify public API changes, choose a SemVer bump, update CHANGELOG.md, create release/vX.Y.Z, push, and open a release PR. Use when asked to cut a release, bump a version, generate a changelog, create a release branch, or publish a new GitHub repository version.4---56<!-- Generated from harness/github-copilot/skills/github-release/SKILL.md by harness/claude-code/scripts/convert_from_copilot.py. Edit the source, not this file. -->78# GitHub release910Release a single-package GitHub repository by reading tags and public diffs, selecting the next SemVer version, updating `CHANGELOG.md`, creating a release branch, and opening a pull request with `gh`. Keep steps 1 through 4 read-only; write only after the version is confirmed.1112## When to invoke1314- "Cut a new GitHub release."15- "Bump the version and generate a changelog."16- "Create a release branch and PR."17- "Let's ship a new version."18- "Publish a new version from this repository."1920## Prerequisites and context2122- Requires `gh` authenticated with `gh auth status` and a working `git` checkout inside a GitHub repository.23- Verify `gh repo view --json nameWithOwner` succeeds before release work.24- Start from a clean working tree unless the user explicitly directs how to handle local changes.25- Ask for the public-facing source directory once and store it as `PUBLIC_PATH`; examples include `src/`, `lib/`, `pkg/`, or `cmd/`; if empty, use `.`.26- Exclude `tests/`, `test/`, `spec/`, `__tests__/`, `docs/`, `*.lock`, `*-lock.json`, `*.sum`, generated files with a do-not-edit header, and build artifacts from public API classification.2728## Procedure29301. Ensure `main` is current:3132 ```bash33 git checkout main34 git pull origin main35 ```36372. Fetch and identify the latest version tag from git tags, not `gh release list`:3839 ```bash40 git fetch --tags41 PREV_TAG=$(git tag --sort=-version:refname | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+' | head -1)42 echo "Latest tag: $PREV_TAG"43 git ls-remote --tags origin | grep "refs/tags/$PREV_TAG$"44 PREV_SHA=$(git rev-list -n 1 "$PREV_TAG" 2>/dev/null || git rev-list --max-parents=0 HEAD)45 ```4647 `PREV_TAG` must preserve the tag spelling exactly, for example `v1.4.2`, while arithmetic strips a leading `v`. If no tag exists, set `PREV_TAG` to `(none)`, set `PREV_SHA` to `git rev-list --max-parents=0 HEAD`, default `NEXT_VERSION` to `1.0.0`, and skip SemVer arithmetic. If the tag is local-only or orphaned, warn before continuing.48493. Analyze the code diff as the primary signal:5051 ```bash52 git diff "$PREV_SHA"..HEAD -- "$PUBLIC_PATH" \53 ':(exclude)tests/' ':(exclude)test/' ':(exclude)spec/' \54 ':(exclude)__tests__/' ':(exclude)docs/' \55 ':(exclude)*.lock' ':(exclude)*-lock.json' ':(exclude)*.sum'56 ```5758 If the diff is huge, triage with `git diff "$PREV_SHA"..HEAD --stat -- "$PUBLIC_PATH"` and then focus on public interface files such as `index.*`, `api.*`, `exports.*`, `public.*`, `mod.*`, and `__init__.*`.59604. Read commit intent as the secondary signal:6162 ```bash63 git log "$PREV_SHA"..HEAD --oneline --no-merges64 ```6566 Use the bundled `references/commit-classification.md` only to interpret messages such as `feat: new API`, `fix: typo`, `chore: refactor`, or a one-line security fix whose intent is not self-explanatory from code; prefer the code diff when signals conflict.67685. Determine the highest SemVer bump and present the proposed `NEXT_VERSION` with evidence. Ask for confirmation before writing. Compute from `MAJOR.MINOR.PATCH` and format as `vMAJOR.MINOR.PATCH`; highest precedence wins: `MAJOR > MINOR > PATCH`.69706. Create and push the release branch only after confirmation:7172 ```bash73 git checkout -b release/vX.Y.Z74 git push -u origin release/vX.Y.Z75 ```76777. Update or create `CHANGELOG.md` in Keep a Changelog format using today's `YYYY-MM-DD` date, show the proposed section to the user, then write it. Add or update the comparison link: `https://github.com/OWNER/REPO/compare/vPREV...vNEXT`.78798. Commit and push:8081 ```bash82 git add CHANGELOG.md83 git commit -m "chore: release vX.Y.Z"84 git push origin release/vX.Y.Z85 ```86879. Open the release PR. Always use `--body-file`, not inline `--body`; inline `\n` can render literally in PowerShell. Create `release_pr_body.md` or an equivalent repo-local scratch file, then run:8889 ```bash90 gh pr create --base main --head release/vX.Y.Z --title "Release vX.Y.Z" --body-file release_pr_body.md91 ```929310. Hand off tagging after merge:9495 ```bash96 git tag vX.Y.Z <merge-commit-sha>97 git push origin vX.Y.Z98 ```99100## SemVer classification101102| Evidence from public diff | Bump | Changelog section |103| --- | --- | --- |104| Removed symbols, changed signatures, or breaking behavior changes | `MAJOR` | `Removed` or `Changed` with breaking note |105| New exported symbols or user-visible features such as `NewClient` or `WithTimeout` in `src/client.go` | `MINOR` | `Added` |106| Bug/logic fix, off-by-one correction, performance improvement, security fix, docs, chore only | `PATCH` | `Fixed` or `Security`; omit purely internal work |107| No commits since last tag | none | Report nothing to release |108109When a commit says `fix: typo` but the diff removes a public method, classify as `MAJOR`. When a commit says `feat: new API` but only private internals changed, classify as `PATCH`. Document every conflict in the changelog review.110111Treat HTTP API surface changes as public when the library exposes handlers, middleware, generated clients, or route contracts. Keep `PATH` changes only when they alter public CLI behavior.112113## Changelog rules114115Insert directly below `# Changelog`:116117```markdown118## [X.Y.Z] - YYYY-MM-DD119120### Added121- ...122123### Changed124- ...125126### Deprecated127- ...128129### Removed130- ...131132### Fixed133- ...134135### Security136- ...137```138139Omit empty headings. Write plain English from the user's perspective, not raw commit messages. Map a new exported symbol to Added, a breaking removal to Removed, a breaking existing API change to Changed, a bug or perf fix to Fixed, and a security fix to Security.140141## PowerShell equivalents142143Use PowerShell-safe forms on Windows:144145```PowerShell146git fetch --tags147$prevTag = git tag --sort='-version:refname' | Select-String '^[vV]?\d+\.\d+\.\d+' | Select-Object -First 1 -ExpandProperty Line148if ($prevTag) { $prevSha = git rev-list -n 1 $prevTag } else { $prevSha = git rev-list --max-parents=0 HEAD }149git diff "$($prevSha)..HEAD" -- $publicPath ':(exclude)tests/' ':(exclude)test/' ':(exclude)spec/' ':(exclude)__tests__/' ':(exclude)docs/' ':(exclude)*.lock' ':(exclude)*-lock.json' ':(exclude)*.sum'150```151152For the PR body, use a here-string, write with `Out-File -FilePath release_pr_body.md -Encoding utf8 -NoNewline`, and pass `--body-file release_pr_body.md`. If `gh` usage appears unexpectedly, check `Get-Command gh`, `gh --version`, `git fetch --tags`, and `git diff --name-only $prevSha..HEAD -- src/`.153154## Progressive disclosure and bundled resources155156Read bundled references only when the release decision needs deeper rules:157158- `references/semver-rules.md`: extended SemVer edge cases for `MAJOR`, `MINOR`, and `PATCH`.159- `references/commit-classification.md`: commit-message heuristics for secondary signal classification.160161## Troubleshooting162163| Situation | Resolution |164| --- | --- |165| `gh auth status` fails | Stop and tell the user to run `gh auth login`. |166| Not inside a git repo | Stop and tell the user to `cd` into the repository. |167| Working tree is dirty | Warn and ask whether to stash, commit, or abort. |168| No commits since last tag | Tell the user there is nothing to release. |169| Tag exists locally but not remotely | Warn that the tag appears local-only and ask whether to push or continue. |170| Tag points to no commit | Use `git rev-list --max-parents=0 HEAD` as the fallback diff base and warn. |171| Diff is empty for `PUBLIC_PATH` but commits exist | Warn that changes may be internal and ask whether to proceed. |172| `git push` fails | Report the error verbatim and suggest checking protected branch rules. |173174## Gotchas175176- **Do not use `gh release list` as source of truth**: releases are optional; tags are authoritative.177- **Do not create `release/vX.Y.Z` before version confirmation**: branch names must match the final version.178- **Do not use inline `--body` for multiline PR text**: use `--body-file` so markdown line breaks survive Bash and PowerShell.179- **Do not classify from commits alone**: code diff is primary; commit log is context.180- **IMPORTANT** defaults: represent no previous tag as `(none)`, default the first release to `1.0.0`, and use `git tag` only after the release PR merges.181182## Output template183184```markdown185## GitHub release result186187**Status:** PR opened | ready for PR | blocked188**Previous tag:** `<PREV_TAG>`189**Previous SHA:** `<PREV_SHA>`190**Next version:** `<NEXT_VERSION>`191**Public path:** `<PUBLIC_PATH>`192193### Classification194| Evidence | Bump impact | Changelog entry |195| --- | --- | --- |196| `<file or commit evidence>` | `MAJOR | MINOR | PATCH` | `<entry or omitted>` |197198### Commands run199- `gh auth status`200- `gh repo view --json nameWithOwner`201- `git status`202- `git fetch --tags`203- `git diff <PREV_SHA>..HEAD -- <PUBLIC_PATH>`204- `git log <PREV_SHA>..HEAD --oneline --no-merges`205- `git checkout -b release/vX.Y.Z`206- `git push -u origin release/vX.Y.Z`207- `gh pr create --base main --head release/vX.Y.Z --title "Release vX.Y.Z" --body-file release_pr_body.md`208209### Handoff210After merge, create the tag:211`git tag vX.Y.Z <merge-commit-sha>`212`git push origin vX.Y.Z`213```214215## Quality gate216217- [ ] `gh auth status`, `gh repo view --json nameWithOwner`, and `git status` were checked before changes.218- [ ] `PREV_TAG`, `PREV_SHA`, `PUBLIC_PATH`, and `NEXT_VERSION` are recorded.219- [ ] Public diff and commit log were both read; conflicts favor code diff.220- [ ] The proposed SemVer bump cites concrete public API evidence.221- [ ] `CHANGELOG.md` follows Keep a Changelog, omits empty sections, and includes the compare URL.222- [ ] The release branch is named `release/vX.Y.Z` and was created only after confirmation.223- [ ] The PR uses `--body-file` and includes the changelog section or a clear placeholder.224- [ ] The final handoff tells the user to tag the merge commit and push the tag.225226## References227228- [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)229- [GitHub compare URL pattern](https://github.com/OWNER/REPO/compare/vPREV...vNEXT)