一键导入
cargo-publish
Publish one or more Rust crates to crates.io from the trusty-tools workspace
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Publish one or more Rust crates to crates.io from the trusty-tools workspace
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | cargo-publish |
| description | Publish one or more Rust crates to crates.io from the trusty-tools workspace |
| user-invocable | true |
| version | 1.0.0 |
| category | local-ops |
| tags | ["cargo","publish","release","workspace","local-ops"] |
| effort | high |
| when_to_use | When publishing one or more crates to crates.io from this workspace; when bumping versions; when investigating dry-run failures; when sequencing multi-crate releases with shared dependencies |
Complete reference for publishing Rust crates to crates.io in the trusty-tools workspace. Codifies lessons from 18+ publishes across two recent sessions.
Every publish follows this exact sequence:
0. scripts/check-publish-ready.sh <crate> — MANDATORY, MUST PASS
1. Pre-flight checks (fmt, clippy, tests)
2. cargo publish --dry-run
3. git tag <crate-name>-v<version>
4. git push origin <crate-name>-v<version>
5. scripts/preflight-publish.sh <crate> — MANDATORY, MUST PASS (run again, immediately before step 6)
6. cargo publish
7. Wait 60-120s for propagation
8. Verify with curl to crates.io API
9. cargo install --path crates/<dir> --locked (binaries only)
10. Verify <binary> --version
Critical: Never skip dry-run. Never publish from the main checkout.
🔴 Publish only from merged main / the pushed release tag — never an
unmerged branch. Before running cargo publish for ANY crate, run:
scripts/check-publish-ready.sh <crate-name-or-dir>
# or
make publish-check CRATE=<crate-name-or-dir>
Do NOT run cargo publish if this fails. It asserts two things against
the true origin/main tip (fetched fresh, never trusted from a possibly-stale
local ref):
origin/main itself or an
ancestor of it.<crate>-v<version> (accepts
tga-v<version> too, for trusty-git-analytics) has been pushed to origin
and its commit is on merged main.Why this exists: issue #2209 — a publish ran from an unmerged feature
branch that was missing a P0 fix. That branch's build became the crates.io
"latest" version, and every concurrent worktree session that ran
cargo install <crate> picked up the regressed build until it was caught and
corrected. With concurrent sessions routinely running in this repo,
"whatever branch happens to be checked out" is a correctness hazard, not just
a process nicety — this script turns "publish only from merged main" into a
mechanical gate instead of a convention someone can forget under pressure.
Escape hatch (rare, deliberate use only): ALLOW_UNMERGED_PUBLISH=1
downgrades both guard failures to a loud warning and exits 0. Only use this
when you have a specific, understood reason to publish from an unmerged
commit — the default path is always "merge to main first, then publish."
🔴 Run scripts/preflight-publish.sh immediately before every cargo publish
— treat any nonzero exit as an absolute stop. On 2026-07-08 a crate was
published to crates.io out-of-band — from an UNMERGED branch, under the
WRONG gh account — burning crates.io version 0.22.0 with fix-less content
(a burned version number can never be reused). check-publish-ready.sh
above already covers "merged main" + "tag pushed"; this script closes the two
gaps that incident fell through — WHO is publishing, and whether the target
version is already live:
scripts/preflight-publish.sh trusty-mpm
Reads the crate's name/version straight from crates/trusty-mpm/Cargo.toml
(pass an explicit version as a second argument to check a hypothetical
version instead). Runs four checks and fails loud on any of them:
origin/main's HEAD
SHA (stricter than check-publish-ready.sh's ancestor check).gh auth status account is bobmatnyc. Any
other active account fails with the remedy gh auth switch --user bobmatnyc.git status --porcelain is empty.https://crates.io/api/v1/crates/<name>/<version>) —
this is the exact guard that would have caught the 0.22.0 collision.scripts/preflight-publish.sh --check-only <crate> runs all four checks
unconditionally and prints a [PASS]/[FAIL] line per check without
assuming you're mid-publish — use it to preview status. --help documents
the rare, logged PREFLIGHT_ALLOW_DETACHED=1 override for check 1 (validated
release worktrees only — misuse of it is exactly how the incident happened).
No override exists for the identity check.
Always operate from a dedicated git worktree, never the main checkout.
# Provision a fresh worktree off origin/main
git fetch origin main
git worktree add -b feature/publish-<crate> \
.claude/worktrees/publish-<crate> origin/main
cd .claude/worktrees/publish-<crate>
# Work, test, tag, and push from inside this worktree
# When complete: git worktree remove --force .claude/worktrees/publish-<crate>
Why: Concurrent sessions may hold uncommitted work in the main checkout. Worktrees are isolated. After a squash-merge, clean up the local branch:
git branch -D feature/publish-<crate>
git push origin --delete feature/publish-<crate>
NEVER do this:
cp target/release/<binary> ~/.cargo/bin/<binary>
The kernel caches code-signing identity by cdhash (executable hash).
A plain cp over an existing on-PATH binary leaves a stale cache.
The next exec is SIGKILL'd as:
EXC_CRASH / CODESIGNING — Taskgated Invalid Signature
zsh: killed (no output — looks exactly like OOM kill)
ALWAYS do this instead:
cargo install --path crates/<dir> --locked
cargo install writes to a temp file and renames atomically, keeping the
kernel cache consistent. If a manual copy is ever unavoidable:
cp target/release/<binary> ~/.cargo/bin/<binary>
codesign --force --sign - ~/.cargo/bin/<binary> # Regenerate signature
All of these must pass. No --allow-dirty, --no-verify, or --force flags:
scripts/check-publish-ready.sh <crate> # Step 0 — merged-main guard (issue #2227)
cargo fmt --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test -p <crate>
cargo check --workspace
Abort if anything fails. Fix it, commit, and restart.
Crates with embedded Svelte UIs (e.g., trusty-search/ui) may generate
untracked node_modules/ and pnpm-workspace.yaml that block dry-run.
If cargo publish --dry-run fails with "working directory is dirty":
git status # Eyeball what's flagged
git clean -fdX # Remove ONLY gitignored content (-X flag)
# If specific untracked yaml remains after review, rm it explicitly
git status # Verify clean
Critical: Never clean tracked files. git clean -fdX removes only
.gitignore-listed files, not source code.
crates.io rejects license = "Elastic-2.0" (not in SPDX registry).
For Elastic-2.0 licensed crates:
# ✗ WRONG
license = "Elastic-2.0"
# ✓ CORRECT
license-file = "LICENSE"
For MIT licensed crates:
license = "MIT"
Local workspace builds resolve internal deps via path, ignoring
[patch.crates-io] overrides.
But cargo publish resolves ALL dependencies from the live crates.io
registry — the same view downstream consumers will see.
Implication: When crate A's public API changes and crate B depends on A
(via workspace = true), you MUST:
crates/A/Cargo.tomlIf you skip this: cargo publish --dry-run for B fails with
"dependency not found" because crates.io doesn't yet have A at the new version.
[dependencies] for workspace = true entriesCargo.lock)curl -s https://crates.io/api/v1/crates/<crate>/<version> | head -c 100
JSON metadata = already live; 404 = not yet publishedPublish library crates before the crates that depend on them. The ordering for this workspace:
trusty-common → trusty-mcp-core → trusty-embedder → trusty-symgraph
→ trusty-search, trusty-memory-core, trusty-analyze
→ trusty-mpm-core → trusty-mpm-client → trusty-mpm-daemon, trusty-mpm-mcp
→ trusty-mpm-cli, trusty-mpm-tui
If only a subset of these crates changed, publish only the changed ones and their direct downstream dependents, in order.
Session publishes: trusty-common 0.8.0, trusty-search 0.13.1, tga 1.4.2
Analysis:
trusty-search depends on trusty-common (workspace = true, resolves to 0.8.0)tga depends on trusty-common (workspace = true, resolves to 0.8.0)Correct order:
What happened if we skipped:
cargo publish --dry-run -p trusty-search
# ERROR: dependency trusty-common v0.8.0 not found on crates.io
# (because we only just published it, crates.io needs 60-120s)
After cargo publish succeeds with status 200 OK:
# Immediately after: ✓ crates.io ingestion complete
# Next 60-120s: ✓ metadata replicating to CDN, search index updating
Before publishing a crate that depends on this one, verify:
# Wait ~100s, then check
curl -s https://crates.io/api/v1/crates/<crate>/<version> | head -c 200
# Success: JSON metadata appears (version is now live)
# {"crate":{"name":"...","versions":[...]},...}
# Still waiting: 404 Not Found
# {"errors":[{"detail":"Crate not found"}]}
If 404 after 120s, something went wrong. Check:
cargo search <crate> --limit 1
Use the crate package name from Cargo.toml, NOT the directory name.
Reference: Abbreviations table from CLAUDE.md:
trusty-git-analytics → -p tga → tag: tga-v1.4.2 ✓trusty-search → -p trusty-search → tag: trusty-search-v0.13.1 ✓trusty-common → -p trusty-common → tag: trusty-common-v0.8.0 ✓open-mpm → -p open-mpm → tag: open-mpm-v0.2.3 ✓tga tag aliases (issue #1128): the binary-release workflow accepts both
tga-v<version>andtrusty-git-analytics-v<version>— they resolve to the same build config and Homebrew formula (the parse step canonicalizes thetgaprefix totrusty-git-analytics). The documented form above (tga-v<version>, matching the published package name) is preferred; you no longer need a secondtrusty-git-analytics-v<version>tag to trigger a successful binary release.
The crate name always comes from the name field in Cargo.toml:
# Inside the worktree, when in doubt:
grep "^name = " crates/<dir>/Cargo.toml
Most match (crates/trusty-search/ → -p trusty-search).
Exceptions (always verify Cargo.toml):
crates/trusty-git-analytics/ → name = "tga" → -p tgacrates/open-mpm/ → name = "open-mpm" → -p open-mpmIf cargo -p <name> returns "package not found":
grep "^name = " crates/<dir>/Cargo.toml
A main crate's binary release must include every binary required to run
that crate. Sidecar daemons are bundled via [[bin]] shims pointing at the
sidecar's run() entry point.
Example: trusty-search
# crates/trusty-search/Cargo.toml
[[bin]]
name = "trusty-search"
path = "src/bin/main.rs"
[[bin]]
name = "trusty-embedder" # Sidecar daemon (optional utility)
path = "src/bin/embedder.rs"
Users invoke:
cargo install trusty-search
# Both trusty-search AND trusty-embedder land in ~/.cargo/bin/
Before running cargo publish for any crate, verify it is not marked non-publishable:
grep "publish" crates/<dir>/Cargo.toml
If the output contains publish = false, do not publish that crate. Common non-published crates include binary/CLI crates and internal tooling crates. When in doubt, read the manifest.
Sidecar lib crates whose lib is a dependency of a published main crate MUST be published to crates.io.
Do NOT set publish = false on such crates. Example:
# crates/trusty-embedder/Cargo.toml
[package]
name = "trusty-embedder"
publish = true # ← REQUIRED even if users never cargo install it directly
Why: When you cargo publish -p trusty-search, Cargo's dependency
resolver requires every transitive lib dependency to exist on crates.io at
the declared version, even if the binary isn't published separately.
Downstream consumers don't manually install the sidecar, but Cargo's
resolution during their build REQUIRES it to be available.
If you set publish = false: cargo publish -p trusty-search --dry-run
fails with "dependency not found" because the sidecar lib can't be resolved.
Always read the git log since the last tag to determine the correct bump before editing any version:
git log <crate-name>-v<last-version>..HEAD --oneline -- crates/<dir>/
Map commit types to semver components:
| Commit type | Version component |
|---|---|
feat: | MINOR (x.Y.0) |
fix:, chore:, perf:, refactor: | PATCH (x.y.Z) |
BREAKING CHANGE in footer, or ! suffix on any type | MAJOR (X.0.0) |
Examples by change type:
| Change Type | Example | Bump Rule |
|---|---|---|
| New public function | feat: add auth handler | Minor (x.y → x.y+1.0) |
| Bug fix | fix: resolve race in async | Patch (x.y.z → x.y.z+1) |
| Chore / perf / refactor | chore: update deps | Patch |
| BREAKING public API | feat!: remove deprecated fn | Major post-1.0; Minor pre-1.0 |
Workspace-pinned versions:
[workspace.package] (trusty-mpm-* family) bump togetherCargo.toml, all members inherit ittrusty-mpm-core-v<ver>, trusty-mpm-cli-v<ver>, etc.)# From git tags
git tag --list '<crate-name>-v*' | sort -V | tail -1
# From crates.io (if published)
cargo search <crate-name> | head -3
# 1. Inside worktree, change to repo root
cd /Volumes/Kemono/Users/masa/Projects/trusty-tools/.claude/worktrees/publish-<crate>
# 2. Verify on origin/main (git status should be clean from worktree creation)
git status
# 3. Edit version(s) in Cargo.toml
vim crates/<crate>/Cargo.toml
# or for workspace-pinned:
vim Cargo.toml
# 4. Run pre-flight checks
cargo fmt --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test -p <crate>
cargo check --workspace
# 5. If UI changes (trusty-search, trusty-memory), check for stale artifacts
git status # Look for node_modules/ or pnpm-workspace.yaml
git clean -fdX # If present
# 6. Commit version bump
git add -A
git commit -m "chore: bump <crate> to v<version>"
# 7. Dry run (essential — catches dependency issues early)
cargo publish --dry-run -p <crate>
# 8. If dry-run fails:
# - Read the error (usually "dependency X version Y not found on crates.io")
# - Publish that dependency first
# - Wait 100s
# - Retry this crate's dry-run
# 9. Tag
git tag <crate>-v<version>
# 10. Push tag to origin
git push -u origin <crate>-v<version>
# 11. Publish to crates.io
cargo publish -p <crate>
# 12. Verification step
sleep 100
curl -s https://crates.io/api/v1/crates/<crate>/<version> | head -c 200
# 13. For binaries: install locally
cargo install --path crates/<crate> --locked
# 14. Verify binary version
<binary> --version
Scenario: trusty-common public API changed (breaking); trusty-search depends on it. Both need to publish.
# === STEP 1: PUBLISH trusty-common 0.8.0 ===
cd .claude/worktrees/publish-trusty-common
# Edit, test, commit, tag
vim crates/trusty-common/Cargo.toml # 0.7.0 → 0.8.0
cargo test -p trusty-common
git commit -m "chore: bump trusty-common to v0.8.0"
git tag trusty-common-v0.8.0
git push origin trusty-common-v0.8.0
# Dry run
cargo publish --dry-run -p trusty-common # ✓ PASS
# Publish
cargo publish -p trusty-common
# === PROPAGATION WAIT ===
sleep 100
# Verify on crates.io
curl -s https://crates.io/api/v1/crates/trusty-common/0.8.0 | head -c 200
# {"crate":{"name":"trusty-common",...},"versions":[...],...} ← LIVE
# === STEP 2: PUBLISH trusty-search 0.13.1 ===
cd .claude/worktrees/publish-trusty-search
# Edit, test, commit, tag
vim crates/trusty-search/Cargo.toml # 0.13.0 → 0.13.1
cargo test -p trusty-search
git commit -m "chore: bump trusty-search to v0.13.1"
git tag trusty-search-v0.13.1
git push origin trusty-search-v0.13.1
# Dry run (now trusty-common 0.8.0 IS on crates.io)
cargo publish --dry-run -p trusty-search # ✓ PASS
# Publish
cargo publish -p trusty-search
# Verify
sleep 100
curl -s https://crates.io/api/v1/crates/trusty-search/0.13.1 | head -c 200
# Install
cargo install --path crates/trusty-search --locked
trusty-search --version
node_modules/, pnpm-workspace.yaml)git clean -fdX (gitignored files only)-f alone (deletes all untracked, including source)license = "Elastic-2.0" (not in SPDX registry)license-file = "LICENSE" instead-p trusty-git-analytics instead of -p tga)name field in Cargo.tomlEach crate is tagged independently: <crate-name>-v<version>
Release flow:
Cargo.tomlcargo test -p <crate> and lint checksgit tag <crate-name>-v<version>git push origin <crate-name>-v<version>cargo publish -p <crate>cargo install --path crates/<dir> --lockedOnce the PR merges and the main branch absorbs your commits:
# From main checkout or any other worktree:
git worktree remove --force .claude/worktrees/publish-<crate>
# Clean up local branch
git branch -D feature/publish-<crate>
# Clean up remote branch (if pushed)
git push origin --delete feature/publish-<crate>
Before declaring a publish complete:
scripts/check-publish-ready.sh <crate> (or make publish-check CRATE=<crate>) passedcargo publish succeeded (status 200 OK)cargo install --path … --locked (if applicable)<binary> --version shows correct versiongit worktree remove)When upgrading a launchd-managed trusty-* daemon (trusty-memory, trusty-search,
trusty-analyze), use SIGTERM via launchctl bootout — never
launchctl kickstart -k which sends SIGKILL and drops live connections.
As of issue #534, all three daemons implement graceful shutdown via
axum::serve(...).with_graceful_shutdown(trusty_common::shutdown_signal()).
When SIGTERM arrives:
SIGKILL bypasses all of this: active requests die mid-stream, cleanup is
skipped, and the mcp_bridge in the Claude Code session receives an abrupt
socket close.
# 1. Stop the daemon gracefully (SIGTERM → drain → exit)
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/<label>.plist
# 2. Rebuild and install the new binary
cargo install --path crates/<crate-dir> --locked
# 3. Restart the daemon
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/<label>.plist
Do NOT use launchctl kickstart -k <label> — the -k flag sends SIGKILL
to the running instance before starting a new one, which kills in-flight
requests without draining.
Prefer restarting between Claude Code sessions (i.e., when no .mcp.json
MCP bridge process is actively connected). Even with graceful shutdown, the
mcp_bridge will need to reconnect after a restart — it does so automatically
with exponential backoff (200ms → 30s cap), so brief mid-session restarts are
now transparent to Claude Code for requests that were between calls. Restarts
during an active in-flight request will still lose that one request.
https://github.com/bobmatnyc/trusty-tools/releaseshttps://crates.io/api/v1/crates/<name>/<version>