Skip to main content

max / makenotwork

9.2 KB · 197 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_file = "sando/api-token"
82 ```
83
84 Per target, the collect directory is rsynced to
85 `<staging_root>/<app>-<version>-<target>/` and sandod is asked to take it in.
86 Sando proves the bundle against its record before anything else happens, so a
87 transfer that dropped or corrupted a file is refused with the file named.
88
89 Two things about the transfer are load-bearing. The artifact record does **not**
90 travel with the bytes: it names the digest of the bundle, the digest covers every
91 file in the bundle, so a record copied in among the artifacts would change the
92 digest it names. It goes in the request body. And the staging directory is
93 mirrored with `--delete` rather than merged, so a retry after a partial transfer
94 holds exactly this attempt — a leftover file is an extra file, and an extra file
95 is a manifest mismatch that would get an honest bundle refused.
96
97 `host` is usually the same machine bentod runs on, and still goes through ssh:
98 sandod runs as `sando`, bentod as a user unit, and `sando@fw13` lands the bytes
99 owned by the process that has to rename them without a group or an ACL on the
100 staging directory. That means bentod's user needs an SSH key in `sando`'s
101 `authorized_keys`, and the staging directory has to exist:
102
103 ```sh
104 ssh sando@fw13 'install -d -m 0755 /srv/sando/staging'
105 ```
106
107 A failed handoff fails that target run at a `handoff` step, which is a step no
108 recipe runs — the daemon performs it after the recipe, and the failure needs a
109 column of its own rather than contradicting a green `collect`. Leave the table
110 out for every app Sando does not deploy, which is most of them.
111
112 ## Auth (CF2)
113
114 On a loopback bind, build triggers are reachable only from the daemon's own
115 host, so no token is required. To operate bentod over the tailnet, bind the
116 tailnet IP in `bento-daemon.toml` and set `BENTO_API_TOKEN` via an
117 `EnvironmentFile` in the unit. bentod refuses to start on a non-loopback bind
118 without it (same posture as Sando's `SANDO_API_TOKEN`).
119
120 **fw13 runs the second form**, not the loopback default the example config
121 ships. `listen` is the tailnet address and `/build` and `/retry` require the
122 bearer token, so a `127.0.0.1` curl gets no answer at all rather than an auth
123 error. Read the address out of the config and the token out of its own file
124 rather than passing either on a command line:
125
126 ```sh
127 set -a; . ~/.config/bento/bento.env; set +a
128 curl -s -X POST http://$(grep -oP '(?<=^listen = ")[^"]+' ~/.config/bento/bento-daemon.toml)/build \
129 -H "authorization: Bearer $BENTO_API_TOKEN" -H 'content-type: application/json' \
130 -d '{"app":"makeover-webview","version":"0.5.1"}'
131 ```
132
133 `/state`, `/status.json` and the log endpoints are read-only and unauthed, so
134 watching a run needs the address but not the token.
135
136 ## Recipes
137
138 A real `/build` reads `<app>/<recipe_dir>/<platform>.rhai` from each app's
139 checkout. The recipes now exist, one per platform per app:
140
141 | App | macOS | iOS | Linux | Windows |
142 |-----|-------|-----|-------|---------|
143 | goingson | yes | yes | yes | yes (unsigned, unsupported) |
144 | audiofiles | yes | — (egui, no iOS) | yes | yes (unsigned, unsupported) |
145 | balanced_breakfast | pending¹ | pending¹ | yes | yes (unsigned, unsupported) |
146
147 ¹ BB has no Developer ID signing / notarization infra or iOS project yet, so its
148 macOS/iOS recipes are deferred until that lands. BB ships in a later wave.
149
150 A library crate (`kind = "library"` in its own `bento.toml`) has no per-platform
151 artifact, so the runner reads a single `publish.rhai` instead of the table above.
152 Every library configured in the topology now carries one: `makeover`,
153 `makeover-geometry`, `makeover-build`, `makeover-webview`, `makeover-layout`,
154 `pter`, `everycycle`, `supernote-push`, `alloy_tui`. That recipe asserts HEAD is
155 exactly a tag before it uploads, so a release wants `git tag -a v<version>`
156 first; a crate published by hand before it was wired up has no such tag, and its
157 next release is the one that gets one.
158
159 A `linux.rhai` serves both arches — `build_host()` resolves to the native host
160 the topology assigns the target (`linux/x86_64` → fw13, `linux/aarch64` → astra),
161 so there is no cross-compilation and no hard-coded host name. macOS/iOS recipes
162 run on the mbp `agent`-transport host where the Developer ID key is usable.
163
164 ### The prebuild gate
165
166 Every recipe runs a `prebuild` step between checkout and build:
167
168 ```
169 cargo clippy --workspace --all-targets <features> -- -D warnings
170 cargo test --workspace <features>
171 ```
172
173 `sh_ok` aborts the recipe on a non-zero exit, and a failed step lands in the
174 step ledger, so a red gate bars publish as well as build. It runs on the target's
175 own build host rather than once centrally: a break confined to one platform
176 (cfg-gated code, a Windows path) is then caught on the machine that would have
177 shipped it. The cost is that the suite runs once per target.
178
179 Features come from `feature_flags()`, so the gate compiles the same
180 configuration the release build does.
181
182 ### Recipe host-function vocabulary
183
184 Beyond `step`/`sh`/`sh_ok`/`log`/`collect`/`publish`/`secret`/`env` and the
185 macOS helpers (`codesign`/`notarize`/`staple`/`verify_gatekeeper`/`keychain_*`),
186 recipes can read their own context:
187
188 | Function | Returns |
189 |----------|---------|
190 | `version()` | the version being built (e.g. `"0.4.2"`) |
191 | `build_host()` | the host this target builds on (e.g. `"fw13"`) |
192 | `repo()` | the app's checkout path on this target's build host (`~`-prefixed on unix; `cd` into it, commands don't auto-cd) |
193 | `target()` / `platform()` / `arch()` | `"linux/x86_64"` / `"linux"` / `"x86_64"` |
194
195 Non-Tauri apps (audiofiles) set `version_path` in the topology to the crate
196 `Cargo.toml` carrying the version, since they have no `tauri.conf.json`.
197