| name | swain-release |
| description | Cut a release — detect versioning context, generate a changelog from conventional commits, bump versions, create a git tag, and optionally squash-merge to a release branch. Use when the user says "release", "cut a release", "tag a release", "bump the version", "create a changelog", "ship a version", "publish", or any variation of shipping/publishing a version. This skill is intentionally generic and works across any repo — it infers context from git history and project structure rather than assuming a specific setup. Supports the trunk+release branch model (ADR-013) when a `release` branch exists. |
| license | UNLICENSED |
| allowed-tools | Bash, Read, Write, Edit, Grep, Glob, AskUserQuestion |
| metadata | {"short-description":"Version bump, changelog, and git tag","version":"1.5.0","author":"cristos","source":"swain"} |
Release
Cut a release by detecting the project's versioning context, generating a changelog, bumping versions, and tagging. Works across any repo by reading context from git history and project structure rather than hardcoding assumptions.
Override file
Before starting, read .agents/release.override.skill.md if it exists. This is a freeform markdown file authored by the project owner whose instructions layer on top of this skill — its contents take precedence where they conflict. It can narrow defaults, specify version file locations, set tag formats, add pre/post-release steps, or anything else.
If no override exists, proceed with context detection alone.
Workflow
1. Gather context
Infer the project's release conventions from what already exists. Do all of these checks up front before proposing anything to the user.
Tag history:
git tag --sort=-v:refname | head -20
From existing tags, infer:
- Tag format —
v1.2.3, 1.2.3, name-v1.2.3, or something else
- Versioning scheme — semver, calver, or custom
- Current version — the most recent tag that matches the detected pattern
If there are no tags at all, note that this is the first release and ask the user what format they want.
Commits since last release:
git log <last-tag>..HEAD --oneline --no-decorate
If no tags exist, use all commits (or a reasonable window — ask the user if there are hundreds).
Version files — scan for files that commonly hold version numbers:
grep -rl 'version' --include='*.json' --include='*.toml' --include='*.yaml' --include='*.yml' -l . 2>/dev/null | head -20
Also check for VERSION files, version: in SKILL.md frontmatter, version fields in package.json, pyproject.toml, Cargo.toml, etc. Don't modify anything yet — just catalog what exists.
2. Determine the bump
Parse commits since the last tag using conventional-commit prefixes to suggest a bump level:
| Commit prefix | Suggests |
|---|
feat | minor bump |
fix | patch bump |
docs, chore, refactor, test, ci | patch bump |
BREAKING CHANGE in body, or ! after type | major bump |
The highest-level signal wins (any breaking change = major, any feat = at least minor, otherwise patch).
If commits don't follow conventional-commit format, fall back to listing them and asking the user what bump level feels right.
3. Propose the release
Present the user with a release plan before executing anything. Include:
- Current version (from latest tag, or "first release")
- Proposed version (with the detected bump applied)
- Changelog preview (thematic narrative — see step 4)
- Files to update (version files found in step 1, if any)
- Tag to create (using the detected format)
Wait for the user to confirm, adjust the version, or abort. If the user wants a different version than what was suggested, use theirs — the suggestion is a starting point, not a mandate.
4. Generate the changelog
Synthesize, don't transcribe. The changelog is for humans reading release notes, not for git log --oneline with extra steps. Dozens of commits should collapse into a few coherent narratives.
Before writing, read the existing CHANGELOG.md (if any) to match the voice, density, and structure the project already uses. The changelog should read like the same person wrote every entry.
Template-driven changelog
The changelog is rendered from a Jinja2 template (templates/changelog.md.j2) fed by a JSON data file. This separates the bucketing decision (which section does a change belong in?) from the rendering (how does the markdown look?).
Step 4a — Classify commits into four buckets. Each commit goes into exactly one:
| Bucket | What belongs here | What does NOT belong here |
|---|
features | Shipped capability: new skills, CLI flags, scripts, bug fixes that change behavior, refactors that change UX | Planning artifacts that describe future work |
roadmap | Forward-looking previews of planned work — what's coming and why it matters to the user. Write as "X is being planned/designed because Y" not "SPEC-NNN created". Omit artifact IDs unless the reader would search for them. Skip items that are pure internal housekeeping. | Artifact state transitions ("EPIC activated", "SPEC created"), anything that shipped (that's features) |
research | Trove collections, spike completions, research artifacts, evidence gathered | Specs that resulted from research (those are roadmap) |
supporting | Chores, dependency bumps, cross-ref enrichment, minor refactors, CI changes | Anything that changes user-visible behavior (that's features) |
The key distinction agents get wrong: creating a SPEC or EPIC is a roadmap change, not a feature. A feature is something the operator can use today because it shipped in this release. A SPEC is a plan for something that will ship later.
Roadmap anti-pattern: "EPIC-029 activated with 3 child SPECs (SPEC-118, SPEC-119, SPEC-120)" is noise — it describes artifact state transitions that only matter to the project maintainer. Instead write: "Trunk detection is being generalized so swain works on any branch name without configuration." The reader should understand what's coming and why they'd care, not which internal tracking artifacts changed state.
Omit mechanical commits entirely: merge commits, lifecycle hash stamps, index refreshes, bookmark advances.
Step 4b — Build the JSON data file. Write a temporary JSON file with this structure:
{
"version": "0.10.0-alpha",
"date": "2026-03-21",
"features": [
{"heading": "CLI Roadmap Renderer", "body": "chart.sh roadmap --cli produces deterministic, terminal-friendly\noutput grouped by Eisenhower quadrant with all first-degree children\nnested under their parent initiative. New swain-roadmap skill wraps\nit as the user-facing entry point: regenerate, open, display."},
{"text": "Dependency graph rendering switched to flowchart TD for clearer layout"}
],
"roadmap": ["Session facilitation rebuild — rethinking how swain helps the operator maintain focus, make decisions, and recover context across sessions"],
"research": ["Google Stitch SDK trove — 7 sources collected"],
"supporting": ["Cross-reference enrichment across ~100 doc files"]
}
Feature and roadmap items use {"heading": "Title", "body": "Narrative..."} for major work (renders as #### Title with a narrative paragraph) or {"text": "..."} for smaller bullets. Roadmap items can also be plain strings for one-liners. Use headings when a topic has enough substance for a paragraph; use bullets for one-liners. Research and supporting sections are flat string arrays. Empty arrays are fine — the template omits empty sections.
Step 4c — Render. Run the render script:
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
uv run --with jinja2 python "$REPO_ROOT/skills/swain-release/scripts/render_changelog.py" /tmp/changelog_data.json
This prints the rendered markdown to stdout. Review it, then prepend to CHANGELOG.md.
If jinja2 is unavailable, fall back to writing the markdown directly using the same four-section structure — the template encodes the format, not the only way to produce it.
What to omit
Merge commits, lifecycle hash stamps, index refreshes, bookmark advances, and other mechanical commits should be omitted entirely — they add noise without information. Commit-type prefixes (feat:, fix:) should be stripped from any text that makes it into the changelog.
Use commit prefixes for bump detection, not changelog structure
Conventional-commit types determine whether it's a major/minor/patch bump (step 2). After that, forget them — the changelog reader doesn't care that something was a feat vs docs.
Where to put the changelog:
- If a
CHANGELOG.md exists, prepend the new section at the top (below any header)
- If no changelog exists, ask the user whether they want one created, and where
- If the user doesn't want a file, just output it to the conversation
5. Bump versions
Update version strings in the files identified in step 1. Be surgical — only change the version value, not surrounding content. For each file type:
- package.json / composer.json: update the
"version" field
- pyproject.toml / Cargo.toml: update
version = "..."
- SKILL.md frontmatter: update
version: in YAML header
- VERSION file: replace contents
If a file has multiple version-like strings and it's ambiguous which one to update, ask the user rather than guessing.
5.5. Security gate
Before tagging, run the security scanner to catch secrets, dependency vulnerabilities, and static analysis issues. Invoke the swain-security-check skill (or run the scanner script directly if the skill isn't available):
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
SCANNER="$(find "$REPO_ROOT" -path '*/swain-security-check/scripts/security-scan.sh' -type f -print -quit 2>/dev/null)"
if [[ -n "$SCANNER" ]]; then
bash "$SCANNER"
fi
If any critical or high severity findings are reported, stop the release and present them to the user. The user can choose to:
- Fix the issues and resume
- Acknowledge the findings and proceed anyway (their call, not yours)
Medium and lower findings should be reported but do not block the release.
If the security scanner is not installed, note the gap and proceed — don't block on a missing tool.
6. Commit and tag
Stage the changed files (changelog + version bumps) and commit:
git add <changed-files>
git commit -m "release: v1.5.0"
Then create an annotated tag:
git tag -a <tag> -m "Release <tag>"
Use the tag format detected in step 1 (or what the user specified).
6.5. Squash-merge to release branch
If a release branch exists (check with git rev-parse --verify release 2>/dev/null), squash-merge the current branch (trunk) into it:
REPO_ROOT=$(git rev-parse --show-toplevel)
TRUNK=$(bash "$REPO_ROOT/scripts/swain-trunk.sh")
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "$CURRENT_BRANCH" != "$TRUNK" ]; then
echo "Warning: not on $TRUNK ($CURRENT_BRANCH). Skipping release branch update."
else
git checkout release
git merge --squash "$TRUNK"
git commit -m "release: <tag>"
git checkout "$TRUNK"
fi
If no release branch exists, skip this step silently — the project hasn't adopted the trunk+release model yet.
7. Offer to push
Ask the user if they want to push. If a release branch was updated in step 6.5:
git push origin "$TRUNK" && git push origin release && git push origin <tag>
If no release branch exists, push as before:
git push && git push origin <tag>
Push only the specific tag — git push --tags tries to push every local tag and produces noisy rejections for tags that already exist on the remote.
Don't push without asking — the user may want to review first, or they may have a CI pipeline that triggers on tags.
Edge cases
Monorepo with multiple version streams: If the tag history suggests per-package tags (e.g., frontend-v1.2.0, api-v3.1.0), ask the user which package they're releasing rather than assuming.
Pre-release versions: If the user asks for a pre-release (alpha, beta, rc), append the pre-release suffix to the version: 1.5.0-alpha.1. Follow the existing convention if prior pre-release tags exist.
No conventional commits: If the commit history doesn't use conventional prefixes, don't force the grouping. Present a flat list and let the changelog be a simple bullet list of changes.
Dirty working tree: If there are uncommitted changes when /swain-release is invoked, warn the user and ask whether to proceed (changes won't be included in the release) or abort so they can commit first.
Session bookmark
After a successful release, update the bookmark: bash "$(find . .claude .agents -path '*/swain-session/scripts/swain-bookmark.sh' -print -quit 2>/dev/null)" "Released v{version}"