Beagle SCM
CI-001: long-running qemu CI workers (Ubuntu + FreeBSD) on the beagle wire
Beagle needs GitHub-Actions-style per-commit CI: build + ctest on Ubuntu and on
FreeBSD (the closest self-hostable stand-in for mac). The Verbs basis is
closed, so CI is an Extensions script, jab ci, not a verb. The runners are
two LONG-RUNNING qemu VMs, each an ssh-reachable host holding a persistent
clone (warm ninja cache, amortized FreeBSD boot), named like any remote:
ssh://ubuntu-ci, ssh://fbsd-ci. No Docker as architecture — Linux
containers cannot run FreeBSD; a containerized localhost is at most one runner
flavor later. Pull-model first; push-into blocks on GIT-022.
Context
- The verb basis is fixed and orthogonal (Verbs); new behaviour ships as a
be/ extension run by jab (Extensions), never as a verb.
- Docker only covers the Ubuntu leg (containers share the Linux kernel);
FreeBSD needs a qemu/bhyve VM or real hardware — so the runner abstraction
is "an ssh-reachable host", not a container framework.
- Push-into a VM needs the ssh-remote receive-pack serve arm, which does not
exist yet — GIT-022; local-exec
receivePack landed via POST-028.
- The worker loop in /meta/work (clone → cmake+ninja → ctest -j16 →
report) is the template; a CI run is that loop minus the code edits.
Goals
- Every trunk commit gets a green/red build+ctest verdict on Ubuntu AND
FreeBSD, published on the site (html/ + feed.rss) without hand-driving.
- Runners are disposable at the VM layer: a wedged VM is reverted/rebuilt
from its base image like any worker worktree — no pet state.
Constraints
- All bytes cross the beagle wire or ssh; NO virtio-9p/shared-mount
shortcuts — a runner stays remote-shaped so real remote hosts work same.
- Runner code lives in the
be/ submodule (beagle-ext) and versions with
it; the CI roster/spec is a greppable page in the journal tree.
- qcow2 base image + backing-file overlay per runner; revert = rm overlay.
- Runners are named by URIs in the roster; all URI composition/resolution
via the standard library (resolve_hash.js / uri), never hand-rolled.
WIP
Design decisions
- Pull model first: each VM loops
be head ssh://journal?* → on change
be get → build+test → append a result row; no inbound auth surface.
- The VM's result log is itself a beagle tree the journal side fetches —
the same wire in reverse; no scp side-channels.
- Flip to push-into (
be put/post ssh://ubuntu-ci) once GIT-022 lands;
the loop then becomes a receive-hook around the same build script.
TODOs
- 1. Spec page
meta/ci.mkd: runner roster (URIs), trees to build, ctest
targets, result-row format (commit sha, host, status, log link).
- 2. Ubuntu runner image: qcow2 base with toolchain (cmake, ninja, ragel,
compiler), jab bin, ssh key; staged checkpoints per CI-003.
- 3. FreeBSD runner image: same via pkg; PROVE jab builds+runs on FreeBSD
first — the mac-substitute load-bearing assumption.
- 4.
be/ci.js runner mode: poll head → get → cmake/ninja → ctest -j16 →
write result row + captured log into the VM's result tree.
- 5.
be/ci.js collector mode (journal side): fetch each runner's result
tree, aggregate per-commit status across runners.
- 6. Render: a make target folding CI status into html/ + feed.rss next
to the blog render; red runs get the log excerpt inline.
- 7. Red-run procedure: collector drafts a
todo/CI/ ticket body with the
log attached as evidence; the user files/approves it.
- 8. Push-into mode: runners accept
be put/post ssh://<runner> and build
on receive — blocked on GIT-022.
Blockers and bummers
- GIT-022 gates step 8 (push mode); steps 1–7 proceed pull-only.
- jab on FreeBSD is unproven (JSC build, io bindings); step 3 verifies
before any FreeBSD automation is written.
Outcome
- Open. Filed 2026-07-24 from the CI design discussion; no code yet.