Release a project end to end: the version in every place it appears, the developer changelog and end-user notes, the README, docs, and landing page, then build, tag, push, GitHub release, and PyPI/npm publish.
Usage
/ship [X.Y.Z | major | minor | patch] [--auto | --no-auto]
- Auto mode (default) runs every step without asking. Each point where interactive mode asks has a fixed outcome: continue, skip with
PARTIAL, or stop withBLOCKED. - Interactive mode (
--no-auto) asks the user to confirm the version, the file changes, the build, the push, the GitHub release, and each publish. "Ask me before each step" selects it too. - Version argument:
X.Y.Zreleases that version;major|minor|patchsets the bump.
Read references/auto-mode.md before Step 1: outcome table, hard stops, version rules.
Scope. A mode removes confirmations; it never widens the request. Only /ship or an explicit request to ship, cut, tag, or publish a release runs Steps 6-9. "What changed?" or "draft release notes" runs Step 2 and the changelog-generator only, and changes no file. "Bump the version" or "update the docs/landing page for the release" runs Steps 1-5 and leaves the changes uncommitted.
Steps
- Pre-flight: mode, resume check, clean tree, branch, sync, release tool
- Determine version from the commits since the last tag, and check the registries
- Prepare changes: (subagents, two waves) version, changelog, docs, landing page
- Review: (subagent) mandatory in auto mode
- Apply and sweep: write the changes, prove no stale version remains
- Build
- Commit, tag, push
- GitHub release
- Publish to PyPI and/or npm
Repo Sync Before Edits (mandatory)
Before creating, updating, or deleting files, sync the current branch with the remote. If the working tree is not clean, run git stash push -u -m "pre-sync" first and git stash pop after:
branch="$(git rev-parse --abbrev-ref HEAD)"
git fetch origin && git pull --rebase origin "$branch"
If a rebase or stash conflict occurs, stop with BLOCKED — sync conflict and list the files under Decision (interactive mode: ask the user). If origin is missing, follow the origin missing row in references/auto-mode.md.
Step 1: Pre-flight (inline)
-
Print the mode, the version argument, and the scope.
-
Full release only: run
git status --porcelain. If it prints anything: auto mode stops withBLOCKED — uncommitted changesand lists the files; interactive mode asks whether to stash (pop after the final report), commit, or abort. Never discard work. -
Full release only: check that the current branch is the default branch:
git symbolic-ref --short refs/remotes/origin/HEADprintsorigin/<default>; if that ref is missing, read theHEAD branch:line ofgit remote show origin. If the branches differ, follow theNot on the default branchrow inreferences/auto-mode.md. -
Full release only: run
git describe --exact-match --tags HEAD. Resume that release only when no version argument was given, or an explicit version equals the tag's release version. A different version ormajor|minor|patchcontinues through the sync and normal version selection. For resume, followreferences/resume.md: skip version preparation, but rebuild and verify artifacts before any unfinished local upload. -
Run the Repo Sync above.
-
Check for a release tool or a monorepo:
ls -d .releaserc* .changeset .versionrc* lerna.json release.config.* pnpm-workspace.yaml 2>/dev/null grep -qs '^\[workspace\]' Cargo.toml && echo "cargo workspace" grep -E '"(release|semantic-release|release-it|@changesets/cli|standard-version|workspaces)"[[:space:]]*:' package.json 2>/dev/nullThe
package.jsongrep matches ascripts.releaseentry, a release-tool dependency, orworkspaces, never"version". On a match, follow theRelease tool detectedorMonoreporow inreferences/auto-mode.md.
Step 2: Determine Version (inline)
last_tag="$(git describe --tags --abbrev=0 2>/dev/null)"
range="${last_tag:+$last_tag..}HEAD"
git log "$range" --oneline --no-merges -n 300
git log "$range" --format=%B --no-merges | grep -E '^BREAKING[ -]CHANGE:'
OLD_VERSION is last_tag without its prefix; the new tag keeps that prefix (v when there are no tags). Choose NEW_VERSION by the first rule that applies:
- A version argument → use it.
last_tagexists and the log is empty →BLOCKED — nothing to release.- A row in
references/auto-mode.md→ Version rules matches → use its outcome. - Commits: MAJOR for a
BREAKING:or!:subject or aBREAKING CHANGE:footer, MINOR forfeat:, else PATCH.
Stop with BLOCKED before any file changes if the tag for NEW_VERSION exists locally or on origin, or if a registry the project publishes to already has NEW_VERSION (references/publishing.md → Decide per registry). Print "N features, M fixes, K breaking since vOLD → vNEW (rule: )". Interactive mode asks the user to confirm or override; auto mode continues.
Step 3: Prepare Changes (subagents, two waves)
Subagents read in isolated context so the main agent's context window stays small. Spawn inputs, file ownership, and the workspace: references/orchestration.md. Without the Agent tool, run each agent file inline in the same order.
- Wave 1, same turn:
version-bumperandchangelog-generator. - Wave 2, same turn, after wave 1 returns:
docs-updaterandlanding-page-updater, both readingversion-changes.jsonanduser-notes.md.
The landing-page-updater owns the files that render the landing site and reports four items, each updated, current, or not on page with evidence: version (stale older values too), changelog for end users (plain language, no hashes, PR numbers, or authors), feature list, and documentation. It never adds a section the page lacks. With no landing page it reports "No landing page — skipped."
Interactive mode shows the consolidated summary and waits for confirmation.
Step 4: Review (subagent)
Spawn release-reviewer. Auto mode always runs it; it replaces the human check. On NEEDS_FIX, apply each issue's suggestion to the workspace proposals and review once more. Still NEEDS_FIX → BLOCKED — release review failed, with no project file written. Without the Agent tool, run the reviewer's checklist inline and note under Uncertainty that the review was not independent.
Step 5: Apply and Sweep (inline)
Apply in the order in references/orchestration.md → Apply changes. Then, unless this is a first release, sweep for the old version:
git grep -n -I -F "$OLD_VERSION" -- . ':(exclude,glob)**/CHANGELOG*' ':(exclude,glob)**/CHANGES*' ':(exclude,glob)**/HISTORY*' ':(exclude,glob)**/NEWS*'
Run the same sweep for each drifted current value in version_sources. Use one class per hit: missed (the project's own version: fix it and re-run the sweep), historical, dependency, or fixture. The step passes when no hit is missed and every version_sources file reads NEW_VERSION; otherwise stop with BLOCKED — version not updated in <file>. Record the class counts under Evidence.
Step 6: Build (inline)
[ -f package.json ] && grep -q '"build"' package.json && echo "npm run build"
[ -f Makefile ] && grep -q '^build:' Makefile && echo "make build"
[ -f Cargo.toml ] && echo "cargo build --release"
[ -f pyproject.toml ] && grep -q '^\[build-system\]' pyproject.toml && echo "python -m build"
Interactive mode asks first; auto mode runs it. A failed build stops the run: BLOCKED — build failed. No build step → skip and say so.
Step 7: Commit, Tag, Push (inline)
Step 1 guaranteed a clean start, so every tracked change belongs to the release, including regenerated lockfiles:
git add -u
git add <each file Step 5 created, such as a new CHANGELOG.md>
git commit -m "chore(release): vX.Y.Z"
git tag -a vX.Y.Z -m "Release vX.Y.Z"
git status --porcelain --untracked-files=no # must print nothing
If a hook rejects the commit, stop: BLOCKED — commit hook failed; never use --no-verify. Interactive mode asks "Push <branch> and tag vX.Y.Z to origin?"; a decline ends with PARTIAL — tag vX.Y.Z created locally, not pushed. Auto mode pushes:
git push origin <branch> && git push origin vX.Y.Z
git ls-remote --tags origin "refs/tags/vX.Y.Z"
A rejected push stops with BLOCKED — push rejected; never force-push. If git ls-remote prints nothing, stop, skip Steps 8-9: BLOCKED — tag vX.Y.Z not on origin (recovery: references/final-report.md).
Step 8: GitHub Release (inline)
If gh auth status fails or the remote is not GitHub, skip with PARTIAL and put the command under Decision. If a workflow creates releases on tag push, verify with gh release view vX.Y.Z instead (references/publishing.md). Interactive mode asks first.
gh release create vX.Y.Z --title "vX.Y.Z" --notes-file "$WORKSPACE/changelog-generator/release-notes.md" --latest
A pre-release (a - suffix, or a PEP 440 a, b, rc, or .dev suffix) uses --prerelease instead of --latest; a version below the highest existing tag uses --latest=false. Append artifact paths if any exist.
Step 9: Publish to Package Registries (inline)
Follow references/publishing.md, using the per-registry decision made in Step 2. Auto mode publishes only where the package already exists for this repository, or to a registry the request names; a missing credential or an npm one-time-password prompt skips that registry with PARTIAL; a CI-owned publish is verified, not repeated.
Expected Output
Every run, including one that stops early, ends with: Result: (COMPLETE, PARTIAL — <reason>, or BLOCKED — <reason>), Mode:, Evidence: (checks that ran, plus each auto decision and its reason), Uncertainty: (not verified), and Decision: (pending user action, or "No approval needed"). Then the post-release checklist. Status rules, an example report for each status, and the checklist: references/final-report.md.
Edge Cases
- No remote: tag locally; skip the push, the GitHub release, and publishing:
PARTIAL — no remote. - Resume: HEAD carries the requested release tag after an earlier stop (no version argument, or the same explicit version). Step 1 sends the run to
references/resume.md, which recovers the notes and registry decisions, rebuilds for unfinished local uploads, and finishes only the steps not yet done. - Failure after the push: never roll back automatically, in either mode. Stop with
BLOCKEDand list the recovery commands (delete the tag locally and on origin, revert the commit) underDecision; the user confirms and runs them. A version published to PyPI or npm cannot be reused; the fix is a new version.
Acceptance Criteria
- The Step 1 report and the final report print the mode; in auto mode, every gate outcome matches
references/auto-mode.mdand appears underEvidence - In interactive mode, the user confirms the version, the file changes, the build, the push, the GitHub release, and each publish
- A narrower request (notes, bump, docs) never commits, tags, pushes, or publishes
- Auto mode releases only from the default branch, and never runs a project's own release script
- Every version source reads the new version and the sweep ends with no
missedhit - The history file has exactly one entry for the release; an existing
## Unreleasedsection is promoted, not duplicated - A landing page's report covers version, end-user changelog, feature list, and documentation, each with a status and evidence
- The tag is confirmed on origin by
git ls-remote; the GitHub release exists whenghis available - The final report passes the reader checks in
references/final-report.md: result findable first, facts separated from assumptions, claims traceable to evidence, next decision named. Without reviewer feedback, human understanding stays unconfirmed
Step Completion Reports
After each step, print a status report (√ pass, × fail, a Criteria line, Result: PASS | FAIL | PARTIAL). Templates: references/step-reports.md.
Reference files
references/auto-mode.md: gate outcomes in both modes, hard stops, version rulesreferences/orchestration.md: workspace, two-wave spawn inputs, file ownership, apply orderreferences/step-reports.md: step report templatesreferences/publishing.md: per-registry decision, CI-owned publishing, PyPI / npm commandsreferences/resume.md: finishing a release that stopped after its tag was createdreferences/final-report.md: status rules, examples, reader checks, post-release checklistagents/:version-bumper,changelog-generator,docs-updater,landing-page-updater,release-reviewer