Skip to content

A mesh node follows the published release, through the one installer, forward only

A mesh node follows the published release, through the one installer, forward only

majordomus://adr/adr-0161proposed distributionreleasemeshruntime

it references

  • put_in_force apps/majordomus-cli/src/lease.rs (file)
  • put_in_force site/static/install.sh (file)
  • put_in_force adr-0131
  • put_in_force adr-0155
  • put_in_force docs/DISTRIBUTION.md (document)
  • put_in_force docs/MESH.md (document)
  • put_in_force safe-self-update
  • put_in_force test/cases/85_installer.sh (test)

referenced by

Nothing in the layer names it. Backlinks are derived, so an empty list is an answer rather than an omission.

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-14what 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.comabout 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.0majordomus 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 itevery 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 status names 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-update stops 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 update wraps 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.json names the new version, verifies each node's reported version, and says so on the board.
  • docs/DISTRIBUTION.md ("Self-update") and docs/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.

Where this comes from

canonical file
.ai/repo/adrs/0161-a-mesh-node-follows-the-published-release.md
identity
adr-0161
also served as
MCP resource majordomus://adr/adr-0161 · majordomus entity show adr/adr-0161 · GET /api/v1/entity?uri=majordomus://adr/adr-0161
in the Cockpit
/cockpit/objects/adr/adr-0161

This page is generated from the file above by scripts/generate-site-data; editing the page changes nothing, editing the file changes every surface. The relations above are edges of docs/generated/graph.json, which the executable writes from the same index.

Searches the titles and descriptions of every page this site publishes.