name: ci on: push: branches: [main] pull_request: jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: shellcheck # -x follows `source`/`.` directives; box has no lib split today, but the # flag costs nothing and keeps the invocation identical to rig's. # globstar so a script in a new subdirectory is linted without anyone # remembering to edit this list; bin/* covers the extensionless entrypoint # (bin/box). The file list is printed so under-coverage shows up in the log. run: | shopt -s globstar files=(bin/* **/*.sh) printf 'shellcheck: %s\n' "${files[@]}" shellcheck -x "${files[@]}" - name: cli tests run: bash test/cli.sh - name: labels state-machine tests run: bash test/labels-reconcile.sh - name: release-flow tests run: bash test/release.sh # The changelog is ARMED for the next entry (#108). Its own step rather # than a line inside test/release.sh: this one asserts a fact about THIS # tree, not about the release machinery, so when it goes red the log # says which check found the drift without anyone reading a suite. - name: changelog is armed for the next entry run: bash .github/scripts/changelog-armed.sh # The multi-user rehearsal, on a REAL incus — a GitHub runner is root on a # disposable VM, which is exactly the substrate the rehearsal needs. It runs # in container mode: the tier's mechanics (grant, confinement, the network # contract, revoke) are identical for containers and VMs — the nft bridge # drop, the ACL, dns.mode=none and port_isolation all bind to boxnet, not # to the instance type. What container mode canNOT validate is the VM trust # boundary itself; that stays a real-hardware ritual (drill/RUNS.md), same # as the full drill. So: every PR proves the tier's semantics, and a # release still proves the boundary. rehearsal: runs-on: ubuntu-latest timeout-minutes: 40 steps: - uses: actions/checkout@v4 - name: install incus run: | sudo apt-get update sudo DEBIAN_FRONTEND=noninteractive apt-get install -y incus - name: global install, via install.sh itself (the #71 layout, versioned) # install.sh, not a cp -r mimic: BOX_INSTALL_SOURCE points it at this # checkout, so CI proves the INSTALLER under review — the versioned # layout, the current symlink, the PATH chain — not a hand-built # imitation of it. Setup is run explicitly in the next step, so its # output is its own CI section. run: | sudo BOX_YES=1 BOX_SKIP_SETUP_HOST=1 BOX_INSTALL_SOURCE="$GITHUB_WORKSPACE" bash install.sh # assert what landed: the layout, the chain, and that it answers readlink -f /usr/local/bin/box | grep '^/opt/box/versions/' /usr/local/bin/box --version /usr/local/bin/box versions - name: setup-host run: sudo bash /opt/box/current/host/setup-host.sh - name: doctor — the baseline is provable before anything is judged run: sudo BOX_TIER=admin bash /opt/box/current/drill/doctor.sh - name: multi-user rehearsal (criteria a-l, container mode) run: sudo BOX_MULTIUSER_REHEARSAL=1 bash /opt/box/current/drill/multiuser.sh --yes --container # The #70 round-trip, on the SAME live daemon: a box's state must # survive 'box rm' via export → import. Container mode for the same # reason the rehearsal uses it — export/import are backup mechanics # (tarball out, tarball in, re-stamp), identical across instance types; # the VM trust boundary stays a real-hardware ritual. Every assertion # is state observed AFTER the original box was deleted: the file # written pre-export, the snapshot, the boundary tag, a live agent. - name: export/import round-trip — state survives 'box rm' (#70) run: | set -eux sudo box new --name keeper --container sudo box exec keeper -- sh -c 'echo survives > /home/dev/proof' sudo box snapshot keeper pre-export sudo box down keeper sudo box export keeper /tmp/keeper.tar.gz sudo test -s /tmp/keeper.tar.gz sudo box rm keeper --force sudo box import /tmp/keeper.tar.gz --name keeper2 test "$(sudo incus config get keeper2 user.box)" = 1 sudo incus exec keeper2 -- true sudo box exec keeper2 -- cat /home/dev/proof | grep -qx survives sudo incus snapshot list keeper2 --format csv | grep -q '^pre-export' # the collision boundary, live: the name is taken, import must refuse if sudo box import /tmp/keeper.tar.gz --name keeper2; then echo 'collision was not refused'; exit 1 fi sudo box rm keeper2 --force - name: uninstall drill — revoke clean, teardown, uninstall, ZERO residue # The full-removal order, end to end on the real daemon: revoke a # granted user (--purge asserts its own absence, incl. the incus-user # state dir), tear the stack down, uninstall the tree — then assert # NOTHING survived: no networks, profiles, ACLs, nft tables, systemd # units, files or symlinks. The uninstall was flaky exactly because # nobody measured this. run: | set -x sudo useradd -m -s /bin/bash uninstdrill sudo BOX_YES=1 /usr/local/bin/box grant uninstdrill uid="$(id -u uninstdrill)" sudo BOX_YES=1 /usr/local/bin/box revoke uninstdrill --purge sudo test ! -e "/var/lib/incus/users/$uid" ! sudo incus project show "user-$uid" ! sudo incus config trust list --format csv | grep -q "incus-user-$uid" # The COMBINED verb, --force only, deliberately no BOX_YES and no # TTY: this is the exact invocation that used to die at teardown's # own prompt when consent was not forwarded (--purge-host now # passes --yes through under --force/BOX_YES). sudo /usr/local/bin/box uninstall --all --purge-host --force # zero residue: the daemon's state... ! sudo incus network show boxnet ! sudo incus profile show box-net ! sudo incus network acl show box-isolate # ...the firewall and its boot persistence... ! sudo nft list table inet box ! sudo nft list table bridge box sudo test ! -e /etc/systemd/system/box-firewall.service sudo test ! -e /usr/local/sbin/box-firewall # ...and the install itself: files AND symlinks, both name generations sudo test ! -e /opt/box sudo test ! -e /usr/local/bin/box sudo test ! -L /usr/local/bin/box sudo test ! -e /usr/local/bin/claudebox sudo test ! -L /usr/local/bin/claudebox # NOT run here: the full drill (drill/drill.sh). It rehearses the whole # surface — cold template mints, expose, migration — and wants a real host # and the better part of an hour. The rehearsal job above is the CI-shaped # slice of the same discipline: isolation claims are still tested on a real # daemon, never reasoned about (docs/box-design.md).