Step-by-step release checklist for Squad — prevents v0.8.22-style disasters
This is the definitive release runbook for Squad. Born from the v0.8.22 release disaster (4-part semver mangled by npm, draft release never triggered publish, wrong NPM_TOKEN type, 6+ hours of broken latest dist-tag).
Rule: No agent releases Squad without following this checklist. No exceptions. No improvisation.
Before starting ANY release work, validate the following:
Rule: Only 3-part semver (major.minor.patch) or prerelease (major.minor.patch-tag.N) are valid. 4-part versions (0.8.21.4) are NOT valid semver and npm will mangle them.
# Check version is valid semver
node -p "require('semver').valid('0.8.22')"
# Output: '0.8.22' = valid
# Output: null = INVALID, STOP
# For prerelease versions
node -p "require('semver').valid('0.8.23-preview.1')"
# Output: '0.8.23-preview.1' = valid
If semver.valid() returns null: STOP. Fix the version. Do NOT proceed.
Rule: NPM_TOKEN must be an Automation token (no 2FA required). User tokens with 2FA will fail in CI with EOTP errors.
# Check token type (requires npm CLI authenticated)
npm token list
Look for:
read-write tokens with NO 2FA requirement = Automation token (correct)How to create an Automation token:
NPM_TOKENIf using a User token: STOP. Create an Automation token first.
Rule: Release from main branch. Ensure clean state, no uncommitted changes, latest from origin.
# Ensure on main and clean
git checkout main
git pull origin main
git status # Should show: "nothing to commit, working tree clean"
# Check tag doesn't already exist
git tag -l "v0.8.22"
# Output should be EMPTY. If tag exists, release already done or collision.
If tag exists: STOP. Either release was already done, or there's a collision. Investigate before proceeding.
Rule: bump-build.mjs is for dev builds ONLY. It must NOT run during release builds (it increments build numbers, creating 4-part versions).
# Set env var to skip bump-build.mjs
export SKIP_BUILD_BUMP=1
# Verify it's set
echo $SKIP_BUILD_BUMP
# Output: 1
For Windows PowerShell:
$env:SKIP_BUILD_BUMP = "1"
If not set: bump-build.mjs will run and mutate versions. This causes disasters (see v0.8.22).
Update version in all 3 package.json files (root + both workspaces) in lockstep.
# Set target version (no 'v' prefix)
VERSION="0.8.22"
# Validate it's valid semver BEFORE proceeding
node -p "require('semver').valid('$VERSION')"
# Must output the version string, NOT null
# Update all 3 package.json files
npm version $VERSION --workspaces --include-workspace-root --no-git-tag-version
# Verify all 3 match
grep '"version"' package.json packages/squad-sdk/package.json packages/squad-cli/package.json
# All 3 should show: "version": "0.8.22"
Checkpoint: All 3 package.json files have identical versions. Run semver.valid() one more time to be sure.
# Commit version bump
git add package.json packages/squad-sdk/package.json packages/squad-cli/package.json
git commit -m "chore: bump version to $VERSION
Co-authored-by: Copilot <[email protected]>"
# Create tag (with 'v' prefix)
git tag -a "v$VERSION" -m "Release v$VERSION"
# Push commit and tag
git push origin main
git push origin "v$VERSION"
Checkpoint: Tag created and pushed. Verify with git tag -l "v$VERSION".
CRITICAL: Release must be published, NOT draft. Draft releases don't trigger publish.yml workflow.
# Create GitHub Release (NOT draft)
gh release create "v$VERSION" \
--title "v$VERSION" \
--notes "Release notes go here" \
--latest
# Verify release is PUBLISHED (not draft)
gh release view "v$VERSION"
# Output should NOT contain "(draft)"
If output contains (draft): STOP. Delete the release and recreate without --draft flag.
# If you accidentally created a draft, fix it:
gh release edit "v$VERSION" --draft=false
Checkpoint: Release is published (NOT draft). The release: published event fired and triggered publish.yml.
The publish.yml workflow should start automatically within 10 seconds of release creation.
# Watch workflow runs
gh run list --workflow=publish.yml --limit 1
# Get detailed status
gh run view --log
Expected flow:
publish-sdk job runs → publishes @bradygaster/squad-sdkpublish-cli job runs → publishes @bradygaster/squad-cliIf workflow fails: Check the logs. Common issues:
Checkpoint: Both jobs succeeded. Workflow shows green checkmarks.
Manually verify both packages are on npm with correct latest dist-tag.
# Check SDK
npm view @bradygaster/squad-sdk version
# Output: 0.8.22
npm dist-tag ls @bradygaster/squad-sdk
# Output should show: latest: 0.8.22
# Check CLI
npm view @bradygaster/squad-cli version
# Output: 0.8.22
npm dist-tag ls @bradygaster/squad-cli
# Output should show: latest: 0.8.22
If versions don't match: Something went wrong. Check workflow logs. DO NOT proceed with GitHub Release announcement until npm is correct.
Checkpoint: Both packages show correct version. latest dist-tags point to the new version.
Verify packages can be installed from npm (real-world smoke test).
# Create temp directory
mkdir /tmp/squad-release-test && cd /tmp/squad-release-test
# Test SDK installation
npm init -y
npm install @bradygaster/squad-sdk
node -p "require('@bradygaster/squad-sdk/package.json').version"
# Output: 0.8.22
# Test CLI installation
npm install -g @bradygaster/squad-cli
squad --version
# Output: 0.8.22
# Cleanup
cd -
rm -rf /tmp/squad-release-test
If installation fails: npm registry issue or package metadata corruption. DO NOT announce release until this works.
Checkpoint: Both packages install cleanly. Versions match.
After main release, sync dev to the next preview version.
# Checkout dev
git checkout dev
git pull origin dev
# Bump to next preview version (e.g., 0.8.23-preview.1)
NEXT_VERSION="0.8.23-preview.1"
# Validate semver
node -p "require('semver').valid('$NEXT_VERSION')"
# Must output the version string, NOT null
# Update all 3 package.json files
npm version $NEXT_VERSION --workspaces --include-workspace-root --no-git-tag-version
# Commit
git add package.json packages/squad-sdk/package.json packages/squad-cli/package.json
git commit -m "chore: bump dev to $NEXT_VERSION
Co-authored-by: Copilot <[email protected]>"
# Push
git push origin dev
Checkpoint: dev branch now shows next preview version. Future dev builds will publish to @preview dist-tag.
If publish.yml workflow fails or needs to be bypassed, use workflow_dispatch to manually trigger publish.
# Trigger manual publish
gh workflow run publish.yml -f version="0.8.22"
# Monitor the run
gh run watch
Rule: Only use this if automated publish failed. Always investigate why automation failed and fix it for next release.
If a release is broken and needs to be rolled back:
WARNING: npm unpublish is time-limited (24 hours) and leaves the version slot burned. Only use if version is critically broken.
# Unpublish (requires npm owner privileges)
npm unpublish @bradygaster/[email protected]
npm unpublish @bradygaster/[email protected]
Preferred approach: Mark version as deprecated, publish a hotfix.
# Deprecate broken version
npm deprecate @bradygaster/[email protected] "Broken release, use 0.8.22.1 instead"
npm deprecate @bradygaster/[email protected] "Broken release, use 0.8.22.1 instead"
# Publish hotfix version
# (Follow this runbook with version 0.8.22.1)
# Delete GitHub Release
gh release delete "v0.8.22" --yes
# Delete tag locally and remotely
git tag -d "v0.8.22"
git push origin --delete "v0.8.22"
# Revert version bump commit
git checkout main
git revert HEAD
git push origin main
Checkpoint: Tag and release deleted. main branch reverted. npm packages deprecated or unpublished.
Symptom: Workflow fails with EOTP error.
Root cause: NPM_TOKEN is a User token with 2FA enabled. CI can't provide OTP.
Fix: Replace NPM_TOKEN with an Automation token (no 2FA). See "NPM_TOKEN Verification" above.
Symptom: Verify step fails with 404 even though publish succeeded.
Root cause: npm registry propagation delay (5-30 seconds).
Fix: Verify step now has retry loop (5 attempts, 15s interval). Should auto-resolve. If not, wait 2 minutes and re-run workflow.
Symptom: Verify step fails with "Package version (X) does not match target version (Y)".
Root cause: package.json version doesn't match the tag version.
Fix: Ensure all 3 package.json files were updated in Step 1. Re-run npm version if needed.
Symptom: Published version on npm doesn't match package.json (e.g., 0.8.21.4 became 0.8.2-1.4).
Root cause: 4-part versions are NOT valid semver. npm's parser misinterprets them.
Fix: NEVER use 4-part versions. Only 3-part (0.8.22) or prerelease (0.8.23-preview.1). Run semver.valid() before ANY commit.
Symptom: Release created but publish.yml never ran.
Root cause: Release was created as a draft. Draft releases don't emit release: published event.
Fix: Edit release and change to published: gh release edit "v$VERSION" --draft=false. Workflow should trigger immediately.
Before starting ANY release, confirm:
node -p "require('semver').valid('VERSION')" returns the version string (NOT null)npm token list shows read-write without OTP requirementgit status shows "nothing to commit, working tree clean"git tag -l "vVERSION" returns emptySKIP_BUILD_BUMP=1 is set: echo $SKIP_BUILD_BUMP returns 1Before creating GitHub Release:
grep '"version"' package.json packages/*/package.jsongit log origin/main..main returns emptygit ls-remote --tags origin vVERSION returns the tag SHAAfter GitHub Release:
gh release view "vVERSION" output doesn't contain "(draft)"gh run list --workflow=publish.yml --limit 1 shows "in_progress"After workflow completes:
npm view @bradygaster/squad-sdk version returns correct versionnpm view @bradygaster/squad-cli version returns correct versionlatest tags correct: npm dist-tag ls @bradygaster/squad-sdk shows latest: VERSIONnpm install @bradygaster/squad-cli succeedsAfter dev sync:
git show dev:package.json | grep version shows next previewThis skill was created after the v0.8.22 release disaster. Full retrospective: .squad/decisions/inbox/keaton-v0822-retrospective.md
Key learnings:
Never again.