
Release Management
- 40 installs
- 213 repo stars
- Updated August 4, 2026
- yonatangross/skillforge-claude-plugin
Helps with ai & agent building tasks.
About
release-management is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- release-management
- AI & Agent Building
- AI-coding skill
Release Management by the numbers
- 40 all-time installs (skills.sh)
- Ranked #8,266 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/yonatangross/skillforge-claude-plugin --skill release-managementAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 40 |
|---|---|
| repo stars | ★ 213 |
| Last updated | August 4, 2026 |
| Repository | yonatangross/skillforge-claude-plugin ↗ |
What it does
Helps with ai & agent building tasks.
Files
Release Management
Automate releases with gh release, semantic versioning, and changelog generation.
CC ≥ 2.1.118 (M122):claude plugin tag <version>validates the plugin manifest hierarchy (marketplace.json, plugin.json, package.json, version.txt) before tagging. Runs--dry-runin CI before release-please opens its release PR; catches manifest drift early. Seesrc/skills/chain-patterns/references/plugin-tag.md.
CRITICAL: Task Management is MANDATORY (CC 2.1.16)
BEFORE doing ANYTHING else, create tasks to track progress:
# 1. Create main task IMMEDIATELY
TaskCreate(
subject="Release: {version}",
description="Creating release with semantic versioning and changelog",
activeForm="Releasing {version}"
)
# 2. Create subtasks for each release phase
TaskCreate(subject="Version & changelog", activeForm="Determining version and generating changelog")
TaskCreate(subject="Create release", activeForm="Creating GitHub release")
TaskCreate(subject="Verify & announce", activeForm="Verifying release and announcing")
# 3. Set dependencies for sequential phases
TaskUpdate(taskId="3", addBlockedBy=["2"])
TaskUpdate(taskId="4", addBlockedBy=["3"])
# 4. Before starting each task, verify it's unblocked
task = TaskGet(taskId="2") # Verify blockedBy is empty
# 5. Update status as you progress
TaskUpdate(taskId="2", status="in_progress") # When starting
TaskUpdate(taskId="2", status="completed") # When doneQuick Reference
Create Release
# Auto-generate notes from PRs
gh release create v1.2.0 --generate-notes
# With custom title
gh release create v1.2.0 --title "Version 1.2.0: Performance Update" --generate-notes
# Draft release (review before publishing)
gh release create v1.2.0 --draft --generate-notes
# Pre-release (beta, rc)
gh release create v1.2.0-beta.1 --prerelease --generate-notes
# With custom notes
gh release create v1.2.0 --notes "## Highlights
- New auth system
- 50% faster search"
# From notes file
gh release create v1.2.0 --notes-file RELEASE_NOTES.mdList & View Releases
# List all releases
gh release list
# View specific release
gh release view v1.2.0
# View in browser
gh release view v1.2.0 --web
# JSON output
gh release list --json tagName,publishedAt,isPrereleaseVerify Releases (gh CLI 2.86.0+)
# Verify release attestation (sigstore)
gh release verify v1.2.0
# Verify specific asset
gh release verify-asset v1.2.0 ./dist/app.zip
# Verify with custom trust policy
gh release verify v1.2.0 --owner myorgManage Releases
# Edit release
gh release edit v1.2.0 --title "New Title" --notes "Updated notes"
# Delete release
gh release delete v1.2.0
# Upload assets
gh release upload v1.2.0 ./dist/app.zip ./dist/app.tar.gz---
Semantic Versioning
MAJOR.MINOR.PATCH
│ │ │
│ │ └── Bug fixes (backwards compatible)
│ └──────── New features (backwards compatible)
└────────────── Breaking changes
Examples:
1.0.0 → 1.0.1 (patch: bug fix)
1.0.1 → 1.1.0 (minor: new feature)
1.1.0 → 2.0.0 (major: breaking change)
Pre-release:
2.0.0-alpha.1 (early testing)
2.0.0-beta.1 (feature complete)
2.0.0-rc.1 (release candidate)---
Release Workflows
Standard and hotfix release procedures using git tags and gh release.
Load Read("${CLAUDE_SKILL_DIR}/references/release-workflows.md") for step-by-step standard and hotfix release workflows.
---
Changelog Generation
Auto-generated from PRs, custom .github/release.yml templates, and manual CHANGELOG.md format.
Load Read("${CLAUDE_SKILL_DIR}/references/changelog-generation.md") for changelog template examples and Keep-a-Changelog format.
---
Release Automation & Checklist
GitHub Actions workflow for tag-triggered releases, version bumping script, and pre/post-release checklist.
Load Read("${CLAUDE_SKILL_DIR}/references/release-automation.md") for CI workflow, bump script, and release checklist.
---
Best Practices
1. Use semantic versioning - Communicate change impact 2. Draft releases first - Review notes before publishing 3. Generate notes from PRs - Accurate, automatic history 4. Close milestone on release - Track completion 5. Tag main only - Never tag feature branches 6. Announce breaking changes - Prominent in release notes
Related Skills
ork:github-operations: Milestones, issues, and CLI referenceork:github-operations: Branch management and git operations
References
Load on demand with Read("${CLAUDE_SKILL_DIR}/references/<file>"):
| File | Content |
|---|---|
semver.md | Semantic versioning rules and decision tree |
release-workflows.md | Standard and hotfix release procedures |
changelog-generation.md | Auto-generated, template, and manual changelog formats |
release-automation.md | GitHub Actions workflow, bump script, and checklist |
Changelog Generation
Auto-Generated (from PRs)
# GitHub auto-generates from merged PRs
gh release create v1.2.0 --generate-notes
# Output includes:
# ## What's Changed
# * feat: Add user auth by @dev in #123
# * fix: Login redirect by @dev in #124
# * docs: Update README by @dev in #125Custom Changelog Template
Create .github/release.yml:
changelog:
categories:
- title: "Breaking Changes"
labels:
- "breaking"
- title: "New Features"
labels:
- "enhancement"
- "feature"
- title: "Bug Fixes"
labels:
- "bug"
- "fix"
- title: "Documentation"
labels:
- "documentation"
- title: "Other Changes"
labels:
- "*"Manual CHANGELOG.md
# Changelog
## [1.3.0] - 2026-01-15
### Added
- User authentication system (#123)
- Dark mode support (#125)
### Changed
- Improved search performance (#126)
### Fixed
- Login redirect loop (#124)
### Security
- Updated dependencies for CVE-2026-1234Release Automation
GitHub Actions Release Workflow
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: npm run build
- name: Create Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create ${{ github.ref_name }} \
--generate-notes \
./dist/*.zipVersion Bumping Script
#!/bin/bash
# bump-version.sh
CURRENT=$(gh release view --json tagName -q .tagName | sed 's/v//')
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
case $1 in
major) NEW="$((MAJOR + 1)).0.0" ;;
minor) NEW="$MAJOR.$((MINOR + 1)).0" ;;
patch) NEW="$MAJOR.$MINOR.$((PATCH + 1))" ;;
*) echo "Usage: $0 [major|minor|patch]"; exit 1 ;;
esac
echo "Bumping $CURRENT -> $NEW"
git tag -a "v$NEW" -m "Release v$NEW"
git push origin "v$NEW"
gh release create "v$NEW" --generate-notesUsage:
./bump-version.sh patch # 1.2.3 -> 1.2.4
./bump-version.sh minor # 1.2.4 -> 1.3.0
./bump-version.sh major # 1.3.0 -> 2.0.0Release Checklist
## Release v1.3.0 Checklist
### Pre-Release
- [ ] All PRs merged to main
- [ ] CI/CD passing on main
- [ ] Version numbers updated in package.json/pyproject.toml
- [ ] CHANGELOG.md updated
- [ ] Documentation updated
- [ ] Milestone closed
### Release
- [ ] Tag created and pushed
- [ ] GitHub release created
- [ ] Release notes reviewed
- [ ] Assets uploaded (if applicable)
### Post-Release
- [ ] Deployment verified
- [ ] Announcement posted (if applicable)
- [ ] Next milestone createdRelease Workflows
Standard Release
# 1. Ensure main is up to date
git checkout main
git pull origin main
# 2. Determine version bump
# Check commits since last release
gh release view --json tagName -q .tagName # Current: v1.2.3
git log v1.2.3..HEAD --oneline
# 3. Create and push tag
git tag -a v1.3.0 -m "Release v1.3.0"
git push origin v1.3.0
# 4. Create GitHub release
gh release create v1.3.0 \
--title "v1.3.0: Feature Name" \
--generate-notes
# 5. Close milestone if used
gh api -X PATCH repos/:owner/:repo/milestones/5 -f state=closedHotfix Release
# 1. Branch from release tag
git checkout -b hotfix/v1.2.4 v1.2.3
# 2. Fix and commit
git commit -m "fix: Critical security patch"
# 3. Tag and release
git tag -a v1.2.4 -m "Hotfix: Security patch"
git push origin v1.2.4
gh release create v1.2.4 --title "v1.2.4: Security Hotfix" \
--notes "Critical security fix for authentication bypass"
# 4. Merge fix to main
git checkout main
git cherry-pick <commit-sha>
git push origin mainSemantic Versioning Guide
Standard versioning for software releases.
Version Format
MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]
Examples:
1.0.0
2.1.3
3.0.0-alpha.1
3.0.0-beta.2+build.123When to Bump
PATCH (x.x.X)
Bug fixes, backwards compatible:
1.0.0 -> 1.0.1- Fix typo in error message
- Fix edge case bug
- Security patch (no API change)
- Performance improvement (no API change)
MINOR (x.X.0)
New features, backwards compatible:
1.0.1 -> 1.1.0- Add new function/method
- Add new optional parameter
- Deprecate (but don't remove) feature
- Add new event/hook
MAJOR (X.0.0)
Breaking changes:
1.1.0 -> 2.0.0- Remove function/method
- Change function signature
- Change return type
- Rename public API
- Change default behavior
Pre-release Versions
2.0.0-alpha.1 # Early development
2.0.0-alpha.2
2.0.0-beta.1 # Feature complete
2.0.0-beta.2
2.0.0-rc.1 # Release candidate
2.0.0-rc.2
2.0.0 # Final releasePrecedence
1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta < 1.0.0-rc.1 < 1.0.0Decision Tree
Is it a breaking change?
├── Yes → MAJOR bump
└── No
└── Is it a new feature?
├── Yes → MINOR bump
└── No → PATCH bumpExamples
| Change | Version Bump | Reason |
|---|---|---|
| Fix null pointer crash | 1.0.0 → 1.0.1 | Bug fix |
Add sort parameter | 1.0.1 → 1.1.0 | New feature |
Change sort to required | 1.1.0 → 2.0.0 | Breaking |
| Improve performance 2x | 1.0.0 → 1.0.1 | No API change |
| Remove deprecated method | 1.5.0 → 2.0.0 | Breaking |
| Add new endpoint | 1.0.0 → 1.1.0 | New feature |
Commands
# View current version
cat package.json | jq '.version'
# Bump with npm
npm version patch # 1.0.0 -> 1.0.1
npm version minor # 1.0.1 -> 1.1.0
npm version major # 1.1.0 -> 2.0.0
# With git tag
npm version patch -m "Release %s"
# Manual
git tag -a v1.2.3 -m "Release v1.2.3"
git push origin v1.2.30.x.x Versions
For pre-1.0 development:
- API is unstable
- MINOR can include breaking changes
- PATCH for any fixes/features
0.1.0 Initial development
0.2.0 Breaking changes OK
0.9.0 Approaching stable
1.0.0 First stable releaseRule Categories
1. Version Integrity (versioning) — HIGH — 1 rule
Validates that the semantic version bump matches the highest-impact commit in the release to protect downstream consumers from broken semver contracts.
version-bump-validation.md— Classify every commit as patch/minor/major; bump must match highest-impact commit; breaking changes require major
2. Changelog Completeness (documentation) — HIGH — 1 rule
Ensures all breaking changes appear in the changelog with migration instructions before a release is published.
changelog-completeness.md— Cross-reference commits with changelog; every!orBREAKING CHANGEcommit must have migration instructions in the changelog
Changelog Must Include All Breaking Changes
Why
Developers read changelogs before upgrading. If a breaking change is missing from the changelog, they upgrade without migration steps and hit runtime errors. After one bad experience, teams stop upgrading and fall behind on security patches.
Rule
Before publishing a release: 1. Cross-reference all commits with the changelog 2. Every commit with ! or BREAKING CHANGE must appear in the changelog 3. Breaking changes must include migration instructions 4. The Breaking Changes section must appear first in the changelog
Incorrect — incomplete changelog missing breaking changes
# Changelog
## [2.0.0] - 2026-03-10
### Added
- Bulk export endpoint (#234)
- Dark mode support (#236)
### Fixed
- Date parser null handling (#235)Problems:
- Version is 2.0.0 (major) but no breaking changes listed
- The commit
feat!: remove /v1/users endpoint (#230)is missing - The commit
refactor!: rename User model to Account (#232)is missing - No migration instructions for breaking changes
Correct — complete changelog with migration instructions
# Changelog
## [2.0.0] - 2026-03-10
### BREAKING CHANGES
- **Removed `/v1/users` endpoint** (#230): The deprecated v1 users
API has been removed. Migrate to `/v2/users` which supports
cursor-based pagination.
- GET /v1/users?page=2&limit=20
+ GET /v2/users?cursor=abc123&limit=20
- **Renamed `User` model to `Account`** (#232): All database tables
and API responses now use `Account`. Run the migration:
npm run migrate:up
Update imports:- import { User } from "./models/user";
+ import { Account } from "./models/account";
### Added
- Bulk export endpoint (#234)
- Dark mode support (#236)
### Fixed
- Date parser null handling (#235)Verification Script
#!/bin/bash
LAST_TAG=$(gh release view --json tagName -q .tagName)
# Find all breaking change commits
BREAKING=$(git log "$LAST_TAG"..HEAD --oneline --format="%s" \
| grep -E '!:|BREAKING CHANGE')
if [ -z "$BREAKING" ]; then
echo "No breaking changes found"
exit 0
fi
echo "Breaking changes to verify in CHANGELOG.md:"
echo "$BREAKING" | while IFS= read -r commit; do
# Extract PR number if present
PR=$(echo "$commit" | grep -oE '#[0-9]+' | head -1)
if [ -n "$PR" ] && ! grep -q "$PR" CHANGELOG.md; then
echo " MISSING: $commit"
else
echo " OK: $commit"
fi
doneChangelog Section Order
| Section | Required When | Position |
|---|---|---|
| BREAKING CHANGES | Any ! or BREAKING CHANGE commit | First |
| Deprecated | Deprecation notices | Second |
| Added | New features | Third |
| Changed | Behavior changes | Fourth |
| Fixed | Bug fixes | Fifth |
| Security | CVE patches | Sixth |
Validate Version Bump Matches Change Scope
Why
Semantic versioning is a contract with consumers. A breaking API change shipped as a patch version (1.2.3 -> 1.2.4) causes runtime failures for anyone pinned to ^1.2.3. A bug fix shipped as major (1.2.3 -> 2.0.0) forces unnecessary migration work.
Rule
Before tagging a release: 1. List all commits since the last release 2. Classify each commit as patch/minor/major based on its impact 3. The version bump must match the highest-impact commit 4. Breaking changes require major bump — no exceptions
Incorrect — bump version without checking changes
# Blindly bump patch without reviewing commits
CURRENT="1.2.3"
NEW="1.2.4" # Assumed patch
git tag -a "v$NEW" -m "Release v$NEW"
git push origin "v$NEW"
gh release create "v$NEW" --generate-notes
# But commits since v1.2.3 include:
# - "feat!: remove deprecated /v1/users endpoint" (BREAKING)
# - "feat: add /v2/users with pagination" (MINOR)
# - "fix: correct date parsing" (PATCH)
# Correct bump should have been MAJOR (2.0.0)Correct — classify commits then determine bump
#!/bin/bash
LAST_TAG=$(gh release view --json tagName -q .tagName)
echo "Commits since $LAST_TAG:"
# Classify commits
MAJOR=0; MINOR=0; PATCH=0
while IFS= read -r msg; do
if echo "$msg" | grep -qE '^(feat|fix|refactor)(\(.+\))?!:|^BREAKING'; then
MAJOR=$((MAJOR + 1))
echo " MAJOR: $msg"
elif echo "$msg" | grep -qE '^feat(\(.+\))?:'; then
MINOR=$((MINOR + 1))
echo " MINOR: $msg"
else
PATCH=$((PATCH + 1))
echo " PATCH: $msg"
fi
done < <(git log "$LAST_TAG"..HEAD --oneline --format="%s")
# Determine correct bump
if [ "$MAJOR" -gt 0 ]; then
echo "Required bump: MAJOR ($MAJOR breaking changes)"
elif [ "$MINOR" -gt 0 ]; then
echo "Required bump: MINOR ($MINOR new features)"
else
echo "Required bump: PATCH ($PATCH fixes)"
fiClassification Guide
| Commit Pattern | Bump | Example |
|---|---|---|
fix: | PATCH | fix: handle null in date parser |
docs:, chore:, ci: | PATCH | docs: update API reference |
feat: | MINOR | feat: add bulk export endpoint |
feat!: or BREAKING CHANGE: | MAJOR | feat!: remove v1 API endpoints |
refactor!: | MAJOR | refactor!: rename User to Account |
Pre-Release Validation
# Before creating the release, verify:
echo "=== Pre-Release Checklist ==="
echo "1. Last release: $LAST_TAG"
echo "2. Commits since: $(git rev-list "$LAST_TAG"..HEAD --count)"
echo "3. Breaking changes: $MAJOR"
echo "4. New features: $MINOR"
echo "5. Fixes/chores: $PATCH"
echo "6. Proposed version: v$NEW"
echo ""
if [ "$MAJOR" -gt 0 ] && ! echo "$NEW" | grep -qE '^[0-9]+\.0\.0'; then
echo "ERROR: Breaking changes found but version is not a major bump"
exit 1
fiCreate release: $ARGUMENTS
Release Context (Auto-Detected)
- Current Version: !
git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0" - Last Release Date: !
git log -1 --format=%ai $(git describe --tags --abbrev=0 2>/dev/null || echo "HEAD") 2>/dev/null || echo "Unknown" - Commits Since Last Release: !
git log $(git describe --tags --abbrev=0 2>/dev/null || echo "HEAD~10")..HEAD --oneline 2>/dev/null | wc -l | tr -d ' ' || echo "0" - Current Branch: !
git branch --show-current || echo "main" - Changed Files: !
git diff --name-only $(git describe --tags --abbrev=0 2>/dev/null || echo "HEAD~10")..HEAD 2>/dev/null | head -20 || echo "No changes detected" - GitHub CLI Available: !
which gh >/dev/null 2>&1 && echo "✅ Yes" || echo "❌ Not found"
Your Task
Create a GitHub release for version: $ARGUMENTS
Review the auto-detected release context above, verify the checklist below, then run the release commands to create the tag and GitHub release.
Release Information
Version: $ARGUMENTS Tag: v$ARGUMENTS
Recent Changes
!git log $(git describe --tags --abbrev=0 2>/dev/null || echo "HEAD~10")..HEAD --format="- %s (%h)" 2>/dev/null | head -30 || echo "No recent commits"
Release Checklist
- [ ] Version number confirmed: $ARGUMENTS
- [ ] All tests passing
- [ ] Changelog updated
- [ ] Version files updated (package.json, pyproject.toml, etc.)
- [ ] Branch is clean (no uncommitted changes)
- [ ] On main/master branch
Create Release
Run the following to create the release:
# Create and push tag
git tag -a "v$ARGUMENTS" -m "Release v$ARGUMENTS"
git push origin "v$ARGUMENTS"
# Create GitHub release
gh release create "v$ARGUMENTS" \
--title "Release v$ARGUMENTS" \
--notes "$(git log $(git describe --tags --abbrev=0 2>/dev/null || echo "HEAD~10")..HEAD --format='- %s' 2>/dev/null || echo 'Release notes')"Or use the release workflow script:
source scripts/release-scripts.sh
create_release "$ARGUMENTS" "Release v$ARGUMENTS"#!/bin/bash
# Release Management Scripts
# Automate semantic versioning and GitHub releases
set -euo pipefail
# =============================================================================
# VERSION DETECTION
# =============================================================================
# Get current version from latest git tag
get_current_version() {
local version
version=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
echo "${version#v}" # Remove 'v' prefix
}
# Parse version components
parse_version() {
local version="$1"
local major minor patch
IFS='.' read -r major minor patch <<< "${version%%-*}"
patch="${patch%%+*}" # Remove build metadata
echo "$major $minor $patch"
}
# =============================================================================
# VERSION BUMPING
# =============================================================================
# Bump version based on type
bump_version() {
local bump_type="$1"
local current
current=$(get_current_version)
local major minor patch
read -r major minor patch <<< "$(parse_version "$current")"
case "$bump_type" in
major)
major=$((major + 1))
minor=0
patch=0
;;
minor)
minor=$((minor + 1))
patch=0
;;
patch)
patch=$((patch + 1))
;;
*)
echo "Invalid bump type: $bump_type"
echo "Use: major, minor, or patch"
return 1
;;
esac
echo "$major.$minor.$patch"
}
# Interactive version bump with change analysis
smart_bump() {
echo "=== Smart Version Bump ==="
echo ""
local current
current=$(get_current_version)
echo "Current version: v$current"
echo ""
# Analyze commits since last release
echo "Analyzing commits since v$current..."
echo ""
local breaking=0 features=0 fixes=0 other=0
while IFS= read -r commit; do
case "$commit" in
*"BREAKING"*|*"!"*)
((breaking++))
;;
feat*)
((features++))
;;
fix*)
((fixes++))
;;
*)
((other++))
;;
esac
done < <(git log "v$current"..HEAD --format=%s 2>/dev/null || true)
echo "Commits since last release:"
echo " Breaking changes: $breaking"
echo " Features: $features"
echo " Fixes: $fixes"
echo " Other: $other"
echo ""
# Suggest bump type
local suggested="patch"
if [[ $breaking -gt 0 ]]; then
suggested="major"
elif [[ $features -gt 0 ]]; then
suggested="minor"
fi
local new_major new_minor new_patch
new_major=$(bump_version major)
new_minor=$(bump_version minor)
new_patch=$(bump_version patch)
echo "Suggested: $suggested"
echo ""
echo "Options:"
echo " 1) major -> v$new_major"
echo " 2) minor -> v$new_minor"
echo " 3) patch -> v$new_patch"
echo " 4) custom"
echo " 5) cancel"
echo ""
read -p "Select option [1-5]: " choice
case "$choice" in
1) echo "$new_major" ;;
2) echo "$new_minor" ;;
3) echo "$new_patch" ;;
4)
read -p "Enter version (without v): " custom
echo "$custom"
;;
5|*)
echo "Cancelled"
return 1
;;
esac
}
# =============================================================================
# RELEASE CREATION
# =============================================================================
# Create a new release
create_release() {
local version="${1:-}"
local title="${2:-}"
local draft="${3:-false}"
local prerelease="${4:-false}"
if [[ -z "$version" ]]; then
echo "Usage: create_release VERSION [TITLE] [draft] [prerelease]"
return 1
fi
# Ensure version has v prefix for tag
local tag="v${version#v}"
echo "=== Creating Release $tag ==="
echo ""
# Check we're on main
local current_branch
current_branch=$(git branch --show-current)
if [[ "$current_branch" != "main" && "$current_branch" != "master" ]]; then
echo "⚠️ Warning: Not on main/master branch (on $current_branch)"
read -p "Continue anyway? (y/N) " confirm
if [[ "$confirm" != "y" ]]; then
echo "Cancelled"
return 1
fi
fi
# Ensure working directory is clean
if [[ -n $(git status --porcelain) ]]; then
echo "❌ Working directory not clean. Commit or stash changes first."
return 1
fi
# Pull latest
echo "Pulling latest changes..."
git pull origin "$current_branch"
# Create tag
echo "Creating tag $tag..."
if [[ -n "$title" ]]; then
git tag -a "$tag" -m "$title"
else
git tag -a "$tag" -m "Release $tag"
fi
# Push tag
echo "Pushing tag..."
git push origin "$tag"
# Create GitHub release
echo "Creating GitHub release..."
local gh_args=(
"--generate-notes"
)
if [[ -n "$title" ]]; then
gh_args+=("--title" "$title")
else
gh_args+=("--title" "Release $tag")
fi
if [[ "$draft" == "true" ]]; then
gh_args+=("--draft")
fi
if [[ "$prerelease" == "true" ]]; then
gh_args+=("--prerelease")
fi
gh release create "$tag" "${gh_args[@]}"
echo ""
echo "✅ Release $tag created successfully!"
echo ""
echo "View: gh release view $tag --web"
}
# Create a draft release for review
create_draft_release() {
local version
version=$(smart_bump)
if [[ -z "$version" || "$version" == "Cancelled" ]]; then
return 1
fi
read -p "Release title (optional): " title
create_release "$version" "$title" "true" "false"
}
# Create a pre-release (alpha/beta/rc)
create_prerelease() {
local base_version="$1"
local stage="${2:-beta}" # alpha, beta, rc
local number="${3:-1}"
local version="${base_version}-${stage}.${number}"
create_release "$version" "Pre-release $version" "false" "true"
}
# =============================================================================
# RELEASE WORKFLOW
# =============================================================================
# Full release workflow
release_workflow() {
echo "=== Release Workflow ==="
echo ""
# Step 1: Ensure clean state
echo "Step 1: Checking repository state..."
git fetch origin
local current_branch
current_branch=$(git branch --show-current)
if [[ "$current_branch" != "main" ]]; then
echo "Switching to main..."
git checkout main
git pull origin main
fi
if [[ -n $(git status --porcelain) ]]; then
echo "❌ Working directory not clean"
return 1
fi
echo "✅ Repository is clean"
echo ""
# Step 2: Show what's changed
echo "Step 2: Changes since last release..."
local current
current=$(get_current_version)
echo ""
echo "Commits since v$current:"
git log "v$current"..HEAD --oneline | head -20
echo ""
# Step 3: Determine version
echo "Step 3: Determine new version..."
local new_version
new_version=$(smart_bump)
if [[ -z "$new_version" || "$new_version" == "Cancelled" ]]; then
return 1
fi
echo ""
echo "New version: v$new_version"
echo ""
# Step 4: Update version files (optional)
echo "Step 4: Update version files..."
update_version_files "$new_version"
# Step 5: Create release
echo "Step 5: Create release..."
read -p "Create as draft first? (Y/n) " draft_choice
local is_draft="true"
if [[ "$draft_choice" == "n" || "$draft_choice" == "N" ]]; then
is_draft="false"
fi
read -p "Release title (or Enter for default): " title
title="${title:-Release v$new_version}"
create_release "$new_version" "$title" "$is_draft" "false"
echo ""
echo "✅ Release workflow complete!"
if [[ "$is_draft" == "true" ]]; then
echo ""
echo "Next steps:"
echo " 1. Review draft release notes"
echo " 2. Publish: gh release edit v$new_version --draft=false"
fi
}
# =============================================================================
# VERSION FILE UPDATES
# =============================================================================
# Update version in common files
update_version_files() {
local version="$1"
# package.json
if [[ -f "package.json" ]]; then
echo "Updating package.json..."
# Use node if available, otherwise sed
if command -v node >/dev/null; then
node -e "
const pkg = require('./package.json');
pkg.version = '$version';
require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
"
else
sed -i.bak "s/\"version\": \".*\"/\"version\": \"$version\"/" package.json
rm -f package.json.bak
fi
fi
# pyproject.toml
if [[ -f "pyproject.toml" ]]; then
echo "Updating pyproject.toml..."
sed -i.bak "s/^version = \".*\"/version = \"$version\"/" pyproject.toml
rm -f pyproject.toml.bak
fi
# Cargo.toml
if [[ -f "Cargo.toml" ]]; then
echo "Updating Cargo.toml..."
sed -i.bak "s/^version = \".*\"/version = \"$version\"/" Cargo.toml
rm -f Cargo.toml.bak
fi
# Commit version bump if changes were made
if [[ -n $(git status --porcelain) ]]; then
echo "Committing version bump..."
git add -A
git commit -m "chore: Bump version to $version"
git push origin main
fi
}
# =============================================================================
# HOTFIX WORKFLOW
# =============================================================================
# Create hotfix release
hotfix_release() {
local fix_description="$1"
if [[ -z "$fix_description" ]]; then
echo "Usage: hotfix_release 'Description of the fix'"
return 1
fi
echo "=== Hotfix Release ==="
echo ""
local current
current=$(get_current_version)
local new_version
new_version=$(bump_version patch)
echo "Current: v$current"
echo "Hotfix: v$new_version"
echo ""
# Create release with hotfix note
create_release "$new_version" "Hotfix: $fix_description" "false" "false"
}
# =============================================================================
# USAGE
# =============================================================================
usage() {
cat << 'EOF'
Release Management Scripts
Commands:
get_current_version Get current version from git tags
bump_version TYPE Bump version (major|minor|patch)
smart_bump Interactive version bump with commit analysis
create_release VER Create a new release
create_draft_release Create a draft release for review
create_prerelease VER Create a pre-release (alpha/beta/rc)
release_workflow Full guided release workflow
hotfix_release DESC Create a hotfix patch release
Examples:
source release-scripts.sh
# Quick patch release
create_release "1.2.4" "Bug fixes"
# Full workflow
release_workflow
# Hotfix
hotfix_release "Fix critical auth bypass"
# Pre-release
create_prerelease "2.0.0" "beta" "1" # Creates v2.0.0-beta.1
EOF
}
# Show usage if run directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
usage
fi
#!/usr/bin/env python3
"""
Version Manager
Manages semantic versioning and changelog generation for releases
Usage: ./version-manager.py [command] [options]
Commands:
current Show current version
bump Bump version (major|minor|patch)
changelog Generate changelog from commits
validate Validate version string
"""
import argparse
import json
import re
import subprocess
import sys
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
@dataclass
class Commit:
"""Represents a parsed commit."""
hash: str
type: str
scope: str | None
description: str
body: str
breaking: bool = False
@dataclass
class Version:
"""Semantic version representation."""
major: int
minor: int
patch: int
prerelease: str | None = None
build: str | None = None
@classmethod
def parse(cls, version_str: str) -> "Version":
"""Parse a version string like '1.2.3' or 'v1.2.3-beta.1+build.123'."""
version_str = version_str.lstrip("v")
# Handle prerelease and build metadata
prerelease = None
build = None
if "+" in version_str:
version_str, build = version_str.split("+", 1)
if "-" in version_str:
version_str, prerelease = version_str.split("-", 1)
parts = version_str.split(".")
if len(parts) < 3:
parts.extend(["0"] * (3 - len(parts)))
return cls(
major=int(parts[0]),
minor=int(parts[1]),
patch=int(parts[2]),
prerelease=prerelease,
build=build,
)
def __str__(self) -> str:
version = f"{self.major}.{self.minor}.{self.patch}"
if self.prerelease:
version += f"-{self.prerelease}"
if self.build:
version += f"+{self.build}"
return version
def bump_major(self) -> "Version":
return Version(self.major + 1, 0, 0)
def bump_minor(self) -> "Version":
return Version(self.major, self.minor + 1, 0)
def bump_patch(self) -> "Version":
return Version(self.major, self.minor, self.patch + 1)
def run_git(args: list[str], cwd: Path | None = None) -> tuple[int, str]:
"""Run a git command and return exit code and output."""
try:
result = subprocess.run(
["git", *args],
capture_output=True,
text=True,
cwd=cwd,
timeout=30,
)
return result.returncode, result.stdout.strip()
except (subprocess.TimeoutExpired, FileNotFoundError):
return 1, ""
def get_current_version(cwd: Path | None = None) -> Version | None:
"""Get current version from git tags."""
code, output = run_git(["describe", "--tags", "--abbrev=0"], cwd)
if code == 0 and output:
return Version.parse(output)
return Version(0, 0, 0)
def get_commits_since_tag(tag: str | None, cwd: Path | None = None) -> list[Commit]:
"""Get commits since the specified tag."""
if tag:
code, output = run_git(["log", f"{tag}..HEAD", "--format=%H|%s|%b%n---COMMIT---"], cwd)
else:
code, output = run_git(["log", "--format=%H|%s|%b%n---COMMIT---"], cwd)
if code != 0:
return []
commits = []
for commit_text in output.split("---COMMIT---"):
commit_text = commit_text.strip()
if not commit_text:
continue
lines = commit_text.split("\n")
if not lines:
continue
first_line = lines[0]
parts = first_line.split("|", 2)
if len(parts) < 2:
continue
commit_hash = parts[0]
subject = parts[1]
body = parts[2] if len(parts) > 2 else ""
# Parse conventional commit format: type(scope): description
match = re.match(r"^(\w+)(?:\(([^)]+)\))?(!)?:\s*(.+)$", subject)
if match:
commit_type = match.group(1).lower()
scope = match.group(2)
breaking = match.group(3) == "!" or "BREAKING" in body.upper()
description = match.group(4)
else:
commit_type = "other"
scope = None
breaking = "BREAKING" in body.upper()
description = subject
commits.append(
Commit(
hash=commit_hash[:7],
type=commit_type,
scope=scope,
description=description,
body=body,
breaking=breaking,
)
)
return commits
def suggest_bump(commits: list[Commit]) -> str:
"""Suggest version bump type based on commits."""
has_breaking = any(c.breaking for c in commits)
has_features = any(c.type == "feat" for c in commits)
if has_breaking:
return "major"
elif has_features:
return "minor"
return "patch"
def generate_changelog(commits: list[Commit], version: Version) -> str:
"""Generate changelog from commits."""
categories = {
"Breaking Changes": [],
"Features": [],
"Bug Fixes": [],
"Performance": [],
"Documentation": [],
"Other": [],
}
type_mapping = {
"feat": "Features",
"fix": "Bug Fixes",
"perf": "Performance",
"docs": "Documentation",
"refactor": "Other",
"style": "Other",
"test": "Other",
"chore": "Other",
"ci": "Other",
}
for commit in commits:
if commit.breaking:
categories["Breaking Changes"].append(commit)
else:
category = type_mapping.get(commit.type, "Other")
categories[category].append(commit)
lines = [
f"## [{version}] - {datetime.now(UTC).strftime('%Y-%m-%d')}",
"",
]
for category, category_commits in categories.items():
if not category_commits:
continue
lines.append(f"### {category}")
lines.append("")
for commit in category_commits:
scope_str = f"**{commit.scope}:** " if commit.scope else ""
lines.append(f"- {scope_str}{commit.description} ({commit.hash})")
lines.append("")
return "\n".join(lines)
def update_version_file(path: Path, version: Version) -> bool:
"""Update version in a file."""
if not path.exists():
return False
content = path.read_text()
version_str = str(version)
# Try different patterns
patterns = [
(r'"version":\s*"[^"]*"', f'"version": "{version_str}"'),
(r"^version\s*=\s*['\"].*['\"]", f'version = "{version_str}"', re.MULTILINE),
(r"__version__\s*=\s*['\"].*['\"]", f'__version__ = "{version_str}"'),
]
updated = False
for pattern in patterns:
flags = pattern[2] if len(pattern) > 2 else 0
if re.search(pattern[0], content, flags):
content = re.sub(pattern[0], pattern[1], content, flags=flags)
updated = True
if updated:
path.write_text(content)
return updated
def find_version_files(cwd: Path) -> list[Path]:
"""Find files that typically contain version numbers."""
candidates = [
"package.json",
"pyproject.toml",
"Cargo.toml",
"setup.py",
"version.py",
"__version__.py",
]
found = []
for candidate in candidates:
path = cwd / candidate
if path.exists():
found.append(path)
return found
def cmd_current(args: argparse.Namespace) -> int:
"""Show current version."""
cwd = Path(args.path).resolve()
version = get_current_version(cwd)
if args.json:
print(
json.dumps(
{
"version": str(version) if version else None,
"major": version.major if version else 0,
"minor": version.minor if version else 0,
"patch": version.patch if version else 0,
}
)
)
else:
print(f"v{version}" if version else "No version found")
return 0
def cmd_bump(args: argparse.Namespace) -> int:
"""Bump version."""
cwd = Path(args.path).resolve()
current = get_current_version(cwd) or Version(0, 0, 0)
# Get commits to analyze
current_tag = f"v{current}" if current.major > 0 or current.minor > 0 or current.patch > 0 else None
commits = get_commits_since_tag(current_tag, cwd)
bump_type = args.type
if bump_type == "auto":
bump_type = suggest_bump(commits)
if bump_type == "major":
new_version = current.bump_major()
elif bump_type == "minor":
new_version = current.bump_minor()
else:
new_version = current.bump_patch()
if args.json:
print(
json.dumps(
{
"current": str(current),
"new": str(new_version),
"bump_type": bump_type,
"commits_analyzed": len(commits),
"has_breaking": any(c.breaking for c in commits),
"has_features": any(c.type == "feat" for c in commits),
}
)
)
else:
print(f"Current version: v{current}")
print(f"Bump type: {bump_type}")
print(f"New version: v{new_version}")
print(f"\nCommits analyzed: {len(commits)}")
if not args.dry_run:
# Update version files
version_files = find_version_files(cwd)
for vf in version_files:
if update_version_file(vf, new_version):
print(f"Updated: {vf}")
print("\nTo create the release:")
print(" git add .")
print(f' git commit -m "chore: Bump version to {new_version}"')
print(f' git tag -a "v{new_version}" -m "Release v{new_version}"')
print(f' git push origin "v{new_version}"')
return 0
def cmd_changelog(args: argparse.Namespace) -> int:
"""Generate changelog."""
cwd = Path(args.path).resolve()
current = get_current_version(cwd) or Version(0, 0, 0)
version = Version.parse(args.version) if args.version else current.bump_patch()
current_tag = f"v{current}" if current.major > 0 or current.minor > 0 or current.patch > 0 else None
commits = get_commits_since_tag(current_tag, cwd)
if not commits:
print("No commits found since last release", file=sys.stderr)
return 1
changelog = generate_changelog(commits, version)
if args.json:
print(
json.dumps(
{
"version": str(version),
"commits": len(commits),
"changelog": changelog,
}
)
)
else:
print(changelog)
return 0
def cmd_validate(args: argparse.Namespace) -> int:
"""Validate version string."""
try:
version = Version.parse(args.version)
if args.json:
print(
json.dumps(
{
"valid": True,
"version": str(version),
"major": version.major,
"minor": version.minor,
"patch": version.patch,
"prerelease": version.prerelease,
"build": version.build,
}
)
)
else:
print(f"Valid: {version}")
return 0
except (ValueError, IndexError) as e:
if args.json:
print(json.dumps({"valid": False, "error": str(e)}))
else:
print(f"Invalid: {e}", file=sys.stderr)
return 1
def main():
parser = argparse.ArgumentParser(description="Manage semantic versions and changelogs")
parser.add_argument("--path", default=".", help="Project path")
parser.add_argument("--json", action="store_true", help="Output as JSON")
subparsers = parser.add_subparsers(dest="command", help="Command")
# current
subparsers.add_parser("current", help="Show current version")
# bump
bump_parser = subparsers.add_parser("bump", help="Bump version")
bump_parser.add_argument("type", choices=["major", "minor", "patch", "auto"], default="auto", nargs="?")
bump_parser.add_argument("--dry-run", action="store_true", help="Don't update files")
# changelog
changelog_parser = subparsers.add_parser("changelog", help="Generate changelog")
changelog_parser.add_argument("--version", help="Target version for changelog")
# validate
validate_parser = subparsers.add_parser("validate", help="Validate version string")
validate_parser.add_argument("version", help="Version string to validate")
args = parser.parse_args()
if args.command == "current":
return cmd_current(args)
elif args.command == "bump":
return cmd_bump(args)
elif args.command == "changelog":
return cmd_changelog(args)
elif args.command == "validate":
return cmd_validate(args)
else:
parser.print_help()
return 0
if __name__ == "__main__":
sys.exit(main())
{
"skill": "release-management",
"version": "1.0.0",
"testCases": [
{
"id": "basic-create-a-new-release",
"rule": "",
"query": "Create a new release for version 2.1.0 with auto-generated notes from merged PRs. We also need to close the milestone and upload the dist/app.zip build artifact.",
"expectedBehavior": [
"Claude ensures main is up to date (git checkout main, git pull)",
"Creates and pushes a git tag v2.1.0",
"Runs gh release create v2.1.0 --generate-notes with the build artifact",
"Closes the milestone via gh api (not --milestone flag)",
"Follows the standard release workflow order: tag -> release -> milestone close"
]
},
{
"id": "edge-we-found-a-critical",
"rule": "",
"query": "We found a critical security vulnerability in production on v1.5.3. We need to ship a hotfix release immediately.",
"expectedBehavior": [
"Claude follows the hotfix release workflow: branch from release tag v1.5.3",
"Creates hotfix branch (hotfix/v1.5.4), applies fix, commits",
"Tags v1.5.4 and creates a GitHub release with clear security hotfix notes",
"Cherry-picks the fix back to main to keep branches in sync",
"Uses semantic versioning patch bump (1.5.3 -> 1.5.4)"
]
},
{
"id": "negative-add-a-new-endpoint",
"rule": "",
"query": "Add a new endpoint to the users API that returns paginated results with cursor-based pagination",
"expectedBehavior": [
"Claude does NOT invoke the release-management skill",
"Implements the API endpoint using standard backend patterns",
"No release creation, version tagging, or changelog generation"
]
}
]
}