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

nordisk/gunnar

brokkr.toml

# gunnar CI/build pipeline — driven by the dwarves forge (brokkr descriptor +
# sindri scheduler). `dwarves validate` / `dwarves plan` / `dwarves run`.
#
# ══ THIS FILE IS A RESTORATION, NOT A NEW IDEA. ════════════════════════════════
#
# It existed once and was LOST. MEASURED 2026-09-17:
#
#   /home/rickard/git/old/private-gunnar  commit 03c34b0
#     "the forge had no job for this repository, so nothing but a human ever
#      built it"                                          — added `brokkr.toml`
#   …still present at that lineage's HEAD (48f3e85).
#
# The current gunnar is the CLEANSED RE-ROOT of that lineage, and the file did
# not come across. Checked, not assumed: across all 132 refs in this repository
# `git cat-file -e <ref>:brokkr.toml` hits ZERO times, and `git log --all --
# brokkr.toml` is empty. The only surviving copy on this box is an untracked
# stray in the pruned `gunnar-gc` worktree, byte-identical to the old lineage's.
#
# Two things say the absence was a loss and not a decision:
#
#   1. `xtask/src/publish.rs:103` still carries an EXCLUSIONS entry for
#      `brokkr.toml` — *"the forge's build declaration: how this tree is built
#      here, not what it is"*. The publisher is still written to keep a file the
#      tree no longer has. That entry survived the re-root; the file did not.
#   2. `dwarves validate /home/rickard/git/gunnar` and `dwarves plan …` both
#      exit 2 with `no brokkr.toml at /home/rickard/git/gunnar` (run 2026-09-17
#      against dwarves 0.1.6 / 23b624b). holger, korp, nornir, edda, skade,
#      tunnr, draupnir, knut, modgunn, ordning, Skidbladnir and znippy-zoomies
#      all answer. gunnar was the hole.
#
# It is NOT a copy of the old file. That one described a repository with 8
# declared variants and a forge that could not assemble a medium; both facts
# have moved. What is restored is the job; the content is re-measured below.
#
# ══ WHAT THIS FILE ACTUALLY BUYS, AND WHAT IT DOES NOT ═════════════════════════
#
# ⚠ It is NOT a build path, and reading it as one is the mistake korp's own
# header had to spend forty lines undoing. The variants live in
# `.nornir/build-variants.toml` and nowhere else: a brokkr `variant`/`tests` step
# reads THE SAME FILE — `RealRunner::discover` → `BuildVariantSet::discover(
# repo_root)` (dwarves/crates/dwarves/src/runner.rs:186) — which is the same
# relationship `cargo xtask build` and `dwarves buildthing gunnar --variant …`
# already have to it. This introduces a second CALLER of the one build path, not
# a second path.
#
# What the absence actually cost, measured in dwarves' source:
#
#   · `dwarves validate|plan|run <repo>` → `discover_pipeline` →
#     `PipelineDescriptor::discover` (brokkr/descriptor.rs:56) reads
#     `<repo_root>/brokkr.toml`. `Ok(None)` ⇒ the three verbs print
#     `no brokkr.toml` and exit 2. gunnar had no plan to show.
#   · `RegisterPipeline` on the server (src/server.rs:398) answers
#     `registered:false, "no brokkr.toml at …"` for a repo without one, and that
#     is the same answer `dwarves repo add <ssh-url>` relays (main.rs:9370).
#     **So gunnar could not be a forge PROJECT at all** — no poll-on-push, no
#     nightly, no DAG, no history rows, no badge.
#   · `RunKind::Tests` exists ONLY inside a pipeline. `dwarves buildthing` and
#     `cargo xtask build` both only BUILD. With no brokkr.toml there was no way
#     to ask the forge to run gunnar's suites at the features gunnar ships —
#     836 `#[test]`/`#[tokio::test]` fns in the two packages the variants name
#     (gunnar-server 507, gunnar-cli 329, counted 2026-09-17).
#
# ── ⚠ 243 ROWS IS NOT 243 VARIANTS ────────────────────────────────────────────
#
# `grep -c '^\[\[build\.' .nornir/build-variants.toml` counts every sugar
# section. The census on 2026-09-17:
#
#    19  [[build.variant]]                    — the ONLY rows a `run = {…}` step
#                                               can name
#   224  sugar rows                           — 73 installer.params, 63
#                                               installer.options.kernel_requires,
#                                               54 installer.requires, 18
#                                               installer.options, 9 installer,
#                                               3 iso.options.kernel_requires,
#                                               1 each of iso, iso.options,
#                                               container, container.layers
#
#   of the 19 variants:  8 CARGO kinds  (7 × exe + 1 × service)
#                       11 PACKAGING    (1 container, 1 iso, 9 installer)
#
# All 8 cargo rows are declared below. Every one of them. (The old file declared
# 5 of 5; the three metered/thin server rows did not exist yet.)
#
# ── ⚠ WHAT EACH STEP KIND ACTUALLY DOES (read before adding one) ──────────────
#
#   run = { variant = "…" }  → `RealRunner::run`, `RunKind::Variant` arm
#                              (runner.rs:590). Forks on the ROW'S OWN `kind`:
#                              a cargo kind goes to `build_one`, a packaging kind
#                              to `assemble_one`. **It is a BUILD. It runs no
#                              test.**
#   run = { tests   = "…" }  → `RealRunner::test_one` (runner.rs:257): the SAME
#                              variant's `cargo_args()` with `build` swapped for
#                              `test` and `--bin`/`--lib` dropped, through
#                              `buildgate::run_cargo`. Same package, same
#                              features, same `--no-default-features`, same
#                              profile — a test run whose features could drift
#                              from the build's would be testing something the
#                              forge does not ship. It compiles AND runs, so it
#                              strictly SUBSUMES a `variant` step on the same row,
#                              which is why no row below gets both.
#   run = { container = … }  → `UnwiredRunner`. FAILS by design. Not declared.
#   run = { vm = … }         → `UnwiredRunner`. FAILS by design. Not declared.
#
# ⚠ gunnar's cargo steps do NOT run on the host. `.nornir/build-variants.toml`
# declares `[build.toolchain] image = "nordisk-cargo-toolchain"`, and
# `RealRunner::build_one` checks that FIRST (runner.rs:495): with a toolchain
# declared every cargo build goes to `build_one_in_container`. The doc comment on
# that fork names gunnar as the reason it exists — *"MEASURED on oden 2026-08-25,
# `dwarves buildthing gunnar --variant gunnar-server` → error[E0463]: can't find
# crate for core, FAILED in 1.8 s, because gunnar targets musl and that box's
# rustup had no musl std"*. This file inherits that and needs no opinion of its
# own about toolchains.
#
# ── ⚠ TWO THINGS ON THIS BOX WOULD STOP A `--real` RUN TODAY ──────────────────
#
# Neither is a property of this pipeline; both are recorded so the next red is
# read correctly instead of being blamed on the steps.
#
#   1. **The installed forge cannot read gunnar's declaration at all.** MEASURED
#      2026-09-17, `dwarves buildthing /home/rickard/git/gunnar --list` (dwarves
#      0.1.6):
#
#        parse .nornir/build-variants.toml: parse [build] variants: TOML parse
#        error at line 331 … unknown field `kernel_requires`, expected one of
#        `name`, `label`, `default`, `feature`, …
#
#      That is the exact failure `nornir-build-thing`'s own reader documents at
#      `variant.rs:1723` ("measured on gunnar's committed recipe against
#      published 0.1.10"). The edda tree on disk parses it; the installed binary
#      predates the schema. Reinstall the forge — do not trim the recipe.
#   2. **The toolchain pin does not match the image on this box.**
#      `[build.toolchain] image_sha256 = "3d5115ba…"`; the local
#      `nordisk-cargo-toolchain:live` manifest digest is `2cf9b012…` and its id
#      `72abf41e…`. `CargoToolchain::image_ref` resolves
#      `nordisk-cargo-toolchain@sha256:3d5115ba…`, which is not present, so a
#      container build would try to PULL. Moving that pin is a change to the
#      recipe (code → emit), not to this file.
#
# ── HOW THE STEPS ARE GROUPED, AND WHY THAT AXIS ──────────────────────────────
#
# Not by suite: all 19 rows are `suites = ["ORIGINAL"]`, so that axis carries
# zero information here. The grouping is the one the recipe itself argues for in
# its own header — *"gunnar is a git client and a git server in ONE binary whose
# two halves have different constraints … which is why the server atom and the
# client atom are two build points and not one with a flag"* — plus the third
# shape that has an init under it:
#
#   stage       the boundary a red names
#   appliance   the PID-1 musl half. The ONLY cross-triple rows in the repo, so a
#               red here with `clients` green names the musl toolchain and not
#               gunnar. Also the only rows naming `dhcp-client`, `wg-client`,
#               `replication`, `grow-runtime` and `metered` — five capabilities
#               `[[build.installer.options]]` lets a site switch off.
#   clients     `-p gunnar-cli` on the host triple. A red here with `appliance`
#               green is the client/control stack: tonic, prost, axum, hyper.
#   test        the suites, at the SAME features the artifacts are built with.
#
# Within a stage the CHEAPEST row is the root and the rest fan out from it, so a
# broken common core reds ONCE instead of eight times, and each `needs` edge is
# picked so the red BETWEEN two rows names the feature that separates them:
#
#   server-thin   the root: server + engine-znippy + mimalloc + dhcp-client +
#                 replication + grow-runtime
#   server        needs server-thin        → `wg-client` alone
#   metered-thin  needs server-thin        → `metered` alone
#   metered       needs server             → `metered` on top of `wg-client`
#   server-product  `gunnar`, the product server (cargo install gunnar)
#   control-client  `gunnar-cli`, the product tool (cargo install gunnar-cli)
#
# ── AND WHICH ROWS RUN TESTS, WHICH ONLY BUILD ────────────────────────────────
#
# The rule, same as holger's and korp's: **run the tests of the WIDEST resolution
# in each family; BUILD the narrower rows, because what a narrower row asserts is
# a LINKAGE or feature-boundary claim, and that is a build property.**
#
# Three test steps for the build points, and the second is the one that is NOT
# obvious:
#
#   server-tests   `gunnar-server`          the widest OPEN appliance resolution
#   metered-tests  `gunnar-server-metered`  ⚠ NOT subsumed by server-tests, and
#                  not subsuming it either. `-p gunnar-server` splits its suite
#                  on `metered` in BOTH directions:
#                    tests/open_build_speaks_no_money.rs:221 and :675 gate two of
#                    that file's six tests on `#[cfg(not(feature = "metered"))]`
#                    — they compile only in the OPEN build;
#                    tests/disk_headroom.rs:53/57 carries both arms.
#                  Neither resolution can run the other's, so declaring one would
#                  leave a named set of tests compiled by nobody. The two `-thin`
#                  rows ARE subsumed: they differ from their roots by
#                  `wg-client`, and no test in the package is gated on it.
#   cli-tests      `gunnar-cli` = the product's `client,control`; the package's
#                  tests turn `server`, `engine-gix` and `full` on through its
#                  dev-dependency on itself, so this is the widest `-p gunnar-cli`
#                  surface there is. (`gunnar-solo` and `gunnar-service` were the
#                  rows here until GUN-PRODUCTS, 2026-10-30: both are `gunnar-cli`
#                  with features now, and `systemd` is compiled by `cargo xtask
#                  ci`'s `systemd-check` and `systemd-tests` steps.)
#
# ── WHAT IS DELIBERATELY NOT DECLARED, AND WHY (measured, not guessed) ────────
#
# **1. The 10 packaging rows.** `gunnar-image` (container), `gunnar-appliance-iso`
# (iso) and the eight installers (`gunnar-installer{,-open,-gce,-aws,-azure,
# -metered,-metered-aws,-metered-azure}`) get no step. (`gunnar-twin` is gone: a twin installs the primary's own medium and gets its role over the wire, `Bootstrap.BecomeTwin`.)
#
# ⚠ The OLD file's reason for this is now WRONG and must not be copied forward.
# It said *"nothing in dwarves calls `nornir_build_thing::assemble`"*, and that
# was true in August. It is not true now: `RealRunner::run` forks on the row's
# own kind and sends a non-cargo kind to `RealRunner::assemble_one` →
# `crate::assemble_cell` (runner.rs:596-600), the same door the ⚒ Build Thing
# pane uses, with the real bollard / pure-Rust-ISO / korp-installer builders
# injected. The route EXISTS.
#
# The reason they are still absent is a different and smaller one: the medium
# rows take an input this pipeline cannot obtain. `[build.iso.kernel_donor]`
# declares `repo = "tunnr", kernel = "tunnr-kernel"` — the ISO's kernel comes
# from ANOTHER REPOSITORY, and every installer chains that ISO. brokkr has a word
# for a cross-project input (`[[pipeline.depends]]`, `ArtifactDep`), but it is
# PARSED AND NEVER READ: outside `brokkr/model.rs` the only use of
# `ArtifactDep::artifacts` is `brokkr/artifact.rs:61`, and no planner or engine
# consults `pipeline.depends` at all. Declaring the chain here would therefore be
# decoration, and declaring the ISO step without it would red on a missing donor
# — a true failure about the wrong subject.
#
# Little is lost meanwhile, and it is the same argument the old file made: the
# container, the ISO and all eight installers take their payload from
# `gunnar-server` (`[[build.container.layers]] from_variant`, `[[build.iso]]
# from_variant`, `[[build.installer]] variant`), and that row is built AND tested
# below. The compilation every medium depends on is covered; only the packaging
# is not. The day `depends` resolves a donor, the steps belong here.
#
# **2. `artifacts = [...]` globs on the steps.** holger's file declares 14 of
# them; korp's declares none, and korp is right. `Step::artifacts` is parsed
# (brokkr/model.rs) and read by nothing — grep for it across
# `brokkr/{plan,dag,engine}.rs` returns zero. What actually flows downstream is
# `StepOutcome::with_artifact_files`, built from the RUNNER's own output list
# (runner.rs:618). A glob here would be a line that looks load-bearing and is
# not. It also could not be right on this box: builds land in
# `$CARGO_TARGET_DIR=/home/rickard/cargo-target`, not `<repo>/target`.
#
# **3. Most of the workspace, and that is not this file's fault.**
# `RunKind::Tests` is PACKAGE-SCOPED by construction — it inherits the variant's
# `cargo_args()`, which carries `-p <package>` — so a `tests` step runs exactly
# one package's suite. gunnar's 19 variants name TWO packages, `gunnar-server`
# and `gunnar-cli`. Everything in the other 17 crates is out of reach of any step
# this file could declare. Fixing that means `kind = "lib"` rows in
# `.nornir/build-variants.toml` (⚠ a `kind = "lib"` row with no `linkage` is
# REFUSED — an omitted linkage reads as `fat`; it wants
# `linkage = "not-applicable"`), which is a change to the delivery axis and
# belongs to a ruling, not to this pipeline.
#
# **4. `bench/`** — permanently, for a reason that is not a gap: it is a DETACHED
# crate with its own `[workspace]`, not a member, and no `[[build.variant]]`
# names it. There is nothing for a step to run. `[build.allow_unbuilt] stages`
# says the same thing in the recipe's own words.

