Release Manager
Create semantic version releases with automated changelog generation from conventional commits, version file updates, and GitHub release publishing.
Quality Guidelines
Release operations are high-consequence and irreversible once pushed:
- Verify every change: analyze actual commits, not assumptions
- Confirm version bump: the detected semver bump must match the change scope
- Validate changelog: every entry must correspond to a real commit
- User approval required: confirm before executing anything in Phase 5
Workflow
Phase 1: Collect Commits Since Last Tag
- Find the latest tag:
git describe --tags --abbrev=0 2>/dev/null || echo "none"
- If no tags exist, collect all commits on the current branch
- If a tag exists, collect commits since that tag
- Collect commits:
# With existing tag
git log <last-tag>..HEAD --format="%H %s" --no-merges
# Without existing tag (first release)
git log --format="%H %s" --no-merges
- Validate preconditions:
- Working tree is clean:
git status --porcelain
- On the expected branch (main/master or release branch)
- Remote is up to date:
git fetch origin && git log HEAD..origin/$(git branch --show-current) --oneline
- If there are no commits since the last tag, abort with a clear message
Phase 2: Auto-Detect Version Bump
- Parse each commit using conventional commit format:
- Extract type:
feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
- Extract scope (optional): text in parentheses after type
- Detect breaking changes:
! after type/scope OR BREAKING CHANGE: in commit body
- For non-conventional commits, classify as
other
This is plain regex/string parsing over commit subjects, do it inline regardless of commit count, no agent needed.
Determine version bump: load references/semver-guide.md for the full commit-type → bump mapping and pre-1.0 rules. The highest-priority bump wins (major > minor > patch).
Calculate new version:
- Parse last tag as semver (strip leading
v if present)
- If no previous tag, start from
0.1.0 (first feature release) or 1.0.0 if user specifies
- Apply the detected bump
- Respect
--major, --minor, or --patch override from arguments
- Display version summary: the counts must reflect commits you actually parsed in step 1, never estimated:
Current version: v1.2.3
Detected bump: minor (2 features, 5 fixes, 3 chores)
New version: v1.3.0
Breaking changes: none
Phase 3: Build the CHANGELOG Entry
Read existing CHANGELOG.md (if it exists) to understand the current format and preserve it
Group commits by type using this order and heading format:
## [1.3.0](https://github.com/owner/repo/compare/v1.2.3...v1.3.0) (YYYY-MM-DD)
### Breaking Changes
- **scope:** description ([hash](url))
### Features
- **scope:** description ([hash](url))
### Bug Fixes
- **scope:** description ([hash](url))
### Performance
- **scope:** description ([hash](url))
### Documentation
- **scope:** description ([hash](url))
### Other Changes
- **scope:** description ([hash](url))
Type-to-heading mapping:
- Breaking changes (any type with
! or BREAKING CHANGE:) → Breaking Changes
feat → Features
fix → Bug Fixes
perf → Performance
docs → Documentation
refactor, style, test, build, ci, chore, revert, other → Other Changes
Only include sections that have entries. Omit empty sections.
- Generate comparison URL:
gh repo view --json url -q .url 2>/dev/null || git remote get-url origin
- Construct the changelog entry:
- Use short commit hashes (7 chars) linked to the full commit URL
- If scope exists, bold it:
**scope:** description
- If no scope: just the description
- Date format:
YYYY-MM-DD
- Insertion logic (defines the mechanics only, nothing is written to disk yet, so the Phase 4 preview and a later abort both stay side-effect-free):
- If CHANGELOG.md exists, insert the entry after the
# Changelog header, preserving existing entries below it
- If CHANGELOG.md does not exist, this entry becomes the file's first entry under a new
# Changelog header
- Maintain a blank line between the header and first entry, and between entries
- The actual file write happens in Phase 5 step 2, or Phase 3b step 2 for changelog-only mode: both reuse this same logic
- Verify the write (same call sites as step 5): after writing the file, re-read it and confirm the new version heading (
## [<new-version>]) is present and that at least one section under it has a real bullet line, not just an empty ### Heading with nothing below. A narrated changelog is not evidence the write succeeded, check the file on disk, e.g.:
grep -A2 "## \[<new-version>\]" CHANGELOG.md
If the heading is missing, or every section under it is empty, abort before creating the release commit: "CHANGELOG.md write produced empty sections, release aborted, no commit created." Do not proceed to Phase 5 step 3 (or, in changelog-only mode, report success) on a failed verification.
Phase 3b: Changelog-Only Mode (if --changelog-only)
When --changelog-only is passed, skip Phases 4-6 entirely:
- Run Phases 1-3 normally (collect commits, detect version bump, build the changelog entry)
- Write CHANGELOG.md using the Phase 3 step 5 insertion logic, including its step 6 verification (abort here on a failed verification, do not report success)
- Display the updated changelog entry to the user
- Stop here: no tag, version bump, commit, or GitHub release
Use case: draft a changelog before deciding on a release, or maintain a running changelog during development.
# Example output for --changelog-only
git-release --changelog-only
# → Scans commits since v1.2.3
# → Writes changelog entry to CHANGELOG.md
# → Reports: "CHANGELOG.md updated with 8 commits. No tag or release created."
Phase 4: User Approval
- Display release summary:
=== Release Summary ===
Version: v1.2.3 → v1.3.0 (minor)
Tag: v1.3.0
Commits: 12 commits since v1.2.3
Branch: main
Changelog preview:
─────────────────────
## [1.3.0](...) (2025-01-15)
### Features
- **auth:** add OAuth2 login support (abc1234)
- **api:** add rate limiting endpoint (def5678)
### Bug Fixes
- **api:** resolve null pointer in user endpoint (ghi9012)
─────────────────────
Version files to update:
- package.json (1.2.3 → 1.3.0)
- pyproject.toml (1.2.3 → 1.3.0)
Actions:
1. Update version files
2. Update CHANGELOG.md
3. Create git commit: "chore(release): v1.3.0"
4. Create git tag: v1.3.0
5. Push commit and tag to origin
6. Create GitHub release with changelog
If --dry-run (or -n) was passed: stop here. The summary above already shows everything that would happen, this flag is the only dry-run entry point, so no separate "preview" option is offered below.
Otherwise, ask for confirmation:
- "Proceed with release": continue to Phase 5
- "Change version": ask for the desired version, recalculate, re-display the summary
- "Abort": exit cleanly with "Release cancelled."
Phase 5: Execute Release
Execute all release actions in strict order. Stop immediately if any step fails and report which step failed and what manual cleanup may be needed.
- Update version files (detect and update all that exist):
package.json: Update "version": "x.y.z" field
package-lock.json: Update "version": "x.y.z" at root level
pyproject.toml: Update version = "x.y.z" under [project] or [tool.poetry]
Cargo.toml: Update version = "x.y.z" under [package]
VERSION or VERSION.txt: Replace entire file content
setup.cfg: Update version = x.y.z under [metadata]
build.gradle / build.gradle.kts: Update version = "x.y.z"
- Other version files: Skip unknown formats, notify user
Write CHANGELOG.md using the Phase 3 step 5 insertion logic, including its step 6 verification (abort before step 3 below if verification fails).
Create release commit:
git add -A
git commit -m "chore(release): v<new-version>"
- Create annotated tag:
git tag -a v<new-version> -m "Release v<new-version>"
- Push commit and tag:
git push origin $(git branch --show-current)
git push origin v<new-version>
- Create GitHub release (unless
--no-github flag is set):
notes_file=$(mktemp -t release-notes)
# write the changelog entry (without the "## [version]" header) to $notes_file
gh release create v<new-version> \
--title "v<new-version>" \
--notes-file "$notes_file" \
--latest
rm -f "$notes_file"
A fixed path (e.g. /tmp/release-notes.md) can collide across concurrent or repeated runs: mktemp guarantees a unique file.
- Display completion summary:
Release v1.3.0 completed successfully!
- Commit: abc1234 chore(release): v1.3.0
- Tag: v1.3.0
- GitHub: https://github.com/owner/repo/releases/tag/v1.3.0
- Changelog: Updated CHANGELOG.md
Argument Parsing
Parse optional arguments from command arguments:
--major: Force a major version bump (overrides auto-detection)
--minor: Force a minor version bump (overrides auto-detection)
--patch: Force a patch version bump (overrides auto-detection)
--dry-run or -n: Show what would happen without making changes (see Phase 4 step 2, the single dry-run entry point)
--no-github: Skip GitHub release creation (only local tag + changelog)
--changelog-only: Generate/update CHANGELOG.md only, skip tagging, version bumps, and GitHub release
When force flags conflict (e.g., --major --minor), use the highest: major > minor > patch.
Edge Cases
- No conventional commits: If commits don't follow conventional format, default to
patch bump and list all commits under Other Changes
- Pre-release versions (e.g.,
0.x.y): Follow semver pre-1.0 rules, breaking changes bump minor, features bump minor, fixes bump patch
- Monorepo: If multiple
package.json files exist, only update the root one. Warn the user about other version files found
- Dirty working tree: Abort with a clear message asking the user to commit or stash changes first
- No remote: If
git push fails due to no remote, skip push and GitHub release, warn the user
- Tag already exists: If the computed tag already exists, abort and suggest a force flag or a different version
- CHANGELOG write verification fails: If the re-read in Phase 3 step 6 shows a missing heading or empty sections, abort before the release commit, never commit a changelog write you haven't confirmed on disk
Important Notes
- Conventional Commits: Works best with conventional commits (see the git-commit skill)
- Tag Format: Always uses
v prefix (e.g., v1.3.0) unless existing tags use a different convention
- CHANGELOG Format: Follows Keep a Changelog conventions
- Semver: Follows Semantic Versioning 2.0.0
- Never skip hooks: Never pass
--no-verify on the release commit
- No inline execution: Nothing in Phase 1-4 writes to the working tree, the first mutation is Phase 5 step 1, after approval
Examples
# Auto-detect version bump from commits
git-release
# Force a major version bump
git-release --major
# Preview without making changes
git-release --dry-run
# Release without creating a GitHub release
git-release --no-github
# Force minor bump, dry run
git-release --minor --dry-run
# Update CHANGELOG.md only (no tag or release)
git-release --changelog-only
1---2name: git-release3description: Create semantic version releases with automated changelog generation from conventional commits, version file bumps (package.json, pyproject.toml, Cargo.toml, etc.), git tagging, and GitHub release publishing, for repos on a simple main-branch workflow (no release/hotfix branches). Use when users want to create a release, tag a version, generate a changelog, bump version numbers, cut a release, or publish a GitHub release. Not for release/hotfix branch topology or promoting one branch to another (use gitflow). Not for everyday conventional commit messages (use git-commit, this skill only creates the single release commit itself).4---56# Release Manager78Create semantic version releases with automated changelog generation from conventional commits, version file updates, and GitHub release publishing.910## Quality Guidelines1112Release operations are high-consequence and irreversible once pushed:131. **Verify every change**: analyze actual commits, not assumptions142. **Confirm version bump**: the detected semver bump must match the change scope153. **Validate changelog**: every entry must correspond to a real commit164. **User approval required**: confirm before executing anything in Phase 51718## Workflow1920### Phase 1: Collect Commits Since Last Tag21221. **Find the latest tag**:23 ```bash24 git describe --tags --abbrev=0 2>/dev/null || echo "none"25 ```26 - If no tags exist, collect all commits on the current branch27 - If a tag exists, collect commits since that tag28292. **Collect commits**:30 ```bash31 # With existing tag32 git log <last-tag>..HEAD --format="%H %s" --no-merges3334 # Without existing tag (first release)35 git log --format="%H %s" --no-merges36 ```37383. **Validate preconditions**:39 - Working tree is clean: `git status --porcelain`40 - On the expected branch (main/master or release branch)41 - Remote is up to date: `git fetch origin && git log HEAD..origin/$(git branch --show-current) --oneline`42 - If there are no commits since the last tag, abort with a clear message4344### Phase 2: Auto-Detect Version Bump45461. **Parse each commit** using conventional commit format:47 - Extract type: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert`48 - Extract scope (optional): text in parentheses after type49 - Detect breaking changes: `!` after type/scope OR `BREAKING CHANGE:` in commit body50 - For non-conventional commits, classify as `other`5152 This is plain regex/string parsing over commit subjects, do it inline regardless of commit count, no agent needed.53542. **Determine version bump**: load `references/semver-guide.md` for the full commit-type → bump mapping and pre-1.0 rules. The highest-priority bump wins (major > minor > patch).55563. **Calculate new version**:57 - Parse last tag as semver (strip leading `v` if present)58 - If no previous tag, start from `0.1.0` (first feature release) or `1.0.0` if user specifies59 - Apply the detected bump60 - Respect `--major`, `--minor`, or `--patch` override from arguments61624. **Display version summary**: the counts must reflect commits you actually parsed in step 1, never estimated:63 ```64 Current version: v1.2.365 Detected bump: minor (2 features, 5 fixes, 3 chores)66 New version: v1.3.06768 Breaking changes: none69 ```7071### Phase 3: Build the CHANGELOG Entry72731. **Read existing CHANGELOG.md** (if it exists) to understand the current format and preserve it74752. **Group commits by type** using this order and heading format:76 ```markdown77 ## [1.3.0](https://github.com/owner/repo/compare/v1.2.3...v1.3.0) (YYYY-MM-DD)7879 ### Breaking Changes80 - **scope:** description ([hash](url))8182 ### Features83 - **scope:** description ([hash](url))8485 ### Bug Fixes86 - **scope:** description ([hash](url))8788 ### Performance89 - **scope:** description ([hash](url))9091 ### Documentation92 - **scope:** description ([hash](url))9394 ### Other Changes95 - **scope:** description ([hash](url))96 ```9798 Type-to-heading mapping:99 - Breaking changes (any type with `!` or `BREAKING CHANGE:`) → **Breaking Changes**100 - `feat` → **Features**101 - `fix` → **Bug Fixes**102 - `perf` → **Performance**103 - `docs` → **Documentation**104 - `refactor`, `style`, `test`, `build`, `ci`, `chore`, `revert`, `other` → **Other Changes**105106 Only include sections that have entries. Omit empty sections.1071083. **Generate comparison URL**:109 ```bash110 gh repo view --json url -q .url 2>/dev/null || git remote get-url origin111 ```1121134. **Construct the changelog entry**:114 - Use short commit hashes (7 chars) linked to the full commit URL115 - If scope exists, bold it: `**scope:** description`116 - If no scope: just the description117 - Date format: `YYYY-MM-DD`1181195. **Insertion logic** (defines the mechanics only, nothing is written to disk yet, so the Phase 4 preview and a later abort both stay side-effect-free):120 - If CHANGELOG.md exists, insert the entry after the `# Changelog` header, preserving existing entries below it121 - If CHANGELOG.md does not exist, this entry becomes the file's first entry under a new `# Changelog` header122 - Maintain a blank line between the header and first entry, and between entries123 - The actual file write happens in Phase 5 step 2, or Phase 3b step 2 for changelog-only mode: both reuse this same logic1241256. **Verify the write** (same call sites as step 5): after writing the file, re-read it and confirm the new version heading (`## [<new-version>]`) is present and that at least one section under it has a real bullet line, not just an empty `### Heading` with nothing below. A narrated changelog is not evidence the write succeeded, check the file on disk, e.g.:126 ```bash127 grep -A2 "## \[<new-version>\]" CHANGELOG.md128 ```129 If the heading is missing, or every section under it is empty, abort before creating the release commit: "CHANGELOG.md write produced empty sections, release aborted, no commit created." Do not proceed to Phase 5 step 3 (or, in changelog-only mode, report success) on a failed verification.130131### Phase 3b: Changelog-Only Mode (if `--changelog-only`)132133When `--changelog-only` is passed, skip Phases 4-6 entirely:1341351. Run Phases 1-3 normally (collect commits, detect version bump, build the changelog entry)1362. Write CHANGELOG.md using the Phase 3 step 5 insertion logic, including its step 6 verification (abort here on a failed verification, do not report success)1373. Display the updated changelog entry to the user1384. Stop here: no tag, version bump, commit, or GitHub release139140Use case: draft a changelog before deciding on a release, or maintain a running changelog during development.141142```bash143# Example output for --changelog-only144git-release --changelog-only145# → Scans commits since v1.2.3146# → Writes changelog entry to CHANGELOG.md147# → Reports: "CHANGELOG.md updated with 8 commits. No tag or release created."148```149150### Phase 4: User Approval1511521. **Display release summary**:153 ```154 === Release Summary ===155156 Version: v1.2.3 → v1.3.0 (minor)157 Tag: v1.3.0158 Commits: 12 commits since v1.2.3159 Branch: main160161 Changelog preview:162 ─────────────────────163 ## [1.3.0](...) (2025-01-15)164165 ### Features166 - **auth:** add OAuth2 login support (abc1234)167 - **api:** add rate limiting endpoint (def5678)168169 ### Bug Fixes170 - **api:** resolve null pointer in user endpoint (ghi9012)171 ─────────────────────172173 Version files to update:174 - package.json (1.2.3 → 1.3.0)175 - pyproject.toml (1.2.3 → 1.3.0)176177 Actions:178 1. Update version files179 2. Update CHANGELOG.md180 3. Create git commit: "chore(release): v1.3.0"181 4. Create git tag: v1.3.0182 5. Push commit and tag to origin183 6. Create GitHub release with changelog184 ```1851862. **If `--dry-run` (or `-n`) was passed**: stop here. The summary above already shows everything that would happen, this flag is the only dry-run entry point, so no separate "preview" option is offered below.1871883. **Otherwise, ask for confirmation**:189 - "Proceed with release": continue to Phase 5190 - "Change version": ask for the desired version, recalculate, re-display the summary191 - "Abort": exit cleanly with "Release cancelled."192193### Phase 5: Execute Release194195Execute all release actions in strict order. Stop immediately if any step fails and report which step failed and what manual cleanup may be needed.1961971. **Update version files** (detect and update all that exist):198 - `package.json`: Update `"version": "x.y.z"` field199 - `package-lock.json`: Update `"version": "x.y.z"` at root level200 - `pyproject.toml`: Update `version = "x.y.z"` under `[project]` or `[tool.poetry]`201 - `Cargo.toml`: Update `version = "x.y.z"` under `[package]`202 - `VERSION` or `VERSION.txt`: Replace entire file content203 - `setup.cfg`: Update `version = x.y.z` under `[metadata]`204 - `build.gradle` / `build.gradle.kts`: Update `version = "x.y.z"`205 - Other version files: Skip unknown formats, notify user2062072. **Write CHANGELOG.md** using the Phase 3 step 5 insertion logic, including its step 6 verification (abort before step 3 below if verification fails).2082093. **Create release commit**:210 ```bash211 git add -A212 git commit -m "chore(release): v<new-version>"213 ```2142154. **Create annotated tag**:216 ```bash217 git tag -a v<new-version> -m "Release v<new-version>"218 ```2192205. **Push commit and tag**:221 ```bash222 git push origin $(git branch --show-current)223 git push origin v<new-version>224 ```2252266. **Create GitHub release** (unless `--no-github` flag is set):227 ```bash228 notes_file=$(mktemp -t release-notes)229 # write the changelog entry (without the "## [version]" header) to $notes_file230 gh release create v<new-version> \231 --title "v<new-version>" \232 --notes-file "$notes_file" \233 --latest234 rm -f "$notes_file"235 ```236 A fixed path (e.g. `/tmp/release-notes.md`) can collide across concurrent or repeated runs: `mktemp` guarantees a unique file.2372387. **Display completion summary**:239 ```240 Release v1.3.0 completed successfully!241242 - Commit: abc1234 chore(release): v1.3.0243 - Tag: v1.3.0244 - GitHub: https://github.com/owner/repo/releases/tag/v1.3.0245 - Changelog: Updated CHANGELOG.md246 ```247248## Argument Parsing249250Parse optional arguments from `command arguments`:251- `--major`: Force a major version bump (overrides auto-detection)252- `--minor`: Force a minor version bump (overrides auto-detection)253- `--patch`: Force a patch version bump (overrides auto-detection)254- `--dry-run` or `-n`: Show what would happen without making changes (see Phase 4 step 2, the single dry-run entry point)255- `--no-github`: Skip GitHub release creation (only local tag + changelog)256- `--changelog-only`: Generate/update CHANGELOG.md only, skip tagging, version bumps, and GitHub release257258When force flags conflict (e.g., `--major --minor`), use the highest: major > minor > patch.259260## Edge Cases261262- **No conventional commits**: If commits don't follow conventional format, default to `patch` bump and list all commits under **Other Changes**263- **Pre-release versions** (e.g., `0.x.y`): Follow semver pre-1.0 rules, breaking changes bump minor, features bump minor, fixes bump patch264- **Monorepo**: If multiple `package.json` files exist, only update the root one. Warn the user about other version files found265- **Dirty working tree**: Abort with a clear message asking the user to commit or stash changes first266- **No remote**: If `git push` fails due to no remote, skip push and GitHub release, warn the user267- **Tag already exists**: If the computed tag already exists, abort and suggest a force flag or a different version268- **CHANGELOG write verification fails**: If the re-read in Phase 3 step 6 shows a missing heading or empty sections, abort before the release commit, never commit a changelog write you haven't confirmed on disk269270## Important Notes271272- **Conventional Commits**: Works best with conventional commits (see the git-commit skill)273- **Tag Format**: Always uses `v` prefix (e.g., `v1.3.0`) unless existing tags use a different convention274- **CHANGELOG Format**: Follows [Keep a Changelog](https://keepachangelog.com/) conventions275- **Semver**: Follows [Semantic Versioning 2.0.0](https://semver.org/)276- **Never skip hooks**: Never pass `--no-verify` on the release commit277- **No inline execution**: Nothing in Phase 1-4 writes to the working tree, the first mutation is Phase 5 step 1, after approval278279## Examples280281```bash282# Auto-detect version bump from commits283git-release284285# Force a major version bump286git-release --major287288# Preview without making changes289git-release --dry-run290291# Release without creating a GitHub release292git-release --no-github293294# Force minor bump, dry run295git-release --minor --dry-run296297# Update CHANGELOG.md only (no tag or release)298git-release --changelog-only299```