A release is year.month.n: the n-th of that month. The number is in firmware/manifest.json
("version"). tools/fw build writes it into the board’s build.py next to the commit, the Device
screen shows 2026.10.1 · <commit>, and the boot line in log.txt carries both. Each release has a
git tag, fw-<version>.
The release fingerprint names what a board runs: the SHA-256 of the image’s source files
(tools/releases/fingerprint.mjs; releases/ has the definition). tools/fw build
writes it into build.py as RELEASE, and the Device screen shows its blockie and first 8 hex
digits (4c86·c754). The emulator computes it over the files it runs, so the same release shows
the same picture in the browser. The app’s Releases page lists each release’s fingerprint.
The README’s badges and build table show each image’s blockie with its first and last 8 digits;
node tools/releases/badge.mjs writes them (docs/releases/badges/), and CI fails when they don’t
name the source. Run it, as you rebuild the browser pages (node emu/build.mjs), whenever the
firmware or its version changes.
To make one (from a clean main):
"version" in firmware/manifest.json, and write its ## <version> (<date>) entry below.tools/release manifest writes docs/releases/<version>.json (each image’s fingerprint, the
hash of every source file, the hash of each web build’s BUILD.json) and its line in
docs/releases/index.json. Rebuild the web pages first if anything in them changed. Commit all of it.tools/release check: the tree is clean, the tag doesn’t exist yet, the entry is here, the
manifest matches the source, and the fast tests pass.tools/release tag --push signs fw-<version> with your SSH key, checks the signature against
.github/allowed_signers (deleting the tag if it doesn’t check), and pushes it..github/workflows/release.yml takes it from there:
release.json (each built file’s hash, the commit, the fingerprints);SHA256SUMS.tools/fw push console --all puts it on every board on USB. tools/fw verify shows each board’s
release fingerprint, which must match the manifest’s.CI (.github/workflows/ci.yml) runs every suite on each pull request and push to main, builds the
images, and fails if docs/ differs from a rebuild of the web pages. Pages serves only what the
source makes.
.github/workflows/site-check.yml checks that from outside. After every Pages deploy and every six
hours, it fetches what the site serves and compares each byte with docs/ at the deployed commit,
and opens an issue on any difference. By hand: tools/release check-site. The app’s Releases page
says which release the page you’re on is.
Tags are signed with an SSH key; git and GitHub both understand it. Keep the key in hardware if you can:
ecdsa-sha2-nistp256) that never leaves the chip. It is the same curve as the
console’s own keys.ssh-keygen -t ed25519-sk -O resident makes a key that needs a touch for each signature.Once:
git config --global gpg.format ssh
git config --global user.signingkey "<the public key, or the path to its .pub file>"
git config --global tag.gpgSign true
.github/allowed_signers as <your git email> namespaces="git" <public key>, in a
reviewed commit. That file decides who can release.On GitHub (Settings → Rules → Rulesets), two rulesets close the gaps CI can’t:
main: require pull requests and the CI checks firmware, fwbuild and web; block force pushes.fw-*: restrict creation, update and deletion to the maintainers. Otherwise anyone with
push access could take a release name, even though CI would refuse to publish it.The tag says who cut a release. The project Safe says the project stands behind it (docs/GOVERNANCE.md).
tools/release approval prints the Safe transaction:
signMessage(D);PicoQuorumRelease(version, firmware, manifest);Propose it from the app’s Releases page (Propose approval), or with any Safe tool.
isValidSignature(D, "0x") on the Safe returns 0x1626ba7e for good,
even after owners change. The Releases page shows Approved by the project Safe.docs/releases/<version>.json never changes after its tag: its SHA-256 is part of D. Later facts
(the commit, the approval) go in index.json.
tools/vectors/release.mjs checks the hashing against a real Safe on Ethereum.
tools/quorum release-e2e runs the whole approval on a fork:
isValidSignature holds, and still holds after the console’s owner is removed.No change in behaviour. The files that came from Austin Griffith’s
picowallet now say so at their top (atecc.py,
trustm.py, p256.py, keccak.py, lcd.py, blockies.py, net.py), a few comments no longer
point at files removed before the repository went public, and the boot screen’s default title is
picoquorum (each image still sets its own). None of the picowallet-only modules removed with them
was in an image. The release fingerprints change because the source did.
Fix: Build 1 sometimes stopped on boot with “MemoryError … allocating 1280 bytes” and 221 KB free. The boot art kept drawing on its timer while the console’s modules loaded, and its small objects ended up between theirs: the largest free block left was 17-26 KB, and sometimes not even 1.3 KB at the moment the code needed it. The art now pauses until the modules have loaded: they load in 0.9 s instead of 3.5 s, and a 95 KB block is left.
Fix: OSError 12 on HTTPS calls, including a confirmation that was signed but never sent. Measured on the board, with the heap full except for one hole:
Build 1’s knob keeps up: a detent reaches the screen in ~140 ms, down from ~2.1 s.
The Sign button only signs (boards with the knob). On a review it signs as before; anywhere else it opens nothing and says “Sign button: only for signing”. The knob does the browsing.
Build 1’s screen turned 180° for its new case: MADCTL 0x60 (MX|MV) in place of 0xA0 on the 2.8”. The 1.8” profile and the Waveshare are unchanged.
Fix: Build 1 froze at “screen: ok” on boot.
secrets.py),
the boot asked the radio to scan for the one in range at the moment the boot art started
drawing on its timer. The scan never came back, and the board stopped answering on USB too.Home is a grid to browse when nothing waits for this key: each Safe, Recent (the last
signatures sent, kept in recent.json), the chip and the device. When a request arrives, home is
the queue list as before. The chip’s blockie also sits small in the header. Walkthrough:
quorum-ui/v2.
The loaded chip’s blockie on the boot screen. Once the console has read its chip, the owner’s blockie appears under the tagline, with the chip’s name and short address. It’s red if the chip isn’t the pinned one. The walkthrough: quorum-ui/v2.
The fix: Device → Wi-Fi on Build 1.
maint file still wins.The first numbered release. It brings three lines of work together on the production console:
tmproof.txt).wifi.json.tools/fw ceremony start|finish, tools/fw attest and tools/quorum verify, with a live
challenge.Boards:
025997e6…), with TM1.aee390c5…), with ATECC chip 2.