161. A mesh node follows the published release, through the one installer, forward only
Context
There is no self-update. To upgrade, a person reruns install.sh with --version on each
machine (docs/DISTRIBUTION.md, "Self-update"). The milestone safe-self-update left the
central question open on purpose: "Whether a background update check is wanted is an owner
decision recorded as an ADR before any code; this milestone does not assume one."
The owner decided on 2026-10-14. The words, as written to the sessions:
- "do vseho se pust max efektivne a deploy asap, bump version, tak abychom to zacali automaticky vsichni v majordomus mesh pouzivat, critical" (get on with all of it as efficiently as possible, deploy asap, bump the version, so that everyone in the majordomus mesh starts using it automatically; critical).
- "safe-self-update rozhodni co je treba, zapis, popis, adr, docs, gh pages, deploy, start using asap, critical as well" (decide what safe-self-update needs, write it down, describe it: ADR, docs, GitHub Pages, deploy, start using it asap; also critical).
What the same day showed:
| what happened on 2026-10-14 | what it cost |
|---|---|
| v0.25.0 reached the five mesh nodes only after one session upgraded each node by hand, shipping the jetson archive over the LAN because jetson has no route to github.com | about an hour of one session, plus a node that silently stays behind whenever nobody repeats it |
| the primary checkout's shared server was a 0.21.0 debug build while the installed CLI was 0.25.0 | majordomus mesh status from the 0.25.0 CLI answered "no running server for this checkout" although a server was serving it |
v0.26.0 was published at 18:58Z, and at 19:19Z releases/latest.json named it | every node still ran 0.25.0, because nothing reads the pointer except a person |
What already exists and is typed: the trust path from share/distribution.yaml to
releases/latest.json, the installer that verifies the digest and size before unpacking and
swaps the launcher by an atomic rename, one versioned tree per release under
~/.local/share/majordomus/versions/<version>/, Releases::latest_stable(), and the lease's
check that a server whose own executable was replaced is serving code that no longer exists
(lease::serving_replaced_code). What is absent is a reader of the pointer, an ordered
hand-off between servers of different versions (I2163, which depends on I2127), and
provenance beyond a checksum (I2165).
Decision
A mesh node follows the published stable release by itself. Every machine the mesh
declaration enrolls, hubs included, keeps its installed Majordomus at the version
releases/latest.json names, without a person on that machine. Following is declared per
node in the mesh declaration (follow: stable, follow: pinned <version> or follow: off).
An enrolled node of the owner's mesh defaults to stable. A repository that adopts
Majordomus without enrolling machines in this mesh is not affected: nothing fetches anything
on entering a repository.
The decision is read from the trust path, never from a peer. A node upgrades only to a
version that releases/latest.json names, and only to an archive whose digest and size match
releases/<tag>.json. A mesh event that announces a release only means "check now". It never
carries a binary or a digest that a node would trust. A node without a route to the site,
such as jetson, may take the archive from a peer. The peer's copy is still verified against
the metadata the node read itself, or against metadata relayed over the authenticated mesh
link and signed by the publishing node once I2165 has landed.
There is one implementation of the risky part. majordomus self update re-runs the
published installer with --version <tag> and the node's own prefix. It does not
reimplement download, verification or swap in Rust. This answers the question
docs/DISTRIBUTION.md left open: one installer, tested by case 85, and the command is a
typed capability around it (--check reports, --apply installs).
Forward only, stable only. A node never moves to a version lower than the one it runs,
never to a pre-release, and never to a version the metadata does not list. A pinned node
stays pinned. Going back is an operator's act through install.sh --version, never an
automatic one, with one exception: rollback after a failed activation, below.
Install beside, activate in order, keep the previous version. The new release is installed into its own versioned tree while the old server keeps serving. Activation follows the hand-off the milestone requires:
- A hub runs under its service manager (systemd user unit, launchd agent) and is restarted onto the new launcher when no session is attached, or after a bounded grace.
- A checkout's shared server that sees a newer installed executable hands its sessions on
and ends (I2127, I2163). Until those land, it is replaced at its next idle end or restart,
and
serve statusnames the version it is behind by.
The previous versioned tree is kept until the new version has passed its health check.
A failed activation rolls back by itself and says so. The health check is the server
answering its readiness route and mesh verify passing a live round. If it fails within the
grace, the launcher is renamed back to the previous version, the node keeps serving the
previous version, and the board carries a rollout failed entry naming the version, the node
and the check that failed. A version that failed on one node is not retried on that node
until a newer one is published.
The mesh shows the rollout. Every runtime reports its version in what it publishes to
the mesh. majordomus mesh peers prints it, and mesh doctor names a node more than one
release behind the stable pointer. A release is rolled out when every enrolled, following
node reports it.
When it checks. At server start, every 15 minutes with jitter, and when the mesh
announces a release. A check is one HTTPS request for releases/latest.json. It never
blocks a request the server is answering.
Alternatives rejected
Reimplement download, verification and swap in Rust. Then the riskiest code in the product exists twice, and only one copy is tested by the installer's case. The installer already does all three. The command wraps it.
Let a peer push a binary, or a digest, to the mesh. Then one compromised or confused node can install code on every other. The mesh only says "check". What a node installs is decided by metadata it can verify.
Upgrade on entering a repository, or at the next command a person runs. The milestone excludes a network fetch on entering a repository. A person's command should not wait on a download, and a machine with no person at it, such as a hub, would never be upgraded.
Keep upgrading by hand, faster. 2026-10-14 is the evidence against it: the mesh is exactly as current as the last session that remembered to do it.
Follow every release, pre-releases included. A node of the owner's mesh is
infrastructure other sessions depend on. A pre-release is tried by a person who chose it,
with --version.
Consequences
- The milestone
safe-self-updatestops being conditional: its desired state now includes a node that follows the release, and it is raised to p0. Its new issues are I2720 (self updatewraps the installer), I2721 (a node follows the stable pointer), I2722 (a hub activates the new version under its service manager, with health check and rollback), I2723 (the mesh shows each node's version and the rollout) and I2724 (a peer-relayed archive for a node without a route to the site). I2163 and I2127 stay p1 and are now on the critical path for checkout servers. I2165 is raised from p3 to p1, because following automatically raises the cost of a bad archive. - Until I2721 and I2722 land, the practice is the one the owner ordered: the session that
holds the release seat upgrades every enrolled node once
releases/latest.jsonnames the new version, verifies each node's reported version, and says so on the board. docs/DISTRIBUTION.md("Self-update") anddocs/MESH.md("The hubs as services") state this decision. The site carries it as a projection of those documents.- A wire, lease or journal schema change now reaches every node within one check interval of a release. That makes the milestone's "a wire or lease or journal schema change without a version bump fails version-surface" a precondition rather than a nicety.
- This decision is
proposed. The owner ordered it in the words quoted above, and accepting the text is the owner's act. If the owner rejects it, nodes go back to being upgraded by hand, and the issues above are cancelled with it.