What I do
- Create an annotated git tag for the newest versioned release in
CHANGELOG.md - Create a GitHub prerelease with notes copied from that same changelog entry
- Treat the first versioned section below
## [Unreleased]as the release to publish - Preserve the release section subsections and bullets exactly as written in the changelog
- Preserve the repo's
### Miscsubsection at the end of release notes when present - Append a
## Full Changelogsection that compares the previous release to the new tag - Handle first-release repositories by comparing the default branch's first commit to the new release tag
When to use me
Use this skill when:
- You already ran
changelog-bump-verand now want to publish that version as a git tag and GitHub prerelease - The repository uses
CHANGELOG.mdwith Keep a Changelog style version headings - You want prerelease notes to come directly from the latest versioned changelog section
Do not use this skill when:
CHANGELOG.mdstill only has unreleased notes and no new versioned section yet; runchangelog-bump-verfirst- You want this workflow to rewrite
CHANGELOG.md - You want to publish a full GitHub release instead of a prerelease
Prerequisites
Required:
- Must be a git repository
CHANGELOG.mdexists at the repository rootghis installed and authenticatedoriginremote exists- The working tree is clean
HEADis on the default branchCHANGELOG.mdcontains exactly one## [Unreleased]sectionCHANGELOG.mdcontains at least one versioned release heading below## [Unreleased]
Expected workflow:
- Run
changelog-bump-verfirst so the newest release already exists as a versioned changelog section - Then run
gh-releaseto publish that version as a prerelease
Validation checks:
# Check git repository
git rev-parse --git-dir
# Check changelog exists
[ -f CHANGELOG.md ]
# Check GitHub CLI auth
gh auth status
# Check origin remote exists
git remote get-url origin
# Check working tree is clean
git status --short
# Detect default branch from GitHub metadata
gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name'
Error messages:
- Missing changelog ->
Error: CHANGELOG.md not found. Run changelog-bump-ver first. - Missing versioned release ->
Error: No versioned release entry found below ## [Unreleased]. Run changelog-bump-ver first. - Dirty working tree ->
Error: Working tree is not clean. Commit or stash changes before creating a release. - Not on default branch ->
Error: Releases must be created from the default branch tip. - Tag already exists ->
Error: Tag vX.Y.Z already exists locally or on origin. - Missing previous tag for non-first release ->
Error: Previous changelog version exists but matching git tag was not found.
References
This skill follows:
- Keep a Changelog: https://keepachangelog.com/en/1.1.0/
- Semantic Versioning: https://semver.org/spec/v2.0.0.html
- GitHub CLI release create: https://cli.github.com/manual/gh_release_create
If you need to confirm compare-link or release-note behavior during execution, use the WebFetch tool for those URLs.
Workflow
Step 1: Validate Repository State
git rev-parse --git-dir 2>/dev/null
If this fails -> error: Error: This skill requires a git repository.
[ -f CHANGELOG.md ] && echo "found" || echo "missing"
If missing -> error: Error: CHANGELOG.md not found. Run changelog-bump-ver first.
gh auth status
If this fails -> error: Error: GitHub CLI not authenticated. Run: gh auth login
git remote get-url origin >/dev/null
If this fails -> error: Error: Git remote 'origin' not found.
default_branch="$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name' 2>/dev/null)"
if [ -z "$default_branch" ]; then
default_branch="$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@')"
if [ -z "$default_branch" ]; then
if git show-ref --verify --quiet refs/heads/main; then
default_branch="main"
elif git show-ref --verify --quiet refs/heads/master; then
default_branch="master"
fi
fi
fi
printf '%s\n' "$default_branch"
Rules:
- Prefer
gh repo viewso repositories with non-maindefault branches are handled correctly - If GitHub metadata is unavailable, fall back to
origin/HEAD - If
origin/HEADis unavailable, fall back to localmain, then localmaster
If empty -> error: Error: Could not determine the default branch.
current_branch="$(git branch --show-current)"
printf '%s\n' "$current_branch"
If current_branch is not equal to default_branch -> error: Error: Releases must be created from the default branch tip.
git status --short
If this prints any output -> error: Error: Working tree is not clean. Commit or stash changes before creating a release.
Step 2: Validate Changelog Shape
Ensure CHANGELOG.md has exactly one unreleased section:
unreleased_count="$(grep -c '^## \[Unreleased\]$' CHANGELOG.md)"
printf '%s\n' "$unreleased_count"
Rules:
0-> error:Error: CHANGELOG.md does not contain a ## [Unreleased] section.1-> continue>1-> error:Error: Multiple ## [Unreleased] sections found. Normalize the changelog before releasing.
Then identify the first versioned release heading after ## [Unreleased].
Accepted heading formats:
## [v1.2.3]## [v1.2.3] - 2026-03-20## [1.2.3]## [1.2.3] - 2026-03-20
Normalization rules:
- Treat
v1.2.3and1.2.3as the same version internally - Always publish the git tag and GitHub release as
v1.2.3 - Ignore comparison-link lines such as
[Unreleased]: ...
Selection rule:
- Starting from
## [Unreleased], the first versioned## [...]heading encountered is the release to publish
If no such heading exists -> error: Error: No versioned release entry found below ## [Unreleased]. Run changelog-bump-ver first.
Step 3: Extract Release Version and Notes
Read the selected release section from its heading line until the next ## heading or end of file.
Rules:
- Exclude the selected heading line from the notes body
- Preserve all subsection headings and bullets exactly as written
- Keep blank lines and subsection order intact
- Do not invent missing
Added,Changed,Fixed, or other subsections - Allow nonstandard subsections and keep them unchanged
- Preserve
### Miscexactly as written as the final subsection when the changelog uses the repo's extended template
Example release body extracted from CHANGELOG.md:
### Added
- Support release digests
### Fixed
- Correct changelog parsing for empty sections
### Misc
- Refactor changelog parsing for prerelease note generation
Normalize the release version to a tag value:
1.2.3->v1.2.3v1.2.3->v1.2.3
Step 4: Resolve Previous Release Context
Determine whether this is the first release.
Rules:
- If there is another versioned release heading after the selected one, that next heading is the previous release version
- If there is no older versioned heading, this is the first release
For non-first releases:
- Normalize the previous version to
vX.Y.Z - Verify that tag exists locally or on origin
- If the previous changelog version exists but the tag does not -> error:
Error: Previous changelog version exists but matching git tag was not found.
For first releases:
- Use the first commit on the default branch as the start of the full changelog range
Commands:
git rev-list --max-parents=0 "$default_branch" | tail -n 1
Use that commit SHA as the start of the compare range.
Step 5: Verify the New Tag Does Not Already Exist
tag="vX.Y.Z"
git rev-parse -q --verify "refs/tags/$tag" >/dev/null && echo "local-exists"
git ls-remote --tags origin "refs/tags/$tag" | grep -q . && echo "remote-exists"
If either check finds the tag -> error: Error: Tag vX.Y.Z already exists locally or on origin.
Step 6: Build the GitHub Prerelease Notes
The prerelease title must be exactly the normalized tag:
vX.Y.Z
The prerelease body must be:
- The extracted changelog body for the selected release
- A blank line
## Full Changelog- A compare line in one of these forms
Non-first release:
## Full Changelog
https://github.com/OWNER/REPO/compare/v0.4.0...v0.5.0
First release:
## Full Changelog
First release: https://github.com/OWNER/REPO/compare/<first-commit-sha>...v0.1.0
Repository URL guidance:
- Use
gh repo view --json url --jq '.url'to get the canonical GitHub repository URL - Build the compare link from that URL
Body construction rules:
- Do not repeat the release heading line from
CHANGELOG.md - Do not add extra summary prose above the changelog notes
- Keep the exact subsection content from the changelog
- Always include the
## Full Changelogsection at the end
Step 7: Write the Prerelease Notes File
Write the assembled prerelease body from Step 6 into a temporary file before creating the release:
notes_file="$(mktemp)"
cat > "$notes_file" <<'EOF'
### Added
- Example release note
### Misc
- Example internal refactor note
## Full Changelog
https://github.com/OWNER/REPO/compare/v0.4.0...v0.5.0
EOF
Rules:
- The file content must be the exact assembled release body from Step 6
- Do not pass an empty temp file to
gh release create --notes-file - Remove the temp file after the release command succeeds or fails
Step 8: Create the Annotated Tag
Create the release tag at the current HEAD commit:
git tag -a "vX.Y.Z" -m "Release vX.Y.Z"
Rules:
- Use an annotated tag, not a lightweight tag
- Tag the current
HEADonly after all validation passes - Do not retag an existing version
Step 9: Push and Verify the Annotated Tag
Push the annotated tag to origin before creating the GitHub prerelease:
git push origin "vX.Y.Z"
git ls-remote --tags origin "refs/tags/vX.Y.Z"
Rules:
- Push the tag before running
gh release create - Ensure the prerelease is published from the annotated tag you created in Step 8
- If the tag push fails, stop and report the error instead of creating the release
Step 10: Create the GitHub Prerelease
Create a temporary notes file, then publish the prerelease:
gh release create "vX.Y.Z" \
--prerelease \
--title "vX.Y.Z" \
--notes-file "$notes_file"
Behavior rules:
- The release must be marked with
--prerelease - The release title must match the tag exactly
- The release notes must come from the generated body written in Step 7
- Use the already-pushed annotated tag from Step 9 rather than relying on GitHub to create a new ref
If gh release create fails after the local tag was created:
- Report the failure clearly
- Tell the user the local and remote tag now exist and may need cleanup or retry
- Remove the temp notes file before exiting
- Do not claim release success
After the release command finishes, clean up the temp file:
rm -f "$notes_file"
Step 11: Report the Result
Report:
- Published tag
- Whether the release was first release or had a previous release
- Compare range used in
Full Changelog - GitHub prerelease URL
Standard success message:
✓ Created prerelease v0.5.0
- Tag: v0.5.0
- Previous release: v0.4.0
- Full Changelog: https://github.com/OWNER/REPO/compare/v0.4.0...v0.5.0
- GitHub prerelease: https://github.com/OWNER/REPO/releases/tag/v0.5.0
First release success message:
✓ Created prerelease v0.1.0
- Tag: v0.1.0
- Previous release: none (first release)
- Full Changelog: https://github.com/OWNER/REPO/compare/<first-commit-sha>...v0.1.0
- GitHub prerelease: https://github.com/OWNER/REPO/releases/tag/v0.1.0
Version Parsing Rules
Accept these heading patterns when scanning CHANGELOG.md:
^## \[(v)?([0-9]+\.[0-9]+\.[0-9]+)\]$^## \[(v)?([0-9]+\.[0-9]+\.[0-9]+)\] - [0-9]{4}-[0-9]{2}-[0-9]{2}$
Ignore:
- Comparison links such as
[Unreleased]: ... - Headings with non-SemVer values like
vNext,1.0, orrelease-1 - Inline code or prose mentions of versions
When scanning for the release to publish:
- Start at the
## [Unreleased]heading - Select the first later heading that matches the accepted version formats
- Use document order, not highest-version sorting, because
changelog-bump-verwrites the newest release directly belowUnreleased
Release Body Example
Given this changelog excerpt:
## [Unreleased]
### Added
### Changed
### Deprecated
### Removed
### Fixed
### Security
### Misc
## [v0.5.0] - 2026-03-20
### Added
- Add `gh-release` skill for annotated tags and GitHub prereleases
### Fixed
- Preserve changelog subsection order in generated release notes
### Misc
- Refactor release note assembly for changelog subsection passthrough
## [v0.4.0] - 2026-03-10
### Added
- Add changelog version bump workflow
Publish v0.5.0 with notes body:
### Added
- Add `gh-release` skill for annotated tags and GitHub prereleases
### Fixed
- Preserve changelog subsection order in generated release notes
### Misc
- Refactor release note assembly for changelog subsection passthrough
## Full Changelog
https://github.com/OWNER/REPO/compare/v0.4.0...v0.5.0
Edge Cases
| Scenario | Behavior |
|---|---|
CHANGELOG.md missing |
Error: CHANGELOG.md not found. Run changelog-bump-ver first. |
No ## [Unreleased] section |
Error: CHANGELOG.md does not contain a ## [Unreleased] section. |
Multiple ## [Unreleased] sections |
Error: Multiple ## [Unreleased] sections found. Normalize the changelog before releasing. |
No versioned release below Unreleased |
Error: No versioned release entry found below ## [Unreleased]. Run changelog-bump-ver first. |
| Release version heading is malformed | Ignore it unless it matches the accepted SemVer patterns |
| Current branch is not the default branch | Error: Releases must be created from the default branch tip. |
| Working tree is dirty | Error: Working tree is not clean. Commit or stash changes before creating a release. |
| Tag already exists locally or remotely | Error: Tag vX.Y.Z already exists locally or on origin. |
| Could not determine default branch from GitHub metadata | Fall back to origin/HEAD, then local main, then local master |
| Previous changelog version exists but tag is missing | Error: Previous changelog version exists but matching git tag was not found. |
| Temp notes file was created but not populated | Error: stop before gh release create; do not publish empty notes |
| Annotated tag was created locally but not pushed | Push and verify the tag on origin before creating the release |
| First release with no older version heading | Compare first commit on default branch to new tag |
| First release with a single commit | Compare that same first commit to the new tag |
Release section contains ### Misc |
Preserve it unchanged in the generated notes body |
gh release create fails after tag creation |
Report partial failure and note that the local tag now exists |
Complete Workflow Example
Scenario: changelog-bump-ver already created ## [v0.5.0] - 2026-03-20 above v0.4.0
Validate repository state:
git rev-parse --git-dir gh auth status git remote get-url origin git status --short git branch --show-currentDetect default branch:
gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name' # mainIdentify the release heading below
Unreleased:- Selected heading:
## [v0.5.0] - 2026-03-20 - Previous heading:
## [v0.4.0] - 2026-03-10
- Selected heading:
Verify tags:
git rev-parse -q --verify refs/tags/v0.5.0 git rev-parse -q --verify refs/tags/v0.4.0Build notes body:
### Added - Add `gh-release` skill for annotated tags and GitHub prereleases ### Fixed - Preserve changelog subsection order in generated release notes ### Misc - Refactor release note assembly for changelog subsection passthrough ## Full Changelog https://github.com/OWNER/REPO/compare/v0.4.0...v0.5.0Write notes, push tag, and create prerelease:
notes_file="$(mktemp)" cat > "$notes_file" <<'EOF' ### Added - Add `gh-release` skill for annotated tags and GitHub prereleases ### Fixed - Preserve changelog subsection order in generated release notes ### Misc - Refactor release note assembly for changelog subsection passthrough ## Full Changelog https://github.com/OWNER/REPO/compare/v0.4.0...v0.5.0 EOF git tag -a "v0.5.0" -m "Release v0.5.0" git push origin "v0.5.0" gh release create "v0.5.0" \ --prerelease \ --title "v0.5.0" \ --notes-file "$notes_file" rm -f "$notes_file"Notify the user:
✓ Created prerelease v0.5.0 - Tag: v0.5.0 - Previous release: v0.4.0 - Full Changelog: https://github.com/OWNER/REPO/compare/v0.4.0...v0.5.0 - GitHub prerelease: https://github.com/OWNER/REPO/releases/tag/v0.5.0