Skip to main content

max / makenotwork

10.1 KB · 212 lines History Blame Raw
1 # Bento deploy units
2
3 `bentod` runs as a systemd **user** service on fw13 (the x86_64 build gate).
4 Unlike `sandod` (a hardened system service with a dedicated `sando` user),
5 bentod is a user service under the operator: building the apps needs the
6 operator's SSH keys (to the tailnet build hosts and the mbp ops-agent), the app
7 checkouts under `~/Code/Apps`, and the `_private` layer for signing secrets
8 (`secrets_root`). A dedicated system user can't reach those without copying keys
9 and bind-mounting home — so a user service is the right model, and it lets the
10 operator redeploy bentod with `systemctl --user restart` (no sudo).
11
12 ## Files
13
14 | File | Where it goes | Purpose |
15 |------|---------------|---------|
16 | `bentod.service` | `~/.config/systemd/user/` | The user service unit. |
17 | `bento-daemon.toml.example` | `~/.config/bento/bento-daemon.toml` | Daemon-local config (paths + listen). Absolute paths only — no shell expansion. |
18 | `bento.toml.example` | `~/.config/bento/bento.toml` | Build topology (hosts + apps). `repo` paths are tilde-expanded; `repo_by_host` overrides the path for one host. |
19 | (none, hand-written 0600) | `~/.config/bento/bento.env` | `BENTO_API_TOKEN=...` for a tailnet bind. Read by the unit's `EnvironmentFile=-`, so a loopback install can leave it absent. No example file ships: it holds a secret. |
20
21 ## Stand-up (no root except enable-linger)
22
23 ```sh
24 # 1. Build + install the binary
25 cd ~/Code/MNW/bento/daemon && cargo build --release
26 install -D -m 0755 target/release/bentod ~/.local/bin/bentod
27
28 # 2. Config + state dirs
29 mkdir -p ~/.config/bento ~/.local/state/bento/logs ~/Dist
30 install -m 0644 ~/Code/MNW/bento/deploy/bento-daemon.toml.example ~/.config/bento/bento-daemon.toml
31 install -m 0644 ~/Code/MNW/bento/deploy/bento.toml.example ~/.config/bento/bento.toml
32 # (edit the two configs: absolute home paths, real host/app rows)
33
34 # 3. Service
35 mkdir -p ~/.config/systemd/user
36 install -m 0644 ~/Code/MNW/bento/deploy/bentod.service ~/.config/systemd/user/
37 loginctl enable-linger "$USER"
38 systemctl --user daemon-reload
39 systemctl --user enable --now bentod
40
41 # 4. Verify (address must match `listen` in bento-daemon.toml, see Auth below)
42 curl -s http://127.0.0.1:8765/state | python3 -m json.tool
43 ```
44
45 ## Where artifacts end up
46
47 Each target's `collect` lands locally at `<dist_root>/<app>/<version>/<target>/`
48 and is then deposited at `<root>/<app>/<version>/<target>/` on the archive host
49 named by the `[archive]` table. Same layout both sides, so the two are one tree
50 at two addresses.
51
52 astra holds the archive: always on, on the tailnet, and already the aarch64 build
53 host and git mirror, so it is the one box every other build host can reach. That
54 is what gives "where is the AppImage for goingson 1.4.0" a single answer covering
55 the macOS and Windows targets too, rather than one per machine that built them.
56
57 The archive host needs the directory to exist and to be writable by the SSH user
58 bentod reaches it as; everything below it is created per release.
59
60 ```sh
61 ssh astra 'sudo install -d -o "$USER" -g "$USER" -m 0755 /var/lib/bento/artifacts'
62 ```
63
64 Retention prunes `dist_root` and `logs_root` on the daemon box and never touches
65 the archive: the archived copy is the one meant to outlive them. A failed deposit
66 fails that target's `collect` step, before sign and publish. Leave the `[archive]`
67 table out to skip archiving entirely.
68
69 ## Handing an artifact to Sando
70
71 Bento builds and packages; Sando decides whether a thing advances a stage. For a
72 product Sando deploys, a `[handoff.<app>]` table names where its bytes go after
73 the recipe finishes:
74
75 ```toml
76 [handoff.pom]
77 host = "sando@fw13"
78 staging_root = "/srv/sando/staging"
79 url = "http://100.103.89.95:7766"
80 sando_app = "pom"
81 token_env = "BENTO_SANDO_TOKEN"
82 ```
83
84 `token_env` names the environment variable holding sandod's bearer token; it
85 never holds the token itself and never points at a file. Set the variable in
86 bentod's `EnvironmentFile` (`~/.config/bento/bento.env`, already there for
87 `BENTO_API_TOKEN`) with the value of `SANDO_API_TOKEN` from
88 `/etc/sando/sando.env` on the Sando host. Those two have to match, and the
89 daemon says so by name when the variable is unset.
90
91 This field used to be `token_file`, a path under `secrets_root`. That put a
92 bearer token on disk in plaintext inside `_private`, which is under git now, and
93 a signing key and a bearer token are not the same kind of secret: a key is a
94 file because a tool opens it by path, while a token is one opaque string held
95 for the life of the process. The old key is refused rather than ignored, so a
96 config that was never migrated fails at startup instead of sending no header and
97 collecting a 401.
98
99 Per target, the collect directory is rsynced to
100 `<staging_root>/<app>-<version>-<target>/` and sandod is asked to take it in.
101 Sando proves the bundle against its record before anything else happens, so a
102 transfer that dropped or corrupted a file is refused with the file named.
103
104 Two things about the transfer are load-bearing. The artifact record does **not**
105 travel with the bytes: it names the digest of the bundle, the digest covers every
106 file in the bundle, so a record copied in among the artifacts would change the
107 digest it names. It goes in the request body. And the staging directory is
108 mirrored with `--delete` rather than merged, so a retry after a partial transfer
109 holds exactly this attempt — a leftover file is an extra file, and an extra file
110 is a manifest mismatch that would get an honest bundle refused.
111
112 `host` is usually the same machine bentod runs on, and still goes through ssh:
113 sandod runs as `sando`, bentod as a user unit, and `sando@fw13` lands the bytes
114 owned by the process that has to rename them without a group or an ACL on the
115 staging directory. That means bentod's user needs an SSH key in `sando`'s
116 `authorized_keys`, and the staging directory has to exist:
117
118 ```sh
119 ssh sando@fw13 'install -d -m 0755 /srv/sando/staging'
120 ```
121
122 A failed handoff fails that target run at a `handoff` step, which is a step no
123 recipe runs — the daemon performs it after the recipe, and the failure needs a
124 column of its own rather than contradicting a green `collect`. Leave the table
125 out for every app Sando does not deploy, which is most of them.
126
127 ## Auth (CF2)
128
129 On a loopback bind, build triggers are reachable only from the daemon's own
130 host, so no token is required. To operate bentod over the tailnet, bind the
131 tailnet IP in `bento-daemon.toml` and set `BENTO_API_TOKEN` via an
132 `EnvironmentFile` in the unit. bentod refuses to start on a non-loopback bind
133 without it (same posture as Sando's `SANDO_API_TOKEN`).
134
135 **fw13 runs the second form**, not the loopback default the example config
136 ships. `listen` is the tailnet address and `/build` and `/retry` require the
137 bearer token, so a `127.0.0.1` curl gets no answer at all rather than an auth
138 error. Read the address out of the config and the token out of its own file
139 rather than passing either on a command line:
140
141 ```sh
142 set -a; . ~/.config/bento/bento.env; set +a
143 curl -s -X POST http://$(grep -oP '(?<=^listen = ")[^"]+' ~/.config/bento/bento-daemon.toml)/build \
144 -H "authorization: Bearer $BENTO_API_TOKEN" -H 'content-type: application/json' \
145 -d '{"app":"makeover-webview","version":"0.5.1"}'
146 ```
147
148 `/state`, `/status.json` and the log endpoints are read-only and unauthed, so
149 watching a run needs the address but not the token.
150
151 ## Recipes
152
153 A real `/build` reads `<app>/<recipe_dir>/<platform>.rhai` from each app's
154 checkout. The recipes now exist, one per platform per app:
155
156 | App | macOS | iOS | Linux | Windows |
157 |-----|-------|-----|-------|---------|
158 | goingson | yes | yes | yes | yes (unsigned, unsupported) |
159 | audiofiles | yes | — (egui, no iOS) | yes | yes (unsigned, unsupported) |
160 | balanced_breakfast | pending¹ | pending¹ | yes | yes (unsigned, unsupported) |
161
162 ¹ BB has no Developer ID signing / notarization infra or iOS project yet, so its
163 macOS/iOS recipes are deferred until that lands. BB ships in a later wave.
164
165 A library crate (`kind = "library"` in its own `bento.toml`) has no per-platform
166 artifact, so the runner reads a single `publish.rhai` instead of the table above.
167 Every library configured in the topology now carries one: `makeover`,
168 `makeover-geometry`, `makeover-build`, `makeover-webview`, `makeover-layout`,
169 `pter`, `everycycle`, `supernote-push`, `alloy_tui`. That recipe asserts HEAD is
170 exactly a tag before it uploads, so a release wants `git tag -a v<version>`
171 first; a crate published by hand before it was wired up has no such tag, and its
172 next release is the one that gets one.
173
174 A `linux.rhai` serves both arches — `build_host()` resolves to the native host
175 the topology assigns the target (`linux/x86_64` → fw13, `linux/aarch64` → astra),
176 so there is no cross-compilation and no hard-coded host name. macOS/iOS recipes
177 run on the mbp `agent`-transport host where the Developer ID key is usable.
178
179 ### The prebuild gate
180
181 Every recipe runs a `prebuild` step between checkout and build:
182
183 ```
184 cargo clippy --workspace --all-targets <features> -- -D warnings
185 cargo test --workspace <features>
186 ```
187
188 `sh_ok` aborts the recipe on a non-zero exit, and a failed step lands in the
189 step ledger, so a red gate bars publish as well as build. It runs on the target's
190 own build host rather than once centrally: a break confined to one platform
191 (cfg-gated code, a Windows path) is then caught on the machine that would have
192 shipped it. The cost is that the suite runs once per target.
193
194 Features come from `feature_flags()`, so the gate compiles the same
195 configuration the release build does.
196
197 ### Recipe host-function vocabulary
198
199 Beyond `step`/`sh`/`sh_ok`/`log`/`collect`/`publish`/`secret`/`env` and the
200 macOS helpers (`codesign`/`notarize`/`staple`/`verify_gatekeeper`/`keychain_*`),
201 recipes can read their own context:
202
203 | Function | Returns |
204 |----------|---------|
205 | `version()` | the version being built (e.g. `"0.4.2"`) |
206 | `build_host()` | the host this target builds on (e.g. `"fw13"`) |
207 | `repo()` | the app's checkout path on this target's build host (`~`-prefixed on unix; `cd` into it, commands don't auto-cd) |
208 | `target()` / `platform()` / `arch()` | `"linux/x86_64"` / `"linux"` / `"x86_64"` |
209
210 Non-Tauri apps (audiofiles) set `version_path` in the topology to the crate
211 `Cargo.toml` carrying the version, since they have no `tauri.conf.json`.
212