[pipeline]
name = "gunnar-ci"

[[pipeline.trigger]]              # fire when the poll sweep sees HEAD move
kind = "poll"
events = ["push"]

# …and nightly at 03:40 — gunnar's original slot, kept: off nornir 03:00,
# skidbladnir 03:20, holger 04:00, korp 04:40.
[[pipeline.trigger]]
kind = "cron"
cron = "40 3 * * *"

# ══ the appliance: the four musl rows every medium chains ══════════════════════
#
# `--no-default-features --target x86_64-unknown-linux-musl --profile release`,
# `-p gunnar-server`. First, and alone in its stage, because everything else in
# the repository is measured against it: xtask's `appliance` guard asserts a
# named set of crates is ABSENT from this exact resolution, and the container,
# the ISO and all eight installers take their payload from `gunnar-server`.
#
# ⚠ All four rows compile the same package to the same bin name, so they write
# the SAME path — `<target-dir>/x86_64-unknown-linux-musl/release/gunnar-server`
# — and each overwrites the last. That is a property of the recipe, not of this
# file; it is why the steps are ordered narrow → wide and why nothing here
# publishes a path (see NOT DECLARED #2).
[[pipeline.stage]]
name = "appliance"

[[pipeline.stage.step]]
name = "server-thin"                     # the family root: no wg-client, no metered
run = { variant = "gunnar-server-thin" }

