SuperAPI AI Directory

git-release

General & other

View repository

Updated 10-16 09:32

About this skill

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:

  1. Verify every change: analyze actual commits, not assumptions
  2. Confirm version bump: the detected semver bump must match the change scope
  3. Validate changelog: every entry must correspond to a real commit
  4. User approval required: confirm before executing anything in Phase 5

Workflow

Phase 1: Collect Commits Since Last Tag

  1. 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
  1. 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
  1. 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
  • Unless --no-github or --changelog-only, gh auth status must pass; if it fails, stop and ask the user to fix it. Requires gh (brew install gh, then gh auth login).

Phase 2: Auto-detect version bump

  1. 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.

  1. 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).

  2. 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
  1. 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

  1. Read existing CHANGELOG.md (if it exists) to understand the current format and preserve it

  2. 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.

  1. Generate comparison URL:
gh repo view --json url -q .url 2>/dev/null || git remote get-url origin
  1. 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
  1. 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 item 3, or Phase 3b step 2 for changelog-only mode: both reuse this same logic
  1. 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 item 4 (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:

  1. Run Phases 1-3 normally (collect commits, detect version bump, build the changelog entry)
  2. 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)
  3. Display the updated changelog entry to the user
  4. 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

  1. 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
  1. 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.

  2. 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

Copy this checklist into your reply and tick each item only after its check passes. Do the items in order. If a gate fails, stop, report which item failed and what manual cleanup may be needed, and do not continue past it.

- [ ] 1. Bump every version file that exists: `package.json`, `package-lock.json` (root `"version"`), `pyproject.toml` (`[project]` or `[tool.poetry]`), `Cargo.toml` (`[package]`), `VERSION` / `VERSION.txt` (whole file), `setup.cfg` (`[metadata]`), `build.gradle` / `build.gradle.kts`. Skip unknown formats and tell the user.
- [ ] 2. Re-parse each bumped manifest and confirm it prints `<new-version>`. Gate: any mismatch, return to item 1.
- [ ] 3. Write CHANGELOG.md (Phase 3 step 5 logic) and run the Phase 3 step 6 verification. Gate: verification fails, stop, no commit.
- [ ] 4. Stage by explicit path: `git add CHANGELOG.md <each bumped manifest>`, then check `git diff --cached --name-only` lists only those files. Gate: any other file staged, run `git reset -q <path>` on it (or stop and ask), never continue with extras.
- [ ] 5. `git commit -m "chore(release): v<new-version>"`. Gate: a hook fails, fix the cause and return to item 4, never `--no-verify`.
- [ ] 6. `git tag -a v<new-version> -m "Release v<new-version>"`. Gate: tag exists, stop (see Edge Cases).
- [ ] 7. `git push origin $(git branch --show-current)`, then `git push origin v<new-version>`. Gate: either push fails, stop and report; nothing is released until the tag is pushed.
- [ ] 8. Unless `--no-github`: `notes_file=$(mktemp "${TMPDIR:-/tmp}/release-notes.XXXXXX")`, write the changelog entry without its `## [version]` header to it, run `gh release create v<new-version> --title "v<new-version>" --notes-file "$notes_file" --latest`, then `rm -f "$notes_file"`. Gate: it fails, follow the recovery note below.
- [ ] 9. Show the completion summary.

Manifest re-parse commands (jq is brew install jq; without it use python3 -c "import json;print(json.load(open('package.json'))['version'])"):

jq -r .version package.json package-lock.json
python3 -c "import tomllib;d=tomllib.load(open('pyproject.toml','rb'));print(d.get('project',{}).get('version') or d['tool']['poetry']['version'])"
python3 -c "import tomllib;print(tomllib.load(open('Cargo.toml','rb'))['package']['version'])"
cat VERSION
grep -E '^version' setup.cfg build.gradle build.gradle.kts

Use mktemp with an explicit XXXXXX template (bare mktemp -t name fails on GNU): a fixed path (e.g. /tmp/release-notes.md) can collide across concurrent or repeated runs.

Recovery: tag pushed but gh release create fails. The release is half done, do not re-tag or re-commit. Fix the cause (gh auth status, network, permissions) and re-run only gh release create for the existing tag. Delete the remote tag (git push origin :refs/tags/v<new-version>) only if the user explicitly confirms.

Completion summary (item 9):

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 item 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