Shipping a change
What to bump, what to rebuild, and in what order.
Magnemite is four moving parts that ship on different clocks: the hub, the dashboard, the agent on every box, and the database schema. This is what each one needs.
The one rule that catches everyone
The agent is versioned by the agent= line of the VERSION file, and the
fleet only moves when that string changes.
The hub compares the version a box reports with the version its own image was
built from, as plain strings. New code on the same agent= is a hub that
believes the fleet is already where it should be — you deploy, nothing happens,
and nothing is wrong in the logs either.
agent=1.0.0 # bump this and the boxes update themselves
redroid=1 # the Redroid image: its base, start script and init service
hub=1.0.0 # a label: shown on the Status page, and this image's tag
web=1.0.0 # the same, for the dashboard
docs=1.0.0 # the same, for the documentation site
edge=1.0.0 # the same, for Caddy and the Caddyfile it carriesOne line per deployable, because they ship on different clocks. Only agent=
changes what the fleet runs; the rest are how you tell, on the Status
page, whether a deploy finished — a hub and a dashboard on different
numbers is a half-applied release.
These numbers are also the published image tags, so a bump is what gives a release a name to pin and to roll back to.
Same string, same fleet. It is not semver, so a lower agent= is a downgrade
the boxes will happily apply — which is also how you roll the agent back on
purpose.
Only agent= changes behaviour. Hub and dashboard code ships by being redeployed and nothing
compares those numbers — bump them anyway, or the Status page will tell you a stale story.
What each change needs
| You changed | Bump VERSION? | What to do |
|---|---|---|
apps/web | web= | Redeploy web |
apps/hub | hub= | Redeploy hub (drops every device socket — see below) |
agent/ | agent= | Redeploy hub; the fleet updates itself |
packages/protocol | agent= | Both sides implement it — redeploy hub and bump the agent |
packages/db/prisma/schema.prisma | hub= | Commit the migration; hub applies it on start |
agent/magisk/ | agent= | make module and reflash — this part is not delivered over the air |
apps/docs | docs= | Redeploy docs |
agent/redroid/ | redroid= | CI publishes the image; machines pull it — see below |
deploy/edge/ | edge= | Redeploy caddy |
Adding a version source is not a release at all: it is a URL in Settings. Only a source publishing something other than the shared index format would need code, and that is the thing this design exists to avoid.
The order that matters
The wire protocol is implemented twice, and the two sides do not update at the same instant: the hub is redeployed in seconds, the fleet converges over minutes. So only ever add optional fields, and deploy the hub first.
The hub tolerating a field an old agent does not send is fine — it is the normal state of the world for a while after every release. An agent sending something the hub does not understand yet is a dropped frame.
Database changes
pnpm db:migrate # creates the migration and applies it locally
git add packages/db/prisma/migrationsThe hub's entrypoint runs prisma migrate deploy on every container start, so
deploying the hub applies whatever is committed. Nothing is applied by the
dashboard. Because the migration lands before the new hub code serves a
request, additive changes are safe on their own; a destructive one (dropping or
renaming a column something still reads) needs two releases, exactly as with
the protocol.
Shipping the agent
# bump agent= (and hub=, since the hub image carries the new binary)
$EDITOR VERSION
git commit -am "agent: report the LAN address"
git push
# wait for the images workflow, then deploy the hub
docker compose --env-file .env -f deploy/docker-compose.yml pull hub
docker compose --env-file .env -f deploy/docker-compose.yml up -d hubThe hub's image cross-compiles the agent, publishes the binaries, and tells each box to update as it reconnects — full flow, settings and failure handling in updating the agent.
The wait is the one new thing a deploy has to think about: a pull started
before .github/workflows/images.yml finished fetches the previous image
and reports success, so the fleet sits on the old agent with nothing in any log
saying why. up -d --build hub is still there and still builds this checkout,
which is the shortcut when you would rather not wait.
Before shipping something that touches the agent's connection or install path, run it against a fake fleet: a binary that crashes is caught by the module's respawn loop, but one that starts and cannot reach the hub takes the boxes offline for good, and that is a physical trip to each of them.
Redeploying the hub drops the fleet
Every device socket goes down with the hub process. The agents reconnect on
their own with backoff and queued jobs resume, so it is a few minutes of noise
rather than damage — but on a large fleet, pick your moment. Narrowing a deploy
so only the dashboard restarts is one setting on
Coolify, and free under plain compose
(docker compose --env-file .env -f deploy/docker-compose.yml up -d web, after a pull web).
The Redroid image ships on its own clock
agent/redroid/ and the redroid= line build an arm64 image in CI, and the
machines pull it: a new Redroid host no longer spends a 780 MB docker build
inside its bootstrap. It is deliberately not rebuilt for an agent change —
the binary inside is a seed each box replaces by self-updating within seconds —
so bumping agent= leaves this image alone, which is why it has its own line.
What a machine runs is still the local tag magnemite/redroid:<redroid=>; the
bootstrap pulls the published image and retags it. That looks redundant and is
not: the rendered compose file names that tag, docker compares it as a string,
and pointing it at a registry would recreate every container on every machine
already running — a full Android cold boot each — to deliver an identical
image.
Checklist
Before the commit
pnpm --filter @magnemite/hub typecheck, pnpm --filter @magnemite/web typecheck,
make agent-test (vet + tests in Docker, no Go toolchain needed on the host).
If the agent or the protocol changed
Bump agent=. Check that new protocol fields are optional on both sides.
Deploy
Hub first, then the dashboard. Migrations ride along with the hub.
Watch it land
The Status page shows the agent target version and how many boxes are updating. Each device page lists its own agent updates in the update history, including failures and why.