release

Create versioned releases - bump version via PR, tag, create GitHub release. CI publishes to npm.

SKILL.md

GitHub
<!-- kspec-managed -->

Release Skill

Create versioned releases with proper git tagging and GitHub release creation. The workflow is:

  1. Bump version via PR (respects branch protection)
  2. Create tag on the merged commit
  3. Create GitHub release with notes
  4. CI publishes to npm automatically

When to Use

  • After completing features/fixes ready for release
  • When creating patch/minor/major version bumps
  • For pre-release versions (alpha, beta, rc)
  • When auditing what changes would be in a release (--dry-run)

Prerequisites:

  • All related kspec tasks should be completed before release
  • Run npm test locally to verify tests pass
  • Ensure you're on main with a clean working tree

Arguments

/release [patch|minor|major|X.Y.Z] [options]
Argument Description
patch Bug fixes, backwards-compatible (0.1.1 -> 0.1.2)
minor New features, backwards-compatible (0.1.1 -> 0.2.0)
major Breaking changes (0.1.1 -> 1.0.0)
X.Y.Z Explicit version number
(none) Auto-detect from commits since last tag
Option Description
--dry-run Preview only - NO changes to git, tags, PRs, or GitHub
--prerelease <type> Create prerelease (alpha, beta, rc)

Workflow

Phase 1: Detect Context

Gather information about current state:

# Get current branch
git branch --show-current

# Check for uncommitted changes
git status --porcelain

# Get current version from package.json
node -p "require('./package.json').version"

# Get last tag
git describe --tags --abbrev=0 2>/dev/null || echo "no-tags"

# Get commits since last tag
git log $(git describe --tags --abbrev=0 2>/dev/null)..HEAD --oneline

Context to gather:

  • Current branch (must be main)
  • Working tree status (must be clean)
  • Current version in package.json
  • Last tag (e.g., v0.1.1)
  • Commits since last tag (count and list)

Phase 2: Validate Constraints

All constraints must pass before proceeding:

Constraint Check Error Message
On main branch git branch --show-current = main "Must be on main branch. Currently on: {branch}"
Clean working tree git status --porcelain is empty "Working tree must be clean. Commit or stash changes first."
Up to date with origin After git fetch, local matches remote "Local main is behind origin. Run: git pull"
Has releasable changes At least one commit since last tag "No changes since last tag {tag}. Nothing to release."
Tag doesn't exist git tag -l v{version} is empty "Tag v{version} already exists."

Dry-run behavior: In dry-run mode, validation failures are reported but the preview continues. In normal mode, ANY validation failure stops execution immediately.

Phase 3: Determine Version

If version type not specified, auto-detect from ALL commits since last tag:

# Get all commit messages since last tag (not just merges)
git log $(git describe --tags --abbrev=0)..HEAD --pretty=format:"%s%n%b"

Auto-detection rules (conventional commits):

Pattern Bump Notes
BREAKING CHANGE: in body major Check commit body, not just subject
feat!: or fix!: (with !) major The ! suffix indicates breaking
feat: or feat(scope): minor Scope is ignored for detection
fix:, perf:, refactor:, chore: patch Any maintenance work

Resolution order:

  1. If ANY commit has breaking change indicator -> major
  2. Else if ANY commit starts with feat: -> minor
  3. Else -> patch

Prerelease handling (--prerelease <type>):

Current Flag Result Rule
0.1.1 --prerelease alpha 0.1.2-alpha.0 Bump patch, add prerelease
0.1.2-alpha.0 --prerelease alpha 0.1.2-alpha.1 Same type: increment counter
0.1.2-alpha.1 --prerelease beta 0.1.2-beta.0 New type: reset counter to 0
0.1.2-rc.0 (no flag) 0.1.2 Stable release: drop prerelease suffix

Prerelease rules:

  1. Same prerelease type -> increment counter (alpha.0 -> alpha.1)
  2. Different prerelease type -> reset counter (alpha.1 -> beta.0)
  3. No prerelease flag on prerelease version -> promote to stable

Phase 4: Analyze Changes for Release Notes

Parse commits and categorize them for user-friendly release notes.

