| name | release-preparations |
| description | Prepare project releases: run test suites, fix broken/obsolete tests, bump versions, build cross-platform binaries via GitHub Actions CI or dsr/rch fallback, create GitHub releases with checksums, verify installers. Use when: release, cut release, prepare release, version bump, pre-release, ship it, tag release, gh actions release, dsr release. |
Release Preparations
Core Principle: Never release broken code. The test gate is mandatory and non-negotiable. Fix tests first, release second.
Session-Mined Gotchas (Read These First)
These are the things that actually go wrong — extracted from 12+ real release sessions across 7 projects. Each "OP-N" links to a full playbook in OPERATOR-PATTERNS.md.
| Gotcha | One-Line Fix | OP |
|---|
| Bot-created tags don't trigger dist.yml | gh workflow run dist.yml -f ref=vX.Y.Z | 2 |
| Clippy passes local, fails CI (nightly drift) | Run clippy with -D warnings BEFORE tagging | 3 |
RCH hooks intercept cargo build --release | RCH_DISABLED=1 cargo build --release | 1 |
| Remote build host out of disk space mid-build | ssh mac-host 'df -h /' BEFORE building | 5 |
| Local path deps break CI | Use Path B (local build) or dsr | 6 |
| release-automation race (main vs master) | Harmless — just verify tag was created | 7 |
| Tag protection blocks force-push after fix | Accept offset or create vX.Y.(Z+1) | 11 |
/tmp full breaks git operations | TMPDIR=/data/tmp git commit ... | 12 |
Installer expects musl, release has gnu | Audit install.sh target triples vs assets | 15 |
ldd false-positive on static binaries | Use file binary | grep "statically linked" | 4 |
| Path deps missing version for crates.io | Add version = "X.Y.Z" alongside path | 8 |
| ort/ONNX doesn't support musl | Feature-gate or use gnu targets | 16 |
Quick Start
cat AGENTS.md README.md 2>/dev/null | head -100
git log --oneline $(git describe --tags --abbrev=0 2>/dev/null || echo HEAD~20)..HEAD | head -30
grep '^version' Cargo.toml 2>/dev/null || jq .version package.json 2>/dev/null
cargo test --workspace 2>&1 | tail -5
npm test 2>&1 | tail -10
pytest -x 2>&1 | tail -10
Phase 1: Pre-Flight
Read Project Context
Always start by reading AGENTS.md. Every project has release conventions — branch naming, tag formats, CI workflows, master/main sync requirements.
cat AGENTS.md
Assess Release Scope
grep '^version' Cargo.toml | head -1
gh release list --limit 3
git tag --sort=-v:refname | head -5
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null)
git rev-list --count ${LAST_TAG}..HEAD 2>/dev/null || echo "No tags yet"
git log --oneline ${LAST_TAG}..HEAD 2>/dev/null | head -20
git status --short
git diff --stat
Determine Version
- Patch (x.y.Z): bug fixes only, no new features
- Minor (x.Y.0): new features, no breaking changes
- Major (X.0.0): breaking changes (rare pre-1.0)
- Pre-1.0 projects: use minor for features, patch for fixes
When in doubt, ask the user what version to use.
Phase 2: Test Gate (MANDATORY)
This is the most important phase. Do NOT skip it. Do NOT proceed to version bump until all tests pass (or all failures are accounted for).
Run the Full Test Suite
cargo test --workspace 2>&1 | tee /tmp/test-output.txt
echo "Exit code: $?"
ls scripts/e2e_test.sh tests/e2e/ 2>/dev/null && echo "E2E tests exist"
cargo clippy --workspace --all-targets 2>&1 | tail -20
Triage Test Failures
When tests fail, categorize each failure:
| Category | What It Means | Action |
|---|
| Obsolete assertion | Code changed, test wasn't updated | Update the test to match current behavior |
| Missing struct fields | New fields added, test constructors incomplete | Add missing fields with defaults |
| Changed API signature | Function signature evolved | Update test call sites |
| Real bug | Test caught actual broken code | Fix the code, not the test |
| Flaky / timing | Race condition or timeout | Fix the flake or mark #[ignore] with comment |
| Pre-existing failure | Already broken on HEAD before your changes | Fix if quick, document if not |
Fix Obsolete Tests
The most common pattern — struct fields were added but test constructors weren't updated:
IndexingProgressSnapshot {
total_files: 100,
indexed_files: 50,
}
IndexingProgressSnapshot {
total_files: 100,
indexed_files: 50,
embedding_retries: 0,
embedding_failures: 0,
semantic_deferred_files: 0,
embedder_degraded: false,
degradation_reason: None,
recent_warnings: vec![],
}
Fix Real Bugs
If a test reveals an actual bug in the code:
- Understand the test's intent (read the test name and assertions)
- Read the code under test
- Fix the code to satisfy the test's contract
- Verify the fix doesn't break other tests
- Run the full suite again
Re-run Until Green
cargo test --workspace
cargo test --workspace -- test_name_pattern
cargo test --workspace 2>&1 | tail -3
Document Pre-Existing Failures
If some tests were already broken before your session and are unrelated to the release:
cargo test --workspace -- --skip known_broken_test
Note these for the user but don't let them block the release if they're clearly pre-existing.
Full test-fixing deep dive: TEST-FIXING.md
Phase 3: Version Bump
Rust Workspace Projects
grep -A5 '\[workspace\]' Cargo.toml
for toml in $(find . -name Cargo.toml -not -path './target/*'); do
grep '^version = ' "$toml"
done
cargo check --workspace
Critical: In workspace projects, bump ALL member crate versions to stay in sync. Check for version.workspace = true (inherits from root) vs explicit versions.
Workspace Version Inheritance
[workspace.package]
version = "0.2.0"
[package]
version.workspace = true
If using workspace inheritance, you only need to bump the root. Otherwise, bump each member.
Node.js Projects
npm version patch
Python Projects
grep version pyproject.toml
Phase 4: Build & Release
There are two paths. Choose one based on the project's infrastructure.
Path A: GitHub Actions (Default — Most Projects)
Most projects have CI workflows that build cross-platform binaries and create GitHub releases automatically when a tag is pushed. This is the preferred path.
Step 1: Verify CI Workflows Exist
ls .github/workflows/*release* .github/workflows/*dist* 2>/dev/null
grep -l 'tags:' .github/workflows/*.yml 2>/dev/null
grep -l 'workflow_dispatch' .github/workflows/*.yml 2>/dev/null
Common patterns (use /gh-actions for deeper reference):
- Tag-triggered:
dist.yml triggers on v* tag push → builds + uploads assets
- Version-bump detection:
release-automation.yml watches for version changes in Cargo.toml → creates tag → triggers dist
- Manual dispatch:
workflow_dispatch input for version
Step 2: Commit Version Bump and Push
git add Cargo.toml Cargo.lock */Cargo.toml
git commit -m "$(cat <<'EOF'
chore: bump version to X.Y.Z for release
Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"
git push
Step 3: Sync Branches (if AGENTS.md requires it)
git push origin main:master
Step 4: Create and Push Tag
VERSION="vX.Y.Z"
git tag -a "$VERSION" -m "Release $VERSION"
git push --tags
If the project uses release-automation.yml that auto-creates tags from version bumps, skip this — but verify the tag appeared:
git ls-remote --tags origin | grep "$VERSION"
Gotcha: If both main and master trigger release-automation, one run may fail with "tag already exists" — this is harmless. See OP-7.
Step 5: Monitor CI Build
gh run list --limit 5
gh run list --workflow dist.yml --limit 3
gh run watch <run-id>
gh run view <run-id> --json jobs | jq '.jobs[] | {name, status, conclusion}'
Gotcha: GitHub Actions bot-created tags don't trigger downstream workflows (anti-recursion). If release-automation.yml created the tag but dist.yml didn't fire, manually trigger it:
gh workflow run dist.yml -f ref=vX.Y.Z
See OP-2.
Step 6: Handle CI Failures
If CI fails (clippy lint, test failure, CI script bug):
gh run view <run-id> --log-failed 2>&1 | tail -40
git add -A && git commit -m "fix: <what broke in CI>"
git push && git push origin main:master
git tag -f "$VERSION"
git push --tags -f
gh workflow run dist.yml -f ref="$VERSION"
gh run list --workflow dist.yml --limit 3
gh run watch <new-run-id>
Key insight from sessions: A prior release required 3 rounds of fix-commit-retag-retrigger (clippy lint, then static binary verification bug, then aarch64-musl build issue). Budget time for CI iteration.
Step 7: Verify the Release
gh release view "$VERSION"
gh release view "$VERSION" --json assets | jq '.assets[].name'
gh release download "$VERSION" --pattern "*linux*x86_64*"
tar xzf *.tar.gz
./<binary> --version
Path B: Local Build with dsr/rch (Fallback)
Use this path when:
- GH Actions queue > 10 min (
dsr check --all)
- CI is broken or unreliable
- Cargo.toml has absolute local path dependencies (CI can't resolve these)
- You need an immediate release and can't wait for CI
- musl-incompatible deps (ort, onnx) and CI only builds musl
Check Infrastructure
which dsr && dsr doctor
which rch && rch check
CRITICAL: Bypass RCH for Release Builds
If RCH hooks are installed, cargo build --release will be intercepted and sent to a remote worker. The binaries won't be in your local target/release/:
RCH_DISABLED=1 cargo build --release
ls -la target/release/<binary>
target/release/<binary> --version
See OP-1.
Check Remote Host Disk Space
ssh mac-host 'df -h / && du -sh ~/projects/*/target 2>/dev/null | sort -h | tail -5'
See OP-5.
Build with dsr
dsr repos list | grep <project>
dsr build <tool> --version <version>
Build with rch
rch workers probe --all
rch exec -- cargo build --release
Manual Cross-Platform Builds
RCH_DISABLED=1 cargo build --release && strip target/release/<binary>
ssh mac-host 'df -h /' && ssh mac-host "cd ~/projects/PROJECT && git pull && cargo build --release"
tar czf <tool>-v<version>-x86_64-unknown-linux-gnu.tar.gz -C target/release <binary>
Path Dependency Remapping for Mac
If Cargo.toml uses absolute local paths (e.g., /projects/), the Mac build host needs remapping since paths differ there:
grep 'path = "/' Cargo.toml
rsync -az --exclude target/ ./ mac-host:~/projects/PROJECT/
ssh mac-host "cd ~/projects/PROJECT && sed -i '' 's|/projects/|$HOME/projects/|g' Cargo.toml && cargo build --release"
See OP-6 for the full story.
Commit, Tag, and Upload
git add Cargo.toml Cargo.lock */Cargo.toml
git commit -m "chore: bump version to X.Y.Z" && git push
git push origin main:master
VERSION="vX.Y.Z"
git tag -a "$VERSION" -m "Release $VERSION" && git push --tags
cd artifacts/ && sha256sum *.tar.gz *.zip > SHA256SUMS.txt
gh release create "$VERSION" --title "$VERSION" --generate-notes \
artifacts/*.tar.gz artifacts/*.zip artifacts/SHA256SUMS.txt
Full build matrix and host details: BUILD-MATRIX.md.
Phase 5: Verify
Verify Release Assets
gh release view "$VERSION"
gh release view "$VERSION" --json assets | jq '.assets[].name'
Test Installer (if project has one)
ssh <host> 'curl -fsSL "https://raw.githubusercontent.com/USER/REPO/main/install.sh" | bash'
ssh <host> '<binary> --version'
Gotcha: Verify the installer's expected target triples match the release asset names. Mismatches cause silent fallback to source compilation. See OP-15.
Verify Binary
gh release download "$VERSION" --pattern "*linux*"
tar xzf *.tar.gz
./<binary> --version
Publish to crates.io (Optional)
If the project is a published crate, use /rust-crates-publishing after the GitHub release is verified. Key points:
- Publish in dependency order (leaves first) — see OP-9
- Path deps need
version = "X.Y.Z" alongside path = "..." — see OP-8
- Wait ~30s between publishes for index propagation
Common Pitfalls from Past Sessions
| Pitfall | What Happened | Prevention | See |
|---|
| Missing struct fields in tests | New fields added to structs, test constructors not updated | Always run full test suite before release | TEST-FIXING.md |
| Installer expects wrong target triple | install.sh expected musl, release had gnu | Audit installer target names vs release asset names | OP-15 |
| Path deps block CI | Absolute local paths can't resolve in CI | Build locally with dsr when path deps exist | OP-6 |
| Workspace version drift | Root bumped but members left behind | Bump ALL workspace members, check for version.workspace = true | |
| /tmp full during release | Git operations fail with no space | Use TMPDIR=/data/tmp as workaround | OP-12 |
| License field mismatch | Cargo.toml says MIT but LICENSE file has a rider | Verify license field matches actual LICENSE | |
| Tag force-push blocked by protection | Can't update tag after extra commit | Accept 1-commit offset or create patch version | OP-11 |
| Missing dependency in CLI crate | Workspace dep exists but CLI doesn't list it | cargo build --release catches this early | |
| RCH intercepts release build | Binaries end up on remote worker, not local | Use RCH_DISABLED=1 cargo build --release | OP-1 |
| GH Actions bot tag doesn't trigger dist | Tag created by automation, dist.yml never runs | Manually trigger: gh workflow run dist.yml | OP-2 |
| Clippy nightly drift | Newer nightly on CI catches lints local misses | Run cargo clippy --workspace --all-targets -- -D warnings BEFORE tagging | OP-3 |
| Static binary verification fails | ldd output contains "dynamic" for static binaries | Use file binary | grep "statically linked" instead | OP-4 |
| Remote build host out of disk space | rsync/build fails mid-way | Check df -h on remote hosts BEFORE building | OP-5 |
| release-automation race (main vs master) | Both branches trigger, one fails harmlessly | Expected — just verify the tag was created | OP-7 |
| Path deps missing version for crates.io | cargo publish fails without version specifier | Add version = "X.Y.Z" alongside path = "..." | OP-8 |
| Missing README blocks crates.io | readme = "README.md" but file doesn't exist | Remove the readme field or create the file | OP-10 |
| ort/ONNX doesn't support musl | Linux musl builds fail for ONNX projects | Feature-gate or use gnu targets | OP-16 |
Anti-Patterns
| Don't | Do Instead |
|---|
| Skip test suite | ALWAYS run tests before bumping version |
| Fix tests by deleting them | Update assertions to match current behavior |
| Release without reading AGENTS.md | Read it first — conventions vary per project |
| Bump version without committing first | Commit all pending changes before version bump |
| Create release before pushing tag | Push tag first, then create release (Path A) |
| Ignore pre-existing test failures | Document them, fix if quick |
| Skip installer verification | Test the installer on a real machine |
| Wait forever for CI that won't trigger | Check if bot-created tag; manually trigger dist (OP-2) |
| Panic when one CI run "fails" | Check if it's just a race (main vs master, OP-7) |
| Jump to local build on first CI failure | Fix the CI issue — it'll bite you next release too |
| Build locally when CI works fine | Let CI do the cross-platform matrix; it's more reproducible |
Integration with Other Skills
| Skill | When to Use |
|---|
/gh-actions | Setting up or debugging CI release workflows (Path A) — the primary path for most projects |
/dsr | Building and releasing when GH Actions is throttled or has path-dep issues (Path B fallback) |
/rch | Offloading builds to remote workers (Path B fallback) |
/commit-and-release | Batch committing before release |
/library-updater | Updating deps before release (do this BEFORE test gate) |
/rust-crates-publishing | Publishing to crates.io after GitHub release is verified |
/ubs | Running static analysis before release |
/installer-workmanship | Writing/fixing curl|bash install scripts |
/changelog-md-workmanship | Generating a proper CHANGELOG.md from git history |
Post-Release: Fresh Eyes Review
After the release is live, do a quick review pass (mined from a prior release session):
git diff $(git describe --tags --abbrev=0 HEAD~1)..HEAD --name-only
Batch Release: Scan All Projects
After releasing one project, scan for others that need releases (mined from an earlier batch release session):
PROJECTS_DIR=~/projects
for d in "$PROJECTS_DIR"/*; do
[ -f "$d/Cargo.toml" ] || continue
cd "$d" 2>/dev/null || continue
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null)
[ -z "$LAST_TAG" ] && { cd "$PROJECTS_DIR"; continue; }
COUNT=$(git rev-list --count ${LAST_TAG}..HEAD 2>/dev/null)
[ "$COUNT" -gt 0 ] && echo "$d: $COUNT commits since $LAST_TAG"
cd "$PROJECTS_DIR"
done
for d in "$PROJECTS_DIR"/*; do
grep -q 'crates.io' "$d/Cargo.toml" 2>/dev/null && echo "$d: on crates.io"
done
Use parallel subagents for independent releases.
References