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=writewith the
checked-in fixture; beta/prod CI renders.envvia Infisical +checkmode
(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_localconnects 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-onlymode
(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.