Commit selection:

  • Prefer merge commits (contain (#N) PR reference) to avoid duplicates
  • If no merge commits, include all commits matching conventional patterns
  • Deduplicate by checking if a commit's changes are already in a merge commit

Categorization (commit type -> section):

Commit Type Release Notes Section
feat: Features & Additions
fix: Bug Fixes
perf: Performance
refactor:, chore:, build: Other Changes
test:, ci:, docs: (excluded unless user-facing)

kspec integration:

# Extract completed task references from commit trailers
git log $(git describe --tags --abbrev=0)..HEAD --pretty=format:"%b" | grep -E "^Task: @" | sort -u

Completed tasks provide context for what was accomplished in this release.

Phase 5: Preview (--dry-run)

For --dry-run, show comprehensive preview. No changes are made - no PRs, no tags, no releases.

## Release Preview (DRY RUN)

**Version bump:** 0.1.1 -> 0.1.2 (patch)
**Detection:** Auto-detected from 5 commits (3 fix, 2 chore)

### Validation
- [x] On main branch
- [x] Working tree clean
- [x] Up to date with origin
- [x] Has 5 commits since v0.1.1
- [x] Tag v0.1.2 does not exist

### Release Notes Preview

Bug fixes and improvements for kspec CLI.

#### Features & Additions
- Add JSON output mode for task list

#### Bug Fixes
- Fix author attribution for auto-generated notes
- Increase timeout for ref resolution

### Actions (dry-run: NOT executed)
1. Create version bump PR (package.json -> 0.1.2)
2. Regenerate kspec-agents.md with new version
3. Wait for CI, then merge PR
4. Create tag: v0.1.2
5. Push tag to origin
6. Create GitHub release with notes above
7. CI will publish to npm

To execute: run `/release` without --dry-run

Phase 6: Bump Version (PR)

Before creating the tag, update package.json via PR:

VERSION="0.1.2"  # calculated version

# Create branch for version bump
git checkout -b release/v$VERSION

# Update version without creating a tag
npm version $VERSION --no-git-tag-version

# Regenerate kspec-agents.md so the version header stays in sync
# For self-hosting (releasing kspec itself), use: npm run dev -- agents generate
kspec agents generate

# Commit and push
git add package.json package-lock.json kspec-agents.md
git commit -m "chore: bump version to $VERSION"
git push -u origin release/v$VERSION

# Create PR
gh pr create --title "chore: bump version to $VERSION" --body "Prepare release v$VERSION"

# Wait for CI to pass, then merge
gh pr merge --merge --delete-branch

Why version bump first?

  • The tagged commit has the correct version in package.json
  • CI publishes exactly what's in package.json
  • No out-of-sync state between git tag and package version

Why regenerate kspec-agents.md?

  • The freshness comment includes the kspec version (Generated by kspec vX.Y.Z)
  • Regenerating after the version bump keeps the header in sync with the release
  • Self-hosting note: When releasing kspec itself, the installed kspec binary still has the old version. Use npm run dev -- agents generate instead so the dev build reads the bumped version from the local package.json

Phase 7: Create Tag and Release

After the version PR is merged:

# Pull the merged version bump
git checkout main && git pull

# Create annotated tag (v prefix is convention for npm/semver)
git tag -a "v$VERSION" -m "Release v$VERSION"

# Push tag to origin
git push origin "v$VERSION"

# Create GitHub release (gh uses current repo automatically)
gh release create "v$VERSION" \
  --title "v$VERSION" \
  --notes "$(cat <<'EOF'
Brief summary of this release.

### Features & Additions
- Feature description

### Bug Fixes
- Fix description

### Other Changes
- Change description

**Full Changelog**: compare/v{previous}...v{version}
EOF
)"

Tag naming: Always use v prefix (e.g., v0.1.2) for consistency with npm versioning conventions and GitHub release detection.

CI behavior:

  • Triggers on release.published event (within 1-2 minutes)
  • Runs tests and publishes to npm with provenance
  • Monitor progress at the repo's Actions tab

Phase 8: Report Summary

After successful release:

## Release Complete

**Version:** v0.1.2
**Tag:** v0.1.2
**Commit:** abc1234

### Completed
- [x] Version PR created and merged
- [x] kspec-agents.md regenerated with new version
- [x] Tag created and pushed
- [x] GitHub release created
- [ ] CI publishing to npm... (check Actions tab)

**Release:** {repo}/releases/tag/v0.1.2
**CI Status:** {repo}/actions

Release Notes Format

Generate friendly, user-facing release notes (not raw commit dumps):

Brief high-level summary of what this release brings (1-2 sentences).

### Features & Additions
- Feature description in plain language
- Another feature

### Bug Fixes
- Fixed issue with X when Y happened
- Resolved problem where Z

### Performance
- Improved speed of X operation

### Other Changes
- Updated internal handling of Y
- Refactored Z for maintainability

**Full Changelog**: {repo}/compare/v0.1.1...v0.1.2

Writing guidelines:

  • Write in past tense ("Fixed", "Added", "Improved")
  • Focus on user impact, not implementation details
  • Group related changes together
  • Omit test-only and CI-only changes
  • Use repo-relative URLs (GitHub auto-links them)

Error Handling

Error Recovery
Not on main git checkout main && git pull
Dirty working tree Commit or stash changes first
Behind origin git pull origin main
No changes Nothing to release since last tag
Tag exists Choose different version or git tag -d v{version} then git push origin --delete v{version}
PR merge fails Check CI status, fix issues, retry merge
gh not installed Install GitHub CLI: https://cli.github.com/
gh not authenticated Run gh auth login
gh API rate limit Wait and retry, or use --limit flag

Rollback Procedure

If something goes wrong after partial release:

If tag was pushed but release failed:

# Delete remote tag
git push origin --delete v$VERSION
# Delete local tag
git tag -d v$VERSION
# Fix issue and retry

If release was created but CI publish failed:

# Option 1: Delete and retry
gh release delete v$VERSION --yes
git push origin --delete v$VERSION
git tag -d v$VERSION
# Fix issue and retry from Phase 6

# Option 2: Recover the existing published release through Actions
# Prerequisite: v$VERSION already names a published, non-draft GitHub release.
# In Actions > "Publish to npm" > "Run workflow", set:
#   release-tag: v$VERSION
#   dry-run: true        # default; reruns every gate without publishing
# Review the dry-run result first. To actually recover npm publication, rerun
# with the same release-tag and explicitly set dry-run: false.
#
# Both runs fail closed unless the remote tag still resolves to the immutable
# release commit selected by preflight. Never move or recreate the tag during
# recovery; a moved tag requires a new version-first release instead.

If version PR was merged but you need to abort:

# Revert the version bump
git revert HEAD
git push origin main
# The tag was never created, so nothing else to clean up

Examples

Standard patch release

User: /release patch
Agent: [Validates state]
Agent: [Creates version bump PR, waits for CI, merges]
Agent: [Creates v0.1.2 tag, pushes, creates GH release]

Release v0.1.2 created successfully.
- Version PR: #122 (merged)
- Tag: v0.1.2
- Release: {repo}/releases/tag/v0.1.2
- CI publishing to npm...

Auto-detect version

User: /release
Agent: [Analyzes 5 commits: 2 feat, 3 fix]
Detected: feat commits present -> minor bump (0.1.1 -> 0.2.0)
[Proceeds with release]

Preview only

User: /release --dry-run
Agent: [Shows full preview, makes NO changes]

Pre-release

User: /release minor --prerelease alpha
Agent: [Creates v0.2.0-alpha.0]

Explicit version

User: /release 1.0.0
Agent: [Creates v1.0.0 - useful for major milestones]

Maintaining release notes

The RELEASE_NOTES.md file at the repository root is the single source of truth for kspec release notes. It is human-authored, reviewed by humans, and shipped in the published npm package so kspec release-notes and kspec upgrade can print it directly. There is no generator — edit the file by hand as part of the normal change flow.

Where the file lives

  • Path: RELEASE_NOTES.md at the repository root (the same directory as package.json).
  • Packaged: listed in package.json#files so the published tarball includes it. The CLI resolves it relative to its own install location.

File layout

  • A short top-of-file preamble explains the file.
  • Each release is a level-2 heading (## vX.Y.Z) with most-recent first.
  • An optional ## Unreleased heading collects changes staged for the next release. It lives above the top versioned section. Promote it to a versioned heading as part of the release workflow (see below).

Required per-version subsections

Every versioned section must contain the following level-3 subsections, in this order. Use the exact headings below — the CLI and release tests rely on them:

  1. ### New or changed configuration — name every new config key and describe what it controls. If there are none, write a single line - No new configuration keys.
  2. ### Breaking changes — call out behavior changes that require consumer action. If there are none, write - None.

Optional additional subsections (use the ones that apply): ### Features & Additions, ### Bug Fixes, ### Performance, ### Documentation, ### CI, ### Other Changes. Match the tone and writing guidelines in Release Notes Format above.

Adding an entry

During normal work, edit the ## Unreleased section to append bullet points describing the change. Put each item in the right subsection (new config under ### New or changed configuration, breaking changes under ### Breaking changes, everything else under the appropriate optional subsection). If a required subsection isn't present yet, add it.

For cross-version aggregation, keep the most recent entries near the top of each subsection. Don't worry about chronological ordering within a section.

Promoting Unreleased at release time

When starting a release:

  1. Rename the ## Unreleased heading to ## vX.Y.Z with the version number being released.
  2. Make sure both required subsections are present and non-empty (write - None. / - No new configuration keys. if there's nothing to say).
  3. Add a fresh empty ## Unreleased section at the top for the next release cycle. Leaving it off is fine, but having a placeholder makes the next change's diff smaller.

Pre-release check

Before running the rest of the release workflow (Phase 5 onwards), verify the notes for the version being released are present and non-empty. The exact check is:

VERSION="0.1.2"  # the version you are about to release
awk -v v="$VERSION" '
  $0 ~ "^## v?" v "([^0-9.]|$)" { in_section = 1; body = ""; next }
  /^## / && in_section { exit }
  in_section { body = body $0 "\n" }
  END {
    gsub(/[[:space:]]/, "", body)
    if (!in_section) { print "error: no section for v" v " in RELEASE_NOTES.md"; exit 1 }
    if (body == "") { print "error: section for v" v " is empty"; exit 1 }
  }
' RELEASE_NOTES.md

If the command exits non-zero, stop and author the missing notes before continuing. The release notes must be written and reviewed before the version bump PR is created — not as a follow-up commit — so the tagged commit and published tarball carry the notes users will read when they upgrade.

Key Principles

  • Version bump first: PR before tag ensures package.json matches release
  • Validation first: Check all constraints before any changes
  • Dry-run is safe: Preview makes zero changes
  • Friendly release notes: Write for users, not developers
  • Branch protection compatible: All changes via PR
  • CI handles publishing: Skill creates release, CI publishes to npm
  • kspec integration: Complete tasks before release, reference them in commits
  • Release notes in the file: Every released version has a non-empty entry in RELEASE_NOTES.md — verified by the pre-release check above

Troubleshooting

npm Trusted Publishers OIDC Failures

If npm publish fails with "Access token expired or revoked" + 404:

Root cause: Older npm versions (10.8.x bundled with Node 18) have bugs with OIDC authentication for trusted publishers.

Fix: Ensure the publish workflow installs npm@latest:

- name: Install latest npm
  run: npm install -g npm@latest && npm --version

npm 11.x+ has the necessary fixes.

PR Merge Blocked

If the version bump PR can't merge:

  1. Check CI status - tests must pass
  2. Check for merge conflicts - rebase if needed
  3. Check branch protection rules - ensure you have merge permissions

Tag Already Exists

If you need to re-release the same version:

# Delete existing tag (local and remote)
git tag -d v0.1.2
git push origin --delete v0.1.2

# Delete GitHub release if it exists
gh release delete v0.1.2 --yes

# Now retry the release

Related Skills

  • /pr - Create pull requests (used internally by this skill)
  • /audit - Pre-release codebase review
  • /kspec:task-work - Task lifecycle and completion tracking
  • /kspec:help - Command lookup and workflow help