From general-oat-skills
Verify and release a Rust crate or workspace. Bumps version, commits, tags, pushes. CI-driven by default; --local publishes directly.
How this skill is triggered — by the user, by Claude, or both
Slash command
/general-oat-skills:publish-rust major|minor|hotfix [--rc] [--local] [--no-watch]major|minor|hotfix [--rc] [--local] [--no-watch]This skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Generic Rust release skill. Runs a full verification suite locally, bumps the version, commits, creates a signed semver tag, and pushes to trigger the project's release CI. With `--local`, publishes to crates.io directly instead of relying on CI.
Generic Rust release skill. Runs a full verification suite locally, bumps the version, commits, creates a signed semver tag, and pushes to trigger the project's release CI. With --local, publishes to crates.io directly instead of relying on CI.
$ARGUMENTS is one of:
major — bump first digit (X.0.0)minor — bump middle digit (0.X.0)hotfix — bump last digit (0.0.X)Optional flags:
--rc — tag a pre-release (X.Y.Z-rc.N). Increments N if already on an RC; drops the suffix if --rc is omitted. Pre-releases can be cut from any branch.--local — skip CI path; run cargo publish directly for each crate. Requires CARGO_REGISTRY_TOKEN in the environment.--no-watch — push the tag and exit without streaming the CI workflow (CI-driven mode only).If $ARGUMENTS is empty or doesn't match major|minor|hotfix, print usage and stop.
If .claude/publish-rust.toml exists in the project root, read it. Supported keys (all optional):
# Branch required for stable releases (not --rc). Default: "main".
release_branch = "main"
# Tag format. Default: "v{version}".
tag_format = "v{version}"
# Override the default verification gates. If present, these REPLACE the defaults.
# Each entry is a shell command; all must pass. Env vars via "env =".
[[verify]]
name = "fmt"
cmd = "cargo fmt --all -- --check"
[[verify]]
name = "clippy"
cmd = "cargo clippy --workspace --all-features --exclude mycrate-foo -- -D warnings"
env = { RUSTFLAGS = "-D warnings" }
[[verify]]
name = "test"
cmd = "cargo test --workspace --all-features --exclude mycrate-foo"
[[verify]]
name = "doc"
cmd = "cargo doc --workspace --no-deps"
env = { RUSTDOCFLAGS = "-D warnings" }
# --local only: crates to publish, in topological order. Default: auto-detect from
# workspace Cargo.toml, skipping members with publish = false.
[local]
publish_order = ["mycrate-core", "mycrate-adapters", "mycrate-cli"]
inter_publish_sleep_secs = 20
Defaults if no config:
release_branch = "main"tag_format = "v{version}"cargo fmt --all -- --checkcargo clippy --workspace --all-targets -- -D warningscargo test --workspacecargo doc --workspace --no-deps with RUSTDOCFLAGS=-D warningscargo publish --dry-run -p <crate> --allow-dirty. For publish = false members: skip. If a dry-run fails because an upstream workspace crate isn't on crates.io, fall back to cargo package --list -p <crate> --allow-dirty >/dev/null and print a warning.Emit short status lines (one sentence each) between steps. On any abort, print why and stop — never auto-fix, never proceed on warnings treated as errors.
Parse the bump kind (major/minor/hotfix), --rc, --local, --no-watch. If invalid, print usage and stop.
Read the workspace Cargo.toml:
[package] version = "X.Y.Z" directly. Bump this.[workspace.package] version = "X.Y.Z". Bump this; member crates use version.workspace = true.[package] version. Bump every member (fail if they're not all identical — the skill requires workspace-wide version parity).Read optional .claude/publish-rust.toml for overrides.
Let current = X.Y.Z[-rc.N] and bump = major|minor|hotfix.
-rc.N suffix and --rc is passed:
X.Y.Z), increment N.-rc.1.-rc.N suffix and --rc is NOT passed, and bump position matches the RC'd base: drop the suffix, tag the stable version as-is. (E.g. 0.8.0-rc.2 + minor → 0.8.0.)--rc is passed: bump + append -rc.1.--rc is not passed: plain bump.New tag = tag_format.replace("{version}", new_version).
Run these checks in order. Print each as "→ checking X ... OK" or "→ checking X ... FAIL: " and stop on the first failure.
git rev-parse --is-inside-work-tree).git status --porcelain empty).git branch --show-current == release_branch), unless --rc is passed (RCs allowed from any branch).git fetch origin <branch>, then compare HEAD with origin/<branch>. Must be equal (neither ahead nor behind).git rev-parse <new_tag> fails locally AND git ls-remote --tags origin <new_tag> is empty.^## \[v?<new_version>\] in CHANGELOG.md. If the file exists and has no such heading, abort with the exact heading the user needs to add..github/workflows/*.yml with on: push: tags: matching v*. If none, print a warning but continue. Skip this check entirely with --local.cargo available: command -v cargo.--local: CARGO_REGISTRY_TOKEN set in the environment. If not, abort.Run each gate from config (or the defaults listed above) in order. Stream stdout/stderr to the user. On the first failure, abort and print:
Verify gate failed: <name>
Command: <cmd>
Exit: <code>
Fix the underlying issue and re-run /publish-rust <args>. The skill has not modified any files.
Do not catch or retry. Do not continue on failure.
Edit the version in the right location(s) per step 2. Leave every other line untouched.
Stage only the edited Cargo.toml files plus Cargo.lock (run cargo check --workspace first to update the lockfile, then stage it). Do not git add -A.
Commit with:
chore: release <new_tag>
Respect the user's git config for commit.gpgsign (don't pass flags either way).
Create an annotated tag:
git tag -a <new_tag> -m "Release <new_tag>"
If git config tag.gpgsign is true, use -s instead of -a. Otherwise -a. Never force-overwrite an existing tag — preconditions already verified it doesn't exist.
Push the bump commit and tag:
git push origin <release_branch>
git push origin <new_tag>
On push failure (e.g., branch protection rejects direct push), abort. Do not bypass. Print the remote's error message verbatim.
Unless --no-watch is passed:
gh run list --event=push --branch=<new_tag> --limit=1 (or poll gh run list --workflow=release.yml --limit=1 if the workflow file was found in step 4). Wait up to 60s for the run to appear.gh run watch <run_id> --exit-status.gh run view <run_id> --log-failed | tail -80. Stop.With --no-watch: print the workflow run URL and the gh run watch <id> command the user can run, then stop.
Topologically sort the workspace members to publish:
config.local.publish_order if set.cargo metadata --format-version 1 --no-deps and do a topological sort of workspace members by their dependency graph (package.dependencies filtered to workspace-local crates). Skip members with publish = false (read from cargo metadata or Cargo.toml directly).For each crate in order:
cargo publish -p <crate> (no --dry-run; dry-run was step 5).config.local.inter_publish_sleep_secs seconds (default 20). Skip sleep after the last crate.For each published crate (CI-driven and --local both):
Poll https://crates.io/api/v1/crates/<crate>/versions (with reqwest/curl) up to 10 times, 10s apart (100s max per crate). Stop polling when the new version appears in the returned list.
If polling times out for any crate, print a warning (not an error) — indexing occasionally takes longer than 100s. The release is done; the index just hasn't caught up yet.
Print a final summary:
Released <new_tag>
Commit: <short_sha>
Tag: <new_tag>
Pushed: <branch> + tag to origin
CI: <success/skipped/--no-watch>
crates.io: <list of indexed crates, or "check manually">
GitHub release: <https URL if gh release view succeeds, else "pending">
Then stop.
Any abort should print in this shape:
[publish-rust] aborted: <one-line summary>
Reason: <specific details>
State: <what changed so far, e.g. "no changes made" / "commit created but tag not pushed">
Next: <what the user should do, e.g. "fix X and re-run" / "manually revert commit with `git reset --hard HEAD~1`">
Never leave the repo in a weird state silently.
git reset --hard HEAD~1 + git tag -d <tag> or retry the push.--force on any push. Never bypass git hooks. If the push is rejected, the skill's job is to tell the user why — not to work around it.--exclude swink-agent-local-llm on --all-features), use .claude/publish-rust.toml.Guides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.
npx claudepluginhub open-agent-tools/general-oat-skills --plugin oat-run-adk-evals