Public Access
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
8.9 KiB
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 runnerrunner0(labelubuntu). The job asks for apython:3.12-slimcontainer, 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'sconfig.yamlallows it undercontainer.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.shruns the command in place whenRORO_NO_DOCKERis 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.pycreates 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.pychecks 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:resoluteand then asubuntu::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 runfor the build) and published v0.10.0 that way.ubuntu:docker://docker.gitea.com/runner-images:ubuntu-latestis 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/latestis 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-Lengthand 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
- 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.
- The connection: the root certificates, an HTTPS client, a check and a list from the Update Service's task; console commands to try them.
- The download: an HTTPS source for the existing install path.
- The screens: the Firmware page's release rows, the release page, Older releases, the setting, the daily check and its Toast.
- Checks on the device, recorded here.