[[pipeline.stage.step]]
name = "server"                          # ⭐ the shipped open appliance (+ wg-client)
needs = ["server-thin"]
run = { variant = "gunnar-server" }

[[pipeline.stage.step]]
name = "metered-thin"                    # server-thin + metered
needs = ["server-thin"]
run = { variant = "gunnar-server-metered-thin" }

[[pipeline.stage.step]]
name = "metered"                         # ⭐ the hosted appliance (wg-client + metered)
needs = ["server"]
run = { variant = "gunnar-server-metered" }

# ══ the client half: -p gunnar-cli on the host triple ══════════════════════════
#
# The products (GUN-PRODUCTS, 2026-10-30): `cargo install gunnar` and `cargo install
# gunnar-cli`, each its own row. Neither waits on the appliance: they share no build
# with it. (`gunnar-ui` is built from the gunnar-ui checkout, by its own pipeline.)
[[pipeline.stage]]
name = "clients"

[[pipeline.stage.step]]
name = "server-product"                  # `gunnar` — cargo install gunnar
run = { variant = "gunnar" }

[[pipeline.stage.step]]
name = "control-client"                  # `gunnar-cli` — git, crypt and the control plane
run = { variant = "gunnar-cli" }

# ══ the suites, at the SAME features the artifacts are built with ══════════════
[[pipeline.stage]]
name = "test"

[[pipeline.stage.step]]
name = "server-tests"                    # -p gunnar-server, the OPEN resolution
needs = ["server"]
run = { tests = "gunnar-server" }

[[pipeline.stage.step]]
name = "metered-tests"                   # the `metered` arm — see the rule above
needs = ["metered"]
run = { tests = "gunnar-server-metered" }

[[pipeline.stage.step]]
name = "cli-tests"                       # the widest -p gunnar-cli surface
needs = ["server", "control-client"]
run = { tests = "gunnar-cli" }