Gunnar

nordisk/dwarves

.nornir/brokkr-guide.md

# ⚒ brokkr — the CI/build-server — guide

> **📍 THIS DOC LIVES IN `dwarves` (moved from `nornir/.nornir/`, 2026-07-22, move=delete).**
> The code it documents is dwarves' own (`crates/dwarves/src/…`), so the doc follows the
> code. nornir keeps only the read-only viz surface that renders this engine's output.
> Source links below are dwarves-repo-relative.


> **Class:** `guide` · audience: a techie writing a `brokkr.toml` or driving the pipeline from the CLI / viz.
> The *what* is in [`brokkr-design.md`](brokkr-design.md); the zero-knowledge walkthrough is [`brokkr-for-idiots.md`](brokkr-for-idiots.md).

## f0 — the whole workflow

You declare a pipeline as data in `brokkr.toml`, then drive it locally:

1. **Write** `brokkr.toml` — triggers, an optional matrix, stages of DAG-ordered steps.
2. **Validate** — `nornir brokkr validate` catches structural breakage before any run.
3. **Plan** — `nornir brokkr plan [--matrix]` prints the topo order (and matrix cells).
4. **Run** — `nornir brokkr run` executes it (demo runner), `--real` builds `variant` steps via cargo.
5. **Read back** — `nornir brokkr history`, `nornir brokkr badge <pipeline>`, or the 🧵 **Brokkr** viz tab.
6. **Trigger check** — `nornir brokkr trigger <event> [--branch B]` shows what an event would fire.

Every leaf reads the **local `brokkr.toml`** (discovered in the cwd, or `--path`) —
no server round-trip. Each run is a tracked `CI_BUILD` job, visible in
`nornir jobs --kind ci_build`.

## f1 — a `brokkr.toml` you can paste

```toml
[pipeline]
name = "nornir-ci"

[[pipeline.trigger]]
kind = "webhook"
events = ["push", "pull_request"]
branch = "main"

[[pipeline.trigger]]
kind = "cron"
cron = "0 3 * * *"

[pipeline.matrix]
rust = ["1.85", "stable"]
target = ["x86_64"]

[[pipeline.stage]]
name = "build"
[[pipeline.stage.step]]
name = "compile"
run = { variant = "server" }
artifacts = ["target/release/nornir"]

[[pipeline.stage]]
name = "test"
[[pipeline.stage.step]]
name = "unit"
needs = ["compile"]
run = { container = { name = "rust:1.85", cmd = ["cargo", "test"] } }
report = "junit"
[[pipeline.stage.step]]
name = "lint"
needs = ["compile"]
run = { container = { name = "rust:1.85", cmd = ["cargo", "clippy"] } }

[[pipeline.stage]]
name = "deploy"
[[pipeline.stage.step]]
name = "ship"
needs = ["unit", "lint"]
run = { vm = "debian" }
when = { branch = "main" }

[pipeline.notify]
on = ["failure", "fixed"]
via = ["slack"]
```

Rules the validator enforces (`descriptor.rs:77`):

- Step `name`s are the **DAG node ids** — unique across the whole pipeline.
- Every `needs` must name a declared step; a self-`needs` or unknown step is rejected.
- A `container` run needs a real image **and** a non-empty `cmd` argv (zero-shell —
  never a `sh -c` string).
- A `cron` trigger needs a `cron` expression; a `webhook`/`poll` trigger must gate on
  `events` or a `branch`.

## f2 — validate, plan, run

```bash
# Validate the discovered ./brokkr.toml (or --path):
nornir brokkr validate
#  ✓ pipeline 'nornir-ci' is valid — 3 stage(s), 4 step(s)

# Plan the topo-ordered step DAG; add --matrix to expand the cells:
nornir brokkr plan --matrix
#  plan 'nornir-ci' (4 steps, topo order):
#    [build] compile (variant)
#    [test]  unit (container) needs ["compile"]
#    [test]  lint (container) needs ["compile"]
#    [deploy] ship (vm) needs ["unit", "lint"]
#  matrix: 2 cell(s)

# Run through the deterministic demo runner (no toolchain, greens every step):
nornir brokkr run
#  run 'nornir-ci' passed — 4/4 steps × 2 matrix cells (12 ms)
#    passed compile — would build variant 'server'
#    ...

# Run for real: `variant` steps actually build via cargo (build-thing, zero shell);
# container/vm steps fall back to the demo runner.
nornir brokkr run --real

# Run just one stage's steps:
nornir brokkr run --stage test
```

The **matrix fans out**: with `rust=[1.85,stable]` the run executes once per cell,
each its own `CI_BUILD` job and its own history row, and the CLI reports the
aggregate verdict across every cell (`brokkr_run` fan-out loop, `nornir.rs:13005`).
A red step **poisons its downstream dependents** (they show `skipped`), and a run
that ran-but-went-red exits non-zero with the detail preserved.

Add `--json` (global, so both `brokkr --json run` and `brokkr run --json` parse) to
get the machine `CommandOutcome`.

## f3 — trigger, history, badge, agents

```bash
# Would a push on main fire this pipeline? (Canonical firing logic — brokkr::trigger.)
nornir brokkr trigger push --branch main
#  event 'push' on main FIRES pipeline 'nornir-ci' (1 trigger(s) match)
nornir brokkr trigger push --branch feature/x
#  event 'push' on feature/x does not fire pipeline 'nornir-ci'

# List historized runs (green/red, trigger, passed/total, duration):
nornir brokkr history
nornir brokkr history --project nornir --json

# Emit a shields-style status SVG for a pipeline's latest run (embeddable, no fetch):
nornir brokkr badge nornir-ci > badge.svg      # green "passing" / red "failing" / grey "unknown"

# The build-agent registry (capability tags / busy-idle):
nornir brokkr agent ls
nornir brokkr agent register linux-1 --tag cargo --tag container
```

Non-firing is a legitimate answer (`ok=true`, real data), not an error. Empty
history is a legitimate cold-start (reported with a count).

## f5 — the Brokkr viz tab

Open `nornir viz` and pick the ⚒ **Brokkr** tab. A sub-view selector switches the
five facets:

- **Results** — press **Run <pipeline>**; the run drives synchronously through the
  StepRunner seam and the run list gets a status-chipped row.
- **Tree** — project → pipeline → stage → step, each step chipped
  idle→running→passed/failed/skipped as the run folds in.
- **DAG** — the stage/step graph as a MetroView: one transit line per stage, each
  station lit green the instant its step passes.
- **Agents** — the pool; a run picks a capable idle agent (busy→idle around the run),
  and **Toggle** flips an agent by hand.
- **Log** — the streaming step-log tail (a bounded 200-line ring; `[failed]` lines
  are red).

Everything on the tab is drivable headless: `state_json` carries one slice per facet,
and `tests/viz_brokkr_robot.rs` clicks Run and asserts the run goes green across all
five.

## Where the same data shows up

- `nornir jobs --kind ci_build` — every run is a `CI_BUILD` job (one parent, a child
  per step); a red run is a `failed` row.
- `<warehouse>/brokkr/runs.jsonl` + `steps.jsonl` — the historized rows the
  `history`/`badge` leaves read.
- The monitor bridge: when `nornir monitor` sees a poll-triggered project's HEAD
  move, `runs_for_report` (moved with brokkr to `dwarves`, now
  `crates/dwarves/src/engine.rs`) is what turns that
  into a queued run.