⚒ brokkr — the CI/build-server — guide
📍 THIS DOC LIVES IN
dwarves(moved fromnornir/.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 abrokkr.tomlor driving the pipeline from the CLI / viz. The what is inbrokkr-design.md; the zero-knowledge walkthrough isbrokkr-for-idiots.md.
f0 — the whole workflow
You declare a pipeline as data in brokkr.toml, then drive it locally:
- Write
brokkr.toml— triggers, an optional matrix, stages of DAG-ordered steps. - Validate —
nornir brokkr validatecatches structural breakage before any run. - Plan —
nornir brokkr plan [--matrix]prints the topo order (and matrix cells). - Run —
nornir brokkr runexecutes it (demo runner),--realbuildsvariantsteps via cargo. - Read back —
nornir brokkr history,nornir brokkr badge <pipeline>, or the 🧵 Brokkr viz tab. - 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
[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
names are the DAG node ids — unique across the whole pipeline. - Every
needsmust name a declared step; a self-needsor unknown step is rejected. - A
containerrun needs a real image and a non-emptycmdargv (zero-shell — never ash -cstring). - A
crontrigger needs acronexpression; awebhook/polltrigger must gate oneventsor abranch.
f2 — validate, plan, run
# 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
# 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 ; 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 aCI_BUILDjob (one parent, a child per step); a red run is afailedrow.<warehouse>/brokkr/runs.jsonl+steps.jsonl— the historized rows thehistory/badgeleaves read.- The monitor bridge: when
nornir monitorsees a poll-triggered project's HEAD move,runs_for_report(moved with brokkr todwarves, nowcrates/dwarves/src/engine.rs) is what turns that into a queued run.