deploy:rehearse — local systemd-container cutover rehearsal

deploy:rehearse — local systemd-container cutover rehearsal

mise run deploy:rehearse runs the real pyinfra deploy path — the same
deploy/pyinfra/v2/operations.py that cutovers beta on the VPS — against a
local systemd-enabled Docker container. BRI-428 / audit Phase B items 6–8:
the deploy path failing at seams nobody could exercise locally.

Run it

mise run deploy:rehearse                  # full run; leaves the container up
mise run deploy:rehearse -- --fresh       # rebuild + recreate from scratch
mise run deploy:rehearse -- --teardown    # remove the container after a green run
mise run deploy:rehearse -- --down        # just remove the container
mise run deploy:rehearse -- --report .agents/reports/x.md   # write the stage table to a file

Requires docker, mise (bun + node installs get bind-mounted in), uv
(pyinfra venv), zstd. Everything else is local: no Infisical, no VPS, no
network side-effects beyond the base-image pull and bun install inside the
pack stage.

What the stages prove

stage what it exercises
container image ubuntu:24.04 pinned by digest (same recipe as the pyinfra-docker CI job: install systemd, boot PID 1) + the rehearsal extras (deploy/rehearse/Dockerfile: caddy, jq, brioso/web-services users, r2 stub)
runtimes host mise’s bun/node are available inside the container (also the activate binary inside the pack)
pack scripts/pack-artifact.ts — the ONE packing implementation; build-artifact.yml calls it in --pack-only mode. Builds the same brioso-web-<sha>.tar.zst contract it ships (.output, migrations, seed-data, activate, dbtools bundles + libsql, env-keys.json) + a <artifact>.sha256 sidecar
seed container gets the on-VPS preconditions: env-file placeholder, brioso-caddy + upstream seed, r2 stub
converge-1 PYINFRA_TARGET=deploy full converge: web desired state + env-render write of deploy/rehearse/beta.env.fixture
converge-2 converge again → every op No changes (idempotency)
dry-empty PYINFRA_TARGET=drift --dry --json reports zero drifted ops
deploy-1 full cutover: flock, fetch file:// artifact, sha256 verify, extract, contract pin, squatter pre-flight, dbtools backup + drizzle migrate + seed-ingest, activate (atomic slot flip + warm gate + caddy upstream rewrite + reload), last-result.json readback
deploy-2 same-SHA redeploy — the real code flips to the other slot and re-warms (no-op-or-recutover semantics asserted)
health-gate /ready + /health return 200 sha-pinned through caddy (Host: beta.usebrioso.com) and on the active slot port
smoke smoke.py origin checks + the slot unit and brioso-caddy are active

deploy:status / deploy:logs

One code path (deploy/rehearse/diag.py), backend selected by --target:

mise run deploy:status                    # beta over SSH (default, tailnet)
mise run deploy:status -- --target prod
mise run deploy:status -- --target local  # the rehearsal container
mise run deploy:logs   -- --target local --unit brioso-beta-web-b -n 50

status prints the operator view: slot units, last-result.json fields,
/health + /ready via the edge, failed units, disk. logs tails journalctl.

What it does NOT prove

  • Env render ≠ Infisical. Rehearsal uses PYINFRA_ENV_MODE=write with the
    checked-in fixture; beta/prod CI renders .env via Infisical + check mode
    (names-only diff). Secrets-sync is BRI-427.
  • CF edge / DNS / TLS. Caddy serves plain HTTP on 127.0.0.1:80; the real
    Cloudflare path isn’t exercised.
  • SSH transport. web_local connects via @docker; SSH-specific failures
    (tailscaled, auth, host keys) stay CI-side.
  • GPU/Modal/RunPod — out of scope entirely.
  • Pack parity is by construction: BRI-449 #5 collapsed both packers into
    scripts/pack-artifact.ts; the workflow calls it in --pack-only mode
    (CI stages the env-dependent inputs) and rehearsal runs it end-to-end.

Debugging a failed stage

The container is kept running on failure (and after green runs without
--teardown):

docker exec -it brioso-deploy-rehearse bash
journalctl -u brioso-beta-web-b.service -n 50     # inside the container
cat /opt/brioso/beta/deploy/last-result.json      # what activate reported
mise run deploy:status -- --target local          # the operator view

Known host quirk: kernels with fs.protected_regular=2 (e.g. this VM) refuse
O_TRUNC on a sticky-dir file owned by another uid — even for root. The
driver rm -fs stale /tmp/brioso-web-*.tar.zst before each deploy for this
reason.