
Ship
- 14 installs
- 2.1k repo stars
- Updated June 18, 2026
- catlog22/claude-code-workflow
Support for ship
About
Provides workflow support for ship. Solo builders use this to streamline development.
- ship
Ship by the numbers
- 14 all-time installs (skills.sh)
- Ranked #2,126 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/catlog22/claude-code-workflow --skill shipAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 14 |
|---|---|
| repo stars | ★ 2.1k |
| Last updated | June 18, 2026 |
| Repository | catlog22/claude-code-workflow ↗ |
What it does
Support for ship
Files
Ship
Structured release pipeline that guides code from working branch to a published release through 7 gated phases: pre-flight checks, automated code review, version bump, changelog generation, PR creation, platform publish (npm/PyPI/etc.), and GitHub release.
Phases 5 (PR) and 6 (Publish) are conditionally skippable — trunk-based workflows that commit directly to main skip Phase 5; private packages skip Phase 6. Phase 7 (GitHub release) is the terminal phase and always runs unless the publish step failed.
Key Design Principles
1. Phase Gates: Each phase must pass before the next begins — no shipping broken code 2. Multi-Project Support: Detects npm (package.json), Python (pyproject.toml), and generic (VERSION) projects 3. AI-Powered Review: Uses CCW CLI to run automated code review before release 4. Audit Trail: Each phase produces structured output for traceability 5. Safe Defaults: Warns on risky operations (direct push to main, major version bumps)
Architecture Overview
User: "ship" / "release" / "publish"
|
v
┌──────────────────────────────────────────────────────────┐
│ Phase 1: Pre-Flight Checks │
│ → git clean? branch ok? tests pass? build ok? │
│ → Output: preflight-report.json │
│ → Gate: ALL checks must pass │
├──────────────────────────────────────────────────────────┤
│ Phase 2: Code Review │
│ → detect merge base, diff against base │
│ → ccw cli --tool gemini --mode analysis │
│ → flag high-risk changes │
│ → Output: review-summary │
│ → Gate: No critical issues flagged │
├──────────────────────────────────────────────────────────┤
│ Phase 3: Version Bump │
│ → detect version file (package.json/pyproject.toml/VERSION)
│ → determine bump type from commits or user input │
│ → update version file │
│ → Output: version change record │
│ → Gate: Version updated successfully │
├──────────────────────────────────────────────────────────┤
│ Phase 4: Changelog & Commit │
│ → generate changelog from git log since last tag │
│ → update CHANGELOG.md │
│ → create release commit, push to remote │
│ → Output: commit SHA │
│ → Gate: Push successful │
├──────────────────────────────────────────────────────────┤
│ Phase 5: PR Creation (skippable for trunk-based) │
│ → gh pr create with structured body │
│ → auto-link issues from commits │
│ → Output: PR URL │
│ → Gate: PR created OR skipped (direct-to-main) │
├──────────────────────────────────────────────────────────┤
│ Phase 6: Platform Publish (skippable if private) │
│ → detect registry (npm / PyPI / crates.io) │
│ → npm publish / twine upload / cargo publish │
│ → verify new version is live on registry │
│ → Output: published artifact metadata │
│ → Gate: registry confirms version OR skipped (private) │
├──────────────────────────────────────────────────────────┤
│ Phase 7: GitHub Release │
│ → git tag -a vX.Y.Z + push tag │
│ → gh release create with structured notes │
│ → Output: release URL │
│ → Gate: release URL returned │
└──────────────────────────────────────────────────────────┘Execution Flow
Execute phases sequentially. Each phase has a gate condition — if the gate fails, stop and report status.
1. Phase 1: Pre-Flight Checks -- Validate git state, branch, tests, build 2. Phase 2: Code Review -- AI-powered diff review with risk assessment 3. Phase 3: Version Bump -- Detect and update version across project types 4. Phase 4: Changelog & Commit -- Generate changelog, create release commit, push 5. Phase 5: PR Creation -- Create PR with structured body and issue links (skip for trunk-based) 6. Phase 6: Platform Publish -- Publish to npm / PyPI / crates.io (skip for private packages) 7. Phase 7: GitHub Release -- Tag release commit and publish GitHub release notes
Pre-Flight Checklist (Quick Reference)
| Check | Command | Pass Condition |
|---|---|---|
| Git clean | git status --porcelain | Empty output |
| Branch | git branch --show-current | Not main/master |
| Tests | npm test / pytest | Exit code 0 |
| Build | npm run build / python -m build | Exit code 0 |
Completion Status Protocol
This skill follows the Completion Status Protocol defined in SKILL-DESIGN-SPEC.md sections 13-14.
Every execution terminates with one of:
| Status | When |
|---|---|
| DONE | All applicable phases completed, GitHub release published |
| DONE_WITH_CONCERNS | Release published but with review warnings, skipped publish (private pkg), or non-critical issues |
| BLOCKED | A gate failed (dirty git, tests fail, push rejected, publish failed, tag conflict) |
| NEEDS_CONTEXT | Cannot determine bump type, ambiguous branch target, unclear registry target |
Escalation
Follows the Three-Strike Rule (SKILL-DESIGN-SPEC section 14). On 3 consecutive failures at the same step, stop and output diagnostic dump.
Reference Documents
| Document | Purpose |
|---|---|
| phases/01-preflight-checks.md | Git, branch, test, build validation |
| phases/02-code-review.md | AI-powered diff review |
| phases/03-version-bump.md | Version detection and bump |
| phases/04-changelog-commit.md | Changelog generation and release commit |
| phases/05-pr-creation.md | PR creation with issue linking (skippable) |
| phases/06-platform-publish.md | npm / PyPI / crates.io publish (skippable) |
| phases/07-github-release.md | Tag and GitHub release notes |
| ../_shared/SKILL-DESIGN-SPEC.md | Skill design spec (completion protocol, escalation) |
Phase 1: Pre-Flight Checks
Validate that the repository is in a shippable state before proceeding with the release pipeline.
Objective
- Confirm working tree is clean (no uncommitted changes)
- Validate current branch is appropriate for release
- Run test suite and confirm all tests pass
- Verify build succeeds
Gate Condition
ALL four checks must pass. If any check fails, stop the pipeline and report BLOCKED status with the specific failure.
Execution Steps
Step 1: Git Clean Check
git_status=$(git status --porcelain)
if [ -n "$git_status" ]; then
echo "FAIL: Working tree is dirty"
echo "$git_status"
# Gate: BLOCKED — commit or stash changes first
else
echo "PASS: Working tree is clean"
fiPass condition: git status --porcelain produces empty output. On failure: Report dirty files and suggest git stash or git commit.
Step 2: Branch Validation
current_branch=$(git branch --show-current)
if [ "$current_branch" = "main" ] || [ "$current_branch" = "master" ]; then
echo "WARN: Currently on $current_branch — direct push to main/master is risky"
# Ask user for confirmation before proceeding
else
echo "PASS: On branch $current_branch"
fiPass condition: Not on main/master, OR user explicitly confirms direct-to-main release. On warning: Ask user to confirm they intend to release from main/master directly.
Step 3: Test Suite Execution
Detect and run the project's test suite:
# Detection priority:
# 1. package.json with "test" script → npm test
# 2. pytest available and tests exist → pytest
# 3. No tests found → WARN and continue
if [ -f "package.json" ] && grep -q '"test"' package.json; then
npm test
elif command -v pytest &>/dev/null && [ -d "tests" -o -d "test" ]; then
pytest
elif [ -f "pyproject.toml" ] && grep -q 'pytest' pyproject.toml; then
pytest
else
echo "WARN: No test suite detected — skipping test check"
fiPass condition: Test command exits with code 0, or no tests detected (warn). On failure: Report test failures and stop the pipeline.
Step 4: Build Verification
Detect and run the project's build step:
# Detection priority:
# 1. package.json with "build" script → npm run build
# 2. pyproject.toml → python -m build (if build module available)
# 3. Makefile with build target → make build
# 4. No build step → PASS (not all projects need a build)
if [ -f "package.json" ] && grep -q '"build"' package.json; then
npm run build
elif [ -f "pyproject.toml" ] && python -m build --help &>/dev/null; then
python -m build
elif [ -f "Makefile" ] && grep -q '^build:' Makefile; then
make build
else
echo "INFO: No build step detected — skipping build check"
fiPass condition: Build command exits with code 0, or no build step detected. On failure: Report build errors and stop the pipeline.
Output
- Format: JSON object with pass/fail per check
- Structure:
{
"phase": "preflight",
"timestamp": "ISO-8601",
"checks": {
"git_clean": { "status": "pass|fail", "details": "" },
"branch": { "status": "pass|warn", "current": "branch-name", "details": "" },
"tests": { "status": "pass|fail|skip", "details": "" },
"build": { "status": "pass|fail|skip", "details": "" }
},
"overall": "pass|fail",
"blockers": []
}Next Phase
If all checks pass, proceed to Phase 2: Code Review. If any check fails, report BLOCKED status with the preflight report.
Phase 2: Code Review
Automated AI-powered code review of changes since the base branch, with risk assessment.
Objective
- Detect the merge base between current branch and target branch
- Generate diff for review
- Run AI-powered code review via CCW CLI
- Flag high-risk changes (large diffs, sensitive files, breaking changes)
Gate Condition
No critical issues flagged by the review. Warnings are reported but do not block.
Execution Steps
Step 1: Detect Merge Base
# Determine target branch (default: main, fallback: master)
target_branch="main"
if ! git rev-parse --verify "origin/$target_branch" &>/dev/null; then
target_branch="master"
fi
# Find merge base
merge_base=$(git merge-base "origin/$target_branch" HEAD)
echo "Merge base: $merge_base"
# If on main/master directly, compare against last tag
current_branch=$(git branch --show-current)
if [ "$current_branch" = "main" ] || [ "$current_branch" = "master" ]; then
last_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -n "$last_tag" ]; then
merge_base="$last_tag"
echo "On main — using last tag as base: $last_tag"
else
# Use first commit if no tags exist
merge_base=$(git rev-list --max-parents=0 HEAD | head -1)
echo "No tags found — using initial commit as base"
fi
fiStep 2: Generate Diff Summary
# File-level summary
git diff --stat "$merge_base"...HEAD
# Full diff for review
git diff "$merge_base"...HEAD > /tmp/ship-review-diff.txt
# Count changes for risk assessment
files_changed=$(git diff --name-only "$merge_base"...HEAD | wc -l)
lines_added=$(git diff --numstat "$merge_base"...HEAD | awk '{s+=$1} END {print s}')
lines_removed=$(git diff --numstat "$merge_base"...HEAD | awk '{s+=$2} END {print s}')Step 3: Risk Assessment
Flag high-risk indicators before AI review:
| Risk Factor | Threshold | Risk Level |
|---|---|---|
| Files changed | > 50 | High |
| Lines changed | > 1000 | High |
| Sensitive files modified | Any of: .env*, *secret*, *credential*, *auth*, *.key, *.pem | High |
| Config files modified | package.json, pyproject.toml, tsconfig.json, Dockerfile | Medium |
| Migration files | *migration*, *migrate* | Medium |
# Check for sensitive file changes
sensitive_files=$(git diff --name-only "$merge_base"...HEAD | grep -iE '\.(env|key|pem)|secret|credential' || true)
if [ -n "$sensitive_files" ]; then
echo "HIGH RISK: Sensitive files modified:"
echo "$sensitive_files"
fiStep 4: AI Code Review
Use CCW CLI for automated analysis:
ccw cli -p "PURPOSE: Review code changes for release readiness; success = all critical issues identified with file:line references
TASK: Review diff for bugs | Check for breaking changes | Identify security concerns | Assess test coverage gaps
MODE: analysis
CONTEXT: @**/* | Reviewing diff from $merge_base to HEAD ($files_changed files, +$lines_added/-$lines_removed lines)
EXPECTED: Risk assessment (low/medium/high), list of issues with severity and file:line, release recommendation (ship/hold/fix-first)
CONSTRAINTS: Focus on correctness and security | Flag breaking API changes | Ignore formatting-only changes
" --tool gemini --mode analysisNote: Wait for the CLI analysis to complete before proceeding. Do not proceed to Phase 3 while review is running.
Step 5: Evaluate Review Results
Based on the AI review output:
| Review Result | Action |
|---|---|
| No critical issues | Proceed to Phase 3 |
| Critical issues found | Report BLOCKED, list issues |
| Warnings only | Proceed with DONE_WITH_CONCERNS note |
| Review failed/timeout | Ask user whether to proceed or retry |
Output
- Format: Review summary with risk assessment
- Structure:
{
"phase": "code-review",
"merge_base": "commit-sha",
"stats": {
"files_changed": 0,
"lines_added": 0,
"lines_removed": 0
},
"risk_level": "low|medium|high",
"risk_factors": [],
"ai_review": {
"recommendation": "ship|hold|fix-first",
"critical_issues": [],
"warnings": []
},
"overall": "pass|fail|warn"
}Next Phase
If review passes (no critical issues), proceed to Phase 3: Version Bump. If critical issues found, report BLOCKED status with review summary.
Phase 3: Version Bump
Detect the current version, determine the bump type, and update the version file.
Objective
- Detect which version file the project uses
- Read the current version
- Determine bump type (patch/minor/major) from commit messages or user input
- Update the version file
- Record the version change
Gate Condition
Version file updated successfully with the new version.
Execution Steps
Step 1: Detect Version File
Detection priority order:
| Priority | File | Read Method |
|---|---|---|
| 1 | package.json | jq -r .version package.json |
| 2 | pyproject.toml | grep -oP 'version\s*=\s*"\K[^"]+' pyproject.toml |
| 3 | VERSION | cat VERSION |
if [ -f "package.json" ]; then
version_file="package.json"
current_version=$(node -p "require('./package.json').version" 2>/dev/null || jq -r .version package.json)
elif [ -f "pyproject.toml" ]; then
version_file="pyproject.toml"
current_version=$(grep -oP 'version\s*=\s*"\K[^"]+' pyproject.toml | head -1)
elif [ -f "VERSION" ]; then
version_file="VERSION"
current_version=$(cat VERSION | tr -d '[:space:]')
else
echo "NEEDS_CONTEXT: No version file found"
echo "Expected one of: package.json, pyproject.toml, VERSION"
# Ask user which file to use or create
fi
echo "Version file: $version_file"
echo "Current version: $current_version"Step 2: Determine Bump Type
Auto-detection from commit messages (conventional commits):
# Get commits since last tag
last_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -n "$last_tag" ]; then
commits=$(git log "$last_tag"..HEAD --oneline)
else
commits=$(git log --oneline -20)
fi
# Scan for conventional commit prefixes
has_breaking=$(echo "$commits" | grep -iE '(BREAKING CHANGE|!:)' || true)
has_feat=$(echo "$commits" | grep -iE '^[a-f0-9]+ feat' || true)
has_fix=$(echo "$commits" | grep -iE '^[a-f0-9]+ fix' || true)
if [ -n "$has_breaking" ]; then
suggested_bump="major"
elif [ -n "$has_feat" ]; then
suggested_bump="minor"
else
suggested_bump="patch"
fi
echo "Suggested bump: $suggested_bump"User confirmation:
- For
patchandminor: proceed with suggested bump, inform user - For
major: always ask user to confirm before proceeding (major bumps have significant implications) - User can override the suggestion with an explicit bump type
Step 3: Calculate New Version
# Parse semver components
IFS='.' read -r major minor patch <<< "$current_version"
case "$bump_type" in
major)
new_version="$((major + 1)).0.0"
;;
minor)
new_version="${major}.$((minor + 1)).0"
;;
patch)
new_version="${major}.${minor}.$((patch + 1))"
;;
esac
echo "Version bump: $current_version -> $new_version"Step 4: Update Version File
case "$version_file" in
package.json)
# Use node/jq for safe JSON update
jq --arg v "$new_version" '.version = $v' package.json > tmp.json && mv tmp.json package.json
# Also update package-lock.json if it exists
if [ -f "package-lock.json" ]; then
jq --arg v "$new_version" '.version = $v | .packages[""].version = $v' package-lock.json > tmp.json && mv tmp.json package-lock.json
fi
;;
pyproject.toml)
# Use sed for TOML update (version line in [project] or [tool.poetry])
sed -i "s/^version\s*=\s*\".*\"/version = \"$new_version\"/" pyproject.toml
;;
VERSION)
echo "$new_version" > VERSION
;;
esac
echo "Updated $version_file: $current_version -> $new_version"Step 5: Verify Update
# Re-read to confirm
case "$version_file" in
package.json)
verified=$(node -p "require('./package.json').version" 2>/dev/null || jq -r .version package.json)
;;
pyproject.toml)
verified=$(grep -oP 'version\s*=\s*"\K[^"]+' pyproject.toml | head -1)
;;
VERSION)
verified=$(cat VERSION | tr -d '[:space:]')
;;
esac
if [ "$verified" = "$new_version" ]; then
echo "PASS: Version verified as $new_version"
else
echo "FAIL: Version mismatch — expected $new_version, got $verified"
fiOutput
- Format: Version change record
- Structure:
{
"phase": "version-bump",
"version_file": "package.json",
"previous_version": "1.2.3",
"new_version": "1.3.0",
"bump_type": "minor",
"bump_source": "auto-detected|user-specified",
"overall": "pass|fail"
}Next Phase
If version updated successfully, proceed to Phase 4: Changelog & Commit. If version update fails, report BLOCKED status.
Phase 4: Changelog & Commit
Generate changelog entry from git history, update CHANGELOG.md, create release commit, and push to remote.
Objective
- Parse git log since last tag into grouped changelog entry
- Update or create CHANGELOG.md
- Create a release commit with version in the message
- Push the branch to remote
Gate Condition
Release commit created and pushed to remote successfully.
Execution Steps
Step 1: Gather Commits Since Last Tag
last_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -n "$last_tag" ]; then
echo "Generating changelog since tag: $last_tag"
git log "$last_tag"..HEAD --pretty=format:"%h %s" --no-merges
else
echo "No previous tag found — using last 50 commits"
git log --pretty=format:"%h %s" --no-merges -50
fiStep 2: Group Commits by Conventional Commit Type
Parse commit messages and group into categories:
| Prefix | Category | Changelog Section |
|---|---|---|
feat: / feat(*): | Features | Features |
fix: / fix(*): | Bug Fixes | Bug Fixes |
perf: | Performance | Performance |
docs: | Documentation | Documentation |
refactor: | Refactoring | Refactoring |
chore: | Maintenance | Maintenance |
test: | Testing | (omitted from changelog) |
| Other | Miscellaneous | Other Changes |
# Example grouping logic (executed by the agent, not a literal script):
# 1. Read all commits since last tag
# 2. Parse prefix from each commit message
# 3. Group into categories
# 4. Format as markdown sections
# 5. Omit empty categoriesStep 3: Format Changelog Entry
Generate a markdown changelog entry:
## [X.Y.Z] - YYYY-MM-DD
### Features
- feat: description (sha)
- feat(scope): description (sha)
### Bug Fixes
- fix: description (sha)
### Performance
- perf: description (sha)
### Other Changes
- chore: description (sha)Rules:
- Date format: YYYY-MM-DD (ISO 8601)
- Each entry includes the short SHA for traceability
- Empty categories are omitted
- Entries are listed in chronological order within each category
Step 4: Update CHANGELOG.md
if [ -f "CHANGELOG.md" ]; then
# Insert new entry after the first heading line (# Changelog)
# The new entry goes between the main heading and the previous version entry
# Use Write tool to insert the new section at the correct position
echo "Updating existing CHANGELOG.md"
else
# Create new CHANGELOG.md with header
echo "Creating new CHANGELOG.md"
fiCHANGELOG.md structure:
# Changelog
## [X.Y.Z] - YYYY-MM-DD
(new entry here)
## [X.Y.Z-1] - YYYY-MM-DD
(previous entry)Step 5: Create Release Commit
# Stage version file and changelog
git add package.json package-lock.json pyproject.toml VERSION CHANGELOG.md 2>/dev/null
# Only stage files that actually exist and are modified
git add -u
# Create release commit
git commit -m "$(cat <<'EOF'
chore: bump version to X.Y.Z
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EOF
)"Commit message format: chore: bump version to X.Y.Z
- Follows conventional commit format
- Includes Co-Authored-By trailer
Step 6: Push to Remote
current_branch=$(git branch --show-current)
# Check if remote tracking branch exists
if git rev-parse --verify "origin/$current_branch" &>/dev/null; then
git push origin "$current_branch"
else
git push -u origin "$current_branch"
fiOn push failure:
- If rejected (non-fast-forward): Report BLOCKED, suggest
git pull --rebase - If permission denied: Report BLOCKED, check remote access
- If no remote configured: Report BLOCKED, suggest
git remote add
Output
- Format: Commit and push record
- Structure:
{
"phase": "changelog-commit",
"changelog_entry": "## [X.Y.Z] - YYYY-MM-DD ...",
"commit_sha": "abc1234",
"commit_message": "chore: bump version to X.Y.Z",
"pushed_to": "origin/branch-name",
"overall": "pass|fail"
}Next Phase
If commit and push succeed, proceed to Phase 5: PR Creation. If push fails, report BLOCKED status with error details.
Phase 5: PR Creation
Create a pull request via GitHub CLI with a structured body, linked issues, and release metadata.
Skip Conditions
Skip this phase (proceed directly to Phase 6) when:
- Current branch is
main/masterand the project uses trunk-based development (release commit was pushed directly to the default branch) - User explicitly passes
--no-pr - No remote write access to create PRs (rare — usually a BLOCKED signal instead)
When skipped, record phase: "pr-creation", overall: "skip", reason: "..." in the skill output and move on.
Objective
- Create a PR using
gh pr createwith structured body - Auto-link related issues from commit messages
- Include release summary (version, changes, test plan)
- Output the PR URL
Gate Condition
PR created successfully and URL returned — OR skip condition matched.
Execution Steps
Step 1: Extract Issue References from Commits
last_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -n "$last_tag" ]; then
commits=$(git log "$last_tag"..HEAD --pretty=format:"%s" --no-merges)
else
commits=$(git log --pretty=format:"%s" --no-merges -50)
fi
# Extract issue references: fixes #N, closes #N, resolves #N, refs #N
issues=$(echo "$commits" | grep -oiE '(fix(es)?|close[sd]?|resolve[sd]?|refs?)\s*#[0-9]+' | grep -oE '#[0-9]+' | sort -u || true)
echo "Referenced issues: $issues"Step 2: Determine Target Branch
# Default target: main (fallback: master)
target_branch="main"
if ! git rev-parse --verify "origin/$target_branch" &>/dev/null; then
target_branch="master"
fi
current_branch=$(git branch --show-current)
echo "PR: $current_branch -> $target_branch"Step 3: Build PR Title
Format: release: vX.Y.Z
pr_title="release: v${new_version}"If the version context is not available, fall back to a descriptive title from the branch name.
Step 4: Build PR Body
Construct the PR body using a HEREDOC for correct formatting:
# Gather change summary
change_summary=$(git log "$merge_base"..HEAD --pretty=format:"- %s (%h)" --no-merges)
# Build linked issues section
if [ -n "$issues" ]; then
issues_section="## Linked Issues
$(echo "$issues" | while read -r issue; do echo "- $issue"; done)"
else
issues_section=""
fiStep 5: Create PR via gh CLI
gh pr create --title "$pr_title" --base "$target_branch" --body "$(cat <<'EOF'
## Summary
Release vX.Y.Z
### Changes
- list of changes from changelog
## Linked Issues
- #N (fixes)
- #M (closes)
## Version
- Previous: X.Y.Z-1
- New: X.Y.Z
- Bump type: patch|minor|major
## Test Plan
- [ ] Pre-flight checks passed (git clean, branch, tests, build)
- [ ] AI code review completed with no critical issues
- [ ] Version bump verified in version file
- [ ] Changelog updated with all changes since last release
- [ ] Release commit pushed successfully
Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"PR body sections:
| Section | Content |
|---|---|
| Summary | Version being released, one-line description |
| Changes | Grouped changelog entries (from Phase 4) |
| Linked Issues | Auto-extracted fixes #N, closes #N references |
| Version | Previous version, new version, bump type |
| Test Plan | Checklist confirming all phases passed |
Step 6: Capture and Report PR URL
# gh pr create outputs the PR URL on success
pr_url=$(gh pr create ... 2>&1 | tail -1)
echo "PR created: $pr_url"Output
- Format: PR creation record
- Structure:
{
"phase": "pr-creation",
"pr_url": "https://github.com/owner/repo/pull/N",
"pr_title": "release: vX.Y.Z",
"target_branch": "main",
"source_branch": "feature-branch",
"linked_issues": ["#1", "#2"],
"overall": "pass|fail"
}Next Phase
After PR creation (or skip), proceed to Phase 6: Platform Publish.
Note: Final completion status is emitted by Phase 7 (the terminal phase), not here. PR URL flows through to the final STATUS output as part of the release record.
Phase 6: Platform Publish
Publish the released version to its package registry (npm, PyPI, etc.). Runs after the release commit is merged to the default branch (or immediately after push for trunk-based workflows).
Objective
- Detect the target registry from the version file type
- Verify the local working tree is on the release commit
- Publish to the registry
- Capture the published artifact metadata (name, version, tarball URL)
Gate Condition
Publish command exits 0 and registry confirms the new version is live.
When to Skip
- Project is marked
"private": trueinpackage.json(or equivalent private flag) — skip and record as N/A - User explicitly passes
--no-publish - Registry is unreachable — report BLOCKED with diagnostic, do not retry blindly
Execution Steps
Step 1: Detect Registry
| Version File | Registry | Publish Command |
|---|---|---|
package.json (not private) | npm | npm publish |
pyproject.toml | PyPI | python -m build && twine upload dist/* |
Cargo.toml | crates.io | cargo publish |
VERSION / other | — | SKIP (no registry) |
if [ -f "package.json" ]; then
is_private=$(node -p "require('./package.json').private === true" 2>/dev/null || echo "false")
if [ "$is_private" = "true" ]; then
echo "SKIP: package.json marked private"
exit 0
fi
pkg_name=$(node -p "require('./package.json').name")
pkg_version=$(node -p "require('./package.json').version")
fiStep 2: Verify Working Tree
Before publishing, confirm the local tree matches what was committed:
git status --porcelain # must be empty
git rev-parse HEAD # record the commit SHA being publishedIf the tree is dirty, abort — partial/uncommitted changes must never land in a published artifact.
Step 3: Publish
npm:
# Default — runs the package's own prepublish hooks (build, clean, etc.)
npm publish 2>&1 | tee /tmp/publish.log
# For scoped packages that need public access:
# npm publish --access publicBackground execution is acceptable for long publish operations (large packages, slow network). Use the Bash tool's run_in_background: true and monitor the output file.
PyPI:
rm -rf dist/ build/
python -m build
twine upload dist/* 2>&1 | tee /tmp/publish.logStep 4: Verify Publish
# npm — query the registry for the new version
npm view "$pkg_name@$pkg_version" version
# PyPI — query the simple index
curl -sfI "https://pypi.org/pypi/$pkg_name/$pkg_version/json" | head -1If verification fails, check the publish log — a common cause is an OTP required prompt for 2FA npm accounts. Surface the prompt to the user.
Output
{
"phase": "platform-publish",
"registry": "npm|pypi|crates|none",
"package_name": "claude-code-workflow",
"published_version": "7.3.7",
"tarball_size": "8.2 MB",
"total_files": 2280,
"published_sha": "3a0d6d71",
"overall": "pass|skip|fail"
}Next Phase
Proceed to Phase 7: GitHub Release.
If publish fails, report BLOCKED — do not proceed to GitHub release, since the release notes would point to a version that doesn't exist on the registry.
Phase 7: GitHub Release
Tag the release commit and publish a GitHub release with auto-generated release notes.
Objective
- Create an annotated git tag
vX.Y.Zpointing at the release commit - Push the tag to origin
- Create a GitHub release with structured release notes (summary, commit list, migration notes, compare link)
Gate Condition
gh release create returns a release URL.
Prerequisite
- Phase 6 (Platform Publish) completed with status
passorskip. Do not create a release for a version that failed to publish. ghCLI authenticated (gh auth status).
Execution Steps
Step 1: Create and Push Annotated Tag
# Tag the release commit
git tag -a "v${new_version}" -m "v${new_version}"
# Push to origin
git push origin "v${new_version}"Do NOT use lightweight tags — annotated tags carry the release metadata and are what gh release create expects.
If the tag already exists (e.g. a prior attempt pushed the tag), decide:
- Same commit → skip tag creation, proceed to release
- Different commit → BLOCKED, tag must be deleted and recreated deliberately (destructive, require user confirmation)
Step 2: Compose Release Notes
Release notes have four sections — keep them focused and scannable:
## Summary
- **{highlight 1}**: one-line description of the most impactful change
- **{highlight 2}**: another key change (feature/fix/perf)
- **Scope**: rough scale indicator (e.g. "74 files across X, Y, Z")
## What's Changed
- `<commit subject>` (<short SHA>)
- `<commit subject>` (<short SHA>)
## Migration
{Migration notes if any, or "No action required."}
**Full Changelog**: https://github.com/{owner}/{repo}/compare/v{prev}...v{new}Sourcing the content:
- Summary bullets: synthesize 2-4 highlights from the commits since last release. Prefer
feat:/refactor:/ breaking changes. Call out compatibility posture explicitly if it's non-trivial. - What's Changed: one line per commit since
git describe --tags --abbrev=0(exclude merge commits and thechore: bump versioncommit itself). - Migration: only include if users need to take action. Default to "No action required."
- Compare link: always include — it's the authoritative diff.
prev_tag=$(git describe --tags --abbrev=0 HEAD^)
commits=$(git log "$prev_tag..v${new_version}^" --pretty=format:"- \`%s\` (%h)" --no-merges)
compare_url="https://github.com/${owner}/${repo}/compare/${prev_tag}...v${new_version}"Step 3: Create GitHub Release
gh release create "v${new_version}" \
--title "v${new_version} — {short descriptive suffix}" \
--notes "$(cat <<'EOF'
## Summary
- ...
## What's Changed
- ...
## Migration
...
**Full Changelog**: ...
EOF
)"Title convention: v{X.Y.Z} — {Short Theme} (e.g. v7.3.7 — Session ID Chronological Sort). The suffix summarizes the release theme in 2-5 words.
Flags:
- Omit
--draftunless the user wants to review notes first - Omit
--prereleasefor standard releases; use it for-beta/-rctags --latestis automatic when the tag is the highest semver
Step 4: Capture Release URL
gh release create prints the release URL on stdout — capture and report it:
release_url=$(gh release create ... 2>&1 | tail -1)
echo "Release: $release_url"Output
{
"phase": "github-release",
"tag": "v7.3.7",
"tagged_sha": "3a0d6d71",
"release_url": "https://github.com/owner/repo/releases/tag/v7.3.7",
"previous_tag": "v7.3.6",
"commits_included": 4,
"overall": "pass|fail"
}Completion
After the GitHub release is created, emit the final Completion Status:
## STATUS: DONE
**Summary**: Released vX.Y.Z — published to {registry} and GitHub
### Details
- Version: {previous} -> {new} ({bump_type})
- npm: https://www.npmjs.com/package/{pkg_name}/v/{new}
- Release: {release_url}
- Tag: v{new} -> {commit_sha}
### Outputs
- CHANGELOG.md (updated)
- Release commit: {sha}
- npm tarball: {size}, {files} files
- GitHub release: {release_url}Use DONE_WITH_CONCERNS if the publish step was skipped (private package) or if the release notes were auto-generated without human review.