Skip to main content

max / alloy

Measure bootc switch against a layered machine, which the flip made load-bearing Every Alloy machine permanently carries a layer since cad26d5, and the harness README's standing result is that bootc refuses to upgrade a machine in that state. Read quickly, that closes the bootc path entirely, and the middle line of Staleness::REMEDY is a bootc switch. So the console has been printing a remedy nobody had checked still works, and the obvious correction would have been to rewrite it onto rpm-ostree upgrade. It works. Measured on the same versions as the original run, from a c1 machine holding a layered alloy-demo, switching to c2: upgrade errors with "Deployment contains local rpm-ostree modifications" and switch queues the deployment and reboots into it. The two verbs differ, and only upgrade consults local modifications. Nothing about the screen needs changing, which is worth more than a fix would have been. What switch does is discard the layer outright rather than leave it inactive: after the reboot the package is gone from both packages and requested-packages and the binary is absent, with the layered state left on the rollback deployment where a rollback can still reach it. On a real machine that self-heals, because alloy-layer-components.service gates on ConditionPathExists=!/usr/bin/alloy read off the filesystem rather than off a stamp, which is the case its own comment says it was written for. Confirmed by re-installing on the switched machine with the network repo disabled: it resolves 0.0.3 out of the carried file:// repo, which is c2's copy and not the version that had been layered. So on this path the component tracks the image with nothing owning it, and the drop-and-re-add that alloy update has to perform by hand on the rpm-ostree upgrade path happens for free. The cost is a second reboot the user is not told about. They run the three printed commands and land on a machine with no console and no terminal while the unit lays the new ones down and reboots again. d866e125 already records that silence against the first boot after an install; this widens it to every rebuild anyone adopts, which is both more common and the case where nobody has a reason to expect it. Recorded at the remedy as well as in the harness, because the harness is not where someone edits those three strings.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-15 18:44 UTC
Signed with PGP, not checked
Commit: c4b54dc31fc97e3c417b1b5e04b42137704b28dc
Parent: d608c61
2 files changed, +77 insertions, -0 deletions
@@ -160,6 +160,66 @@
160 160 earlier note about moving users off `bootc upgrade` is a permanent condition
161 161 rather than a step in a migration.
162 162
163 + ## `bootc switch` is not `bootc upgrade`, measured 2026-08-15
164 +
165 + The paragraph above is about `upgrade`, and reads as though the whole bootc
166 + path were closed to a layered machine. It is not, and the difference matters
167 + because **`bootc switch` is the command Alloy actually prints**: it is the
168 + middle line of `Staleness::REMEDY` in `crates/alloy/src/stale.rs` and the
169 + adoption step in docs/IMAGE.md.
170 +
171 + Measured on the same versions, from a `c1` machine holding a layered
172 + `alloy-demo`, switching to `c2`:
173 +
174 + # bootc upgrade
175 + error: Upgrading: Deployment contains local rpm-ostree modifications;
176 + cannot upgrade via bootc.
177 +
178 + # bootc switch --transport registry 10.0.2.2:5000/alloy-layertest:c2
179 + Queued for next boot: 10.0.2.2:5000/alloy-layertest:c2
180 +
181 + **`switch` succeeds where `upgrade` refuses.** So the rebuild-and-adopt loop
182 + the console prints is not broken by the component flip, which is the thing
183 + worth knowing before anyone "fixes" that screen.
184 +
185 + **It discards the layer, silently.** After the reboot the request is gone
186 + outright rather than left inactive, and the binary with it:
187 +
188 + 0 booted ...alloy-layertest:c2
189 + packages: [] requested: []
190 + 1 ...alloy-layertest:latest
191 + packages: ['alloy-demo'] requested: ['alloy-demo']
192 +
193 + # alloy-demo
194 + alloy-demo ABSENT
195 +
196 + The rollback deployment keeps the layered state, so nothing is lost that a
197 + rollback would not restore.
198 +
199 + **On a real machine that self-heals, and it is the drain rule for free.**
200 + `etc/systemd/system/alloy-layer-components.service` gates on
201 + `ConditionPathExists=!/usr/bin/alloy`, read off the filesystem rather than off
202 + a stamp, which is exactly the case its comment says it was written for. The
203 + switch makes that condition true again, so the unit re-fires and lays the
204 + components down from the **new** image's carried repo. Confirmed by re-running
205 + the install on the switched machine with the network repo disabled: it
206 + resolves `0.0.3` out of `/usr/share/alloy-demo/rpm`, which is `c2`'s copy and
207 + not the version that was layered before.
208 +
209 + That answers, for the rebuild path, the problem recorded above as "the layer
210 + does not follow the image". On the `rpm-ostree upgrade` path a satisfied
211 + request is not re-resolved and `alloy update` has to drop and re-add. On the
212 + `bootc switch` path the drop happens on its own and the unit does the re-add,
213 + so the component tracks the image with nothing owning it.
214 +
215 + **The cost is a second reboot, and it is silent.** The user types the three
216 + commands, reboots, and lands on a machine with no console and no terminal
217 + while the unit lays them down and reboots again. That is the same silent
218 + double boot already recorded against the first-install path in GO alloy
219 + `d866e125` subtask 7 — this measurement widens it from "the first boot after
220 + an install" to "every rebuild anyone adopts", which is the more common case
221 + and the one where the user has no reason to expect it.
222 +
163 223 ## How an installed machine gets the components, 2026-08-14
164 224
165 225 Decided by Max: **they travel on the ISO**, not over the network. That is the
@@ -229,6 +229,23 @@
229 229 /// build escalates with sudo or run0 (GO alloy `3587c247` moves it), and
230 230 /// duplicating the command here would make this screen wrong the day that
231 231 /// lands.
232 + ///
233 + /// `switch` still works on a layered machine, and that is not obvious now
234 + /// that every Alloy machine permanently carries a layer. `bootc upgrade`
235 + /// refuses outright on local rpm-ostree modifications; `bootc switch` does
236 + /// not, so the component flip did not break this screen. Measured
237 + /// 2026-08-15, `build/layertest/README.md`, "`bootc switch` is not `bootc
238 + /// upgrade`". Do not "fix" these three lines onto `rpm-ostree upgrade`
239 + /// without re-reading it.
240 + ///
241 + /// What the switch does do is discard the layer, which is why the machine
242 + /// comes up once with no console before
243 + /// `alloy-layer-components.service` lays the new image's components down
244 + /// and reboots a second time. That self-heal is what makes a rebuilt
245 + /// console reach the user at all, so the extra boot is the mechanism
246 + /// rather than a wart — but nothing here says so, and the user is not
247 + /// told. Same silent double boot as the first-install path in GO alloy
248 + /// `d866e125`.
232 249 pub(crate) const REMEDY: [&'static str; 3] = [
233 250 "build/build-image.sh",
234 251 "bootc switch --transport containers-storage localhost/alloy:local",