Updating a fleet that is already installed
The same delivery, repeated — and the three traps that make it look like it worked when it did not.
An update is the same delivery as the first install, repeated — there is no separate upgrade path and no in-place self-update. What changes is only that machines already have binaries, so every step below must be safe against a machine that is running legs right now.
Step 1 — make sure the machine you deliver from holds the release you mean to deliver.
An update is delivered from a machine of your fleet that already holds the newer binaries, exactly as a first install is. ⚠ A machine that was not refreshed delivers the version it last held, with no error anywhere — so the first question of an update is which release the delivering machine is actually holding, and Verifying an install is how you answer it by content rather than by a version string.
Step 3 (Verifying an install) is not a formality for that reason. A delivery is verified when the machine
says which commit it is running, not when the installer says done.
Step 2 — deliver, one of two ways.
With a server (the same route as the first install, Fleet machines, from a running server): saphan server start from the
engine repo, then run the printed one-liner on each machine. install.sh re-detects
OS/arch, downloads the whole machine payload — {saphan,saphan-agent,saphan-gateway} —
re-verifies against SHA256SUMS, and refuses on mismatch with nothing installed. Prefer this route for an update: it is the
only one that delivers every binary and verifies what it delivered.
Over ssh, without a server, when you just want the new binaries on hosts you already reach:
⚠ Ship the WHOLE payload install.sh would have. An earlier version of this section
shipped only the CLI, and a reader following it literally leaves the fleet with a new
engine and a stale saphan-agent — the agent-loop backend's own executable — with nothing
anywhere reporting the mismatch.
⚠ This route verifies nothing. Unlike install.sh, scp checks no checksum; the content
check in Step 3 is the only verification you get, and it is against the file you shipped, not
against SHA256SUMS. ⚠ And "same version" is not "same artifact": two builds of one release
can differ byte for byte, so compare against the tree you actually shipped from and never
against a version string.
Two properties of that pair are load-bearing, not style:
- Match the architecture per machine.
uname -s/uname -mon each host; a fleet is routinely mixed (Linux runners plus a macOS seat). - ⚠ Ship from the
<os>-<arch>/subdirectory, never from~/.saphan/dist/saphan. The distribution tree carries both: fresh per-architecture directories, and a top-level pair left by an older build. The top-level file is not refreshed by every build path, so copying it can silently ship an engine months older than the one you just built — with no error anywhere. mv -fis a rename, not an overwrite. A rename publishes a new inode, so a leg already executing the old binary keeps running against its own open file instead of being killed mid-run; an in-placecpover a live binary can also poison macOS's per-inode code-signature cache.
Step 3 — verify every machine, by content and by behaviour (Verifying an install). A fleet where some hosts took the update and others did not is worse than one where none did: the halves disagree about which rungs exist, and the disagreement is silent. Verify the whole roster before calling the update done:
What the installer does
What the one line fetches, what it verifies before anything lands, and the act it offers you next.
First run: setting the machine up at /setup
A machine with no owner is set up in a browser, and that is the only surface that can do it: the window, the six screens, and the ceremony that does not repeat.