box/.github/workflows/ci.yml
dan-claude-bot 82f7f9d51a test+ci: real installs driven offline, uninstall proven to zero residue
test/cli.sh drives REAL installer runs via BOX_INSTALL_SOURCE (temp
BOX_HOME/BOX_BIN, a fabricated second version for upgrades): fresh install
lands versioned, --version answers through the chain, side-by-side installs,
same-version no-op, BOX_REINSTALL, the 0.6.0 flat-tree migration, stale-
symlink healing, single-version and full uninstalls asserted to zero residue
(a planted survivor makes it scream INCOMPLETE), working-tree refusals, and
byte-identity of the existing_boxes copies. The rehearsal job now installs
via install.sh itself — CI proves the installer under review — and ends with
the uninstall drill: revoke --purge, teardown --yes, box uninstall, then
assert nothing is left (networks, profiles, nft, units, files, symlinks).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 16:02:37 +00:00

99 lines
4.9 KiB
YAML

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
# 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: 30
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
- 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"
sudo BOX_YES=1 /usr/local/bin/box teardown-host
sudo BOX_YES=1 /usr/local/bin/box uninstall --all
# 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).