How Chronicle nodes learn about and apply code updates, and how the hub drives updates across the cluster. (Phase 1+2 of the fleet-update plan: compute nodes. Mobile OTA and ESP32 firmware distribution are separate, later phases.)
A node is a git checkout, so its version is git describe on that checkout
(e.g. v0.2.2-32-g07e194ee). updates.py (repo root) owns this:
repo_version()→{describe, commit, branch, dirty}—branchisNonefor detached (release-tag) installs.- The node agent reports it in
GET /node(version) and rides it on the Tailnet advertisement (chronicle-nodelabelversion), so the hub sees cluster-wide version drift without polling. - Container images bake
CHRONICLE_BUILD_VERSIONat build time (git describe locally, the release tag in CI) — the backend serves it atGET /versionand in/health. If the image version lags the checkout version, the containers need a rebuild/restart.
updates.py picks the mode from how the node was installed:
- branch mode — HEAD is on a branch with an upstream (dev checkouts,
edge/install.sh --branchinstalls):git pull --rebase --autostash. - release mode — HEAD is detached (root
install.shclones the latest release tag) or an explicit--tag: fetch tags, check out the target (default: latestv*tag). Refuses on a dirty tree.
After the checkout moves, every enabled service (per config/config.yml services:) is restarted with up --build — or with prebuilt registry images
when a prebuilt tag is given (same CHRONICLE_REGISTRY/CHRONICLE_TAG env
contract as services.py start --use-prebuilt).
Rollback: if any service fails to come up on the new code, the checkout is restored to the previous commit (detached — local branches are never rewritten) and the services are restarted from the old code.
uv run --with-requirements setup-requirements.txt python services.py update --check # is an update available?
uv run --with-requirements setup-requirements.txt python services.py update # update + rebuild/restart services
uv run ... python services.py update --tag v0.3.0 # pin a specific tag/ref
uv run ... python services.py update --prebuilt v0.3.0 # pull GHCR images instead of building
uv run ... python services.py update --no-restart # move the checkout onlyGET /update?node=<host>&target=<ref>— update check (fetches origin;nodeforwards to a peer's agent over the Tailnet, like service actions).POST /update{target?, prebuilt?, node?}— runs the update as a standard agent operation ({operation}to poll viaGET /operations/{id}), with progress inphase. On success the agent restarts itself last (via its systemd unit, else re-exec) so it also runs the new code.
The backend proxies these for the WebUI as GET/POST /admin/update (admin
only); the System page shows per-node versions with check/update actions.
Updating the hub restarts the backend — the WebUI briefly disconnects.
- CI (
advanced-docker-compose-build.yml) builds images on release/main and pushes to GHCR withCHRONICLE_BUILD_VERSIONbaked in. - A node's update check compares its checkout against its upstream branch or
the latest
v*release tag (viagit fetch, no GitHub API dependency). - Applying an update moves the checkout and rebuilds locally by default;
prebuiltswitches to pulling the CI images instead. - The hub fans updates out node-by-node through the node agents.
- The agent re-exec path doesn't re-resolve Python deps; if
setup-requirements.txtchanged, re-run./start.sh(systemd-managed agents are fine — the unit restart goes throughuv run). - No automatic/scheduled updates — checks and applies are operator-triggered (by design for a system doing live audio capture).
- Mobile app (expo-updates currently disabled) and ESP32 firmware (ESPHome push-flash) are not covered yet.