The stable channel is designed well — a weekly promotion with four health
gates, a dispatch-only rollback with dry_run defaulting to true, cosign on
every pushed digest. The gap is on the ISO half: the boot gate runs after
the ISO is published, an upload failure cannot fail the run, and rollback
never touches the download surface at all.
1. The boot gate runs after publish, so it is not a gate
In .github/workflows/build-iso.yml, Upload ISO to Cloudflare R2 is step
128; Boot verification (UEFI + serial) is step 160. The upload
overwrites the mutable name consumers download:
rclone copyto ... "$ISO" "R2:${R2_BUCKET}/${r2_prefix}/tromso-live-latest.iso"
So an ISO that never reaches TROMSO_LIVE_READY is already live at
https://download.tunaos.org/tromso[/stable]/tromso-live-latest.iso by the
time QEMU says so. The job then fails, and nothing un-publishes it — the bad
ISO stays at the download URL until a later build happens to succeed. The
boot check is a post-publish notification, not a release gate.
2. Upload failure is swallowed, and the health check believes the result
The upload step carries continue-on-error: true (build-iso.yml:158), and
the step echoes the download URL unconditionally without ever reading it
back. A run where every rclone copyto failed still concludes success.
That conclusion is load-bearing: promote-stable.yml's health check gates
promotion on the newest completed run of Build and Publish Tromsø Live ISO
having conclusion == "success". A publish that uploaded nothing therefore
satisfies the ISO gate.
Right now https://download.tunaos.org/tromso/tromso-live-latest.iso and
.../tromso/stable/tromso-live-latest.iso both return 404. That is
consistent with the workflow never having succeeded (gh run list --workflow "Build and Publish Tromsø Live ISO" --status success is empty), so this is
not a silent regression today — but there is no check anywhere in the
pipeline that would distinguish the two cases, which is the point.
3. Published ISO filenames name the wrong commit
Compute ISO name builds the dated filename from ${{ github.sha }}:
SHA=$(echo "${{ github.sha }}" | cut -c1-7)
echo "dated=tromso-live-${DATE}-${SHA}.iso" >> "$GITHUB_OUTPUT"
The workflow's dominant trigger is workflow_run (the push trigger only
fires for tromso/**, Justfile, iso.justfile changes on main), and for
workflow_run GitHub documents GITHUB_SHA as "last commit on default
branch" — not the commit that was built. The checkout step in this same
workflow already compensates:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
The naming step does not. So every workflow_run-built ISO — including
every stable ISO, since stable ISOs only ever arrive via
workflow_run from the stable branch — is filed under a main commit SHA.
The dated ISOs are precisely the rollback material, and their names cannot
be trusted to identify what is inside them.
output/tromso-live-latest.iso-CHECKSUM is also generated (build-iso.yml:108)
and then never uploaded, so the latest download has a .sig/.cert but no
checksum file.
4. Rollback restores less than a release publishes
A release moves four surfaces: the x86_64 tags (:stable,
:stable-YYYYMMDD, :<sha>), the aarch64 tags (:stable-aarch64, …), the
R2 tromso/stable/ ISO objects, and the stable branch.
rollback-stable.yml restores the first and the last. After it runs:
:stable-aarch64 still points at the bad build (build-tromso-multirunner.yml:472-475
publishes it; the rollback never mentions it).
tromso/stable/tromso-live-latest.iso is still the bad ISO. For a
bad installed system that is survivable, because the ISO resolves
ghcr.io/tuna-os/tromso:stable at install time. For a bad live
environment — ISO does not boot, installer does not launch — the image
rollback fixes nothing for anyone downloading it.
- The target is retagged onto
:stable without a cosign verify. The
workflow header says "no cosign yet, see the signing issue" and
docs/ci-and-iso-pipeline.md says "once signing lands, add a cosign-verify
step"; signing landed — build-tromso-multirunner.yml:268-272 signs every
pushed digest. Both notes are stale.
5. The weekly promotion has failed silently for five weeks
Promote main to stable has concluded failure on every run since
2026-08-04 (2026-08-04, 08-11, 08-18, 08-25, 09-01). It is failing
correctly — the gates are red, there is no stable branch, and the ISO
workflow has never succeeded — but the only signal is a red run in the
Actions tab. A release channel that has been stuck for over a month
produces no notification, which is what makes items 1-4 easy to leave in
place.
Proposed fixes
Docs half is in the linked PR (a rollback runbook covering the surfaces
automation does not restore, plus the stale-cosign correction). The workflow
half needs a maintainer — the diffs are in the PR body, since the hive bot's
token has no workflows permission and cannot push .github/workflows/*:
- Move
Boot verification (UEFI + serial) above Install cosign, so a
non-booting ISO is never uploaded. Trade-off accepted deliberately: a
flaky QEMU boot then blocks publish, which is the correct direction for a
release artifact.
- Drop
continue-on-error: true from the R2 upload, and curl -fsI the
published URL afterwards so "success" means "downloadable".
- Name the ISO from
github.event.workflow_run.head_sha || github.sha, and
upload the tromso-live-latest.iso-CHECKSUM that is already generated.
- Extend
rollback-stable.yml to retag :stable-aarch64 and to
cosign verify the target before retagging; either restore the R2 ISO in
the same workflow or keep it manual and linked to the runbook.
- Alert on a failed
Promote main to stable run (the cheapest version: an
if: failure() step that opens or bumps a tracking issue), so a stuck
channel is visible without watching the Actions tab.
— hive: agent=operations backend=claude model=claude-opus-5 claude=2.1.226
The stable channel is designed well — a weekly promotion with four health
gates, a dispatch-only rollback with
dry_rundefaulting to true, cosign onevery pushed digest. The gap is on the ISO half: the boot gate runs after
the ISO is published, an upload failure cannot fail the run, and rollback
never touches the download surface at all.
1. The boot gate runs after publish, so it is not a gate
In
.github/workflows/build-iso.yml,Upload ISO to Cloudflare R2is step128;Boot verification (UEFI + serial)is step160. The uploadoverwrites the mutable name consumers download:
So an ISO that never reaches
TROMSO_LIVE_READYis already live athttps://download.tunaos.org/tromso[/stable]/tromso-live-latest.isoby thetime QEMU says so. The job then fails, and nothing un-publishes it — the bad
ISO stays at the download URL until a later build happens to succeed. The
boot check is a post-publish notification, not a release gate.
2. Upload failure is swallowed, and the health check believes the result
The upload step carries
continue-on-error: true(build-iso.yml:158), andthe step echoes the download URL unconditionally without ever reading it
back. A run where every
rclone copytofailed still concludessuccess.That conclusion is load-bearing:
promote-stable.yml's health check gatespromotion on the newest completed run of
Build and Publish Tromsø Live ISOhaving
conclusion == "success". A publish that uploaded nothing thereforesatisfies the ISO gate.
Right now
https://download.tunaos.org/tromso/tromso-live-latest.isoand.../tromso/stable/tromso-live-latest.isoboth return404. That isconsistent with the workflow never having succeeded (
gh run list --workflow "Build and Publish Tromsø Live ISO" --status successis empty), so this isnot a silent regression today — but there is no check anywhere in the
pipeline that would distinguish the two cases, which is the point.
3. Published ISO filenames name the wrong commit
Compute ISO namebuilds the dated filename from${{ github.sha }}:The workflow's dominant trigger is
workflow_run(thepushtrigger onlyfires for
tromso/**,Justfile,iso.justfilechanges on main), and forworkflow_runGitHub documentsGITHUB_SHAas "last commit on defaultbranch" — not the commit that was built. The checkout step in this same
workflow already compensates:
The naming step does not. So every
workflow_run-built ISO — includingevery stable ISO, since stable ISOs only ever arrive via
workflow_runfrom the stable branch — is filed under amaincommit SHA.The dated ISOs are precisely the rollback material, and their names cannot
be trusted to identify what is inside them.
output/tromso-live-latest.iso-CHECKSUMis also generated (build-iso.yml:108)and then never uploaded, so the
latestdownload has a.sig/.certbut nochecksum file.
4. Rollback restores less than a release publishes
A release moves four surfaces: the x86_64 tags (
:stable,:stable-YYYYMMDD,:<sha>), the aarch64 tags (:stable-aarch64, …), theR2
tromso/stable/ISO objects, and thestablebranch.rollback-stable.ymlrestores the first and the last. After it runs::stable-aarch64still points at the bad build (build-tromso-multirunner.yml:472-475publishes it; the rollback never mentions it).
tromso/stable/tromso-live-latest.isois still the bad ISO. For abad installed system that is survivable, because the ISO resolves
ghcr.io/tuna-os/tromso:stableat install time. For a bad liveenvironment — ISO does not boot, installer does not launch — the image
rollback fixes nothing for anyone downloading it.
:stablewithout acosign verify. Theworkflow header says "no cosign yet, see the signing issue" and
docs/ci-and-iso-pipeline.mdsays "once signing lands, add a cosign-verifystep"; signing landed —
build-tromso-multirunner.yml:268-272signs everypushed digest. Both notes are stale.
5. The weekly promotion has failed silently for five weeks
Promote main to stablehas concludedfailureon every run since2026-08-04 (2026-08-04, 08-11, 08-18, 08-25, 09-01). It is failing
correctly — the gates are red, there is no
stablebranch, and the ISOworkflow has never succeeded — but the only signal is a red run in the
Actions tab. A release channel that has been stuck for over a month
produces no notification, which is what makes items 1-4 easy to leave in
place.
Proposed fixes
Docs half is in the linked PR (a rollback runbook covering the surfaces
automation does not restore, plus the stale-cosign correction). The workflow
half needs a maintainer — the diffs are in the PR body, since the hive bot's
token has no
workflowspermission and cannot push.github/workflows/*:Boot verification (UEFI + serial)aboveInstall cosign, so anon-booting ISO is never uploaded. Trade-off accepted deliberately: a
flaky QEMU boot then blocks publish, which is the correct direction for a
release artifact.
continue-on-error: truefrom the R2 upload, andcurl -fsIthepublished URL afterwards so "success" means "downloadable".
github.event.workflow_run.head_sha || github.sha, andupload the
tromso-live-latest.iso-CHECKSUMthat is already generated.rollback-stable.ymlto retag:stable-aarch64and tocosign verifythe target before retagging; either restore the R2 ISO inthe same workflow or keep it manual and linked to the runbook.
Promote main to stablerun (the cheapest version: anif: failure()step that opens or bumps a tracking issue), so a stuckchannel is visible without watching the Actions tab.
— hive: agent=operations backend=claude model=claude-opus-5 claude=2.1.226