Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/dwarves

.nornir/release-rehearsal.md

Release rehearsal — the holger marriage end-to-end

How nornir releases a multi-repo workspace safely: rehearse the whole publish cascade against an embedded, throwaway crates.io (/sparring) before touching the real one. This is the full reference; the short version is release-for-idiots.md; the design rationale is in design.md → The holger marriage.

The marriage (why there's no holger-server to run)

nornir links holger (holger-server-lib) directly — the embed-holger cargo feature, on by default. src/holger_embed.rs::EmbeddedHolger::start spins a dedicated tokio runtime, writes the dev-pair config, instantiates the backends, wires the routes and starts holger's gRPC + HTTP listeners in-process; dropping the handle stops them. So nornir cache up and release stage --execute bring holger up inside the nornir process — one binary, no orchestration.

A lean client build opts out: cargo install nornir --no-default-features …. Then the same call sites spawn a real holger-server dev-pair subprocess instead (pass --holger-bin /path/to/holger-server).

Two sparse-index gateways on one HTTP port (default 127.0.0.1:18464, gRPC at :18465):

Endpoint What it is Used by
/cache transparent crates.io mirror/proxy nornir cache up, builds
/sparring writable fake crates.io the release rehearsal

The rehearsal writes a cargo config (src/release/stage.rs::cargo_config_toml) that (a) defines /sparring as a publish registry and (b) source-replaces crates.io with it — so a dependent's build resolves the just-published workspace crates from /sparring, and anything else falls through /sparring's crates.io upstream.

The three tiers

1. nornir release doctor — advisory (read-only)

Mutates nothing. Composes these signals (src/release/doctor.rs):

--json for machine output.

Real output (run 2026-06-20, nordisk constellation via the workspace_holger/release/nornir.toml recipe — znippy/holger/nornir on disk):

External dependency skew:
  arrow (target 58.3.0)
      · nornir 58
      ✓ znippy 58.3.0
    💡 bump → 58.3.0: nornir
  …
  toml (target 1.1.2+spec-1.1.0)
      ✓ holger 1.1.2+spec-1.1.0
      ⛔ nornir 0.8  (held: lock already resolves 1.1.2+spec-1.1.0, a transitive dep pins 0.8)
    ⛔ blocked → nornir: a transitive dep pins toml (run `cargo tree -i toml` to find it)

Publish order (dependencies first):
  znippy → holger → nornir

That ⛔ is the transitive-pin tell working on a real repo: nornir declares toml 0.8 but its lock carries 1.1.2 too (pulled in transitively), so the bump won't take until the pinning dep moves. (After the arrow-58 migration landed, arrow is a plain 💡 bump for nornir, no longer held.)

1b. nornir release gate edition <repo> — the Rust 2024 gate

Separate from doctor, the edition gate (src/release/edition.rs) answers "is this crate on the current Rust/Cargo standard?": static (every publishable package on edition = "2024", rust-version >= 1.85, workspace resolver = "3") plus dynamic (cargo check forcing rust-2024-compatibility to warn). Real output (2026-06-20, on nornir):

$ nornir release gate edition nornir
  ✗ nornir-saga — edition = "2021" — want "2024"
  ✗ nornir-saga — no rust-version (MSRV) — edition 2024 needs ≥ 1.85
  ✗ 2024-incompat — edition_2024_expr_fragment_specifier: …
  …
Error: edition gate: 2 static + 55 lint finding(s)

publish = false crates are skipped (they never ship). The viz Gate pane runs the static layer on load (cheap); the dynamic lint pass is the heavy, CLI-driven bit.

2. nornir release stage — rehearse

plan_stage is pure and testable: topo order + per-repo verify set (direct dependents). Plain stage prints the plan; --execute runs it (src/release/stage.rs::execute):

  1. bring up embedded /sparring (wait until the gateway serves);
  2. per repo in order: cut the ephemeral release branch, then cargo publish --workspace --no-verify --allow-dirty --registry sparring;
  3. per dependent: cargo build against /sparring — proof the just-published crates resolve from a registry, not from path deps.

Per-step failures are collected (not fatal) so the outcome shows how far it got. holger is torn down on return; /sparring is ephemeral. --registry / --branch override the defaults; the embedded server is brought up on whatever host:port the --registry URL advertises, so plan and execution never disagree.

3. nornir release promote

Replays the same dependency order against real crates.io once the sparring run is green.

Bridge: doctor --rehearse

nornir release doctor --rehearse [--registry … --branch …]

Prints the advisory report, then runs the same stage engine (run_stage_rehearsal in src/bin/nornir.rs — one shared call path with stage --execute, no duplicated logic). Blocks if the plan has a cycle. doctor without --rehearse stays strictly read-only.

The release surfaces in the viz

You don't have to drive any of this from the shell. The 🧬 nornir release dashboard (the 🧬 nornir tab in nornir viz) paints the publish graph with the doctor verdict overlaid on every node — ⛔ held / ✗ edition-fail (RED) · ▼ behind / 🟡 dirty (AMBER) · clean (GREEN) — and each cycle as a dashed ✂ cut · … edge. Click a node to scope the release and light its blast radius. The 🚀 Release button (enabled only on a go) opens the 🧙 release wizard: a gated Advise → Fix → Rehearse → Promote walk where you can't advance past a red step without an explicit override, Rehearse runs stage --execute into /sparring, and Promote demands you type PROMOTE. See design.md → Viz — the release surfaces.

Test coverage

Test Kind Proves
release::doctor::tests::* unit skew classify, topo order, blast, cycle, transitive pins
release::edition::tests::* unit static edition/MSRV/resolver findings, 2024-lint parse
release::stage::tests::* unit plan order + verify set, cargo config, cycle block
tests/holger_embed.rs integration embedded pair boots; /cache + /sparring serve
tests/release_stage_execute.rs #[ignore] heavy full cascade: publish into /sparring + build back out
tests/robot_viz.rs robot (viz) dashboard gate overlay/badges/cut-edges, wizard gating, saga replay → matrix
tests/saga_roundtrip.rs integration record → replay plan → robot scenario → matrix rows
tests/workspace_holger.rs integration real-sibling cross-repo graph + build order

(All green 2026-06-20: doctor/stage/edition 23/23, robot_viz 11/11, saga_roundtrip 3/3.)

Run the heavy one explicitly:

cargo test --test release_stage_execute -- --ignored --nocapture

It uses uniquely-named fixture crates (nornir-stage-fixture-{lib,app}) precisely because /sparring mirrors crates.io — a real-world name (acme-lib etc.) would collide on publish and resolve the real upstream crate (and its deps) instead.


Go-live runbook — releasing workspace_nordisk

Status as of this writing: ready to rehearse. The plan is a clean, executable DAG for the locally-resolvable members.

The monitored workspace_nordisk/nornir-workspace.toml lists every member by git URL; to run doctor/stage fat you need the members on disk. The verified way (used for the real output above) is the constellation release recipe, with the sibling repos checked out next to it:

cd <git-root>               # the dir holding workspace_holger/, holger/, znippy/, nornir/

# 1. advise — REAL output (2026-06-20), trimmed:
nornir --config workspace_holger/release/nornir.toml release doctor
#   Working trees:        ✅ all clean
#   arrow (target 58.3.0)   · nornir 58   ✓ znippy 58.3.0   💡 bump → 58.3.0: nornir
#   toml  (target 1.1.2…)   ⛔ nornir 0.8 (held: a transitive dep pins 0.8)
#   Publish order:        znippy → holger → nornir

# 2. rehearse — REAL plan (green):
nornir --config workspace_holger/release/nornir.toml release stage
#   Publish order (dependencies first):
#     1. cargo publish znippy → /sparring   verify (build from /sparring): holger, nornir
#     2. cargo publish holger → /sparring
#     3. cargo publish nornir → /sparring
#   → green plan; run with --execute to publish into /sparring, then promote.
nornir release stage --execute            # publishes into /sparring + verifies

# 3. when green, ship
nornir release promote

(From inside a workspace dir with --server, the same verbs run remote — the server resolves the members from its monitored clones.)

Preconditions / caveats for the FULL constellation

What "done" means

You can start using nornir for nordisk releases the moment release stage prints a green plan (it does) and release stage --execute comes back with ✅ no errors. promote is the only step that touches the outside world; gate it behind a green rehearsal every time.

The wired end-to-end flow (task #31)

The pieces above are now wired into ONE coherent, validated path — they reuse the existing engines, no new release logic: