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

nordisk/dwarves

crates/dwarves-slsa/scripts/relbuild-user.sh

#!/bin/bash
# relbuild-user.sh — make dwarf's release-build account (lane RELBUILD-ACCT, owner 2026-10-29).
#
#   sudo bash relbuild-user.sh <dwarves binary>
#
# Run it as ROOT on dwarf's HOST (not inside the claude container). Run it again at any
# time: it is idempotent and prints what it did. Run it again with a new binary to
# upgrade the release runner.
#
# <dwarves binary> is the release runner to install: a path on the host, or a path inside
# the claude container (it is then pulled with `incus file pull`). Either `dwarves` built
# with the `release-build` feature or the standalone `dwarves-slsa`; both take
# `release-build serve`.
#
# What it does:
#  1. the account `relbuild`: a system account, no password login, no sudo or other
#     admin group, no SSH login, its own subuid/subgid range (rootless podman), linger on;
#  2. the binary at /usr/local/lib/dwarves-release/dwarves, 0755 root:root (only root can
#     change it); prints its sha256;
#  3. /srv/relbuild/runs (relbuild's; the lanes can read it, not write it) and
#     /srv/relbuild/inbox (the lanes write requests there; the runner reads each one as
#     data: build release X at commits Y, never code); both mounted into the claude
#     container (runs read-only);
#  4. relbuild's git access: anonymous for public repositories; its own SSH key for the
#     private ones (printed: the owner adds it on gunnar.rs as a read-only key);
#  5. the ci4 toolchain image in relbuild's podman store, checked against the pin;
#  6. /etc/dwarves-release/runner.toml, the system unit dwarves-release-podman.service
#     (relbuild's own rootless podman API: every sealed container of a release pipeline
#     runs there, lane RELBUILD-RUNA) and the system unit dwarves-release-runner.service,
#     which runs `dwarves release-build serve` as relbuild over the inbox.
#
# It never touches the YubiKey: signing stays on the owner's machine with
# script-sign-release.sh.
#
# Environment (all optional): CT (the incus container, default claude), CT_USER (rickard),
# CI4_TAR (the ci4 OCI archive: a host path or a path in the container,
# default /home/rickard/ci-images/ci4-53fec13a5c1c7f2c.oci.tar), INBOX_UID / INBOX_GID (the
# host ids that own the inbox; default: the container user's), NO_INCUS_MOUNTS=1 (do not
# add the container mounts), JOBS (cargo jobs per build, default 4).
set -euo pipefail

ACCOUNT=relbuild
HOME_DIR=/var/lib/relbuild
INSTALL_DIR=/usr/local/lib/dwarves-release
BIN="$INSTALL_DIR/dwarves"
SRV=/srv/relbuild
RUNS="$SRV/runs"
INBOX="$SRV/inbox"
CONF_DIR=/etc/dwarves-release
CONF="$CONF_DIR/runner.toml"
UNIT=dwarves-release-runner.service
UNIT_FILE="/etc/systemd/system/$UNIT"
PODMAN_UNIT=dwarves-release-podman.service
PODMAN_UNIT_FILE="/etc/systemd/system/$PODMAN_UNIT"
ENGINE_DIR=/run/dwarves-release
ENGINE="unix://$ENGINE_DIR/podman.sock"
IMAGE_NAME=localhost/nordisk-cargo-toolchain
IMAGE_DIGEST=sha256:875a7a325dfe7f6b838535d085a2a502e8ad250076c947f6d023b1e224f03334
IMAGE="$IMAGE_NAME@$IMAGE_DIGEST"
CT=${CT:-claude}
CT_USER=${CT_USER:-rickard}
CI4_TAR=${CI4_TAR:-/home/rickard/ci-images/ci4-53fec13a5c1c7f2c.oci.tar}
JOBS=${JOBS:-4}
# git.gunnar.rs's SSH host key (pinned; the same line as the lanes' known_hosts).
GUNNAR_HOSTKEY='[git.gunnar.rs]:2222 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETubAK37Kf/dTbNeNELXB/aYoJnVZtIWIplpSzQ8IQP'
SUBID_COUNT=65536
# Above incus's default map (1000000 + 1000000000), so no container shares relbuild's ids.
SUBID_FLOOR=2000000000

say() { echo "==> $*"; }
ok() { echo "    ok   $*"; }
did() { echo "    did  $*"; }
die() { echo "RED  $*" >&2; exit 1; }

[ "$(id -u)" = 0 ] || die "run as root on dwarf's host: sudo bash $0 <dwarves binary>"
[ $# -eq 1 ] || die "usage: sudo bash $0 <dwarves binary (a host path, or a path in the $CT container)>"
SRC_BIN=$1
if command -v systemd-detect-virt >/dev/null && systemd-detect-virt -cq; then
  die "this is a container ($(systemd-detect-virt -c)); run it on dwarf's HOST"
fi
have_incus() { command -v incus >/dev/null && incus info "$CT" >/dev/null 2>&1; }
# incus exec, never reading this shell's stdin.
ix() { incus exec "$@" </dev/null; }
# The host id of a container id (incus's idmap; python3, else the container rootfs owner).
host_id() { # kind(uid|gid) id
  local key; [ "$1" = uid ] && key=Isuid || key=Isgid
  if command -v python3 >/dev/null; then
    incus config get "$CT" volatile.idmap.current | python3 -c 'import json,sys
k,i=sys.argv[1],int(sys.argv[2])
print(next(x["Hostid"]+i-x["Nsid"] for x in json.load(sys.stdin) if x[k] and x["Nsid"]<=i<x["Nsid"]+x["Maprange"]))' "$key" "$2"
  else
    local f
    for f in /var/lib/incus/storage-pools/*/containers/"$CT"/rootfs/home/"$CT_USER"; do
      [ -d "$f" ] || die "no python3 and no rootfs for $CT: set INBOX_UID and INBOX_GID"
      if [ "$1" = uid ]; then stat -c %u "$f"; else stat -c %g "$f"; fi
      return
    done
  fi
}
cu=""; cg=""
if have_incus; then
  cu=$(ix "$CT" -- id -u "$CT_USER")
  cg=$(ix "$CT" -- id -g "$CT_USER")
fi
as_relbuild() { runuser -u "$ACCOUNT" -- env HOME="$HOME_DIR" XDG_RUNTIME_DIR="/run/user/$RUID" "$@"; }

# ── 0. tools ────────────────────────────────────────────────────────────────────────
say "tools"
need=()
for t in podman:podman newuidmap:uidmap git:git git-lfs:git-lfs curl:curl ssh:openssh-client ssh-keygen:openssh-client sha256sum:coreutils; do
  command -v "${t%%:*}" >/dev/null || need+=("${t#*:}")
done
if [ ${#need[@]} -gt 0 ]; then
  mapfile -t need < <(printf '%s\n' "${need[@]}" | sort -u)
  DEBIAN_FRONTEND=noninteractive apt-get install -y "${need[@]}" >/dev/null
  did "apt-get install ${need[*]}"
else
  ok "podman, uidmap, git, git-lfs, curl, ssh, sha256sum"
fi

# ── 1. the account ──────────────────────────────────────────────────────────────────
say "account $ACCOUNT"
if id "$ACCOUNT" >/dev/null 2>&1; then
  ok "exists: $(id "$ACCOUNT")"
else
  useradd --system --create-home --home-dir "$HOME_DIR" --shell /usr/sbin/nologin --user-group \
    --comment "dwarves release runner (SLSA L3 release builds)" "$ACCOUNT"
  did "useradd --system $ACCOUNT (home $HOME_DIR, shell nologin)"
fi
RUID=$(id -u "$ACCOUNT")
[ "$(getent passwd "$ACCOUNT" | cut -d: -f7)" = /usr/sbin/nologin ] || usermod -s /usr/sbin/nologin "$ACCOUNT"
passwd -l "$ACCOUNT" >/dev/null 2>&1 || usermod -L "$ACCOUNT"
ok "password locked, shell /usr/sbin/nologin"
for g in sudo admin wheel adm root docker lxd incus incus-admin libvirt kvm disk shadow; do
  if id -nG "$ACCOUNT" | tr ' ' '\n' | grep -qx "$g"; then
    gpasswd -d "$ACCOUNT" "$g" >/dev/null
    did "removed $ACCOUNT from group $g"
  fi
done
ok "groups: $(id -nG "$ACCOUNT")"
if grep -rqsw "$ACCOUNT" /etc/sudoers /etc/sudoers.d/ 2>/dev/null; then
  die "/etc/sudoers or /etc/sudoers.d names $ACCOUNT: remove that line (the release account has no sudo)"
fi
ok "no sudoers entry"
install -d -o "$ACCOUNT" -g "$ACCOUNT" -m 0750 "$HOME_DIR"
install -d -o "$ACCOUNT" -g "$ACCOUNT" -m 0700 "$HOME_DIR/.ssh"
if [ -e "$HOME_DIR/.ssh/authorized_keys" ]; then
  rm -f "$HOME_DIR/.ssh/authorized_keys"
  did "removed $HOME_DIR/.ssh/authorized_keys (no SSH login)"
fi
ok "no SSH login (no authorized_keys, shell nologin)"

# subordinate ids, above every range in use (and above incus's map).
next_subid() {
  awk -F: -v floor="$SUBID_FLOOR" -v step="$SUBID_COUNT" '
    NF >= 3 { e = $2 + $3; if (e > m) m = e }
    END { if (m < floor) m = floor; printf "%.0f\n", int((m + step - 1) / step) * step }' /etc/subuid /etc/subgid
}
touch /etc/subuid /etc/subgid
if grep -q "^$ACCOUNT:" /etc/subuid && grep -q "^$ACCOUNT:" /etc/subgid; then
  ok "subuid $(grep "^$ACCOUNT:" /etc/subuid | head -1), subgid $(grep "^$ACCOUNT:" /etc/subgid | head -1)"
else
  s=$(next_subid)
  e=$((s + SUBID_COUNT - 1))
  grep -q "^$ACCOUNT:" /etc/subuid || { usermod --add-subuids "$s-$e" "$ACCOUNT"; did "subuid $s-$e"; }
  grep -q "^$ACCOUNT:" /etc/subgid || { usermod --add-subgids "$s-$e" "$ACCOUNT"; did "subgid $s-$e"; }
fi

if [ "$(loginctl show-user "$ACCOUNT" -p Linger --value 2>/dev/null || true)" = yes ]; then
  ok "linger on"
else
  loginctl enable-linger "$ACCOUNT"
  did "loginctl enable-linger $ACCOUNT"
fi
for _ in $(seq 1 30); do [ -d "/run/user/$RUID" ] && break; sleep 1; done
[ -d "/run/user/$RUID" ] || die "/run/user/$RUID did not appear (user@$RUID.service)"
ok "/run/user/$RUID"

# ── 2. the binary ───────────────────────────────────────────────────────────────────
say "release runner binary"
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
if [ -f "$SRC_BIN" ]; then
  cp "$SRC_BIN" "$TMP/dwarves"
  ok "from the host: $SRC_BIN"
elif have_incus && incus file pull "$CT$SRC_BIN" "$TMP/dwarves" 2>/dev/null; then
  ok "from the $CT container: $SRC_BIN"
else
  die "$SRC_BIN: not on the host, and not in the $CT container"
fi
[ "$(head -c4 "$TMP/dwarves" | od -An -c | tr -d ' ')" = '177ELF' ] || die "$SRC_BIN is not an ELF binary"
NEW_SHA=$(sha256sum "$TMP/dwarves" | cut -d' ' -f1)
install -d -o root -g root -m 0755 "$INSTALL_DIR"
OLD_SHA=$( [ -f "$BIN" ] && sha256sum "$BIN" | cut -d' ' -f1 || true)
if [ "$OLD_SHA" = "$NEW_SHA" ]; then
  ok "$BIN is already this binary"
else
  install -o root -g root -m 0755 "$TMP/dwarves" "$BIN.new"
  mv -f "$BIN.new" "$BIN"
  did "installed $BIN (0755 root:root)"
fi
chown root:root "$BIN" && chmod 0755 "$BIN"
BIN_SHA=$(sha256sum "$BIN" | cut -d' ' -f1)
echo "    sha256 $BIN_SHA  $BIN"
as_relbuild "$BIN" release-build help >/dev/null || die "$BIN release-build help failed as $ACCOUNT (no release-build mode in this binary?)"
ok "runs as $ACCOUNT, has release-build"

# ── 3. runs and inbox ───────────────────────────────────────────────────────────────
say "runs and inbox"
install -d -o root -g root -m 0755 "$SRV"
install -d -o "$ACCOUNT" -g "$ACCOUNT" -m 0755 "$RUNS"
install -d -o "$ACCOUNT" -g "$ACCOUNT" -m 0755 "$RUNS/requests"
ok "$RUNS: $ACCOUNT's, 0755 (everyone reads, only $ACCOUNT writes)"
if [ -z "${INBOX_UID:-}" ]; then
  if have_incus; then
    INBOX_UID=$(host_id uid "$cu")
    INBOX_GID=$(host_id gid "$cg")
    ok "the inbox belongs to $CT_USER in $CT: host uid $INBOX_UID gid $INBOX_GID"
  else
    INBOX_UID=$(id -u "$CT_USER")
    INBOX_GID=$(id -g "$CT_USER")
    ok "no $CT container: the inbox belongs to the host's $CT_USER ($INBOX_UID)"
  fi
fi
INBOX_GID=${INBOX_GID:-$INBOX_UID}
install -d -o "$INBOX_UID" -g "$INBOX_GID" -m 0755 "$INBOX"
ok "$INBOX: the lanes write requests, $ACCOUNT reads them (data only)"
if as_relbuild test -w "$INBOX"; then die "$ACCOUNT can write the inbox"; fi
if [ "$INBOX_UID" != "$RUID" ] && [ "$(stat -c %u "$RUNS")" = "$INBOX_UID" ]; then die "the lanes own $RUNS"; fi
if id "$CT_USER" >/dev/null 2>&1 && runuser -u "$CT_USER" -- test -w "$RUNS"; then
  die "the host's $CT_USER can write $RUNS"
fi
ok "the lanes cannot write $RUNS"

if [ "${NO_INCUS_MOUNTS:-}" != 1 ] && have_incus; then
  add_disk() { # name source path readonly
    if incus config device get "$CT" "$1" source >/dev/null 2>&1; then
      ok "$CT has device $1 ($2 -> $3)"
    else
      incus config device add "$CT" "$1" disk source="$2" path="$3" readonly="$4" >/dev/null
      did "incus device $1: $2 -> $CT:$3 (readonly=$4)"
    fi
  }
  add_disk relbuild-runs "$RUNS" "$RUNS" true
  add_disk relbuild-inbox "$INBOX" "$INBOX" false
  if ix "$CT" --user "$cu" -- test -w "$RUNS"; then die "$CT_USER in $CT can write $RUNS"; fi
  ix "$CT" --user "$cu" -- test -r "$RUNS/requests" || die "$CT_USER in $CT cannot read $RUNS"
  ix "$CT" --user "$cu" -- test -w "$INBOX" || die "$CT_USER in $CT cannot write $INBOX"
  ok "in $CT: $CT_USER reads $RUNS (read-only) and writes $INBOX"
fi

# ── 4. git access ───────────────────────────────────────────────────────────────────
say "git access"
printf '%s\n' "$GUNNAR_HOSTKEY" >"$HOME_DIR/.ssh/known_hosts"
cat >"$HOME_DIR/.ssh/config" <<EOF
# written by relbuild-user.sh: only git.gunnar.rs, its pinned host key, relbuild's own key
Host git.gunnar.rs
  Port 2222
  IdentityFile $HOME_DIR/.ssh/id_ed25519
  IdentitiesOnly yes
  StrictHostKeyChecking yes
  UserKnownHostsFile $HOME_DIR/.ssh/known_hosts
  BatchMode yes
EOF
if [ ! -f "$HOME_DIR/.ssh/id_ed25519" ]; then
  as_relbuild ssh-keygen -q -t ed25519 -N '' -C "relbuild@dwarf (release runner, read-only)" -f "$HOME_DIR/.ssh/id_ed25519"
  did "made relbuild's SSH key"
fi
chown -R "$ACCOUNT:$ACCOUNT" "$HOME_DIR/.ssh"
chmod 0600 "$HOME_DIR/.ssh/config" "$HOME_DIR/.ssh/known_hosts"
if as_relbuild git ls-remote ssh://anonymous@git.gunnar.rs:2222/nordisk/dwarves.git HEAD >/dev/null 2>&1; then
  ok "anonymous clone of public repositories works"
else
  echo "    WARN anonymous git ls-remote of nordisk/dwarves failed (network?)"
fi
if as_relbuild git ls-remote ssh://git@git.gunnar.rs:2222/mikronova/tunnr.git HEAD >/dev/null 2>&1; then
  ok "relbuild's key reads mikronova/tunnr"
else
  echo "    TODO the private repositories (mikronova/tunnr, which Holger's release pins) need relbuild's key"
  echo "         on gunnar.rs with READ access only (never write). The public key:"
  echo "         $(cat "$HOME_DIR/.ssh/id_ed25519.pub")"
fi

# ── 5. the ci4 image ────────────────────────────────────────────────────────────────
say "ci4 image $IMAGE"
if as_relbuild podman image exists "$IMAGE" 2>/dev/null; then
  ok "already in $ACCOUNT's podman store"
else
  tar_f="$HOME_DIR/ci4.oci.tar"
  rm -f "$tar_f"
  if [ -f "$CI4_TAR" ]; then
    install -o "$ACCOUNT" -g "$ACCOUNT" -m 0600 "$CI4_TAR" "$tar_f"
    ok "the OCI archive from the host: $CI4_TAR"
  elif have_incus && ix "$CT" -- test -f "$CI4_TAR"; then
    ix "$CT" -- cat "$CI4_TAR" | as_relbuild sh -c "cat > '$tar_f'"
    ok "the OCI archive from $CT: $CI4_TAR"
  elif have_incus; then
    # podman save as the lanes' user, in the container.
    ix "$CT" --user "$cu" --group "$cg" --env HOME="/home/$CT_USER" --env XDG_RUNTIME_DIR="/run/user/$cu" \
      -- podman save --format oci-archive "$IMAGE" | as_relbuild sh -c "cat > '$tar_f'"
    ok "podman save as $CT_USER in $CT"
  else
    die "no ci4 archive: set CI4_TAR to the OCI archive"
  fi
  idx=$(tar -xOf "$tar_f" index.json 2>/dev/null || true)
  case "$idx" in
    *"\"digest\":\"$IMAGE_DIGEST\""*) ok "the archive's manifest is $IMAGE_DIGEST" ;;
    *) echo "    note: the archive's index does not name $IMAGE_DIGEST; the store's digest decides below" ;;
  esac
  as_relbuild podman load -q -i "$tar_f" >/dev/null
  rm -f "$tar_f"
  did "podman load as $ACCOUNT"
fi
as_relbuild podman image exists "$IMAGE" || die "after the load, $ACCOUNT's store has no $IMAGE: the digest does not match the pin"
got=$(as_relbuild podman image inspect --format '{{.Digest}}' "$IMAGE")
[ "$got" = "$IMAGE_DIGEST" ] || die "the image's digest $got is not the pin $IMAGE_DIGEST"
ok "digest $got = the pin"

# ── 6. config and unit ──────────────────────────────────────────────────────────────
say "config $CONF and unit $UNIT"
install -d -o root -g root -m 0755 "$CONF_DIR"
cat >"$CONF.new" <<EOF
# dwarves release-build serve: release mode (written by relbuild-user.sh; root's file).
account = "$ACCOUNT"
refuse_accounts = ["$CT_USER", "rickard", "root"]
inbox = "$INBOX"
runs = "$RUNS"
image = "$IMAGE"
binary = "$BIN"
binary_sha256 = "$BIN_SHA"
forge = "ssh://anonymous@git.gunnar.rs:2222"
engine = "$ENGINE"
poll_seconds = 30
jobs = $JOBS

[org_forge]
mikronova = "ssh://git@git.gunnar.rs:2222"
EOF
chown root:root "$CONF.new" && chmod 0644 "$CONF.new"
changed=0
if cmp -s "$CONF.new" "$CONF"; then rm -f "$CONF.new"; ok "$CONF unchanged"; else mv -f "$CONF.new" "$CONF"; did "wrote $CONF"; changed=1; fi
cat >"$PODMAN_UNIT_FILE.new" <<EOF
# written by relbuild-user.sh (lane RELBUILD-RUNA)
[Unit]
Description=podman API of the dwarves release runner (rootless, as $ACCOUNT): the sealed containers of every release pipeline
After=network-online.target user@$RUID.service
Wants=network-online.target user@$RUID.service

[Service]
Type=simple
User=$ACCOUNT
Group=$ACCOUNT
WorkingDirectory=$HOME_DIR
Environment=HOME=$HOME_DIR
Environment=XDG_RUNTIME_DIR=/run/user/$RUID
RuntimeDirectory=dwarves-release
RuntimeDirectoryMode=0700
UMask=0077
Nice=10
ExecStart=/usr/bin/podman --cgroup-manager=cgroupfs --events-backend=file system service --time=0 $ENGINE
Restart=on-failure
RestartSec=30

[Install]
WantedBy=multi-user.target
EOF
podman_changed=0
if cmp -s "$PODMAN_UNIT_FILE.new" "$PODMAN_UNIT_FILE"; then rm -f "$PODMAN_UNIT_FILE.new"; ok "$PODMAN_UNIT_FILE unchanged"; else mv -f "$PODMAN_UNIT_FILE.new" "$PODMAN_UNIT_FILE"; did "wrote $PODMAN_UNIT_FILE"; changed=1; podman_changed=1; fi
cat >"$UNIT_FILE.new" <<EOF
# written by relbuild-user.sh (lane RELBUILD-ACCT; RELBUILD-RUNA: a release is a Runa pipeline on $PODMAN_UNIT)
[Unit]
Description=dwarves release runner (SLSA L3 release builds from $INBOX, as $ACCOUNT)
After=network-online.target user@$RUID.service $PODMAN_UNIT
Wants=network-online.target user@$RUID.service
Requires=$PODMAN_UNIT

[Service]
Type=simple
User=$ACCOUNT
Group=$ACCOUNT
WorkingDirectory=$HOME_DIR
Environment=HOME=$HOME_DIR
Environment=XDG_RUNTIME_DIR=/run/user/$RUID
Environment=CARGO_BUILD_JOBS=$JOBS
Environment=DOCKER_HOST=$ENGINE
# the engine is relbuild's own rootless podman: container root is relbuild
Environment=DWARVES_RUNA_USER=0:0
UMask=0022
Nice=10
ProtectSystem=full
ExecStart=$BIN release-build serve --config $CONF
Restart=on-failure
RestartSec=60

[Install]
WantedBy=multi-user.target
EOF
if cmp -s "$UNIT_FILE.new" "$UNIT_FILE"; then rm -f "$UNIT_FILE.new"; ok "$UNIT_FILE unchanged"; else mv -f "$UNIT_FILE.new" "$UNIT_FILE"; did "wrote $UNIT_FILE"; changed=1; fi
[ "$OLD_SHA" = "$NEW_SHA" ] || changed=1
systemctl daemon-reload
if ! systemctl is-enabled -q "$PODMAN_UNIT"; then systemctl enable -q "$PODMAN_UNIT"; did "enabled $PODMAN_UNIT"; fi
if [ "$podman_changed" = 1 ] || ! systemctl is-active -q "$PODMAN_UNIT"; then systemctl restart "$PODMAN_UNIT"; did "restarted $PODMAN_UNIT"; fi
for _ in $(seq 1 30); do [ -S "$ENGINE_DIR/podman.sock" ] && break; sleep 1; done
[ -S "$ENGINE_DIR/podman.sock" ] || { journalctl -u "$PODMAN_UNIT" -n 20 --no-pager; die "$PODMAN_UNIT made no socket at $ENGINE_DIR/podman.sock"; }
ok "$PODMAN_UNIT: $ENGINE"
timeout 120 runuser -u "$ACCOUNT" -- env HOME="$HOME_DIR" XDG_RUNTIME_DIR="/run/user/$RUID" \
  "$BIN" release-build serve --check --config "$CONF" | sed 's/^/    /'
[ "${PIPESTATUS[0]}" = 0 ] || die "serve --check as $ACCOUNT failed (above)"
ok "serve --check as $ACCOUNT: account, binary, runs directory and inbox pass"
if id "$CT_USER" >/dev/null 2>&1 && timeout 120 runuser -u "$CT_USER" -- "$BIN" release-build serve --check --config "$CONF" >/dev/null 2>&1; then
  die "serve --check passed as $CT_USER: release mode must refuse the lanes' account"
fi
if timeout 120 "$BIN" release-build serve --check --config "$CONF" >/dev/null 2>&1; then
  die "serve --check passed as root: release mode must refuse root"
fi
ok "serve refuses $CT_USER and root"
if ! systemctl is-enabled -q "$UNIT"; then systemctl enable -q "$UNIT"; did "enabled $UNIT"; fi
if [ "$changed" = 1 ] || ! systemctl is-active -q "$UNIT"; then systemctl restart "$UNIT"; did "restarted $UNIT"; fi
sleep 2
systemctl is-active -q "$UNIT" || { journalctl -u "$UNIT" -n 20 --no-pager; die "$UNIT is not active"; }
ok "$UNIT active"

echo
echo "DONE  release runner: $ACCOUNT (uid $RUID), $BIN sha256 $BIN_SHA"
echo "      requests: $INBOX (dwarves release-build request ...); runs: $RUNS; status: $RUNS/requests/"
echo "      log: journalctl -u $UNIT -f"
echo "      the YubiKey was not touched: sign with script-sign-release.sh on your machine."