release-ship
Skill14 starsChanged 4 days agoMITView on GitHub
Executes the release workflow for a repository after release-gate passes. Use when tagging, cutting, or shipping a release. Follows the prep-PR pattern: version bump, CHANGELOG, and walkthrough ship in one PR before tagging.
- Commits and pushes
In this repository12 other files it gives its agents.
- .claude/CLAUDE.mdCLAUDE.md
- CLAUDE.mdCLAUDE.md
- cc-release-reviewSkill
- verifySkill
- assisted-logSkill
- code-theorySkill
- code-walkthroughSkill
- downloads-triageSkill
- editorSkill
- release-gateSkill
- release-sweepSkill
- rexSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
17 rules. About 0 are common, 7 are distinctive, and 10 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈7
Distinctive · estimate
This author's own choices. Worth a look.
10
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 19
Read the plan → 1. Prep branch → 2.
- Distinctive: line 35
If any prerequisite is unclear, ask rather than proceeding.
- Very specific: line 39
Every phase below is driven by one command.
- Distinctive: line 84
If git tag -l prints the tag, stop: the version is already released, or a tag was created out of band.
- Very specific: line 110
Add the ## <version> section above the previous entry:
- Very specific: line 126
Verify: grep -n "^## " CHANGELOG.md | head -3 shows the new section above the previous release's.
- Very specific: line 130
If the plan names a walkthrough, regenerate it via the code-walkthrough skill so its snippets reflect the release state.
- Distinctive: line 156
If the plan names a build command, run it before committing:
- Distinctive: line 183
Run the repo's local gate on the committed prep branch.
- Very specific: line 204
Verify: git show --stat HEAD lists exactly the files staged above.
- Distinctive: line 207
Stop here and wait for the prep branch to merge.
- Very specific: line 305
If yq is unavailable, grep for the files: block and read the lines that follow.
- Very specific: line 313
In
release_mode=tag, skip this phase: No release object exists to carry notes: the CHANGELOG at the tag is the release notes, and Phase 3 … - Very specific: line 341
If the printed conclusion is not success, report the failure and stop.
- Very specific: line 353
Add --prerelease when the plan says prerelease=1 (the version carries a -alpha, -beta or -rc suffix).
- Distinctive: line 360
Write the notes from the CHANGELOG, never from memory of the session.
- Very specific: line 365
If the plan's deploy_cmd is empty, skip this phase.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Release
Final step in the release pipeline. Assumes release-gate has already passed and the target version has been decided.
This skill follows the prep-PR pattern: version bump + CHANGELOG + walkthrough ship as one atomic PR. The tag is applied after merge, pointing at the merged commit. Never use a tool that bumps and tags in one step (npm version, bun version, hatch version --tag) — they tag immediately and skip the CHANGELOG and walkthrough entirely.
The workflow is a single linear pass — run the phases in order:
- Read the plan → 1. Prep branch → 2. Version bump → 3. CHANGELOG → 4. Walkthrough → 5. Build, commit & open PR → (merge) → 6. Gate, then tag → 7. Push tag → 8. Release notes → 9. Deploy → 10. Verify
Prerequisites
Before starting, confirm:
release-gatepassed with no FAIL status, or exited3(NOT STARTED), which is the normal state before a release: it means the current version is already tagged and phases 1-5 below are exactly what it is asking for. Phase 6 re-runs the gate itself and requires exit0— that check is a step in the workflow, not a precondition to remember.- Working tree is clean and on the default branch
- Target version decided
jqis onPATH, and so isghwhen the plan saysis_github=1or arelease_modeother thantag
If any prerequisite is unclear, ask rather than proceeding.
Phase 0: Read the Plan
Every phase below is driven by one command. Run it first with the target version — prep_branch and tag are derived from it:
~/.claude/skills/release-ship/scripts/release-plan.sh <version>It prints key=value lines resolved from the same profile the gate uses, so the two skills cannot disagree about the repo:
profile=Obsidian plugin profile=Taskfile (.release-gate)
version=2.1.0 version=2.1.0
tag=2.1.0 tag=v2.1.0
last_tag=2.0.5 last_tag=v2.0.0
prep_branch=release/2.1.0 prep_branch=release/2.1.0
primary_version_file=package.json primary_version_file=cx.js
primary_version_spec=jq:.version primary_version_spec=grep
derived_version_files=manifest.json versions.json
version_sync_cmd=npm_package_version="2.1.0" bun run version
build_cmd=bun run build build_cmd=
gate_cmd=bun install … && bun test gate_cmd=task check
changelog=CHANGELOG.md changelog=absent
walkthrough=WALKTHROUGH.md walkthrough=WALKTHROUGH.md
expected_changes=… expected_changes=cx.js WALKTHROUGH.md
release_mode=gh release_mode=gh
release_assets=main.js manifest.json release_assets=
deploy_cmd= deploy_cmd=task deploy
is_github=1 is_github=1
prerelease=0 prerelease=0Show the plan to the user before starting. An empty value means that step does not apply here — build_cmd= is no build, not a missing key.
release_mode is the one fork that changes the shape of the work, and getting it wrong is visible in both directions: workflow means pushing the tag triggers a workflow that creates the GitHub release itself, so Phase 8 waits for it and edits the notes onto the release it made; gh means nothing is watching the tag, so Phase 8 creates the release. Wait in gh mode and you poll for a run that will never appear. Create in workflow mode and you race the workflow for the same release. tag means the annotated tag and the CHANGELOG are the whole release, so Phase 8 creates nothing. It is the default off GitHub.
is_github is the other fork. At 0 the origin is not GitHub: there is no PR to open or look up, and Phases 5 and 6 work from git alone.
Phase 1: Prep Branch
Confirm the tag is free, then branch off the default branch:
git tag -l "<tag>" # expect empty
git checkout -b <prep_branch>If git tag -l prints the tag, stop: the version is already released, or a tag was created out of band. Do not proceed.
Phase 2: Version Bump
Edit primary_version_file by hand — with Edit, not a scripted substitution. The plan names the file; it does not name the line, and for primary_version_spec=grep the version string may appear in the file more than once (a comment, a compatibility note, a test literal). A blanket sed s/<old>/<new>/g rewrites all of them.
primary_version_spec=jq:.version— theversionfield of that JSON file.primary_version_spec=grep— the version literal in that source file, usually a constant.
Then regenerate the derived files, if the plan names a sync command:
bash -c '<version_sync_cmd>'The plan prints that command with the target version already substituted, so it can be run
verbatim. Do not hand-edit anything in derived_version_files; that command owns them.
Verification is Phase 6's gate check 10, which compares every declared file against the target. There is no need to re-implement it here — but if you want to see it now, run the gate with the target version and read row 10 alone; the other rows are meaningless until the prep is finished.
Phase 3: CHANGELOG
Draft the entry from git log <last_tag>..HEAD and present it to the user for review before committing.
If changelog=absent, create CHANGELOG.md in this phase. The gate merely skips a missing changelog; ship requires one, because in gh mode it is the only source for the release notes and extract-changelog.sh exits 1 without it. A first changelog starts with this release's section — do not reconstruct the project's history.
Add the ## <version> section above the previous entry:
## 1.4.0
### Added
- Support for nested callouts in preview mode
### Fixed
- Frontmatter properties no longer duplicate on saveHeadings are bare ## <version>, newest first. A trailing date (## 1.4.0 - 2026-03-08) is accepted by both the gate and the extractor, but write the bare form.
Verify: grep -n "^## " CHANGELOG.md | head -3 shows the new section above the previous release's.
Phase 4: Walkthrough
If the plan names a walkthrough, regenerate it via the code-walkthrough skill so its snippets reflect the release state.
Regenerating the snippets is not enough. Gate check 8 confirms every quoted snippet is still in the file its label names; nothing checks the prose around them. A release that renames or deletes an identifier leaves the commentary describing something that no longer exists, and check 8 still goes green. After regenerating, grep the prose for what this release changed:
git diff <last_tag>..HEAD --name-only | xargs -n1 basename | sort -u
grep -n '<renamed-or-deleted-identifier>' WALKTHROUGH.mdFix the prose in the same commit. Stale commentary is the failure mode the walkthrough exists to prevent.
Verify every quoted snippet after the formatter runs, not before. Prose reflows and fenced blocks may be rewritten; a snippet checked pre-format is a snippet unchecked. Gate check 8 is that check, so run the gate on the prep branch before opening the PR.
This is also the phase where THEORY.md, README.md and CLAUDE.md come current, if the
repo has them. They are release-time work by design: documents that cross-reference
each other can only be made consistent from a settled state, all at once.
Phase 5: Build, Commit and Open PR
If the plan names a build command, run it before committing:
<build_cmd>Where a repo tracks its build output, gate check 15 fails when the committed artifact does not match a fresh build, which is the normal state after any dependency PR that merged without a rebuild. No other phase stages it, so the prep PR is the release's only chance to carry a current one.
Stage what actually changed rather than a fixed list:
git add -u
git status --porcelaingit add -u stages every tracked modification and no untracked file. Compare the result against the plan's expected_changes:
- A path in
expected_changes— expected. - A path the build rewrote — expected, and the reason the build ran before staging.
- Anything else, or any untracked file — stop and show the user. An untracked file after a build is a new build output nothing tracks (a source map, a second stylesheet, typically after a bundler bump); it either belongs in
.gitignoreor belongs in the release, and that is the user's call. A fixed file list is how such a file gets silently left out.
Then one atomic commit for the whole prep:
git commit -m "chore: prepare release <version>"Run the repo's local gate on the committed prep branch. It is the check; no CI is waited on:
~/.claude/bin/pr-cycle gateWith no argument it runs the plan's gate_cmd, the command gate check 12 will run after the
merge. An empty gate_cmd makes it refuse; set GATE_CMD in .release-gate.
Exit 3 means the gate failed or rewrote files; fix, commit, and run it again. Push only on 0.
Confirm with the user before pushing, then:
git push -u origin <prep_branch>
gh pr create --title "chore: prepare release <version>" --body "..." # is_github=1 onlyDraft the PR body from the CHANGELOG entry. At is_github=0 there is no PR: the pushed
branch is the review copy, and the user merges it locally or on their forge.
Verify: git show --stat HEAD lists exactly the files staged above. At is_github=1, also
check that gh pr view --json state,url reports the PR as OPEN.
Stop here and wait for the prep branch to merge. The user reviews and merges it.
Phase 6: Gate, Then Tag After Merge
Once the PR is merged, sync the default branch:
git checkout <default-branch>
git pull --ff-only origin <default-branch>Then re-run the gate against the merged commit and require exit 0:
~/.claude/skills/release-gate/scripts/release-check.sh <version>; echo "exit=$?"Exit 0 is the only value that proceeds. On 1, 2 or 3: show the table, name the rows
that block, and stop without tagging. The gate says so itself — exit 2 prints
Result: WARNINGS ... not ready, clear these and re-run.
Now tag the merged commit, using the plan's tag (the prefix convention is the repo's, not this skill's):
At is_github=1, find the prep merge from the PR:
MERGED_SHA=$(gh pr list --state merged --head "<prep_branch>" \
--json mergeCommit --jq '.[0].mergeCommit.oid')At is_github=0 there is no PR to ask, so use git. Accept HEAD if it carries the prep
commit's subject, which survives a squash, a rebase or a fast-forward. Also accept it if
HEAD is a merge commit whose second parent is the pushed prep branch:
MERGED_SHA=""
if git log -1 --format=%s HEAD | grep -qE '^chore: prepare release <version>( \(#[0-9]+\))?$'; then
MERGED_SHA=$(git rev-parse HEAD)
elif [ "$(git rev-parse -q --verify HEAD^2)" = "$(git rev-parse -q --verify origin/<prep_branch>)" ]; then
MERGED_SHA=$(git rev-parse HEAD)
fiEither way, then:
if [ -z "$MERGED_SHA" ]; then
echo "No prep merge found for <prep_branch>."
echo "Look at 'git log --oneline -5' and ask which commit is the prep merge."
exit 1
fi
if [ "$MERGED_SHA" != "$(git rev-parse HEAD)" ]; then
echo "Prep merge is not HEAD — the gate did not check the commit this would tag."
echo " prep merge: $MERGED_SHA"
echo " HEAD: $(git rev-parse HEAD)"
echo "Commits that landed after the prep merge:"
git log --oneline "$MERGED_SHA"..HEAD
echo "Stop. Ask which commit to tag; do not choose one."
exit 1
fi
git tag -a <tag> -m "Release <version>" "$MERGED_SHA"The HEAD check is not a formality. The gate in the step above runs against
HEAD; this lookup resolves a commit independently. Nothing makes them the same
commit, and when they differ the tag publishes a tree no gate ever saw — a green
table on screen for one commit while a different one ships.
When it fires, stop and ask — do not pick. Neither answer is automatically
right. Tagging the prep merge publishes an ungated tree; tagging HEAD tags a
commit that is not a release/<version> prep merge, against the tag convention
this repo states. Which one is correct depends on what those later commits are,
and that is the user's call. Landing them before the prep PR, or re-cutting the
prep branch on top, are the two ways to make the question not arise.
On GitHub the lookup keys on the prep branch, not on a commit message. A human can reword the subject in the squash-merge dialog, while the branch name is set by Phase 1 and survives squash, rebase and subject rewrites. Off GitHub the subject is all git keeps of a squash. If it was reworded, the git lookup finds nothing and stops, which is the safe way to fail.
Confirm with the user before pushing the tag.
Phase 7: Push Tag
git push origin refs/tags/<tag>Spell it refs/tags/<tag>. That form can only push a tag, never a branch that happens to
share its name, and it is the form the user's permission rules allow.
In release_mode=workflow this is what starts the release: the push triggers release.yml, which builds the project and creates a GitHub release whose assets are whatever that workflow's files: block lists. Extract the actual list before producing the final output:
yq '.jobs.build.steps[] | select(.uses == "softprops/action-gh-release*") | .with.files' .github/workflows/release.ymlIf yq is unavailable, grep for the files: block and read the lines that follow.
In release_mode=gh the push does nothing on its own, and Phase 8 creates the release.
In release_mode=tag the pushed tag is the release.
Phase 8: Release Notes
In release_mode=tag, skip this phase. No release object exists to carry notes: the
CHANGELOG at the tag is the release notes, and Phase 3 already wrote and reviewed them.
Otherwise, extract this version's CHANGELOG section first. Both GitHub modes need it, and it fails the same way in both if Phase 3 went wrong:
NOTES="$(mktemp -t notes)"
~/.claude/skills/release-ship/scripts/extract-changelog.sh <version> > "$NOTES"It prints everything between ## <version> and the next ## heading, trimmed, and
exits 1 if that section does not exist. Avoid sed -n '/^## X/,/^## /p' — it prints the
next release's heading, and the version's dots act as regex wildcards.
In release_mode=workflow, wait for the workflow to finish, then edit the notes onto the release it created. The poll is bounded by a script that ships with this skill (no timeout(1) dependency — macOS BSD userland doesn't ship it):
~/.claude/skills/release-ship/scripts/wait-for-release.sh <tag>
gh release edit <tag> --notes-file "$NOTES"- It prints the run's conclusion (e.g.
success,failure) and exits 0 when the run finishes; exits 1 on timeout or if no run is found. - Optional args override the defaults:
wait-for-release.sh <TAG> [MAX_SECONDS] [INTERVAL_SECONDS](default600 15). - The tag argument is required. There is a gap between pushing the tag and the run
appearing in the API; without the tag filter the script polls whichever release run
is newest, which during that gap is the previous release — already
completed/success. It would printsuccessfor a release that never started.
If the printed conclusion is not success, report the failure and stop.
In release_mode=gh, create the release directly, attaching the plan's release_assets (none means a notes-only release):
git checkout --detach <tag> && <build_cmd>
gh release create <tag> --title "<version>" --notes-file "$NOTES" <release_assets>
git checkout <default branch>Build at the tag first when the plan names a build_cmd. A gitignored build output, such as an Obsidian plugin's main.js, is whatever the working tree last built, and that can be a different branch.
Add --prerelease when the plan says prerelease=1 (the version carries a -alpha,
-beta or -rc suffix). Without it gh marks the release "Latest", above the last stable
one. In workflow mode, check the release the workflow made, and gh release edit <tag>
--prerelease if it is not marked.
The assets come from the tagged checkout, so run this from the default branch at the tag — the same tree Phase 6 gated. For an Obsidian plugin they are what users install; a release without them is broken, not merely bare.
Write the notes from the CHANGELOG, never from memory of the session. Improvised release notes are how a public artifact acquires a sentence nobody checked.
Phase 9: Deploy
If the plan's deploy_cmd is empty, skip this phase.
Otherwise, deploy runs from the tagged commit, the tree Phase 6 gated. Confirm with the user, then:
git checkout --detach <tag>
bash -c '<deploy_cmd>'; echo "exit=$?"
git checkout <default-branch>A nonzero exit is reported with its output and not retried. The tag and release already exist, so a failed deploy is a release that has not reached the site yet, not one to undo. Whether to fix and redeploy the same tag or ship a patch is the user's call.
Phase 10: Verify
Confirm the release is live and report. In release_mode=tag:
git ls-remote --tags origin <tag>Otherwise:
gh release view <tag> --json tagName,name,body,assetsOutput
Release: <version>
====================
Profile: <profile>
Prep PR: #<num> (merged <sha>) (or: <prep_branch> merged <sha>, off GitHub)
Files bumped: <primary + derived>
Gate: exit 0 on <sha>
CHANGELOG: ## <version> added
Walkthrough: regenerated
Tag: <tag> → <sha>
Release: <url> created by release.yml | <url> created by gh | tag only
Assets: <release_assets, the workflow's files: list, or "none">
Release notes: from CHANGELOG.md
Deploy: <deploy_cmd> exit 0 on <tag> | not deployedDo not use when
- Pre-tag validation hasn't run — use
release-gatefirst - The repo has no version source —
release-plan.shrefuses, and the fix is a.release-gatefile, not a guessed version
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

