--- name: release description: Autonomously cut a Rove (`@sma1lboy/rove`) release end-to-end — detect the semver bump from pending changesets (flagging an upstream `minor` you didn't intend), run the release gates, dispatch the Changesets workflow (or run `scripts/release.sh` locally), then poll the GitHub Actions Release workflow with `gh` until npm publish completes, diagnosing CI failures (npm token, registry 404, lint, branch mismatch) instead of leaving them silent. Use when the user says "cut a release", "ship a version", "release Rove", "release rove", "发版", "release.sh", or "bump the version". Never force-pushes; always verifies the release landed on `main`. metadata: internal: true --- # Release Rove Autonomous release driver for `@sma1lboy/rove`. This is the supervised loop the manual flow in [`docs/RELEASING.md`](../../../docs/RELEASING.md) describes — read that doc once if anything here is ambiguous; it is the source of truth and this skill must never contradict it. > **Releases are on demand.** Merging a PR only banks its changeset; > nothing ships until someone runs `.github/workflows/changesets.yml` > (CI-green wait → version + commit → tag → dispatch `release.yml`). That > workflow is the default path: do Step 1 against `origin/main` (the workflow > releases whatever is banked there), then `gh workflow run changesets.yml > --ref main`, find the run with `gh run list --workflow=changesets.yml > --limit 1`, watch it with `gh run watch --exit-status`, and go to > Step 4. Use the local `scripts/release.sh` flow (Steps 0–3) only when > Actions is unavailable or the user asks for it, and never while a > changesets.yml run is in flight on the same version (they'd race to tag it). The job is: **detect the bump → gate → bump/tag/push → watch CI → confirm published, or stop with a precise report.** Do the whole chain without hand-holding, but stop and surface (never guess) at the two human-judgment gates marked **⚠ ASK** below. ## Hard rules (non-negotiable) - **Never force-push.** No `git push -f`, no `--force-with-lease`, no `git reset --hard` on a shared branch, no retag-over-existing. If a tag or push conflicts, stop and report — recovery is the user's call. - **Bump default is `patch`.** Per AGENTS.md: pre-1.0 Rove ships features as patches. A `minor`/`major` only happens when the user *explicitly* said so this turn, OR a pending changeset already carries that bump — and the second case is exactly the trap to flag (see Step 1). - **Release lands on `main` only.** Verify branch before *and* after. A release on a stray feature branch is the #1 historical failure — catch it early. - **No `--no-verify`, no skipping hooks.** If a gate fails, fix the cause or stop. - The release commit is `chore: release — X.Y.Z`. No AI/Claude attribution anywhere (commit, tag, GitHub release body). ## Step 0 — Preflight Confirm the working tree is sane and you're where you think you are: ```bash git rev-parse --abbrev-ref HEAD # MUST be main (see Step 3) git status --porcelain # working tree must be clean git fetch origin && git log --oneline origin/main..HEAD # any unpushed commits? git log --oneline HEAD..origin/main # are we behind? if so, surface — don't auto-merge gh auth status # gh must be authed for CI polling ``` `scripts/release.sh` itself refuses a dirty tree (except the files it rewrites), but do this first so you fail fast with a clear message instead of mid-script. If `origin/main` is ahead of HEAD, **stop and surface** — Rove main moves fast (often several releases/day); releasing from a stale base is how versions collide. Let the user decide whether to pull/rebase. ## Step 1 — Detect the bump (and flag the surprise minor) ⚠ ASK The bump is **not** chosen by you — it's the max of the pending `.changeset/*.md` bump types, computed by `changeset version`. Inspect before consuming: ```bash bun run changeset:status # shows pending changesets + resulting bump ls .changeset/*.md | grep -v README.md # raw list # read each one — the first line frontmatter is the bump type: # --- # "@sma1lboy/rove": minor ← THIS is the bump that file forces # --- ``` Then decide: - **No pending changesets** → nothing to release. `release.sh` will abort. Tell the user and offer to draft one (the `changelog-generator` skill does this). - **All pending are `patch`** → proceed silently; this is the normal case. - **Any pending is `minor` or `major`** → **⚠ STOP AND ASK.** This is the documented annoyance: an upstream/peer changeset silently promotes the release to a minor the user didn't intend. Quote the offending file + its bump line and confirm: *"`.changeset/foo.md` carries a `minor` — the release will be X.(Y+1).0, not a patch. Intended?"* Only continue on an explicit yes. Do **not** edit someone's changeset bump without permission. Record the predicted next version (current `packages/rove/package.json` version applied with the detected bump) so you can verify it later. ## Step 2 — Run the gates locally (abort on failure) `scripts/release.sh` enforces `lint && typecheck && (cd packages/rove && bun run test)` itself before touching version/CHANGELOG, and `release.yml` re-runs lint + typecheck + test + build + the behavior suite before `npm publish`. Running the same set here first just fails fast, before burning a `changeset version` cycle: ```bash bun run lint bun run typecheck bun run test # fast Vitest + unix-socket daemon/bridge suite bun run build cd packages/rove && bun run perf:golden # golden perf doctor (~90s incl. binary compile smoke; docs/HARNESS.md §Performance contracts) ``` `perf:golden` ceilings are 2-3× the reference numbers, so a FAIL means a real structural regression (startup, PTY spawn/wake, per-tab memory, park reclaim) — treat it like a red test, not jitter; rerun once to confirm before digging. `perf:golden` is not part of the enforced `release.sh`/`release.yml` gate (opt-in, local/pre-release only per docs/HARNESS.md), so run it manually here. `bun run test:behavior` exercises the built CLI against an isolated daemon and standalone PTY Host with a fake `claude` shim; cases that drive the outer terminal also need native `node-pty` support. `release.yml` runs the same black-box suite before `npm publish`. Running it locally first is optional but catches a failure before the tag push. If a gate fails: report the exact failing command + output, fix it if it's an obvious in-scope issue (and re-run the full set), or stop. Never proceed to tag a red tree. ## Step 3 — Verify branch, then bump/tag/push ```bash git rev-parse --abbrev-ref HEAD # MUST print: main ``` If not on `main`, **stop** — do not `checkout`/`merge` to "fix" it autonomously (concurrent sessions + branch juggling is the documented git-tangle failure). Surface the actual branch and ask. On `main` with gates green, run the release script. It is the single source of the bump→version→CHANGELOG→commit→tag→push sequence — don't reimplement those steps by hand: ```bash scripts/release.sh ``` What it does (don't fight it): gate (`lint` → `typecheck` → `test` → `build` → `behavior`) → `changeset version` → `bun install` + `--frozen-lockfile` → `lint:fix` on the regenerated JSON → commits `chore: release — X.Y.Z` (**no tag yet**) → **prompts**, pushes the release commit to `main`, **waits for that commit's `ci.yml` run to go green** (the Linux/macOS gates the local macOS run can't prove — v0.8.66 died exactly there), and only then tags `vX.Y.Z` and pushes the tag. - Confirm the printed `CURRENT → NEW (vX.Y.Z)` matches your Step 1 prediction. A mismatch means a changeset changed under you — stop and re-inspect. - The script asks `Push now? [y/N]`. Answer `y` only after the version line checks out. If the user wanted a dry run / review-before-push, answer `N` — re-running the script later resumes (push → wait CI → tag). - If the CI wait comes back RED, no tag exists and the version is NOT burned: land the fix on `main` (no new changeset) and re-run `scripts/release.sh` — with zero pending changesets and an untagged committed version it enters resume mode and tags the same version at the fixed HEAD. The push of tag `vX.Y.Z` is what triggers `.github/workflows/release.yml`. ## Step 4 — Poll CI until publish completes The tag push starts the **Release** workflow (`publish` job: gates → npm publish → GitHub release). npm is the sole distribution channel — standalone binaries were dropped 2026-08-02, so an empty release-assets list is normal. Watch the run to terminal state — don't declare success on push alone: ```bash gh run list --workflow=release.yml --limit 5 # find the run for this tag gh run watch --exit-status # blocks until done; nonzero on failure # or poll: gh run view --json status,conclusion,jobs ``` On success, verify the packages actually landed (don't trust the green check alone): ```bash npm view @sma1lboy/rove@ version # the published package; must echo the new version # Every Rove release checks the SDK's current version and publishes it if missing. npm view @sma1lboy/rove-plugin-sdk@ version gh release view v --json name -q .name # GitHub release exists ``` Confirm: `@sma1lboy/rove` and both SDK names report their expected versions, the Rove version matches the tag and `packages/rove/package.json`, and the release landed on `main` (`git log --oneline -1 origin/main` is the `chore: release` commit). Then report done with the version, the npm dist-tag it went to (`latest` for plain semver), and the release URL. ## Step 5 — Diagnose CI failure (auto-fix or stop precisely) If the run fails, identify the job + step before doing anything: ```bash gh run view --log-failed ``` Map the failure to a cause and act. **Never** retry blindly or force-push. | Symptom in the log | Likely cause | Action | |---|---|---| | `npm publish` → `401`/`403`, `ENEEDAUTH`, `EOTP` | `NPM_TOKEN` secret missing/expired/wrong scope | Code is fine and the tag is published-or-not — **stop and report**. Token rotation is the user's job (Settings → secrets → `NPM_TOKEN`, automation token with `@sma1lboy` publish rights). After they fix it, a re-publish needs a *new* version (npm won't overwrite) — never retag the same version. | | `npm publish` → `404` on registry / scope | registry URL or scope access wrong | Report; check `.npmrc` auth line + `access: public`. Don't mutate published state. | | `Verify tag matches package.json` step fails | tag ≠ `package.json` version (retag drift) | Means the tag and the committed version disagree — surface it; do **not** force-retag. The fix is to bump+commit then tag fresh, which is the user's call. | | Typecheck / test / build red | real regression that local gates somehow missed | Reproduce locally (`bun run typecheck|test|build`), fix in-scope, and note that the tag already pushed — a fix needs a **new** patch release, not a force-push over the tag. | | `npm publish` → `E409`/`cannot publish over` | version already on npm | The version is already out — likely a double-run. Stop; the next release is a new version. | | A sibling job (`behavior`/`render-track`/`visual-ground-truth`) fails but `publish` succeeded | flake in a non-blocking rerun | npm already has the package; report it. `gh run rerun --failed` is safe for those jobs; re-running `publish` is NOT — it'll hit E409. | The principle: anything that *changes published artifacts or rewrites history* (retag, force-push, republish) is **stop-and-report**, not auto-fix. Anything local and idempotent (re-run a flaky binary matrix, fix a lint/type error for the *next* release) you may do. ## Prerelease note For `vX.Y.Z-.N` tags (e.g. `v0.7.0-experimental.0`), the workflow publishes to the npm dist-tag named after the identifier (`experimental`), so `latest` stays stable. These come from Changesets prerelease mode (`changeset pre enter ` … `changeset pre exit`), not `release.sh`. If the user asks for a prerelease, follow RELEASING.md's prerelease section rather than this default flow.