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

nordisk/dwarves

.nornir/release-events.md

Release events — the release-op DAG

Epic B (B1 + B2) of THE_BIG_PLAN. nornir release run now records a clean, row-level DAG of every release operation into the warehouse, readable from both the server and fat-CLI. The viz tabs (B3/B4) consume this table.

Why a second release table

release_lineage answers what shipped — one row per (release, repo) with the git SHA, gate outcome, and time-machine pins. It is an outcome record. It cannot tell you the shape of the run: the order ops happened in, which ops a repo waited on, or exactly where a partial release stalled mid-flight. Today the operator infers that from gate_status text, and the viz can't draw a live op graph.

release_events fills that gap. It is the structural twin of the funnel DAG (funnel_events): an append-only event log where each row is one component × op × phase boundary, carrying the depends_on edges from the dep-graph. Replaying a run's rows in seq order reconstructs the op DAG; the depends_on lists draw the cross-repo edges the viz lights up.

The table

release_events — defined in src/warehouse/iceberg_schema.rs::release_events, registered at warehouse open in src/warehouse/iceberg.rs::ensure_layout (unpartitioned — a run is a handful of rows; the reader's run_id/repo residual filter is enough).

col type n notes
run_id string req groups one release run's rows (UUID server-side; epoch-secs run id in the fat CLI)
seq long req per-run monotonic counter minted at emit time — the durable ordering key (Iceberg scan order is unspecified)
ts_micros timestamptz req event time
component string req the component the op acts on (the repo today; a sub-component later)
repo string req workspace repo name (== component today; kept distinct for crate-grained ops)
op string req run | test | bench | gate | publish | push | snapshot | …
phase string req start | end | skip
status string req ok | fail | warn | running
detail string req short human message ("" when none — never a surprise null)
depends_on list<string> opt components this op waited on (dep-graph producers). null on run/* envelope rows; a real (possibly empty) list on per-repo rows
elapsed_ms long opt wall-clock duration; only phase = end rows carry it

depends_on is the load-bearing column: for repo R it is { e.to | e ∈ dep_graph where e.from == R } — i.e. the in-workspace producers R consumes (edges point consumer→producer). A dep-graph root records Some([]) (an explicit empty list, distinct from the None on envelope rows), so the viz can tell "no deps" apart from "not a repo op".

Identity / relations

Write paths — server AND fat CLI

The work-log lands in the warehouse either way (mirrors how bench rows persist). Both paths drive a single helper, ReleaseEventEmitter: it holds the run id + a shared atomic seq and appends one row per boundary. Emits are best-effort + non-fatal — a write error is logged to stderr and swallowed, so a logging-row failure can never abort an otherwise-green release.

  1. Library / server path — release::pipeline::run_pipeline (src/release/pipeline.rs). This is what the nornir-server release RPC and the smoke-test orchestrator call. It brackets the run with run/start … run/end and emits test (start/end), bench (start/end), and gate (end) per repo as it walks the build order. The depends_on for each repo comes from depends_on_for(graph, repo) over the same WorkspaceGraph the build order was derived from.

  2. Fat-CLI path — run_release_run in src/bin/nornir.rs (nornir release run). Same event shape, driven synchronously via wh.block_on(...). depends_on is computed from the latest dep_graph_edges snapshot's edges (the same snapshot that ordered the run). Covers the extra fat-CLI phases too: publish (start/end) and test/skip / bench/skip for the --skip-* flags. Early bail!s (test/bench/publish failure) emit a run/end with status = fail carrying where it stalled.

Both open the warehouse exactly as the existing bench-persist code does (the fat-CLI path reuses the same wh handle that already writes bench_runs), so there is no new locking story.

Read path — CLI parity (mandatory)

nornir release events <repo|run_id> [--json] (ReleaseOp::Events in src/bin/nornir.rs) opens the warehouse read-only + lock-tolerant (open_read_only, so it coexists with a live server holding the catalog lock via the copied-aside snapshot) and prints the DAG:

The read helpers live in src/warehouse/release_events.rs: query_release_events(wh, EventSelector::{Run|Repo|All}) returns rows sorted by (run_id, seq); render_topo and rows_to_json render them.

How the viz (B3/B4) lights up

The table is shaped so the future "Release Ops" tab is a thin read:

scan_preview("release_events", N) already makes the table browsable in the generic Warehouse introspection tab today.

Guide — quickstart

# Record events (automatic — every release run now writes the DAG):
nornir release run                      # all repos, topo order
nornir release run --repo holger        # one repo

# Read the DAG back:
nornir release events holger            # all of holger's release runs (topo view)
nornir release events <run_id>          # one specific run
nornir release events holger --json     # raw rows for tooling / viz

The run id is the UUID/release_id printed by the run (library path) or the epoch-seconds id in the 📡 live events: line (fat-CLI path).

Tests

tests/release_events.rs (inject-and-assert, temp warehouse — never the live server's):

Plus unit tests in src/warehouse/release_events.rs (emitter seq sharing across clones; append/query round-trip with list-column nullability).