Files
roro9stack/docs/milestones/R1.md
T

8.9 KiB

R1 — Releases

Status: in progress. CI and signed releases on Gitea (issue #5) are in place since 2026-10-06: every tag from v0.1.0 to v0.10.0 has its release. Updates from Gitea (issue #6) is being built, locally, on branch gitea-updates. The Issues App (#4) comes after.

Goal: a tag is a release, built the same way every time and published where a device can find it.

CI and releases (issue #5)

Until now the tests, the builds, the signing and the flashing all happened on one machine, through scripts/ci.sh and scripts/flash.sh. Nothing was published.

Decisions (design round 2026-10-06)

# Decision
Q151 A push, to any branch: the host tests (with their coverage). A pull request: the same and both builds; changes reach main through pull requests. A tag v*: all of it, then a release. (First: everything on every push, which rebuilt the firmware far more often than anyone looked at it.)
Q152 CI signs. The signing key is the repository secret OTA_SIGNING_KEY; a tag push makes a complete, signed release with no manual step (ADR 0008).
Q153 The Debug Build is built in CI with a token of the runner's own, to prove it compiles, and isn't published: it would hand everyone its Debug Console token.
Q154 Pull requests from forks don't start a run.
Q155 A release carries roro9stack-<version>.ota (signed), -factory.bin for USB, .elf.gz to decode crashes, and SHA256SUMS.
Q156 Its text is the tag's message, what the files are, and the commits since the tag before.
Q157 No cache service to begin with: measure first.
Q158 Reproducible builds aren't needed for signing any more (Q152); not pursued here.
Q159 The tags from before CI get their releases too, v0.1.0 to v0.10.0, built from each tag's own sources by running the workflow by hand.
Q160 Actions is switched on for the repository.
Q161 Changes reach main through pull requests, merged as "rebase, then a merge commit", the only style the repository allows: the branch's commits keep their messages, the merge commit marks the pull request, and what CI tested is what lands. No squash, no fast-forward.

As built

  • One workflow, .gitea/workflows/ci.yml, one job, on the runner runner0 (label ubuntu). The job asks for a python:3.12-slim container, installs git, a compiler, openssl and PlatformIO, and runs the same scripts as a developer's machine. No Docker inside the job.
  • The cache is a Docker volume, roro9stack-pio, mounted at /pio; the runner's config.yaml allows it under container.valid_volumes. A first run downloads about 1 GB and rebuilds the framework (17 minutes); with the volume filled, tests and both builds take about 6.
  • No JavaScript actions, so the image needs no Node and nothing is fetched from GitHub: the checkout is four git commands.
  • scripts/_docker.sh runs the command in place when RORO_NO_DOCKER is set (a CI job is already in a build container), and in the project's image otherwise. The Debug Build's token is made on the spot in CI and goes with the container.
  • scripts/release_build.sh <checkout> <out> builds a tag's own sources with today's tools, signs, verifies against the public key in those sources, and writes the files and the release's text. scripts/release_publish.py creates the Gitea release or completes it; run twice, it replaces what's there. Both run the same on a developer's machine.
  • scripts/ota_verify.py checks an Update File as a device does, on a PC.
  • The job's own token (secrets.GITEA_TOKEN) is enough to create a release and upload its files.
  • Old tags. v0.1.0 to v0.3.0 are from before the framework was rebuilt with our settings (ADR 0006) and can't link against a rebuilt one left in the cache: the release build puts the stock framework libraries back for them. v0.1.0 to v0.2.1 have no public key in their sources (Firmware Updates came with v0.3.0); their files are checked against today's.

How it went

  • The runner's label took three tries. Registered as ubuntu://docker:ubuntu:resolute and then as ubuntu::docker://..., Gitea took the whole string for the label's name; with the first, jobs ran on the runner's host itself. The first version of the workflow was written for that (plain shell, docker run for the build) and published v0.10.0 that way. ubuntu:docker://docker.gitea.com/runner-images:ubuntu-latest is the form that works.
  • Gitea 1.27's API can't cancel a run that isn't finished, only delete a finished one; switching Actions off and on for the repository doesn't either. Runs queued for a label that no longer exists stay queued until cancelled in the web UI.
  • CI's image isn't byte-identical to a local build of the same tag (same size, different bytes). Not pursued (Q158).

Updates from Gitea (issue #6)

The device looks at the project's Gitea for a newer release, says so, and installs it on request, with the same signed Update Files, Probation and rollback as a push from the PC or an install from the card.

What the server gives (checked 2026-10-06)

  • Its certificate is Let's Encrypt, all ECDSA: leaf git.twis.la (renewed every few months, next expiry 2026-12-14) under the intermediate YE2, Root YE and ISRG Root X2, which X1 cross-signs. Pinning the leaf would ask a question at every renewal.
  • The API answers over HTTP/1.1, chunked: releases/latest is 3.2 KB (about 350 bytes of it matter), a list of ten releases is 33 KB.
  • A download is a direct 200 with Content-Length and no redirect; ranges work.

Decisions (design round 2026-10-06)

# Decision
Q162 Trust: the firmware carries ISRG Root X1 and X2 and checks the server's chain and name against them, not the framework's bundle of about 130 CAs. Shared with #4. If the server moves to another CA, the next firmware comes from the PC.
Q163 The Update File's own signature stays the real guard. A hijacked connection could hide a release or offer an older signed one, never install firmware that isn't ours.
Q164 The source, git.twis.la and twisla/roro9stack, is a constant in the firmware. A fork changes it, and has its own key.
Q165 When: on request in Settings > Firmware, and once a day in the background while Wi-Fi is up and the Clock is set (certificate dates need it). A setting, Check for updates, on by default. It installs nothing by itself; it skips quietly below the memory floor and never runs during an install.
Q166 A Toast, "Update v0.11.0 available: see Settings > Firmware", once per version per boot.
Q167 The download goes straight into the inactive slot, no card needed. A truncated or tampered file is refused after 160 bytes or at its end, and the running firmware is untouched. A failed download starts over.
Q168 A version that failed (rolled back) isn't announced again by the background check until a newer one exists; it can still be installed by hand.
Q169 Older releases: a list of the last ten, newest first, the running one marked. Installing an older one asks with a stronger warning.
Q170 Enter on a release shows its version, date, size and the tag message, with Install.
Q171 Debug Builds check and show the latest release, but don't install it: it would replace the Debug Build and its console (Q153: Debug Builds aren't published). Their updates come from the PC.
Q172 The memory floors of Q86: no check or download below 55 KB free.
Q173 Left out, each with its issue: installing automatically (#52), a release channel (#53), resuming a download (#54).
Q174 Ships as v0.11.0. Tested on the device with the real signed releases; a Debug Build command pretends the device runs an older version, so v0.10.0 counts as an update.

Done when

  • A check, by hand or daily, tells the right thing: up to date, newer available, no network, bad certificate, no clock, too little memory.
  • A newer release installs from the Firmware page with no card and no PC, and the device restarts into it and confirms it.
  • A tampered or truncated download is refused and the running firmware keeps running.
  • The Older releases list shows ten, and installing one asks first.
  • A release that rolled back isn't announced again.
  • A Debug Build shows the latest release and doesn't install it.
  • The daily check never runs below the memory floor, during an install, or without a clock.

Work breakdown

  1. Model (host-tested): a streaming JSON scanner, the release list read from it, HTTP response heads and chunked bodies, URLs, which release counts as an update.
  2. The connection: the root certificates, an HTTPS client, a check and a list from the Update Service's task; console commands to try them.
  3. The download: an HTTPS source for the existing install path.
  4. The screens: the Firmware page's release rows, the release page, Older releases, the setting, the daily check and its Toast.
  5. Checks on the device, recorded here.