Autoship
Contract
| Field | Bound contract |
|---|---|
| Trigger | release this, publish this package, autoship, cut a release, npm release flow |
| Authority | Human-only. Invoking autoship is standing consent for the full release flow; gate risky steps on objective preconditions instead of pausing mid-flow. Force-push, history rewrites, and destructive git operations require explicit confirmation. |
| Side effect | Writes a changeset file, runs quality-gate fixers, commits and pushes, merges the bot-opened Version Packages PR, and publishes to npm. Restricted to VCS-tracked targets and the npm registry. |
| Done | New version is published on npm and confirmed by npm view <package> version matching the merged package.json. |
Inputs
- A changesets-based npm package with a release workflow in
.github/workflows/usingchangesets/action. - Optional bump level: defaults to
patch;minorormajoronly on explicit user instruction. - No new changeset is required if one already exists on the default branch and the intent is to watch or merge only.
Procedure
The release workflow runs twice. First, push a commit carrying a pending .changeset/*.md file. The workflow runs changeset version in CI and opens a "Version Packages" PR on branch changeset-release/main. Merge that PR; the workflow runs again and, with no pending changesets left, runs changeset publish. The local job ends at pushing the changeset file. CI owns versioning and publishing; anything versioned locally breaks the loop.
Create the changeset. Check for pending changesets (
ls .changeset/*.mdexcludingREADME.md). If any exist, ask whether to add another or skip. Write the file directly becausenpm run changesetneeds a TTY:cat > .changeset/<short-id>.md << 'EOF' --- "<package-name>": patch --- <one or two sentences of user-facing change> EOFGenerate the short id with
node -e "console.log(Math.random().toString(36).slice(2,10))". The description ships verbatim inCHANGELOG.mdand is the only thing consumers read about this version. Done when: the changeset file is written with a valid package bump and user-facing description.Fix quality gates. Discover scripts from
package.json(jq '.scripts | keys[]' -r); in non-npm repos checkMakefile,Cargo.toml,pyproject.toml,go.mod. Run gates in order: lint, typecheck, test, format. Scope auto-fixers (lint --fix,format) to changed files where supported; after any fixer checkgit statusandgit restoreunrelated churn (MDX is a frequent casualty). On failure, parse output for file, line, code, and message; fix syntax errors first, then type errors, then lint errors, one root cause at a time when errors cascade. Re-run every gate from the start after any code change, not just the failed one, because a type fix routinely breaks lint and a lint autofix can break a test. Retry each gate up to 5 fix iterations, reporting the gate, remaining error count, and last error output on exhaustion. Done when: every gate passes or the retry budget is exhausted with the failure reported.Commit and push the changeset. Stage the changeset file and in-scope fixes only with explicit paths — never
git add -A, which silently commits pre-commit-hook artifacts such as a rootschema.gql. Sweepgit status --porcelainfor stray generated artifacts and unrelated fixer churn before committing. Commitchore: add <type> changeset for <package>and push. Do NOT runnpx changeset versionlocally: it consumes the.changeset/*.mdfile, the pushed commit loses its pending changeset, the "Changeset Status" check fails, and no Version Packages PR opens. Do NOT runnpm publishdirectly or hand-editCHANGELOG.mdorpackage.jsonversion; CI generates both in the Version Packages PR and local edits create conflicts that make the bot PR unmergeable. Done when: the changeset commit is pushed and no local version or publish command ran.Monitor CI and merge the Version Packages PR. Start a background watch scoped to the pushed commit SHA (not branch, so older pushes do not bleed in), emitting state changes and a
TERMINAL:line when every workflow run completes. Do not stop on an idle first poll; runs take time to queue. Keepsleepat 30s or more to avoid burning the GitHub API rate limit:SHA=$(git rev-parse HEAD); LAST="" while true; do CUR=$(gh run list --commit "$SHA" --limit 20 \ --json status,conclusion,workflowName,databaseId \ --jq 'map("\(.databaseId)|\(.workflowName)|\(.status)|\(.conclusion // \"\")")|.[]') [ "$CUR" != "$LAST" ] && { echo "---"; echo "$CUR"; LAST="$CUR"; } [ -z "$CUR" ] && { sleep 30; continue; } echo "$CUR" | grep -qv '|completed|' && { sleep 30; continue; } echo "$CUR" | grep -qv '|success$' && echo "TERMINAL: failure" || echo "TERMINAL: success" exit 0 doneAny non-
successconclusion (failure,cancelled,timed_out,action_required,neutral,skipped,stale) is a failure. On failure, classify viagh run view <id> --log-failed: flaky or infrastructure failures getgh run rerun <id> --failedup to 3 times; real failures get a fix, commit, push, and a fresh watch. Once green, find the open PR titled "Version Packages" on branchchangeset-release/main(gh pr list --search "Version Packages" --state open --json number,headRefName; the valid JSON field isheadRefName, notheadBranch). If absent, watch for it capped at 10 minutes. Verify every merge precondition before merging: the PR title is exactly "Version Packages" or the head branch ischangeset-release/main— never merge any other PR. Confirm all required checks pass. Merge the PR. Done when: the Version Packages PR is merged with all preconditions verified.Watch the publish run and verify on npm. Merging triggers the same workflow again, which publishes. Identify the workflow file in
.github/workflows/(commonlyrelease.yml,npm-publish.yml,publish.yml) and watch its latest run on the default branch for aTERMINAL:line. On failure, report with logs and stop — never auto-retry; publish failures are real (npm auth, registry, OIDC/provenance, tag conflict). On success, verifynpm view <package> versionagainst the mergedpackage.json, stop all background watches, and report the version. Thenpm viewoutput is the completion evidence; never report success without it. Done when:npm view <package> versionmatches the mergedpackage.jsonand all watches are stopped.
Failure and recovery
- Quality gate still failing after 5 iterations: stop, report the gate, remaining error count, and last error output.
- CI fails after the changeset push: classify flaky or infrastructure (rerun
--failed, max 3) versus real (fix, push, fresh watch). - "Changeset Status" check fails: missing changeset → add one (Step 1); consumed state from a local
changeset version→ revert the version bump andCHANGELOG.mdedit, re-add the changeset file, and force-push. Rerunning CI cannot fix consumed state. - Version Packages PR absent after 10 minutes: check that pending changesets exist on the default branch, the workflow file exists, and the action ran (
gh run list). - Merge precondition fails: stop and report; never override failing checks or resolve conflicts in the bot PR.
- Publish run fails: never auto-retry; report with logs.
- Partial-result rule: a failure at any step leaves the release half-shipped (changeset pushed, PR open, or publish pending). Report the exact state and the next human action rather than widening scope or pretending the done predicate holds. Rollback is version control: revert the changeset commit or close the Version Packages PR; never rewrite published registry state.
Output
The published version string from npm view <package> version confirmed against the merged package.json, plus a one-line release summary — on any blocked or non-converged state, a report naming the failed step, remaining error count or failing check, and next human action.