Release doctor — guide
A practical, in-order how-to for releasing the whole nordisk constellation with nornir release doctor. For the model behind it see release-doctor-design.md; for the absolute-minimum recipe see release-doctor-for-idiots.md.
Every command reads the configured constellation (the [repo] set in your workspace's nornir.toml). Run them from your workspace root, or pass --config <workspace>/release/nornir.toml.
The command in one picture
doctor is one command with four escalating tiers — read-only by default, each mutating stage behind an opt-in flag:
nornir release doctor # ① ASSESS (read-only)
nornir release doctor --fix # ② FIX (write version= on path deps)
nornir release doctor --rehearse # ③ PROVE (dry-publish into /sparring)
nornir release doctor --publish --bump … # ④ PUBLISH (the real crates.io cascade)
You never reach ④ on a tree that hasn't passed ② (and ideally ③): --publish refuses while any hard blocker remains.
Step 1 — assess (read-only)
nornir release doctor
# or explicitly:
nornir release doctor --config workspace_nordisk/release/nornir.toml
Mutates nothing. Composes, in one report:
- Working trees — which repos have uncommitted changes.
- Publish pre-flight — publishable crates whose
pathdeps lack aversion =(the exactcargo publishblocker), split into🔧 auto-fixable(member-owned, with the suggested version) and⛔ needs-decision(a non-member dep — it must join the cascade or be published first). - External dependency skew — per shared external crate, the target (highest declared version) and each repo's status. Two hint flavours:
💡 bump → Xis a free edit;⛔ blocked → Xmeans a transitive dep pins the old major (a manifest bump won't take —cargo tree -i Xto find it). - Publish order (dependencies first) and any cycles (crate-level, so a repo-level mutual dep that publishes fine as an interleaved crate DAG is not flagged), with suggested cuts.
- Patch-fork promote gate — foreign forks that block a crates.io publish vs safe own-crate overrides.
- Blast radius of each dirty repo.
Useful knobs: --forbid arrow=56 bans an external version (repeatable); --json emits the full DoctorReport for tooling.
Step 2 — fix the mechanical blockers (--fix)
nornir release doctor --fix
Auto-applies the publish pre-flight: writes version = "…" to every path dep of a publishable crate that lacks one, sourcing the version from the dep crate's own manifest (following the path, resolving version.workspace = true), covering [target.'cfg(…)'] tables. It is formatting-preserving and idempotent — safe to re-run. It prints how many it wrote and skips anything with no resolvable version. After fixing, it re-runs the assessment so the report reflects the clean tree.
Only the "auto-fixable" (member-owned) gaps are handled. A ⛔ needs-decision gap (a non-member dep) is yours to resolve: add that repo to the release cascade, or publish it to crates.io first — the engine won't guess.
Step 3 — prove the cascade against /sparring (--rehearse)
nornir release doctor --rehearse
Escalates from advice to a real dry-publish against the embedded holger /sparring registry: cuts an ephemeral release/staging branch per repo, cargo publishes in topo order, then builds each dependent FROM /sparring. It mutates (opt-in) but self-restores every repo to its main branch afterward. Nothing hits real crates.io. A residual, non-static blocker surfaces here as a real cargo error. Refuses if the plan has a dependency cycle.
Flags: --registry <url> and --branch <name> override the default sparring registry / ephemeral branch. (nornir release stage [--execute] runs the same rehearsal standalone.)
Step 4 — rehearse the REAL crates.io cascade (no upload)
nornir release doctor --publish --publish-dry-run --bump patch
Plans the whole crates.io cascade and runs cargo publish --dry-run for every crate — end-to-end, mutating nothing (no bump, no branch, no upload). Use it as the final look before the real thing.
Step 5 — publish the constellation (--publish)
nornir release doctor --publish --bump patch
The one command. After assessing (and only with zero blockers — it bails on any path-dep-version gap, promote-block, or cycle), it publishes every public repo to crates.io in crate-topo order, deps-first, fail-stop, each recorded for release undo. Per repo it: regenerates docs (README + docs/book.pdf), runs the promote gate, bumps + commits versions, strips [patch.crates-io] on an ephemeral branch, publishes deps-first waiting for each crate to index before its dependents, and records every step. Private repos (private = true) are skipped — staged and verified, never pushed to crates.io.
--bump selects the level:
--bump |
effect |
patch (default) / minor / major |
bump every publishable crate by that level before publishing |
keep / none |
publish the CURRENT versions unchanged — the idempotent finish/resume path |
crates.io rejects an already-uploaded version, so a fresh full release wants a real bump. But "already uploaded" is treated as a skip, not an error — which is what makes keep and resume work.
The --bump keep finish path
After a partial or fail-stopped run, don't re-bump everything (that churns the irreversible registry). Fix the cause, then finish with:
nornir release doctor --publish --bump keep
keep publishes the current versions and skips every crate already on crates.io — so it ships exactly the not-yet-published crates and leaves the rest untouched. It is idempotent: run it as many times as you need until the cascade is complete.
Resuming mid-cascade with --publish-from
If a fail-stop happened at, say, facett, and everything before it in topo order already shipped, resume without re-publishing the earlier repos:
nornir release doctor --publish --bump keep --publish-from facett
--publish-from <repo> skips every repo before <repo> in the topo order, then proceeds normally (skipping private repos and already-uploaded crates).
If it stops — fail-stop and undo
--publish is fail-stop: the instant a repo reports a per-crate publish failure, the cascade halts (it will not sail on to a dependent whose dependency never shipped) and prints the failing crates. Read the error, fix the cause, then resume with --bump keep (+ --publish-from if useful).
To reverse a run entirely:
nornir release undo <release-id> --yank
It un-bumps (restores the exact pre-bump manifests), restores any stripped [patch.crates-io] block, and resets the release branch. A crates.io upload is immutable, so it is yanked (with --yank) rather than deleted. With no id it undoes the most recent run; --dry-run reports without acting. The release-id is printed at the end of every real --publish run.
Not everything is automatable
The doctor solves what it safely can and explains what it can't — it never guesses. Two classes stay with a human:
- Needs-decision (lane ②) — a non-member hard dep (e.g.
modgunn), a cycle that needs a crate split, or skew held by a transitive pin. The doctor surfaces one crisp ask; you make the call (add to the cascade / feature-gate / hold / extract a leaf crate). - Unsolvable by the engine (lane ③) — no crates.io token, a crate name already taken, the registry down, a real upstream API break. The doctor gives a clear why and what you must do. The one thing it cannot manufacture is a crates.io API token: create a scoped one at crates.io → Account → API Tokens, then
cargo login <token>(or setCARGO_REGISTRY_TOKEN) — once per machine.
Prefer to click? Drive it in the viz
nornir viz → the 🧬 nornir tab paints the publish graph with every problem overlaid on each node (⛔ held · ▼ behind · 🟡 dirty), each cycle as a dashed cut edge. The 🚀 Release button opens the 🧙 wizard — a gated Advise → Fix → Rehearse → Promote walk (you can't skip a red step without an override; Promote makes you type PROMOTE). Same engine as the CLI.
Over MCP
release_doctor is an embedded-only MCP tool — read-only advisory, run on the host (it reads the local git trees, so it's refused against a NORNIR_SERVER target). An agent calls it to see skew / dirty-trees / promotability before cutting a release.
Deeper go-live runbook and traps: release-rehearsal.md, release-publish-traps.md.