Releaserator Skill
Automate the release process with semantic versioning, Keep A Changelog format, and GitHub release creation.
Implementation Steps
Step 1: Pre-flight Checks
Task: Verify repository state is clean and ready for release
Run these checks:
# Check git status (must be clean)
git status --porcelain
# Check current branch
git branch --show-current
# Check for plugin.json
test -f .claude-plugin/plugin.json && echo "EXISTS" || echo "MISSING"
# Check for gh CLI
command -v gh >/dev/null 2>&1 && echo "INSTALLED" || echo "NOT_FOUND"
Validation:
- Working directory MUST be clean (no uncommitted changes)
- If dirty: Error with message: "Working directory has uncommitted changes. Please commit or stash changes before creating a release."
- Current branch SHOULD be main/master
- If not: Ask user to confirm creating release from current branch
- plugin.json MUST exist
- If missing: Error with message: "File .claude-plugin/plugin.json not found. Are you in a plugin directory?"
- gh CLI MUST be installed
- If missing: Error with message: "GitHub CLI (gh) not found. Install with: brew install gh"
Optional check: Verify docs/CONTEXT.md is recent (< 24 hours old)
- If stale: Warn user and suggest running
/session-wrapupfirst
Step 2: Determine Current Version and Last Release
Task: Read current version and find last release tag
# Read current version from plugin.json (use Read tool)
# Parse the "version" field
# Find last tag (if any)
git describe --tags --abbrev=0 2>/dev/null || echo "NO_TAGS"
# List all tags for reference
git tag --list --sort=-version:refname
Logic:
- Parse version from plugin.json (e.g., "1.0.0")
- If no tags exist, this is first release (baseline: use all commits)
- If tags exist, use latest tag as baseline for commit collection
Store:
CURRENT_VERSION= version from plugin.jsonLAST_TAG= latest git tag (or "NO_TAGS")IS_FIRST_RELEASE= true if no tags exist
Step 3: Collect Commits Since Last Release
Task: Get all commits since last release for changelog generation
# If tags exist: commits since last tag
git log LAST_TAG..HEAD --oneline --no-merges
# If no tags: all commits (first release)
git log --oneline --no-merges
Parse each commit:
For each commit line, extract:
- Commit hash (first 7 characters)
- Commit type (feat, fix, docs, refactor, perf, chore, test, ci, build, style)
- Scope (optional, in parentheses)
- Breaking change indicator (exclamation mark before colon)
- Description (after colon)
- PR reference (look for PR number pattern)
Example parsing:
abc1234 feat(auth): add OAuth support (#42)
→ hash: abc1234
→ type: feat
→ scope: auth
→ breaking: false
→ description: add OAuth support
→ pr: 42
Check commit body for BREAKING CHANGE:
For commits that might have breaking changes, also check the full commit message:
git log LAST_TAG..HEAD --format="%H %s%n%b%n---" --no-merges
Look for BREAKING CHANGE: in commit body.
Store parsed commits in a data structure:
commits = [
{hash, type, scope, breaking, description, pr},
...
]
Step 4: Determine Version Bump
Task: Analyze commits to determine MAJOR.MINOR.PATCH bump
Rules (Conventional Commits):
-
MAJOR bump if ANY commit has:
- BREAKING CHANGE in commit body/footer, OR
- Exclamation mark before colon (e.g., feat!, fix!)
-
MINOR bump if (and no MAJOR):
- ANY
feat:commits exist
- ANY
-
PATCH bump if (and no MAJOR/MINOR):
- ANY
fix:orperf:commits exist
- ANY
-
No bump if only:
docs:,refactor:,chore:,test:,ci:,build:,style:
Calculate new version:
# Pseudocode for version bump
def bump_version(current, bump_type):
major, minor, patch = map(int, current.split('.'))
if bump_type == 'MAJOR':
return f"{major + 1}.0.0"
elif bump_type == 'MINOR':
return f"{major}.{minor + 1}.0"
elif bump_type == 'PATCH':
return f"{major}.{minor}.{patch + 1}"
else:
return current # No bump
Edge case: If no version bump needed (only docs/chore commits), ask user:
⚠️ No version bump needed (only documentation/chore commits).
Do you want to create a release anyway? (y/N)
Store:
BUMP_TYPE= "MAJOR" | "MINOR" | "PATCH" | "NONE"NEW_VERSION= calculated new version
Step 5: Generate Changelog Entry
Task: Create Keep A Changelog formatted entry for this release
Group commits by section:
| Commit Type | Changelog Section | Variable |
|-------------|-------------------|----------|
| feat: | Added | ADDED |
| refactor: (behavior change) | Changed | CHANGED |
| Mentions "deprecat" | Deprecated | DEPRECATED |
| Mentions "remov" | Removed | REMOVED |
| fix: | Fixed | FIXED |
| Mentions "security" or "CVE" | Security | SECURITY |
Format each commit line:
- Description ([#PR](https://github.com/OWNER/REPO/pull/PR)) ([hash](https://github.com/OWNER/REPO/commit/hash))
If no PR reference, omit PR link. Always include commit hash link.
Breaking changes:
Collect all breaking changes (commits with exclamation mark or BREAKING CHANGE) and add to the top of the relevant section with ⚠️:
- ⚠️ **BREAKING**: Description of breaking change
Extract repo info:
# Get remote URL and parse owner/repo
git remote get-url origin
# Parse: git@github.com:owner/repo.git → owner/repo
# Or: https://github.com/owner/repo.git → owner/repo
Load template: Read skills/releaserator/templates/changelog-entry.md
Substitute variables:
{{VERSION}}→ NEW_VERSION{{DATE}}→ today's date (ISO format: YYYY-MM-DD){{ADDED}}→ formatted commit lines (or empty if none){{CHANGED}}→ formatted commit lines (or empty if none){{DEPRECATED}}→ formatted commit lines (or empty if none){{REMOVED}}→ formatted commit lines (or empty if none){{FIXED}}→ formatted commit lines (or empty if none){{SECURITY}}→ formatted commit lines (or empty if none){{BREAKING}}→ breaking changes summary (or omit section if none)
Remove empty sections: If a section has no commits, remove the entire section heading and content.
Store: CHANGELOG_ENTRY = generated markdown
Step 6: Update or Create CHANGELOG.md
Task: Prepend new version entry to CHANGELOG.md
Check if CHANGELOG.md exists:
test -f CHANGELOG.md && echo "EXISTS" || echo "MISSING"
If CHANGELOG.md exists:
- Read existing CHANGELOG.md
- Find insertion point:
- If
## [Unreleased]exists, insert after it - Otherwise, insert after the header (before first version)
- If
- Insert CHANGELOG_ENTRY
- Update comparison links at bottom:
- Add new version link:
[VERSION]: https://github.com/OWNER/REPO/releases/tag/vVERSION - Update Unreleased link:
[Unreleased]: https://github.com/OWNER/REPO/compare/vVERSION...HEAD
- Add new version link:
If CHANGELOG.md doesn't exist:
- Load template:
skills/releaserator/templates/CHANGELOG.md - Substitute variables:
{{VERSION}}→ NEW_VERSION{{DATE}}→ today's date{{REPO}}→ owner/repo from git remote{{ADDED}},{{CHANGED}}, etc. → from CHANGELOG_ENTRY
- Write new CHANGELOG.md
Use Write tool to create/update CHANGELOG.md
Step 7: Update plugin.json Version
Task: Write new version to .claude-plugin/plugin.json
- Read .claude-plugin/plugin.json
- Parse JSON
- Update
"version"field to NEW_VERSION - Write back with proper formatting (2-space indentation, trailing newline)
Use Read and Write tools (or Edit tool for simple replacement)
Step 8: Commit Version Bump
Task: Create commit for version and changelog changes
# Stage files
git add .claude-plugin/plugin.json CHANGELOG.md
# Create commit
git commit -m "chore: bump version to NEW_VERSION"
Commit message format: chore: bump version to X.Y.Z
This follows conventional commits (type: chore, as it's a release task).
Store commit hash for later reference:
git rev-parse HEAD
Step 9: Create Git Tag
Task: Create annotated git tag for release
# Create annotated tag (not lightweight)
git tag -a vNEW_VERSION -m "Release vNEW_VERSION"
Tag format: v prefix + semantic version (e.g., v1.2.3)
Annotated tags are used (not lightweight) so they contain:
- Tagger name and email
- Tag date
- Tag message
Step 10: Push Changes and Tag
Task: Push commit and tag to remote
Ask user for confirmation:
Ready to push release v1.2.3 to remote?
- Commit: abc1234 "chore: bump version to 1.2.3"
- Tag: v1.2.3
This will:
1. Push commit to main branch
2. Push tag v1.2.3
3. Trigger any CI/CD workflows
Continue? (y/N)
If confirmed:
# Push commit to current branch
git push origin $(git branch --show-current)
# Push tag
git push origin vNEW_VERSION
If not confirmed: Stop here and report that release is ready locally but not pushed.
Step 11: Create GitHub Release
Task: Create GitHub release using gh CLI
Prepare release notes file (temporary file):
- Load template:
skills/releaserator/templates/release-notes.md - Substitute variables:
{{VERSION}}→ NEW_VERSION{{CHANGELOG_ENTRY}}→ the changelog entry content{{REPO}}→ owner/repo
- Write to temporary file (e.g., /tmp/release-notes-NEW_VERSION.md)
Create GitHub release:
gh release create vNEW_VERSION --title "vNEW_VERSION" --notes-file /tmp/release-notes-NEW_VERSION.md
Flags explained:
vNEW_VERSION: Tag name (must already exist from step 9)--title: Release title shown on GitHub--notes-file: Release body (our changelog entry + links)
Capture release URL:
# Get release URL
gh release view vNEW_VERSION --json url --jq .url
Store: RELEASE_URL = GitHub release URL
Step 12: Report Success
Task: Display release information to user
Output a summary:
✅ Release vNEW_VERSION created successfully!
**Changes**:
- Updated plugin.json (OLD_VERSION → NEW_VERSION)
- Created/updated CHANGELOG.md with entry for vNEW_VERSION
- Committed changes (abc1234: "chore: bump version to NEW_VERSION")
- Created git tag vNEW_VERSION
- Pushed commit and tag to remote
- Created GitHub release: RELEASE_URL
**Release includes**:
- X features added
- Y bugs fixed
- Z changes made
**Next steps**:
- Announce release to users
- Update plugin registry (if applicable)
- Monitor for issues in new release
- Continue development
View release: RELEASE_URL
View changelog: https://github.com/OWNER/REPO/blob/main/CHANGELOG.md
Template Variables
Templates use {{VARIABLE}} substitution syntax:
| Variable | Description | Example |
|----------|-------------|---------|
| {{VERSION}} | New version number | "1.2.3" |
| {{VERSION_TAG}} | Version with v prefix | "v1.2.3" |
| {{OLD_VERSION}} | Previous version | "1.2.2" |
| {{DATE}} | Release date (ISO) | "2026-01-11" |
| {{REPO}} | GitHub owner/repo | "owner/repo" |
| {{ADDED}} | Added section content | "- New feature\n- Another feature" |
| {{CHANGED}} | Changed section content | "- Updated behavior" |
| {{DEPRECATED}} | Deprecated section | "- Old API deprecated" |
| {{REMOVED}} | Removed section | "- Dropped support for X" |
| {{FIXED}} | Fixed section | "- Bug fix" |
| {{SECURITY}} | Security section | "- CVE fix" |
| {{BREAKING}} | Breaking changes summary | "⚠️ BREAKING: API changed" |
| {{CHANGELOG_ENTRY}} | Full changelog entry | (entire entry content) |
Error Handling
Dirty Working Directory
Error: Git status shows uncommitted changes
Message:
❌ Working directory has uncommitted changes.
Please commit or stash changes before creating a release.
Run: git status
Action: Exit without making any changes
No gh CLI
Error: gh command not found
Message:
❌ GitHub CLI (gh) not found.
Install with: brew install gh
After installing, run: gh auth login
Action: Exit without making any changes
Version Conflict
Error: Tag vX.Y.Z already exists
Message:
❌ Version X.Y.Z already exists as git tag.
Current version in plugin.json: X.Y.Z
Existing tag: vX.Y.Z
Please update plugin.json manually to a higher version,
or delete the tag if it was created in error:
git tag -d vX.Y.Z
git push origin :refs/tags/vX.Y.Z
Action: Exit without making any changes
No Commits Since Last Release
Error: No new commits found
Message:
⚠️ No commits found since last release vX.Y.Z
Current HEAD: abc1234
Last release tag: vX.Y.Z (abc1234)
These are the same commit. Nothing to release.
Action: Exit without making any changes
No Version Bump Needed
Warning: Only docs/chore commits
Message:
⚠️ No version bump needed (only documentation/chore commits).
Commits since v1.2.3:
- docs: update README
- chore: fix typo in comment
Do you want to create a release anyway? This will keep version at 1.2.3. (y/N)
Action: Ask user to confirm, proceed if yes
Plugin Not in GitHub
Error: Remote URL is not GitHub
Message:
❌ This plugin is not hosted on GitHub.
Remote URL: https://gitlab.com/owner/repo.git
The releaserator currently only supports GitHub.
GitLab and Gitea support coming soon!
Action: Exit without making any changes
Platform Abstraction
Current implementation: GitHub-only
GitHub-specific code is isolated in:
skills/releaserator/platforms/github.md
Platform interface (for future expansion):
- detect_platform() - Detect hosting platform from git remote
- create_release(version, notes) - Create platform release
- get_pr_link(number) - Format PR link URL
- get_commit_link(hash) - Format commit link URL
- check_cli() - Check for platform CLI tool
Platform detection logic:
REMOTE_URL=$(git remote get-url origin 2>/dev/null)
if echo "$REMOTE_URL" | grep -q "github.com"; then
PLATFORM="github"
elif echo "$REMOTE_URL" | grep -q "gitlab.com"; then
PLATFORM="gitlab"
elif echo "$REMOTE_URL" | grep -qE "gitea|codeberg"; then
PLATFORM="gitea"
else
PLATFORM="unknown"
fi
To add GitLab support:
- Create
skills/releaserator/platforms/gitlab.md - Implement same interface methods for GitLab
- Update SKILL.md to load platform-specific code based on detection
- No changes needed to core logic (commit parsing, version bumping, changelog generation)
Benefits:
- GitHub implementation doesn't need refactoring when adding platforms
- Each platform has dedicated documentation file
- Core logic (steps 1-7, 9-12) stays unchanged
- Only step 8 (create release) and link formatting is platform-specific
Common Issues and Solutions
Issue: Tag already exists
Cause: Version bump calculation matched an existing tag
Solution: This shouldn't happen if logic is correct. If it does:
- Check if plugin.json version was manually changed
- Verify last tag detection works correctly
- Ask user if they want to skip tagging or delete old tag
Issue: Commit parsing fails
Cause: Commit messages don't follow Conventional Commits format
Solution:
- Parse commits that do follow format
- Group unparsed commits under "Other changes" section
- Warn user about non-standard commits
Issue: Push fails (no permission)
Cause: User doesn't have push access to remote
Solution:
- Check git push output for authentication errors
- Suggest running
gh auth loginor setting up SSH keys - Report that release is ready locally but not pushed
Issue: GitHub release fails
Cause: gh CLI not authenticated
Solution:
❌ GitHub CLI authentication failed.
Run: gh auth login
Then try creating the release again.
Issue: Can't determine repo owner/name
Cause: Git remote URL in unexpected format
Solution:
- Try multiple parsing patterns:
git@github.com:owner/repo.githttps://github.com/owner/repo.githttps://github.com/owner/repo
- Ask user to manually provide owner/repo if parsing fails
Success Criteria
After running releaserator, verify:
- ✅ CHANGELOG.md created or updated with new entry
- ✅ CHANGELOG.md follows Keep A Changelog format
- ✅ plugin.json version field updated correctly
- ✅ Git commit created with conventional commit message
- ✅ Annotated git tag created (not lightweight)
- ✅ Tag pushed to remote
- ✅ GitHub release created with correct notes
- ✅ Release URL accessible and displays correctly
- ✅ All links in changelog work (commit hashes, PR numbers)
- ✅ Changelog sections correctly categorized (Added/Fixed/etc)
- ✅ Breaking changes marked with ⚠️ if present
- ✅ No errors or warnings during execution