# sando Home-rolled CI/CD controller for the MNW server. Axum daemon (`sandod`) + ratatui TUI (`sando`). Gates a tiered deploy flow: ``` git push mm -> MakeMachine (build + tests + migration dry-run + boot smoke) -> A (testnot.work) -> B (prod-1) -> C (prod-2) ``` Each tier's progression gates are declared in `sando.toml`. Tiers and nodes live in the TOML, not in code — adding a node or a new tier is a config edit. ## Crates | Path | Binary | Role | |------|--------|------| | `daemon/` | `sandod` | Axum daemon. Runs on the MakeMachine. Owns SQLite state, the bare git repo, and all build/gate/deploy logic. | | `tui/` | `sando` | ratatui front-end. Runs on the laptop. Talks to `sandod` over the tailnet. | ## Quickstart: localhost dev loop The MakeMachine hardware does not exist yet, so v0 runs entirely on a single host. Bare repo, releases dir, "remote" A node — everything is a local directory. ```bash # 1. Build both binaries. cd MNW/sando/daemon && cargo build cd ../tui && cargo build # 2. Create a workspace + config. mkdir -p /tmp/sando-dev cat > /tmp/sando-dev/daemon.toml < /tmp/sando-dev/sando.toml </server`, then runs the MM tier's gates. On green, MM's `tier_state` advances. Promote with: ```bash curl -X POST http://127.0.0.1:7766/promote/a \ -H 'Content-Type: application/json' \ -d '{"version":"0.8.2"}' ``` ## API | Method | Path | Body | Purpose | |--------|------|------|---------| | GET | `/state` | — | Tier list + current/previous version + last gate outcomes | | POST | `/rebuild` | `{sha?: string}` | Force a build; if `sha` is absent, resolves the configured deploy branch. Aborts any in-flight build (latest wins). | | POST | `/promote/{tier}` | `{version?, hotfix?, reset_burn_in?}` | Verify predecessor gates, deploy to tier nodes, advance state. `version` defaults to the predecessor tier's `current_version`. | | POST | `/rollback/{tier}` | — | Swap `current` symlink to `previous_version` on every node in the tier | | POST | `/confirm/{tier}` | — | Insert a passing `manual_confirm` gate row for the tier's `current_version`. Replaces hand-SQL. | | POST | `/backup/fetch` | — | Pull the prod backup. Supports `file://`, `rsync://`, `ssh://user@host[:port]/path`. | | GET | `/metrics` | — | Prometheus exposition | | GET | `/events` | — | WebSocket stream of typed events (RebuildRequested, BuildStart/Ok/Failed, GateStart/Done, DeployStart/Ok/Failed, PromoteComplete, Rollback, BackupFetched, ManualConfirm, BuildAborted). | ## TUI `sando` (the TUI binary) connects to `$SANDO_DAEMON` (default `http://127.0.0.1:7766`), polls `/state` every 2s, and subscribes to `/events` over WS. Keybindings: | key | action | |-----|--------| | ↑/↓ or j/k | select tier | | p | `POST /promote/` (no body — version defaults to predecessor's current) | | R | `POST /rollback/` | | b | `POST /backup/fetch` | | c | `POST /confirm/` | | r | refresh hint (poller is already every 2s) | | q / Esc / Ctrl-C | quit | Action results show up in the events log a moment later (the actions themselves emit events from the daemon side). ## Hotfix flow `POST /promote/{tier}` accepts: - `hotfix: true` — skips the `burn_in` gate on the predecessor tier only. All other gates still apply. - `reset_burn_in: true` (default `false`) — additionally nulls `tier_state.burn_in_started_at` on the source tier, restarting the clock for whatever else is still burning in there. Use this only when the hotfix meaningfully changes the surface area under burn-in. ## v0 limitations - `migration_dry_run` requires a scratch Postgres at `scratch_db_url`. The gate drops every non-system schema on every run; do not point this at anything that matters. ## License MIT. The surrounding MNW monorepo is PolyForm-Noncommercial — sando is deliberately MIT'd because it's deploy infra, not the product.