
Releasing
- 1 installs
- 21 repo stars
- Updated August 5, 2026
- joaquimscosta/arkhe-claude-plugins
Automates semantic-version releases with CHANGELOG validation, comparison-link management, and GitHub Actions workflow triggering, or scaffolds release infrastructure.
About
Executes semantic versioning releases by validating the CHANGELOG, managing comparison links, and triggering a GitHub Actions release workflow. A developer uses it to cut a release or scaffold release scripts and workflow for a new project.
- CHANGELOG validation and comparison-link management
- GitHub Actions release trigger plus --setup scaffolding
Releasing by the numbers
- 1 all-time installs (skills.sh)
- Ranked #210 of 248 Release Management skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/joaquimscosta/arkhe-claude-plugins --skill releasingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 21 |
| Last updated | August 5, 2026 |
| Repository | joaquimscosta/arkhe-claude-plugins ↗ |
What it does
Automates semantic-version releases with CHANGELOG validation, comparison-link management, and GitHub Actions workflow triggering, or scaffolds release infrastructure.
Files
Release Workflow
Execute semantic versioning releases or scaffold release infrastructure for projects.
Usage
This skill is invoked when:
- User runs
/releasecommand with a version argument - User runs
/release --setupto scaffold release infrastructure
Two Operation Modes
Mode 1: Release (/release <version>)
Validates, prepares, and triggers a GitHub release.
Mode 2: Setup (/release --setup)
Scaffolds release scripts and GitHub Actions workflow into the target project.
Supported Arguments
Parse arguments from user input:
- `<version>`: Version to release (e.g.,
1.6.0orv1.6.0) - `--setup`: Scaffold release infrastructure into current project
- `--skip-monitor`: Trigger workflow but don't wait for completion
Prerequisites
ghCLI installed and authenticated (gh auth status)- Git configured with push access to remote
- CHANGELOG.md with a version entry (use
/changelogto generate) .github/workflows/release.ymlexists (use--setupto create)
Release Flow Steps
Step 1: Parse and Validate Version
VERSION="${1:-}"
# Strip 'v' prefix if present
VERSION="${VERSION#v}"
# Validate semver format
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Invalid version format '$VERSION'"
echo "Expected semantic version (e.g., 1.6.0)"
exit 1
fi
echo "Releasing version: $VERSION"Step 2: Pre-flight Checks
# Check CHANGELOG.md entry exists
if ! grep -qE "^## \[$VERSION\]" CHANGELOG.md; then
echo "Error: No CHANGELOG entry found for version $VERSION"
echo "Add entry with header: ## [$VERSION] - $(date +%Y-%m-%d)"
echo "Tip: Use '/changelog' to generate the entry."
exit 1
fi
echo "CHANGELOG entry found"
# Check release doesn't already exist
if gh release view "v$VERSION" &>/dev/null; then
echo "Error: Release v$VERSION already exists"
echo "Delete first: gh release delete v$VERSION --yes"
exit 1
fi
echo "Release does not exist, proceeding"Step 3: Add Comparison Link to CHANGELOG.md
REPO_URL=$(gh repo view --json url -q '.url')
# Skip if link already exists
if grep -qE "^\[$VERSION\]:" CHANGELOG.md; then
echo "Comparison link already exists"
else
# Find previous version (second version header)
PREV_VERSION=$(grep -E "^## \[[0-9]+\.[0-9]+\.[0-9]+\]" CHANGELOG.md \
| head -2 | tail -1 | sed 's/.*\[\([0-9.]*\)\].*/\1/')
if [[ "$PREV_VERSION" != "$VERSION" ]] && [[ -n "$PREV_VERSION" ]]; then
COMPARE_LINK="[$VERSION]: $REPO_URL/compare/v$PREV_VERSION...v$VERSION"
else
COMPARE_LINK="[$VERSION]: $REPO_URL/releases/tag/v$VERSION"
fi
# Update [Unreleased] link and add version link
sed -i.bak "s|\[Unreleased\]:.*|[Unreleased]: $REPO_URL/compare/v$VERSION...HEAD|" CHANGELOG.md
sed -i.bak "/^\[Unreleased\]:/a\\
$COMPARE_LINK" CHANGELOG.md
rm -f CHANGELOG.md.bak
echo "Added comparison link for $VERSION"
fiStep 4: Commit and Push CHANGELOG Changes
Check if CHANGELOG.md has uncommitted changes. If so, confirm with the user before committing:
if ! git diff --quiet CHANGELOG.md 2>/dev/null; then
echo "CHANGELOG.md has uncommitted changes."
# Ask user: "CHANGELOG.md updated with comparison links. Commit and push to main?"
# If confirmed:
git add CHANGELOG.md
git commit -m "docs: prepare release $VERSION"
git push origin main
echo "Changes committed and pushed"
fiImportant: Use conversation to confirm with the user before committing and pushing. Do NOT auto-commit without confirmation.
Step 5: Trigger Release Workflow
echo "Triggering release workflow for v$VERSION..."
gh workflow run release.yml -f version="$VERSION"
echo "Workflow triggered"Step 6: Monitor Workflow
sleep 5
RUN_ID=$(gh run list --workflow=release.yml --limit=1 --json databaseId -q '.[0].databaseId')
REPO_URL=$(gh repo view --json url -q '.url')
echo "Monitoring workflow run $RUN_ID..."
echo "View at: $REPO_URL/actions/runs/$RUN_ID"
# Poll for completion
while true; do
STATUS=$(gh run view "$RUN_ID" --json status -q '.status')
if [[ "$STATUS" == "completed" ]]; then
CONCLUSION=$(gh run view "$RUN_ID" --json conclusion -q '.conclusion')
break
fi
echo " Status: $STATUS..."
sleep 5
done
if [[ "$CONCLUSION" == "success" ]]; then
echo "Release v$VERSION created successfully!"
echo "View: $REPO_URL/releases/tag/v$VERSION"
else
echo "Workflow failed: $CONCLUSION"
echo "Logs: $REPO_URL/actions/runs/$RUN_ID"
exit 1
fiIf --skip-monitor is specified, skip this step and report the workflow URL instead.
Setup Mode (--setup)
When --setup is specified, scaffold release infrastructure into the current project.
What Gets Scaffolded
| Template File | Target Location | Purpose |
|---|---|---|
scripts/release.sh | scripts/release.sh | Local release orchestrator |
templates/release.yml | .github/workflows/release.yml | GitHub Actions workflow |
scripts/validate-changelog.sh | .github/workflows/scripts/validate-changelog.sh | CHANGELOG validator |
scripts/extract-release-notes.sh | .github/workflows/scripts/extract-release-notes.sh | Release notes extractor |
scripts/create-github-release.sh | .github/workflows/scripts/create-github-release.sh | GitHub release creator |
Scaffolding Process
1. Read each template file from this skill's scripts/ and templates/ directories 2. Create target directories (scripts/, .github/workflows/scripts/) 3. Write files to target project locations 4. Set executable permissions (chmod +x) on all shell scripts 5. Verify all files exist 6. Report created files and next steps
# Create directories
mkdir -p scripts
mkdir -p .github/workflows/scripts
# Set permissions after writing files
chmod +x scripts/release.sh
chmod +x .github/workflows/scripts/validate-changelog.sh
chmod +x .github/workflows/scripts/extract-release-notes.sh
chmod +x .github/workflows/scripts/create-github-release.shPost-Setup Next Steps
After scaffolding, inform the user: 1. Ensure CHANGELOG.md exists (use /changelog to create) 2. Commit the scaffolded files 3. Run /release <version> to create the first release
Important Notes
- CHANGELOG format: Assumes Keep a Changelog format with
## [VERSION]headers - Branch: Commits and pushes to the current branch (auto-detected via
git rev-parse) - Integration with /changelog: Use
/changelogto generate CHANGELOG entries before releasing - GitHub CLI required: Must have
ghinstalled and authenticated - Workflow file: Expects
.github/workflows/release.yml(use--setupto create)
Supporting Documentation
- [WORKFLOW.md](WORKFLOW.md) - Detailed 7-phase release process and script template reference
- [EXAMPLES.md](EXAMPLES.md) - Real-world release scenarios including first release, patch release, and setup
- [TROUBLESHOOTING.md](TROUBLESHOOTING.md) - Common issues with workflow triggers, CHANGELOG format, and gh CLI
- [templates/release.yml](templates/release.yml) - GitHub Actions workflow template for scaffold setup
Release Skill: Examples
Example 1: Standard Release
Release version 1.6.0 after adding a CHANGELOG entry.
# 1. Generate CHANGELOG entry
/changelog --version 1.6.0
# 2. Execute release
/release 1.6.0What happens: 1. Validates 1.6.0 is valid semver 2. Checks ## [1.6.0] exists in CHANGELOG.md 3. Checks v1.6.0 release doesn't exist on GitHub 4. Adds comparison link: [1.6.0]: https://github.com/user/repo/compare/v1.5.0...v1.6.0 5. Updates [Unreleased] link to compare/v1.6.0...HEAD 6. Asks to commit and push CHANGELOG.md changes 7. Triggers release.yml workflow 8. Monitors until completion 9. Reports: Release v1.6.0 created successfully!
Example 2: First Release
Release the initial version of a new project.
# 1. Set up release infrastructure
/release --setup
# 2. Create CHANGELOG.md
/changelog --version 1.0.0
# 3. Commit scaffolded files and CHANGELOG
git add -A && git commit -m "chore: add release infrastructure"
git push origin main
# 4. Execute first release
/release 1.0.0Comparison link for first release:
[1.0.0]: https://github.com/user/repo/releases/tag/v1.0.0Example 3: Patch Release
Quick bug fix release.
# 1. Fix the bug and commit
/commit
# 2. Update CHANGELOG
/changelog --version 1.5.1
# 3. Release
/release 1.5.1Example 4: Release with v Prefix
The v prefix is automatically stripped.
/release v2.0.0
# Equivalent to: /release 2.0.0Example 5: Trigger Without Monitoring
Trigger the workflow and return immediately.
/release 1.6.0 --skip-monitorOutput:
Workflow triggered. Skipping monitoring.
View at: https://github.com/user/repo/actionsExample 6: Setup Scaffolding
Scaffold the full release pipeline into a new project.
/release --setupFiles created:
scripts/
└── release.sh # Local release orchestrator
.github/
└── workflows/
├── release.yml # GitHub Actions workflow
└── scripts/
├── validate-changelog.sh # CHANGELOG validator
├── extract-release-notes.sh # Release notes extractor
└── create-github-release.sh # GitHub release creatorExample 7: Using release.sh Directly
After scaffolding, the release script can be run directly from the terminal.
# Interactive mode (prompts for confirmation)
./scripts/release.sh 1.6.0
# Non-interactive mode (for CI)
./scripts/release.sh 1.6.0 --yes
# Skip monitoring
./scripts/release.sh 1.6.0 --yes --skip-monitorExample 8: CHANGELOG Format Expected
The skill expects Keep a Changelog format:
# Changelog
All notable changes to this project will be documented in this file.
## [Unreleased]
## [1.6.0] - 2026-03-03
### Added
- New release automation skill in git plugin
### Changed
- Updated plugin description to include release
## [1.5.0] - 2026-02-15
### Added
- Previous feature
[Unreleased]: https://github.com/user/repo/compare/v1.6.0...HEAD
[1.6.0]: https://github.com/user/repo/compare/v1.5.0...v1.6.0
[1.5.0]: https://github.com/user/repo/releases/tag/v1.5.0#!/usr/bin/env bash
#
# create-github-release.sh - Create GitHub Release via gh CLI
#
# Usage: create-github-release.sh <tag>
# Example: create-github-release.sh v1.4.0
#
# Prerequisites:
# - GH_TOKEN environment variable set
# - release_notes.md file exists
#
# Exit codes:
# 0 - Release created successfully
# 1 - Missing arguments or release creation failed
set -euo pipefail
TAG="${1:-}"
if [[ -z "$TAG" ]]; then
echo "::error::Usage: create-github-release.sh <tag>"
exit 1
fi
# Ensure tag has 'v' prefix
if [[ ! "$TAG" =~ ^v ]]; then
TAG="v$TAG"
fi
RELEASE_NOTES_FILE="release_notes.md"
if [[ ! -f "$RELEASE_NOTES_FILE" ]]; then
echo "::error::$RELEASE_NOTES_FILE not found. Run extract-release-notes.sh first."
exit 1
fi
if [[ -z "${GH_TOKEN:-}" ]]; then
echo "::error::GH_TOKEN environment variable not set"
exit 1
fi
# Create the release
# - Uses the tag that was created/verified earlier
# - Reads body from release_notes.md
# - No custom artifacts (GitHub auto-includes source tarball/zipball)
gh release create "$TAG" \
--title "$TAG" \
--notes-file "$RELEASE_NOTES_FILE"
echo "Successfully created GitHub Release $TAG"
#!/usr/bin/env bash
#
# extract-release-notes.sh - Extract version section from CHANGELOG.md
#
# Usage: extract-release-notes.sh <version>
# Example: extract-release-notes.sh 1.4.0
#
# Output: Creates release_notes.md with the extracted content
#
# Exit codes:
# 0 - Release notes extracted successfully
# 1 - Missing arguments or extraction failed
set -euo pipefail
VERSION="${1:-}"
if [[ -z "$VERSION" ]]; then
echo "::error::Usage: extract-release-notes.sh <version>"
exit 1
fi
# Strip 'v' prefix if present
VERSION="${VERSION#v}"
CHANGELOG_FILE="CHANGELOG.md"
OUTPUT_FILE="release_notes.md"
if [[ ! -f "$CHANGELOG_FILE" ]]; then
echo "::error::$CHANGELOG_FILE not found"
exit 1
fi
# Extract content between ## [VERSION] and the next ## [ header
# Using awk for reliable multi-line extraction
awk -v version="$VERSION" '
# Match the start of our target version section
/^## \[/ {
# Check if this is our target version
if ($0 ~ "^## \\[" version "\\]") {
capture = 1
next # Skip the header line itself
} else if (capture) {
# We hit the next version section, stop capturing
exit
}
}
# Capture lines when in our target section
capture { print }
' "$CHANGELOG_FILE" > "$OUTPUT_FILE"
# Check if we captured anything
if [[ ! -s "$OUTPUT_FILE" ]]; then
echo "::error::Failed to extract release notes for version $VERSION"
echo "Ensure CHANGELOG.md has content under the ## [$VERSION] header"
exit 1
fi
# Remove leading blank lines
sed -i.bak '/./,$!d' "$OUTPUT_FILE" && rm -f "$OUTPUT_FILE.bak"
# Remove trailing blank lines
sed -i.bak -e :a -e '/^\n*$/{$d;N;ba' -e '}' "$OUTPUT_FILE" && rm -f "$OUTPUT_FILE.bak"
echo "Extracted release notes for version $VERSION to $OUTPUT_FILE"
#!/usr/bin/env bash
#
# release.sh - Automate the full release process
#
# Usage: release.sh <version> [--yes] [--skip-monitor]
# Example: release.sh 1.6.0
#
# This script:
# 1. Validates version format
# 2. Checks CHANGELOG.md entry exists
# 3. Adds comparison link to CHANGELOG.md (if missing)
# 4. Commits and pushes CHANGELOG.md changes
# 5. Triggers the GitHub Actions release workflow
# 6. Monitors workflow and reports result
#
# Options:
# --yes, -y Skip interactive confirmation prompts
# --skip-monitor Trigger workflow but don't wait for completion
#
# Prerequisites:
# - gh CLI installed and authenticated
# - Git configured with push access
# - CHANGELOG.md entry already added for the version
set -euo pipefail
# --- Output Helpers ---
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
function print_error() {
echo -e "${RED}Error: $1${NC}" >&2
}
function print_success() {
echo -e "${GREEN}✓ $1${NC}"
}
function print_warning() {
echo -e "${YELLOW}$1${NC}"
}
function print_banner() {
local color="$1"
local message="$2"
echo -e "${color}=======================================${NC}"
echo -e "${color} $message${NC}"
echo -e "${color}=======================================${NC}"
}
# --- Validation Functions ---
function validate_version_arg() {
if [[ -z "${1:-}" ]]; then
print_error "Version required"
echo "Usage: release.sh <version> [--yes] [--skip-monitor]"
echo "Example: release.sh 1.6.0"
exit 1
fi
}
function validate_semver_format() {
local version="$1"
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
print_error "Invalid version format '$version'"
echo "Expected semantic version (e.g., 1.6.0)"
exit 1
fi
}
function check_changelog_entry() {
local version="$1"
local changelog="$2"
echo "Checking CHANGELOG.md for version $version..."
if ! grep -qE "^## \[$version\]" "$changelog"; then
print_error "No CHANGELOG entry found for version $version"
echo ""
echo "Please add an entry to CHANGELOG.md with the header:"
echo " ## [$version] - $(date +%Y-%m-%d)"
echo ""
echo "Tip: Use '/changelog' in Claude Code to generate the entry."
exit 1
fi
print_success "CHANGELOG entry found"
}
function check_release_not_exists() {
local tag="$1"
echo "Checking if release $tag already exists..."
if gh release view "$tag" &>/dev/null; then
print_error "Release $tag already exists"
echo "Delete it first with: gh release delete $tag --yes"
exit 1
fi
print_success "Release does not exist"
}
# --- Changelog Link Management ---
function get_previous_version() {
local version="$1"
local changelog="$2"
# Find the second version header (first is current version)
local prev
prev=$(grep -E "^## \[[0-9]+\.[0-9]+\.[0-9]+\]" "$changelog" \
| head -2 \
| tail -1 \
| sed 's/.*\[\([0-9.]*\)\].*/\1/')
# Return empty if this is the first version
if [[ "$prev" == "$version" ]]; then
echo ""
else
echo "$prev"
fi
}
function add_comparison_link() {
local version="$1"
local changelog="$2"
local repo_url="$3"
echo "Checking comparison links..."
# Skip if link already exists
if grep -qE "^\[$version\]:" "$changelog"; then
print_success "Comparison link already exists"
return 0
fi
print_warning "Adding comparison link for $version..."
local prev_version
prev_version=$(get_previous_version "$version" "$changelog")
# Build comparison link based on whether previous version exists
local compare_link
if [[ -n "$prev_version" ]]; then
compare_link="[$version]: $repo_url/compare/v$prev_version...v$version"
else
compare_link="[$version]: $repo_url/releases/tag/v$version"
fi
# Update [Unreleased] link and add new version link
if ! grep -qE "^\[Unreleased\]:" "$changelog"; then
print_warning "Warning: Could not find [Unreleased] link to update"
echo "Please manually add: $compare_link"
return 0
fi
# Update Unreleased to point to new version, then add version link
sed -i.bak "s|\[Unreleased\]:.*|[Unreleased]: $repo_url/compare/v$version...HEAD|" "$changelog"
sed -i.bak "/^\[Unreleased\]:/a\\
$compare_link" "$changelog"
rm -f "$changelog.bak"
print_success "Added comparison link"
}
# --- Git Operations ---
function commit_changelog_changes() {
local version="$1"
local changelog="$2"
local auto_confirm="$3"
# Skip if no changes
if git diff --quiet "$changelog" 2>/dev/null; then
return 0
fi
echo ""
echo "CHANGELOG.md has uncommitted changes."
if [[ "$auto_confirm" == "true" ]]; then
echo "Auto-confirming commit and push..."
else
read -p "Commit and push? [y/N] " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
print_warning "Skipping commit. Run manually before triggering workflow."
exit 0
fi
fi
local branch
branch=$(git rev-parse --abbrev-ref HEAD)
git add "$changelog"
git commit -m "docs: prepare release $version"
git push origin "$branch"
print_success "Changes committed and pushed"
}
# --- Workflow Management ---
function trigger_workflow() {
local version="$1"
local tag="v$version"
echo ""
echo "Triggering release workflow for $tag..."
gh workflow run release.yml -f version="$version"
print_success "Workflow triggered"
}
function wait_for_workflow() {
local version="$1"
local repo_url="$2"
echo ""
echo "Waiting for workflow to start..."
sleep 5
local run_id
run_id=$(gh run list --workflow=release.yml --limit=1 --json databaseId -q '.[0].databaseId')
if [[ -z "$run_id" ]]; then
print_error "Could not find workflow run"
exit 1
fi
echo "Monitoring workflow run $run_id..."
echo "View at: $repo_url/actions/runs/$run_id"
echo ""
# Poll for completion
local status conclusion
while true; do
status=$(gh run view "$run_id" --json status,conclusion -q '.status')
if [[ "$status" == "completed" ]]; then
conclusion=$(gh run view "$run_id" --json conclusion -q '.conclusion')
break
fi
echo " Status: $status..."
sleep 5
done
# Report result
echo ""
if [[ "$conclusion" == "success" ]]; then
print_banner "$GREEN" "Release v$version created successfully!"
echo ""
echo "View release: $repo_url/releases/tag/v$version"
else
print_banner "$RED" "Workflow failed with: $conclusion"
echo ""
echo "View logs: $repo_url/actions/runs/$run_id"
exit 1
fi
}
# --- Main ---
function main() {
local version=""
local auto_confirm="false"
local skip_monitor="false"
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--yes|-y) auto_confirm="true"; shift ;;
--skip-monitor) skip_monitor="true"; shift ;;
*) version="$1"; shift ;;
esac
done
# Validate inputs
validate_version_arg "$version"
version="${version#v}" # Strip 'v' prefix if present
validate_semver_format "$version"
local changelog="CHANGELOG.md"
local repo_url
repo_url=$(gh repo view --json url -q '.url')
echo "Preparing release v$version..."
echo ""
# Pre-flight checks
check_changelog_entry "$version" "$changelog"
check_release_not_exists "v$version"
# Update changelog links
add_comparison_link "$version" "$changelog" "$repo_url"
# Commit if needed
commit_changelog_changes "$version" "$changelog" "$auto_confirm"
# Trigger and monitor workflow
trigger_workflow "$version"
if [[ "$skip_monitor" == "true" ]]; then
echo ""
echo "Workflow triggered. Skipping monitoring."
echo "View at: $repo_url/actions"
else
wait_for_workflow "$version" "$repo_url"
fi
}
main "$@"
#!/usr/bin/env bash
#
# validate-changelog.sh - Verify CHANGELOG.md has entry for specified version
#
# Usage: validate-changelog.sh <version>
# Example: validate-changelog.sh 1.4.0
#
# Exit codes:
# 0 - CHANGELOG entry found
# 1 - Missing arguments or CHANGELOG entry not found
set -euo pipefail
VERSION="${1:-}"
if [[ -z "$VERSION" ]]; then
echo "::error::Usage: validate-changelog.sh <version>"
exit 1
fi
# Strip 'v' prefix if present
VERSION="${VERSION#v}"
CHANGELOG_FILE="CHANGELOG.md"
if [[ ! -f "$CHANGELOG_FILE" ]]; then
echo "::error::$CHANGELOG_FILE not found"
exit 1
fi
# Look for version header: ## [X.Y.Z] or ## [X.Y.Z] - DATE
# The header format follows Keep a Changelog: ## [1.4.0] - 2025-01-15
if grep -qE "^## \[$VERSION\]" "$CHANGELOG_FILE"; then
echo "Found CHANGELOG entry for version $VERSION"
exit 0
else
echo "::error::No CHANGELOG entry found for version $VERSION"
echo ""
echo "Please add an entry to CHANGELOG.md with the header:"
echo " ## [$VERSION] - $(date +%Y-%m-%d)"
echo ""
echo "You can use the /changelog skill to generate the entry."
exit 1
fi
name: Create Release
on:
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g., 1.4.0)'
required: true
type: string
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Validate version format
id: validate
run: |
VERSION="${{ github.event.inputs.version }}"
# Strip 'v' prefix if present
VERSION="${VERSION#v}"
# Validate semver format (X.Y.Z)
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Invalid version format '$VERSION'. Expected semantic version (e.g., 1.4.0)"
exit 1
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "tag=v$VERSION" >> $GITHUB_OUTPUT
echo "Validated version: $VERSION (tag: v$VERSION)"
- name: Check if release already exists
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${{ steps.validate.outputs.tag }}"
if gh release view "$TAG" &>/dev/null; then
echo "::error::Release $TAG already exists"
exit 1
fi
echo "Release $TAG does not exist, proceeding..."
- name: Check if tag exists
id: tag_check
run: |
TAG="${{ steps.validate.outputs.tag }}"
if git rev-parse "$TAG" &>/dev/null; then
echo "tag_exists=true" >> $GITHUB_OUTPUT
echo "Tag $TAG already exists, will use existing tag"
else
echo "tag_exists=false" >> $GITHUB_OUTPUT
echo "Tag $TAG does not exist, will create it"
fi
- name: Validate CHANGELOG entry
run: |
chmod +x .github/workflows/scripts/validate-changelog.sh
.github/workflows/scripts/validate-changelog.sh "${{ steps.validate.outputs.version }}"
- name: Extract release notes
run: |
chmod +x .github/workflows/scripts/extract-release-notes.sh
.github/workflows/scripts/extract-release-notes.sh "${{ steps.validate.outputs.version }}"
echo "--- Release Notes ---"
cat release_notes.md
echo "---------------------"
- name: Create git tag
if: steps.tag_check.outputs.tag_exists == 'false'
run: |
TAG="${{ steps.validate.outputs.tag }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG" -m "Release $TAG"
git push origin "$TAG"
echo "Created and pushed tag $TAG"
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
chmod +x .github/workflows/scripts/create-github-release.sh
.github/workflows/scripts/create-github-release.sh "${{ steps.validate.outputs.tag }}"
Release Skill: Troubleshooting
Common Issues
"No CHANGELOG entry found for version X.Y.Z"
Cause: CHANGELOG.md doesn't have a ## [X.Y.Z] header.
Fix:
# Generate the entry
/changelog --version X.Y.Z
# Or manually add the header
echo "## [X.Y.Z] - $(date +%Y-%m-%d)" >> CHANGELOG.md"Release vX.Y.Z already exists"
Cause: A GitHub Release with this tag already exists.
Fix:
# Delete the existing release
gh release delete vX.Y.Z --yes
# Optionally delete the tag too
git push origin --delete vX.Y.Z
git tag -d vX.Y.Z
# Retry
/release X.Y.Z"Invalid version format"
Cause: Version doesn't match semantic versioning (X.Y.Z).
Valid formats:
1.6.0v1.6.0(v prefix is stripped automatically)
Invalid formats:
1.6(missing patch)1.6.0-beta(pre-release suffixes not supported)v1.6(missing patch even with prefix)
"Could not find workflow run"
Cause: The workflow didn't start within the 5-second wait period, or the workflow file doesn't exist.
Fix: 1. Check the workflow exists:
gh workflow list2. If missing, scaffold it:
/release --setup3. If it exists but didn't trigger, check permissions:
gh api repos/{owner}/{repo}/actions/permissionsgh CLI Not Authenticated
Cause: gh CLI is not installed or not authenticated.
Fix:
# Install gh CLI
brew install gh # macOS
# or see https://cli.github.com/
# Authenticate
gh auth login"Could not find [Unreleased] link to update"
Cause: CHANGELOG.md doesn't have an [Unreleased]: link reference at the bottom.
Fix: Add the link reference manually:
[Unreleased]: https://github.com/user/repo/compare/vX.Y.Z...HEADWorkflow Fails with "CHANGELOG entry not found"
Cause: The CHANGELOG.md was not pushed to the remote before the workflow ran.
Fix: Ensure Step 4 (commit and push) completed before the workflow triggers. Re-run:
git push origin main
/release X.Y.ZSetup Files Already Exist
Cause: Running --setup when release infrastructure already exists.
Fix: The skill will warn about existing files. Choose to:
- Overwrite — Replace with latest templates
- Skip — Keep existing files unchanged
sed Errors on macOS vs Linux
Cause: sed -i behaves differently on macOS (BSD) and Linux (GNU).
Fix: The scripts use sed -i.bak which works on both platforms. The .bak files are cleaned up automatically with rm -f *.bak.
Debugging Tips
Check Workflow Status Manually
# List recent workflow runs
gh run list --workflow=release.yml --limit=5
# View specific run
gh run view <run-id>
# View logs
gh run view <run-id> --logVerify CHANGELOG Format
# Check version headers
grep -E "^## \[" CHANGELOG.md
# Check link references
grep -E "^\[" CHANGELOG.md | tail -10Test Release Script Locally
# Dry run (validate only, don't trigger)
# Check prerequisites
gh auth status
gh repo view --json url -q '.url'
grep -E "^## \[1.6.0\]" CHANGELOG.md
gh release view v1.6.0 2>/dev/null; echo "Exit: $?"Release Workflow: Detailed Process
Overview
The release workflow operates in 7 phases for creating releases, plus a separate scaffolding workflow for setting up new projects.
Phase 1: Input Validation
Parse and validate all arguments before any side effects.
Arguments:
<version>— Semantic version (e.g.,1.6.0orv1.6.0)--yes/-y— Skip interactive confirmation (for CI/automation)--skip-monitor— Trigger workflow without waiting for completion--setup— Switch to scaffolding mode
Validation Rules:
- Version must match
^[0-9]+\.[0-9]+\.[0-9]+$after strippingvprefix - Version argument is required (unless
--setupmode)
Phase 2: Pre-flight Checks
Verify all prerequisites before making changes.
1. CHANGELOG entry exists — grep -qE "^## \[$VERSION\]" CHANGELOG.md 2. Release doesn't exist — gh release view "v$VERSION" returns non-zero 3. gh CLI authenticated — gh auth status succeeds 4. Git remote accessible — gh repo view returns repo URL
If any check fails, report the issue and exit with clear instructions.
Phase 3: CHANGELOG Link Management
Add comparison links to the bottom of CHANGELOG.md.
Previous Version Detection
# Find the second ## [X.Y.Z] header (first is current version)
PREV_VERSION=$(grep -E "^## \[[0-9]+\.[0-9]+\.[0-9]+\]" CHANGELOG.md \
| head -2 | tail -1 | sed 's/.*\[\([0-9.]*\)\].*/\1/')Link Format
- With previous version:
[1.6.0]: https://github.com/user/repo/compare/v1.5.0...v1.6.0 - First version:
[1.0.0]: https://github.com/user/repo/releases/tag/v1.0.0
Idempotency
Skip if grep -qE "^\[$VERSION\]:" CHANGELOG.md already matches.
Unreleased Link Update
The [Unreleased] link is updated to compare from the new version to HEAD:
[Unreleased]: https://github.com/user/repo/compare/v1.6.0...HEADPhase 4: Git Operations
Commit and push CHANGELOG.md changes if modified.
1. Check for uncommitted changes: git diff --quiet CHANGELOG.md 2. Confirm with user before committing (unless --yes flag) 3. Stage: git add CHANGELOG.md 4. Commit: git commit -m "docs: prepare release $VERSION" 5. Push: git push origin main
Phase 5: Workflow Triggering
Trigger the GitHub Actions release workflow.
gh workflow run release.yml -f version="$VERSION"The workflow must:
- Accept a
versioninput viaworkflow_dispatch - Have
contents: writepermission - Use
actions/checkout@v4withfetch-depth: 0
Phase 6: Workflow Monitoring
Poll workflow status until completion (unless --skip-monitor).
1. Wait 5 seconds for workflow to register 2. Get run ID: gh run list --workflow=release.yml --limit=1 --json databaseId 3. Poll every 5 seconds: gh run view "$RUN_ID" --json status -q '.status' 4. On completion, check conclusion: gh run view "$RUN_ID" --json conclusion
Status Values
queued— Waiting for runnerin_progress— Runningcompleted— Finished (check conclusion)
Conclusion Values
success— Release createdfailure— Step failedcancelled— Manually cancelled
Phase 7: Post-Release
Report result with actionable URLs.
On success:
Release v1.6.0 created successfully!
View: https://github.com/user/repo/releases/tag/v1.6.0On failure:
Workflow failed: failure
Logs: https://github.com/user/repo/actions/runs/12345Setup Mode Workflow
When --setup is specified, scaffold the full release pipeline.
Step 1: Check Existing Files
Before scaffolding, check if any target files already exist:
scripts/release.sh.github/workflows/release.yml.github/workflows/scripts/validate-changelog.sh.github/workflows/scripts/extract-release-notes.sh.github/workflows/scripts/create-github-release.sh
If files exist, warn the user and ask whether to overwrite.
Step 2: Create Directories
mkdir -p scripts
mkdir -p .github/workflows/scriptsStep 3: Copy Templates
Read each template from the skill's directories and write to target locations:
| Source (in skill) | Target (in project) |
|---|---|
scripts/release.sh | scripts/release.sh |
scripts/validate-changelog.sh | .github/workflows/scripts/validate-changelog.sh |
scripts/extract-release-notes.sh | .github/workflows/scripts/extract-release-notes.sh |
scripts/create-github-release.sh | .github/workflows/scripts/create-github-release.sh |
templates/release.yml | .github/workflows/release.yml |
Step 4: Set Permissions
chmod +x scripts/release.sh
chmod +x .github/workflows/scripts/*.shStep 5: Report
List all created files and provide next steps: 1. Ensure CHANGELOG.md exists 2. Commit the scaffolded files 3. Run /release <version> for the first release
Script Templates Reference
release.sh
Local release orchestrator that wraps the full workflow. Supports --yes for non-interactive use and --skip-monitor to skip polling.
release.yml (GitHub Actions)
Workflow triggered by workflow_dispatch with a version input. Steps: 1. Checkout with full history 2. Validate semver format 3. Check release doesn't exist 4. Check/create git tag 5. Validate CHANGELOG entry 6. Extract release notes 7. Create GitHub Release
validate-changelog.sh
Verifies ## [VERSION] header exists in CHANGELOG.md. Uses Keep a Changelog format.
extract-release-notes.sh
Extracts content between ## [VERSION] and the next ## [ header using awk. Outputs to release_notes.md.
create-github-release.sh
Creates a GitHub Release via gh release create using the extracted release notes. Requires GH_TOKEN environment variable.
Integration with Other Skills
- `/changelog` (generating-changelog) — Generate CHANGELOG entries before releasing
- `/commit` (creating-commit) — Commit changes during release preparation
- `/create-pr` (creating-pr) — Create PRs if releasing from a branch