Release procedure

Authoritative checklist for cutting a devboy-tools release. Reflects ADR-022 — the workspace ships through two channels:

  • npm@devboy-tools/cli and per-platform binary subpackages. Primary user-facing channel; this is what devboy onboard and the agent plugins assume.
  • crates.io — reusable workspace libraries with publish != false. Application binaries (devboy-cli, devboy-mcp, devboy-secrets-ui-bin) and internal secret-source plugins are intentionally excluded; the CLI ships through npm and GitHub Release binaries.

Both channels publish from the same v* git tag, in parallel: pushing the tag fans out to two GitHub Actions workflows (.github/workflows/release.yml for npm, .github/workflows/release-crates-io.yml for crates.io).

CI tokens

Two repo secrets drive the two channels. Set both at https://github.com/meteora-pro/devboy-tools/settings/secrets/actions.

SecretChannelWhat it isScopes
DEVBOY_TOOLS_NPM_TOKENnpmnpm automation tokenautomation
CARGO_REGISTRY_TOKENcrates.iocrates.io API tokenpublish-update (every release after first); add publish-new until each crate has had its first publish

The CARGO_REGISTRY_TOKEN env var is read by cargo publish natively — no cargo login step is needed in CI. Generate the token at https://crates.io/settings/tokens, scope it to "All crates" (or an explicit devboy-* allowlist), pick an expiry you're willing to rotate, and paste it into the repo secret.

Before you start

  • Decide the target version (workspace-wide, single bump in [workspace.package].version).
  • Confirm main is green: CI, tests, plugin manifest drift check, and cargo publish --dry-run -p devboy-core all passed on the merge commit.
  • Confirm you have:
    • Push access to the meteora-pro/devboy-tools git remote (both release pipelines trigger from v* tags).
    • Both DEVBOY_TOOLS_NPM_TOKEN and CARGO_REGISTRY_TOKEN set as repo secrets (see the table at the top of this doc).

Step 1 — Bump the version

  1. Update [workspace.package].version in the root Cargo.toml. Every member crate inherits it.

  2. Update every [workspace.dependencies] devboy-* = { version = "X.Y.Z", path = "..." } to the new version. Local builds keep resolving via path; published consumers resolve via version.

  3. Run scripts/release/sync-plugin-version.sh so the Claude/Codex plugin manifests match the workspace version.

  4. Regenerate the committed references:

    cargo run -p devboy-cli --quiet -- tools docs \
      --output docs/guide/reference/tools.md
    cargo run -p devboy-cli --quiet -- docs cli \
      --output docs/guide/reference/cli.md
  5. Run cargo check --workspace --all-targets, cargo test --workspace, and scripts/release/sync-plugin-version.sh --check locally.

  6. Commit: chore(release): bump workspace to X.Y.Z.

  7. Open a PR. Wait for CI. Merge.

Step 2 — Tag

git checkout main
git pull
git tag -a vX.Y.Z -m "release X.Y.Z"
git push origin vX.Y.Z

Pushing the tag fans out to two parallel GitHub Actions workflows:

  • .github/workflows/release.yml → builds platform binaries, signs them, and publishes the npm package + GitHub Release.
  • .github/workflows/release-crates-io.yml → publishes every workspace crate to crates.io in topological order using CARGO_REGISTRY_TOKEN.

No further manual steps. The workflows are idempotent on retry-after-failure (re-runs from the failed step), but not on already-published versions — see "Recovery" below.

Step 3 — Verify

Once both workflows are green:

  • npm page — new version visible
  • GitHub Releases — tag with platform binaries attached
  • https://crates.io/crates/<name> — every crate printed by python3 scripts/release/crates-publish-order.py is at the new version
  • https://docs.rs/<name> — docs build green (5–10 min)

If a docs.rs build is red, ship a patch version with the doc fix (you can't re-upload the same version).

Manual fallback (rare)

The first ever crates.io release (0.27.0) was run by hand because each crate had to claim its name. From 0.27.x onwards CI handles it; this section is here for break-glass scenarios.

git checkout vX.Y.Z

# The script derives a topological order from cargo metadata and excludes every
# package with `publish = false`, matching the release workflow.
while read -r crate; do
  [ -z "$crate" ] && continue
  cargo publish -p "$crate"
  sleep 15
done < <(python3 scripts/release/crates-publish-order.py)

cargo login once with a token that has publish-new + publish-update (or just rely on CARGO_REGISTRY_TOKEN env var if you'd rather not persist the token).

Recovery

If a step fails partway through the wave, stop and investigate. crates.io rejects re-uploads of the same version, so:

  1. Fix the underlying issue on main.
  2. Bump the patch version to X.Y.(Z+1) in [workspace.package].version and every [workspace.dependencies] entry.
  3. Retag as vX.Y.(Z+1) and push.
  4. Re-running CI publishes from the failed crate onwards — the earlier ones already on crates.io are skipped automatically when they hit "no token" or "already exists" with --no-verify. (If they fail loudly, edit the workflow to comment out the already-published layers temporarily.)

Settling delay. crates.io's index sometimes needs ~30 s before a freshly-published crate becomes resolvable as a dependency. The CI workflow has explicit sleep calls between layers to handle this; if you're publishing manually and hit no matching package named …, wait 30 seconds and retry.

Step 4 — Verify the wave landed

For each crate that was just published:

  • https://crates.io/crates/<name> — version page exists, README rendered.
  • https://docs.rs/<name> — docs build is green (docs.rs typically completes within 5–10 minutes).

If a docs.rs build is red, fix the docs and publish a patch version (you cannot re-upload the same version on crates.io).

Step 5 — Post-release hygiene

  • Update the "Use as a library" section of the root README.md if new crates joined the wave.
  • Close the release issue if there is one.
  • Open a milestone for the next version.

Smoke-test snippets

# Verify package metadata before publishing
cargo publish --dry-run -p devboy-core

# Inspect the tarball Cargo would upload
cargo package -p devboy-core --list

# Confirm the published version once the upload finishes
cargo search devboy-core --limit 1

Failure modes seen so far

SymptomCauseFix
error: failed to verify package tarball … couldn't read … no such fileAn include_str!/rust-embed/include_bytes! path resolves outside the crate root. Cargo packaged only the crate dir, so the file is missing in the tarball.Move the data inside the crate, or add a build.rs that mirrors it into OUT_DIR.
error: no matching package named devboy-core found (running dry-run)Cargo resolves deps through the registry. The dependency is not on crates.io yet (or hasn't propagated).Publish the dependency first, or wait ~30s for index propagation.
error: 1 files in the working directory contain changes that were not yet committed into gitYou're publishing from a dirty tree.Commit first. --allow-dirty exists for emergencies but should not be the default.
docs.rs build is red on https://docs.rs/<crate>/<version>/buildsA doctest or feature-gated path didn't compile in the docs.rs sandbox.Fix locally with cargo doc --no-deps -p <crate>, then publish a patch version.