forked from heavy-duty/box
CI's shellcheck sweep set globstar and globbed `bin/* **/*.sh`. globstar makes `**` descend into subdirectories, but a glob still does not MATCH a dot-prefixed name, so `**/` never entered `.github/` and three scripts were never linted: changelog-armed.sh (the #108/#110 guard that gates every PR), release-notes.sh (which produces the published release body), and labels-reconcile.sh (the label state machine). That is the entire release path, while the step's own comment promised the opposite — that a script in a new subdirectory is linted without anyone editing a list. Latent, not broken: all three pass shellcheck as-is, so this is a no-op on current code. What changes is that a regression in them would be caught. dotglob alongside globstar closes it, measured rather than assumed: it adds exactly those three and nothing else — a checkout's .git carries no *.sh, its hooks shipping as *.sample, so `**/*.sh` does not wander into it. The one-time fix is dotglob; what keeps the gap shut is the CLASS check, in the same shape as the eof_guard_sweep of #112. The sweep now compares the globbed set against `git ls-files '*.sh'` and fails naming any tracked script it does not cover, so a future dot-directory or shopt subtlety cannot silently lint a subset and pass. eof_guard_sweep carried the identical blind spot — it rebuilds the same glob — and is widened the same way. A no-op today: the three scripts set errexit, so they are in that class by construction, but none of them reads. Refs #116
164 lines
8.6 KiB
YAML
164 lines
8.6 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.
|
|
#
|
|
# dotglob is not decoration (#116): globstar makes `**` descend, but a
|
|
# glob still does not MATCH a dot-prefixed name, so `**/` never entered
|
|
# `.github/` — and the whole release path (changelog-armed.sh, which
|
|
# gates every PR, release-notes.sh, labels-reconcile.sh) went unlinted
|
|
# while the comment above told the next author it was covered.
|
|
# Measured on this tree: dotglob adds exactly those three and nothing
|
|
# else — a checkout's `.git` carries no `*.sh` (its hooks ship as
|
|
# `*.sample`), so `**/*.sh` does not wander into it.
|
|
#
|
|
# The sweep below is the CLASS check, same shape as the eof_guard_sweep
|
|
# in test/cli.sh (#112): the one-time fix is `dotglob`, but what keeps
|
|
# the gap from reopening is asserting that every TRACKED script is in
|
|
# the set actually handed to shellcheck. `git ls-files` is the authority
|
|
# on what the repo contains; if the glob ever drifts from it again —
|
|
# another dot-directory, another shopt subtlety — CI says which files
|
|
# escaped instead of quietly linting a subset and passing.
|
|
run: |
|
|
shopt -s globstar dotglob
|
|
files=(bin/* **/*.sh)
|
|
printf 'shellcheck: %s\n' "${files[@]}"
|
|
missing="$(comm -13 \
|
|
<(printf '%s\n' "${files[@]}" | sort -u) \
|
|
<(git ls-files '*.sh' | sort -u))"
|
|
if [ -n "$missing" ]; then
|
|
echo "tracked scripts the shellcheck sweep does not cover (#116):"
|
|
printf ' %s\n' $missing
|
|
exit 1
|
|
fi
|
|
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).
|