ADR 0006
Upgrades come from the repository, on a stable or a dev channel
Context
Trying a change on a real machine meant building an ISO and installing it
again. Installed systems already had everything an upgrade needs, a signed
repository and apk upgrade. But packages were built only with a release,
alongside the ISOs, and every change had to wait for a hand-signed publish
behind a pkgrel bump.
We want two things: to upgrade an installed system to whatever is on main without a new image, and to keep a channel that only moves at a release.
Decision
An ISO is for installing, not for upgrading. It is built for a release. Every installed system upgrades with
apk upgrade, from whichever channel it follows.Two channels, two repositories.
stable dev Repository https://pkgs.alpymist.org/v3.24/alpymisthttps://dev.pkgs.alpymist.org/v3.24/alpymistBuilt by the Release workflow the Dev workflow, on every push to main Index signed by CI, key alpymist-2026CI, key alpymist-dev-2026Site bisand/alpymist-packagesbisand/alpymist-packages-devA channel is one line in
/etc/apk/repositories.alpymistctl channel dev|stablerewrites that line and upgrades. It is also in the menu under Update → Release channel. New systems follow stable.Dev versions itself.
ci/build-packages.shwithCHANNEL=devappends_git<UTC time of the last commit touching the package's sources>to each first-party pkgver. apk orders0.0.4 < 0.0.4_git20260915120301 < 0.0.5, so dev moves ahead with each push and no bump, and meets the next release when it arrives. squint andalpymist-keysare versioned by hand on both channels. (ADR 0008 later made the pkgver itself the release's, and the pkgrel the build's run number.)Leaving dev downgrades. Dev's versions sort above stable's, so
alpymistctl channel stableupgrades with--available, which takes the repository's versions even when they are older.
Dev's key, and what CI can reach
apk trusts every key in /etc/apk/keys for every repository. If the dev key
were there on every system, anyone who could sign with it and serve an index
at the stable address would be trusted by all of them. So:
alpymist-keysships the dev public key to/usr/share/alpymist/keys, where apk does not look.alpymistctl channel devcopies it into/etc/apk/keys;alpymistctl channel stableremoves it.- The dev signing key and the deploy key for
bisand/alpymist-packages-devare secrets of thedev-channelGitHub environment, which only main may deploy to. That deploy key cannot push to the stable site. cargo xtask publishrefuses to put stable in the hands of anything but a Release run, and refuses dev-versioned packages on stable.
This kept dev's blast radius to systems that chose dev. It no longer does:
since 2026-09-16 the release key is a secret of the stable-channel
environment and CI signs stable too, so a compromised workflow reaches every
Alpymist system, not only those following dev. The separation above still
holds within itself — dev's key and deploy key cannot touch the stable site,
and cargo xtask publish still refuses dev-versioned packages on stable — but
it is no longer the boundary it was written to be. See ADR 0002's addendum of
that date. Anyone following either channel is trusting GitHub Actions, and
should know that.
Consequences
- A change reaches a dev machine within one build, with
doas apk upgrade -Uor the menu's Update. - Stable needs a new version per build, which
ADR 0008 now gives it automatically;
cargo xtask publishstill enforces that it got one. - Dev rebuilds and re-downloads every Rust package whenever the workspace changes, because each one copies the whole workspace. They are small.
- The
v3.24in both addresses ties each channel to an Alpine release. Moving Alpine releases is a new address for both, as it was before. - Rotating the dev key follows the stable key's procedure: ship the new public
key in
alpymist-keys, withalpymistctlcopying it on the next switch, before signing with it.
Addendum — 2026-10-01: dev is built without link-time optimisation
Dev was built exactly as stable is, with the release profile's full LTO. That profile has every program optimise all of its dependencies again as it links, and with twenty programs it was most of the twelve minutes between a merge and a package. Measured on one machine, twelve of the packages took 508 s with it and 233 s without.
So the dev channel now builds with LTO off and sixteen codegen units, and stable is unchanged. What that gives up: a dev package is no longer byte for byte what the next release ships. Its programs are about two thirds bigger, and a fault that only full LTO brings out would first be seen in a Release build, not on dev. Release still builds the full profile, and its smoke test boots an image made of those packages, which is where such a fault would show before anything is published to stable.
Addendum — 2026-10-04: dev is built as stable is again, a group of programs at a time
The addendum above traded what dev tests for time: its programs were not the ones the next release ships. The time was the links. Full LTO has each program optimise everything it links, on about one core, and the packages were built one after another, so nineteen cargo runs linked twenty-three programs one at a time on a runner with four cores.
Now ci/build-programs.sh builds the programs in two cargo runs, and each
package's build() calls it: the first package of a group builds every
program in it, linking as many at once as there are cores, and the others find
theirs built. Measured on one machine with four jobs, stable's profile
throughout:
| time | the programs | |
|---|---|---|
| one package at a time, as it was | 635 s | 28.86 MB |
| two runs, a group each | 296 s | 28.80 MB |
| one at a time without LTO, as dev was | 246 s | 43.1 MB |
Thin LTO was measured too and is not the answer: 459 s one at a time, and programs 24% bigger.
So both channels build the release profile as it is, and the first addendum's decision is undone: a dev package is again the program the release ships, and a fault that only full LTO brings out is seen on dev first.
What that gives up is what ci/README.md used to promise, that a package
builds only its own crates with their own features. Cargo gives a crate the
features of every package it is built beside, so within a group they are
shared: alpymist and Settings link the Wi-Fi, power, AI usage and
screensaver crates with their popups compiled in, where alone they would not.
The optimiser drops what a program does not call — twenty of the programs came
out the same size to the byte, and none more than 4 kB bigger — but they are
not the same bytes, and a crate that only compiles because a neighbour turns a
feature on is no longer caught by the package build. CI catches it in its
place: ci/build-programs.sh alone has cargo check each package by itself,
with its own features, on every pull request. The groups are by what
must not be shared: the programs that run with no desktop (the greeter, the
installer, the splash) are built without default features and never beside
the rest, so the window backend cannot reach them this way.