Saphan StudioDocs
Deployment

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:

for b in saphan saphan-agent saphan-gateway; do \
  scp "~/.saphan/dist/<os>-<arch>/$b" "<host>:/tmp/.$b.new"; \
done
ssh <host> 'for b in saphan saphan-agent saphan-gateway; do \
  chmod +x "/tmp/.$b.new" && mv -f "/tmp/.$b.new" "$HOME/.saphan/bin/$b"; \
done'

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 -m on 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 -f is 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-place cp over 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:

saphan machine list --workspace <workspace> # the roster to sweep