| 1 |
# Sando daemon config (production). |
| 2 |
# Install at /etc/sando/sando-daemon.toml on the Sando host. |
| 3 |
# |
| 4 |
# Deploy-API auth is NOT configured here — the bearer token lives in the |
| 5 |
# EnvironmentFile (/etc/sando/sando.env: SANDO_API_TOKEN), generated by |
| 6 |
# bootstrap-sandod-host.sh. With a non-loopback `listen` (below) the daemon |
| 7 |
# REFUSES TO START unless that token is set (CF2): a tailnet bind is reachable |
| 8 |
# by every tailnet node, so deploy authority must be gated by more than network |
| 9 |
# position. |
| 10 |
|
| 11 |
listen = "100.103.89.95:7766" # fw13 tailnet IP; bind tailnet-only, not 0.0.0.0 — requires SANDO_API_TOKEN |
| 12 |
db_path = "/srv/sando/state/sando.db" |
| 13 |
topology_path = "/etc/sando/sando.toml" |
| 14 |
# Hostname (/proc/sys/kernel/hostname) this daemon is permitted to build on. |
| 15 |
# build::run refuses to compile on any other host, so a sandod misdeployed onto |
| 16 |
# a prod node cannot build there ("never build on prod" as an invariant). |
| 17 |
# Required — there is no safe default. |
| 18 |
build_host = "fw13" |
| 19 |
workdir = "/srv/sando/work" |
| 20 |
release_root = "/srv/sando" |
| 21 |
# Name the user explicitly. libpq (psql) defaults a missing user to the OS user, |
| 22 |
# but sqlx in a minimal systemd env (no USER/LOGNAME) defaults it to "anonymous", |
| 23 |
# so an unqualified URL peer-auth-fails and crash-loops sandod at the startup |
| 24 |
# scratch preflight (--check-config does not catch it — no DB connect). See the |
| 25 |
# maintainer notes / memory reference_sando_scratch_db. |
| 26 |
scratch_db_url = "postgres:///sando_scratch?host=/var/run/postgresql&user=sando" |
| 27 |
bin_names = ["makenotwork", "mnw-admin"] |
| 28 |
logs_root = "/srv/sando/logs" |
| 29 |
# Shared cargo target dir across per-sha worktrees. Without it every /rebuild |
| 30 |
# clean-compiles a fresh worktree (~10 min) even for a 1-line diff; with it the |
| 31 |
# incremental rebuild reuses the previous sha's compiled deps (1–2 min). Safe |
| 32 |
# because builds are serialized (a new /rebuild aborts the in-flight one). The |
| 33 |
# sando user must be able to write it; cargo creates it if absent. |
| 34 |
cargo_target_dir = "/srv/sando/cargo-target" |
| 35 |
|
| 36 |
# Databases the migration_dry_run gate dry-runs, in order: restore that |
| 37 |
# database's prod dump into a scratch DB, then run the worktree's migrations on |
| 38 |
# top. `backup` names a [[backup]] entry in the topology (sando.toml). Omit the |
| 39 |
# whole key to get just the first entry, which is the historical behavior. |
| 40 |
# |
| 41 |
# The server check leaves `scratch_db` unset, so it runs against scratch_db_url |
| 42 |
# itself and leaves it in migrated state for the cargo_test gate to reuse. Every |
| 43 |
# other check must name its own database; the daemon creates it (DROP + CREATE) |
| 44 |
# at the start of the check, so a new entry owes no host bootstrap step. |
| 45 |
[[migration_check]] |
| 46 |
dir = "server/migrations" |
| 47 |
backup = "server" |
| 48 |
|
| 49 |
# multithreaded applies its own 36+ migrations at boot (multithreaded/src/main.rs, |
| 50 |
# `sqlx::migrate!()`) against its own database, so it carried the server's |
| 51 |
# exposure with none of the server's gate: an edited already-applied migration |
| 52 |
# would not fail a dry run, it would fail to boot in prod. `owner_role` is that |
| 53 |
# dump's owner — pg_dump emits `ALTER ... OWNER TO multithreaded` for every |
| 54 |
# object, and the role must exist in the scratch cluster before the restore. |
| 55 |
[[migration_check]] |
| 56 |
dir = "multithreaded/migrations" |
| 57 |
backup = "multithreaded" |
| 58 |
scratch_db = "sando_scratch_mt" |
| 59 |
owner_role = "multithreaded" |
| 60 |
|
| 61 |
# TypeScript frontends the code_smoke gate compiles (npm run build), before it |
| 62 |
# creates a database or boots anything. Both crates compile these from a build |
| 63 |
# script that reports a tsc error as a cargo::warning and lets the Rust build |
| 64 |
# succeed against whatever static/dist/ already holds — deliberately, so a type |
| 65 |
# error in a chat widget cannot stop the forum from compiling. The consequence is |
| 66 |
# that nothing downstream noticed either: the deploy rsynced the previous build's |
| 67 |
# bundle. This is the one place that failure is fatal. |
| 68 |
[[frontend_build]] |
| 69 |
dir = "server/frontend" |
| 70 |
|
| 71 |
[[frontend_build]] |
| 72 |
dir = "multithreaded/frontend" |
| 73 |
|
| 74 |
# Crates built from the same worktree/sha as the server and staged into the |
| 75 |
# release bundle as `companions/<name>`, so a service that shares the server's |
| 76 |
# contract cannot drift out of lockstep. This list only says what to BUILD and |
| 77 |
# stage; which nodes install which companion is per-node in the topology (see |
| 78 |
# the [[tier.node.companion]] blocks in sando.toml). |
| 79 |
# |
| 80 |
# mnw-cli is the public git-SSH server that proxies to /api/internal/* — a |
| 81 |
# two-month drift there is what broke git hosting during the 0.10.14 deploy. |
| 82 |
[[companion]] |
| 83 |
name = "mnw-cli" |
| 84 |
manifest_dir = "mnw-cli" |
| 85 |
bin = "mnw-cli" |
| 86 |
|
| 87 |
# multithreaded (forums.makenot.work) runs on alpha-west-1, the same host as the |
| 88 |
# server and the same architecture, so the build host can compile it. Replaces |
| 89 |
# multithreaded/deploy/deploy-hetzner.sh, which cross-compiled on macOS via |
| 90 |
# cargo zigbuild and scp'd to root@ — against the native-build rule, and the |
| 91 |
# real deploy path until 2026-08-02. |
| 92 |
# |
| 93 |
# One file to install: mt embeds its own static/ tree |
| 94 |
# (multithreaded/src/static_assets.rs) rather than serving it from a directory |
| 95 |
# beside the binary, which is what makes it fit a mechanism that ships exactly |
| 96 |
# one file per companion. |
| 97 |
# |
| 98 |
# NOT a `bin_names` entry — that list is the SERVER crate's bin targets. |
| 99 |
[[companion]] |
| 100 |
name = "multithreaded" |
| 101 |
manifest_dir = "multithreaded" |
| 102 |
bin = "multithreaded" |
| 103 |
|
| 104 |
# Crates the cargo_test gate runs, in order; the gate stops at the first red one. |
| 105 |
# This list used to be absent here, which means the default applied — a single |
| 106 |
# `server` entry — so everything else in the repo shipped ungated, the companions |
| 107 |
# installed onto prod-1 included. A target with no Cargo.toml in the sha being |
| 108 |
# built is skipped with a warning rather than failing, so this list can describe |
| 109 |
# the tip and still bisect. |
| 110 |
# |
| 111 |
# scratch_db exports DATABASE_URL/TEST_DATABASE_URL. Only crates whose sqlx |
| 112 |
# macros type-check against a live DB need it; the rest ship offline `.sqlx` data |
| 113 |
# and are left in offline mode. |
| 114 |
[[test_target]] |
| 115 |
dir = "server" |
| 116 |
features = ["fast-tests"] |
| 117 |
scratch_db = true |
| 118 |
|
| 119 |
# Companion: ships to prod-1, so it is gated first among the rest. |
| 120 |
[[test_target]] |
| 121 |
dir = "mnw-cli" |
| 122 |
|
| 123 |
# Companion: ships to testnot-1 and prod-1 (see sando.toml). |
| 124 |
[[test_target]] |
| 125 |
dir = "multithreaded" |
| 126 |
scratch_db = true |
| 127 |
|
| 128 |
[[test_target]] |
| 129 |
dir = "pom" |
| 130 |
|
| 131 |
[[test_target]] |
| 132 |
dir = "wam" |
| 133 |
|
| 134 |
# docengine lives in its own repo since 2026-07-30 and reaches the build through |
| 135 |
# the [[aux_repo]] checkout, which sits beside the worktree rather than under it. |
| 136 |
# `aux_repo` resolves `dir` against that checkout instead; `dir` is empty because |
| 137 |
# the crate is at the repo root. Gated because the checkout is at branch HEAD and |
| 138 |
# is compiled into these binaries — a break there breaks this build. |
| 139 |
[[test_target]] |
| 140 |
aux_repo = "docengine" |
| 141 |
all_features = true |
| 142 |
|
| 143 |
# shared/* — every app and service in the repo links these. |
| 144 |
[[test_target]] |
| 145 |
dir = "shared/egui-updater" |
| 146 |
|
| 147 |
[[test_target]] |
| 148 |
dir = "shared/kberg" |
| 149 |
all_features = true |
| 150 |
|
| 151 |
[[test_target]] |
| 152 |
dir = "shared/livechat" |
| 153 |
all_features = true |
| 154 |
|
| 155 |
[[test_target]] |
| 156 |
dir = "shared/ops-core" |
| 157 |
|
| 158 |
[[test_target]] |
| 159 |
dir = "shared/ops-exec" |
| 160 |
all_features = true |
| 161 |
|
| 162 |
[[test_target]] |
| 163 |
dir = "shared/pom-contract" |
| 164 |
|
| 165 |
[[test_target]] |
| 166 |
dir = "shared/s3-storage" |
| 167 |
|
| 168 |
[[test_target]] |
| 169 |
dir = "shared/synckit-client" |
| 170 |
all_features = true |
| 171 |
|
| 172 |
[[test_target]] |
| 173 |
dir = "shared/tagtree" |
| 174 |
|
| 175 |
# Non-binary content shipped as part of each release. Multiple entries can |
| 176 |
# target the same `dst` (additive merge — used to build `docs/` from three |
| 177 |
# worktree sources). Sources are relative to the worktree root; dsts are |
| 178 |
# relative to the bundle root (`<release_root>/releases/<digest16>/` once the |
| 179 |
# bundle is published; release dirs are named for their content digest, not the |
| 180 |
# version). |
| 181 |
# |
| 182 |
# error-pages is now only load-bearing for 502.html: the 404 and 500 pages are |
| 183 |
# embedded in the server binary and served at /__errors/, which Caddy proxies to |
| 184 |
# (server/deploy/Caddyfile). 502 is the app-is-down page, so it cannot come from |
| 185 |
# the app, and Caddy keeps reading it off disk. |
| 186 |
[[release_contents]] |
| 187 |
src = "server/deploy/error-pages" |
| 188 |
dst = "error-pages" |
| 189 |
|
| 190 |
[[release_contents]] |
| 191 |
src = "server/static" |
| 192 |
dst = "static" |
| 193 |
required = true |
| 194 |
|
| 195 |
[[release_contents]] |
| 196 |
src = "server/site-docs/public" |
| 197 |
dst = "docs/public" |
| 198 |
required = true |
| 199 |
|
| 200 |
[[release_contents]] |
| 201 |
src = "server/site-docs/examples" |
| 202 |
dst = "docs/examples" |
| 203 |
|
| 204 |
[[release_contents]] |
| 205 |
src = "server/docs/business/assumptions.toml" |
| 206 |
dst = "docs/assumptions.toml" |
| 207 |
required = true |
| 208 |
|
| 209 |
# Migrations ride in the bundle, which is what puts them inside the digest. |
| 210 |
# |
| 211 |
# `migration_dry_run` used to read them out of the worktree. That proved |
| 212 |
# something about a checkout sitting next to the artifact rather than about the |
| 213 |
# artifact, and the checkout can be edited between the dry run and the deploy. |
| 214 |
# The gate now resolves `[[migration_check]].dir` against the bundle first and |
| 215 |
# falls back to the worktree, so these two entries are what move the guarantee |
| 216 |
# from "the source we had" to "the bytes that ship". |
| 217 |
# |
| 218 |
# It is also what lets the gate run at all against an artifact Sando did not |
| 219 |
# build: an accepted bundle has no worktree to fall back to, and a builder that |
| 220 |
# does not ship its migrations gets told the gate has nothing to dry-run rather |
| 221 |
# than a green run over nothing. |
| 222 |
[[release_contents]] |
| 223 |
src = "server/migrations" |
| 224 |
dst = "server/migrations" |
| 225 |
required = true |
| 226 |
|
| 227 |
[[release_contents]] |
| 228 |
src = "multithreaded/migrations" |
| 229 |
dst = "multithreaded/migrations" |
| 230 |
required = true |
| 231 |
|