pi-qemu-release
Build TriMixxx's SD card images and releases with pi-qemu - a deck's dev card from stock Raspberry Pi OS (`pi-qemu image build`), the A/B release card and its RAUC update bundle from HEAD's pi/vX.Y.Z
SKILL.md.claude/skills/pi-qemu-release/SKILL.md
GitHubpi-qemu: images, releases, cards
The deck's software reaches a card two ways, both through pi-qemu:
- A dev card (
pi-qemu image build DECK): stock Raspberry Pi OS, then every deploy step (pi-qemu/deploy/NNN_*), as against any deck. What the emulated decks run, and what a release is made from. - A release (
pi-qemu release build): that system sealed read-only into an A/B card every deck can run, and its RAUC bundle for updates over the air; then each deck's card, with its identity on p7 (release card DECK).
pi-qemu/PLAN.md Part 1 is the design (its invariants are numbered; the
code cites them), pi-qemu/rauc-pi-4-setup.md the how-to, and PLAN.md §7
the runbook for putting real decks on release cards.
Rules
- Real decks only when asked.
--host trimixxx-pi,trimixxx-pi-2,trimixxx2, ... are real decks: ship to one, or prepare one, only when the person asked for that deck. Try everything on emulated decks first.- Over its cable:
deck shipanddeck preparetake a real deck only over the Mac's cable, by its-ethalias (--host trimixxx-pi-2-eth;pi-qemu deck alias --host trimixxx-pi-2writes it, the deck cabled). - Over Wi-Fi they refuse, unless
--wifi: the person's call, never yours. Refused, go back to them. - Commands handed to the person name the
-ethalias; with--wifionly if they asked for Wi-Fi.
- Over its cable:
- The person flashes cards. Build the image and give them the exact
path; never run
diskutil,ddor anything else on a card or disk. A release's cards are.img.xz: Raspberry Pi Imager and balenaEtcher take them as they are, orxz -dc CARD | sudo dd of=/dev/rdiskN bs=4m. - Releases are tags, and Sam picks the version. A release's version is
HEAD's
pi/vX.Y.Ztag, andrelease buildrefuses a dirty tree. Tags are signed, so they need a message:git tag -m "TriMixxx X.Y.Z" pi/vX.Y.Z. The repo's other parts tagmidi-s3/v…andprolinks-compat/v…. Tagging is the person's call, and so is the version: ask which part goes up ("Versions", below), never work it out from the changes. - The golden pair follows main; by hand, it is the person's call. Every
new instance starts from it, and it is main's latest by itself:
deck upand crew merge have it made again from the main checkout whenever main changes what goes onto a deck (image golden, which anyone may run: it only builds main's code; pi-qemu/README.md, "The golden: main's latest").image build trimixxx0anddeck golden CARDby hand replace it with other code: only when asked. Try image builds on a scratch DECK name, and delete its outputs afterwards (below). - The bootloader is the person's call.
deck prepare --eepromflashes a real deck's EEPROM and reboots it. - Docker's disk: builds need ~6 GB free; pi-qemu checks and stops.
Freeing space is the person's call: say so, never prune. Keep the build
caches (
.cache/base, Docker's build trees).pi-qemu cachelists them all, with their sizes.
Dev cards
pi-qemu image build onetest # -> .cache/build/onetest.img, its log onetest.log
pi-qemu image build onetest --no-window --rebuild-base
- It builds in an instance of its own,
build-DECK:pi-qemu deck ssh build-DECKlooks at a build in progress. A failed build leaves its machine off and its card for a look (deck up build-DECK, thendeck rm build-DECK). - The first boot and the base step are cached in
.cache/base/while their inputs are unchanged (the stock card, the deck's name and panel, the key, the password, the seed,000_base.pi.sh). The rest takes minutes, most of it Docker builds that are cached too. - The stock card is pinned in
pi-qemu/image/stock.env; the first-boot seed isimage/user-data.inandnetwork-config; the console password and the home Wi-Fi are inimage/secrets.env(gitignored, never printed or committed). - A build window follows the log unless
--no-window; its steps come from the build's own==> plan:line. .cache/andarchives/are at the main checkout's root, git-ignored, the same for every worktree (pi-qemu/README.md, "Caches and archives").- A scratch build's outputs to delete afterwards:
.cache/build/DECK.img,DECK.json,DECK.log,DECK.build.json,DECK.lock, and.cache/base/DECK-*.img. - Each card has what it was built from beside it (
DECK.json: the commit, and whether the checkout held exactly that commit's code). One build of a deck runs at a time: a second refuses, saying which runs.
Versions
From 1.0.0 (2026-10-15) on, a release's version follows Sam's scheme, in his words:
From then on, versioning will be following this scheme: X.Y.Z . X is for "large" change, such as new feature etc. Y is for more minor new features and refactors, and Z is for bug fixes. It's subjective and I will decide.
So each version is his to give: ask him which part goes up (at night, the nightman asks him). 0.0.1, 0.1.0 and 0.1.1 came before the scheme.
Releases
A release is made in this order:
- The version: Sam's (above).
- Its notes,
docs/releases/X.Y.Z.md: everything since the last release's tag, fromgit log pi/vA.B.C.., the PRs merged since, and the fork's and prolink's logs between the commits the two releases pin. What changed for the DJ, in the deck's system and in the tooling; what updating from the last release means; whether the S3's firmware changed (git log pi/vA.B.C.. -- firmwares/), which a release doesn't carry. They go through a PR like any change. - The tag, on main's commit that has the notes:
git tag -m "TriMixxx X.Y.Z" pi/vX.Y.Z SHA, thengit push origin pi/vX.Y.Z, by Sam. A crew session can't push a tag. It builds from a local one,git -c tag.gpgSign=false tag pi/vX.Y.Z SHA, and the GitHub release (6) puts the same tag on GitHub: lightweight and unsigned where Sam's are signed, and not on gitea, whereoriginpushes too. Sam then pushes it on (git push origin pi/vX.Y.Z), or signs it first (git tag -f -m "TriMixxx X.Y.Z" pi/vX.Y.Z SHA, thengit push -f origin pi/vX.Y.Z). release buildfrom a checkout at that commit, clean (in a worktree,pi-qemu worktree preparefirst).- A rehearsal on emulated decks (below): a deck on the last release takes the new bundle and keeps it, and the new release's card boots.
- The GitHub release, once 5 has passed:
crew gh -R usr-ein/TriMixxx release create pi/vX.Y.Z --target SHA --title "TriMixxx X.Y.Z" --notes-file docs/releases/X.Y.Z.md. The notes only: the cards and the bundle stay inarchives/releases/(Sam's call for 1.0.0; the repo is public). Thengit ls-remote origin refs/tags/pi/vX.Y.Zmust print SHA: that is GitHub's tag itself, where a fetch would keep the local one whatever GitHub's names. If it doesn't, stop and say so: Sam's push of the local tag would be refused by GitHub and taken by gitea. - Shipping to the real decks: Sam's, or on his ask (rule 1), each deck
over its cable (
--host trimixxx-pi-eth,--host trimixxx-pi-2-eth).
git tag -m "TriMixxx 1.2.0" pi/v1.2.0 # the person's call
pi-qemu release build # -> archives/releases/1.2.0/
pi-qemu release card trimixxx2 # -> releases/1.2.0/trimixxx2-1.2.0.img.xz, to flash
pi-qemu deck ship --host trimixxx-pi-2-eth # the update, over its cable (only when asked)
pi-qemu release faults # releases/1.2.0-nonet, -nossh, -noinitramfs, -panic, -hang
release buildbuilds the system (image build trimixxx-release), then seals it in the release container (release/Dockerfilerunningrelease/container.sh):trimixxx-X.Y.Z.img.xz(the card each deck's own is made from),trimixxx-X.Y.Z.raucb(the update),rootfs.squashfs,boot.vfat,manifest.txt.--keep-systemseals the system card built last time;--rehearsal X.Y.Zlets an untagged or dirty tree through, and says so in the manifest.- Releases always land in the main checkout's
archives/releases/, also from a worktree. Nothing clears the archives.--version X.Y.Znames another release than HEAD's forrelease card,release faultsanddeck ship. - A deck's identity (its name, ssh host keys kept across reflashes, its
hotspot, the home Wi-Fi) is in
archives/decks/DECK/, made the first timerelease cardneeds it. The release's own card holds none: a deck started fromtrimixxx-X.Y.Z.img.xzistrimixxx, with no Wi-Fi and no ssh (it has no host keys). A deck is flashed with its own card. deck shipstreams the bundle into RAUC's state partition, installs it into the other slot and reboots into it as a trial; the deck's health check (network up and ssh answering) keeps it or rolls it back. Sending and installing show how far they have got: a bar on a terminal; run by an agent (no terminal), a plain line every 10% or 30 s (sent, rate, time left; then RAUC's percent).
Trying a release on emulated decks
pi-qemu release build --rehearsal 0.0.9 --keep-system --no-window
pi-qemu release card trimixxx0 --version 0.0.1 # the emulated deck's card of a release
pi-qemu deck up abtest --from archives/releases/0.0.1/trimixxx0-0.0.1.img.xz # unpacked, sparse
pi-qemu deck ship abtest --version 0.0.9 # a trial of B, then its health check
pi-qemu deck ssh abtest 'tail -3 /var/lib/rauc/trimixxx-health.log; rauc status'
pi-qemu deck ssh abtest 'sudo rauc status mark-active other && sudo systemctl reboot' # back to A as a trial
The last line is what Diagnostics' RESTART INTO does, for a release from before it (0.1.1 and older).
Afterwards, delete what the rehearsal made, and only that: deck rm abtest,
archives/releases/0.0.9*, and the emulated deck's cards it made
(releases/0.0.1/trimixxx0-0.0.1.img.xz; rehearsing a tagged release, its
own trimixxx0-X.Y.Z.img.xz too). Keep the tagged releases and the real
decks' cards: an untagged release is never left in archives/releases/.
Readying a real deck (only when asked)
pi-qemu deck prepare --host trimixxx-pi-2-eth trimixxx2 # reads it only
pi-qemu deck prepare --host trimixxx-pi-2-eth trimixxx2 --eeprom # bootloader + boot watchdog: reboots it
It writes the board's serial into mixxx_config/units/DECK.json (commit it
before the release is built: the release's config.txt gives each board its
own section), keeps the deck's ssh host keys for its identity, checks the
bootloader (2022-12-01 or newer knows tryboot_a_b), and captures its boot
files and profiles in archives/captures/ (private: Wi-Fi passwords).
An emulated deck can stand in to try it (deck prepare NAME DECK); it has no
bootloader, so --eeprom stops there.