用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/bobmatnyc/trusty-tools --skill cargo-publish命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
Essential Git patterns for effective version control, eliminating redundant Git guidance per agent.
Full cargo command reference for the trusty-tools workspace — release builds, feature-gated tests, ignored/ONNX integration tests, single-test-by-name runs, the trusty-search performance regression suite, dependency audit, and crate-name-vs-directory resolution
Six-stage quality-gate pipeline for any code implementation task
正在显示 SKILL.md
| 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
6b. scripts/check-tag-publish-parity.sh --vcs-info auto <crate> — confirm the
tag names the commit cargo actually recorded
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."
Also now enforced independently of any human running a script (issue
#3366): .github/workflows/release.yml's preflight job verifies the SAME
rule mechanically for the tag-triggered binary-release pipeline — a real
ancestry check (git merge-base --is-ancestor against a full-history clone
of the tagged commit vs. origin/main), not a naive ref-string comparison
(which would never match on a tag push and would silently disable every
release). A tag pushed from an unmerged branch now fails that job loudly and
the entire pipeline (build/release/homebrew-bump AND the publish-dry-run job
below) is skipped — this is a second, independent enforcement point, not a
replacement for running check-publish-ready.sh yourself before cargo 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 six checks and fails loud on any of them:
merged-main: current HEAD's commit SHA is EXACTLY origin/main's HEAD
SHA (stricter than check-publish-ready.sh's ancestor check).
identity: the active gh auth status account is bobmatnyc. Any
other active account fails with the remedy gh auth switch --user bobmatnyc.
clean-tree: git status --porcelain is empty.
version-not-live: the target version is not already published on
crates.io (queries https://crates.io/api/v1/crates/<name>/<version>) —
this is the exact guard that would have caught the 0.22.0 collision.
semver (#5149): runs scripts/check_semver.sh --crate <pkg>, which
compares the crate's public API against its latest non-yanked crates.io
release and fails when a break is not carried by a breaking version bump
(0.x crates break in the MINOR position). This is the ONLY place that can
block a bad publish — a crates.io upload is irreversible except by yank, and
#4088 is what a gate arriving afterwards costs. Requires
cargo install cargo-semver-checks@0.50.0 --locked; a missing tool is a
failure, not a skip. No override — the fix is to bump the breaking position,
which the gate then skips as an already-breaking release.
.github/workflows/semver-checks.yml runs the same check on the tag push
(step 4), so a red run there is visible before you reach step 6.
tag/publish-commit parity: the release tag <crate>-v<version> (or the
tga-v<version> alias) must name EXACTLY the commit this publish will ship.
Delegates to scripts/check-tag-publish-parity.sh. No override.
Why this is not already covered by 1-5 or by check-publish-ready.sh:
nothing bound the tag to the upload. GUARD 2 asks whether the tag is an
ANCESTOR of origin/main; CHECK 1 asks whether HEAD EQUALS origin/main.
A tag several commits behind HEAD satisfies both — which is where a release
run lands whenever main moves and the run is fast-forwarded to satisfy CHECK
tga-v2.17.0 tagged at 246e4ca2 while the
published .cargo_vcs_info.json recorded 7d5cf82e1, with every gate green.🔴 , then re-run preflight. The gate prints that command with the SHAs filled in.
scripts/preflight-publish.sh --check-only <crate> runs all six 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.
Before bumping a crate's version to publish, confirm the crate ISN'T already
drifted — i.e. that its CURRENT (pre-bump) Cargo.toml version, if already
live on crates.io, still matches the local src/ tree. This is the
trusty-common/trusty-agents-common incident: several commits of source
changes landed on main without a version bump while the old version number
was already published, so cargo publish --dry-run for a downstream crate
failed with "symbol not found" for symbols that only existed in the drifted,
unpublished source.
make version-parity-check
# or directly:
scripts/check-version-parity.sh
This also runs automatically on every push to main
(.github/workflows/version-parity.yml) so drift is caught right after it
merges rather than discovered as a release blocker. See
crates/trusty-publish-guard for the underlying check (fails closed: a
crate whose live/local comparison can't be verified is treated as a failure,
never a silent pass).
For the RELATED but distinct cross-crate ordering hazard (publishing a crate
before a sibling it depends on is live — the 2026-07-20 incident), see
scripts/publish-dry-run-order.sh in "Cross-Crate Publish Ordering" below —
it now computes the dependency order mechanically instead of by hand.
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: report the worktree path to the PM — see Cleanup below
Why: Concurrent sessions may hold uncommitted work in the main checkout. Worktrees are isolated.
🔴 Do not remove the worktree yourself (#5791). tm hook --pm-guard denies
a dispatched agent's git worktree remove, so reaching for it mid-publish
fails; rm -rf is not the workaround. Report the path and let the PM run the
prune verb — see "Cleanup After Publishing" below.
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.
Automated and CI-enforced (issue #3366): scripts/publish-dry-run-order.sh
computes the dependency order mechanically from cargo metadata and runs
cargo publish --dry-run -p <crate> for every publishable crate in that
order (dependencies before dependents), stopping at the first failure — this
replaces re-deriving the manual recipe below by hand.
This now ALSO runs automatically, with no human action required: every
*-v* tag push runs .github/workflows/release.yml's publish-dry-run job,
which invokes this script scoped to just the tagged crate + its publishable
dependency closure. A human forgetting to run the manual command below no
longer means an unsafe publish goes unnoticed — the tag push itself proves
(or disproves) publish-safety, independent of whether anyone remembered the
manual step. See that job's header comment for why it is scoped
per-tag/per-crate rather than full-workspace-per-PR (cost, registry rate
limits), and why it is deliberately independent of (not blocking) the binary
build/release jobs.
For a partial-release dry run before you've even tagged anything, or to dry run a set of crates together, use the script directly. Pass specific crate names to restrict it (it still includes any publishable crate they depend on, in order):
scripts/publish-dry-run-order.sh --list-only # print the order, run nothing
scripts/publish-dry-run-order.sh # dry-run every publishable crate, in order
scripts/publish-dry-run-order.sh trusty-search trusty-common
The manual recipe below remains useful for reasoning about WHY a given order is correct, and the crate list in "Dependency Publish Order" further down is historical/illustrative rather than mechanically maintained — trust the script's computed order, not that hand-written list.
[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 ✓trusty-agents → -p trusty-agents → tag: trusty-agents-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 tgaIf 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, trusty-analyze, trusty-console),
# check for stale artifacts AND stale bundle content (issue #3606)
git status # Look for node_modules/ or pnpm-workspace.yaml
git clean -fdX # If present
# Freshness (not just presence) matters: verify the COMMITTED ui-dist/ /
# ui/dist/ bundle actually reflects current ui/src — cargo publish cannot
# catch this for you (see step 7 note). Check the source digest directly
# (no rebuild needed):
# bash scripts/check-ui-bundle-freshness.sh <crate>
# If it reports BUNDLE-STALE, rebuild and re-stamp before continuing:
# cd crates/<crate>/ui && pnpm install --frozen-lockfile && pnpm run build
# # trusty-search only: also copy ui/dist/* into ../ui-dist/ (or `make release-prep`)
# bash scripts/stamp-ui-bundle.sh <crate>
# then commit the regenerated bundle. `.github/workflows/ui-bundle-freshness.yml`
# runs this same check on every push to `main`, and `preflight-publish.sh`
# CHECK 7 runs it again immediately before step 7's dry-run — but catching
# it here, before tagging, is cheaper.
# 6. Commit version bump
git add -A
git commit -m "chore: bump <crate> to v<version>"
# 7. Dry run (essential — catches dependency issues early)
# UI-embedding crates (trusty-search, trusty-memory, trusty-analyze,
SKIP_UI_BUILD=1 cargo publish --dry-run -p <crate>
git tag <crate>-v<version>
git push -u origin <crate>-v<version>
SKIP_UI_BUILD=1 cargo publish -p <crate>
100
curl -s https://crates.io/api/v1/crates/<crate>/<version> | -c 200
cargo install --path crates/<crate> --locked
<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, report — do not
remove. Worktree removal is PM-executed (#5791, owner ruling 2026-08-19): name
the merged PR, the worktree path (.claude/worktrees/publish-<crate>), and the
branch (feature/publish-<crate>), then stop.
The PM reclaims the tree:
tm session prune-worktrees --merged-prs # preview, the default
tm session prune-worktrees --merged-prs --force # reclaim
That pass removes the checkout only. The remote branch is usually already gone
via gh pr merge --delete-branch; the local branch survives the sweep.
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 versionWhen 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>git reset --hard <tag>🔴 Never reach for git tag -f or git tag -d here — tags on this repo
are immutable (#6178). An enforcing ruleset rejects both a force-update
and a delete with GH013, admin: true does not lift it, and the ruleset
is invisible to the GitHub API, so you find out when the push fails. Move
the checkout onto the tag; never the tag onto the checkout.
If the tagged commit can never pass the publish gate, that version number is
burned — bump to the next version and tag fresh. Proven 2026-08-22:
trusty-search-v0.49.0 and trusty-review-v0.24.0 are permanently stranded
at 4af0ef8ee, and the release shipped as 0.49.1 / 0.24.1. Tag as late
as possible, immediately before cargo publish, so nothing can strand.
After cargo publish, verify what cargo recorded rather than what it should
have recorded — the only check that still works post-upload:
scripts/check-tag-publish-parity.sh --vcs-info auto <crate>
Full rationale, the four finding codes, and the residual gap: docs/reference/release-workflow.md.