ci and iso pipeline
How TromsΓΈ gets from .bst elements to a bootable, installable live ISO β
and how to debug it when it breaks. (Same architecture as tuna-os/xfce-linux;
patterns originate in projectbluefin/dakota and dakota-iso.)
Build chainβ
elements/** βββΊ Build Tromso (Multi-Runner) βββΊ ghcr.io/tuna-os/tromso:latest
β (workflow_run)
βΌ
Build and Publish TromsΓΈ Live ISO βββΊ R2: tromso/tromso-live-*.iso
β
βΌ (boot gate: TROMSO_LIVE_READY on serial + screenshot)
LUKS Install End-to-End Test (PR / weekly / dispatch)
Multi-runner build (build-tromso-multirunner.yml)β
Planning + core + parallel dependency chunks (steps 1-3 below) are a
shared reusable workflow, tuna-os/bst-ci
β identical across every BuildStream desktop repo except image
name/target/chunk count. build_final (step 4) stays local: cosign's
keyless signing embeds this workflow's identity in the Fulcio
certificate, which is what README.md's verify instructions point at.
Free GitHub runners can't hold the whole KDE build, so it's split:
- planning β bst-ci's
scripts/ci-build-matrix.pyrunsbst showand splits uncached elements into a core set (firstCORE_SPLIT) andNUM_CHUNKSround-robin chunks, each with a composite cache key. It's checked out from bst-ci at run time β this repo no longer carries its own copy. - build_core β builds the bootstrap set, pushes the CAS as
ghcr.io/β¦/cache-tromso-core:latest(zstd tarball via oras). - build_deps (matrix) β each chunk restores core + its own previous CAS,
builds, pushes
cache-tromso-<chunk>:{latest,<cache-key>}. A chunk whose exact cache key already exists on GHCR is skipped entirely. - build_final β merges all chunk CAS tarballs, builds the final target,
just export(squash + OCI labels + chunkify),just lint(bootc container lint), pusheslatest+ date + sha tags (main only).
BuildStream settings CI uses live in the checked-in buildstream-ci.conf.
Cache-key invalidation warning: anything that changes every element's
cache key (e.g. renaming name: in project.conf) triggers a full world
rebuild β expect chunk jobs to run for hours or hit their 6 h timeout once,
then recover from the refreshed GHCR caches.
Live ISO (iso.justfile + tromso/)β
just iso-sd-boot tromso (see iso.justfile, imported from Justfile):
just container tromsoβ 3-stagetromso/Containerfile: ghcr payload (kernel modules) β Debian stage builds a dmsquash-live initramfs (incl. the95tromso-isofileVentoy dracut module) β final stage installs flatpaks (src/install-flatpaks.sh) and configures the live env (src/configure-live.sh).- The payload image is squashed and imported into a VFS containers-storage inside the squashfs β that's what makes the offline self-install work.
tromso/src/build-iso.shassembles a systemd-boot UEFI ISO.
The live session autologs into Plasma as liveuser and autostarts
org.tunaos.InstallerKde (from the tuna-os OCI flatpak remote); fisherman is
symlinked to /usr/local/bin/fisherman with the shared
org.tunaos.Installer.install polkit action (see INSTALLER-FRONTENDS.md in
the org workspace).
LUKS end-to-end test (test-luks-install.yml)β
Local equivalent:
just debug=1 iso-sd-boot tromso # debug=1 enables SSH (liveuser/live)
just luks-test-qemu tromso # boot β fisherman LUKS install β reboot β unlock
tromso/src/luks-unlock.py drives the QEMU monitor: waits for Plymouth via
screendump polling, types the passphrase with sendkey, verifies the
installed system boots. Screenshots (live desktop, Plymouth prompt, installed
desktop) are published to the ci-screenshots branch and PR comments.
Source updatesβ
- Renovate (
renovate.json) β GitHub Actions, container tags. Automerge on green CI, majors included. track-bst-sources.ymlβ Renovate can't parse.bst; this runsbst source trackdaily. Local elements (elements/tromso,elements/gnomeos-deps) go into one automergeable PR; thekde-build-meta.bstjunction gets a separate review-required PR (a junction bump can rebuild the world). PRs made with the defaultGITHUB_TOKENdon't trigger CI β set aBOT_TOKENsecret to fix that.
Troubleshooting log (symptom β root cause β fix)β
| Date | Symptom | Root cause | Fix |
|---|---|---|---|
| 2026-07-19 | Every just call in CI fails: "multiple candidate justfiles" | justfile + Justfile both at root; just β₯1.30 hard-errors | ISO recipes moved to iso.justfile, imported from Justfile |
| 2026-07-19 | just iso-sd-boot tromso in build-iso.yml never worked | recipe + tromso/Containerfile + dracut module never existed in this repo | ported from xfce-linux/dakota-iso (PR #74) |
| 2026-07-19 | tromso/Containerfile missing from git after commit | .gitignore had unanchored Containerfile rule | anchored to /Containerfile |
| 2026-07-19 | All 10 chunk jobs building for 5+ h | project.conf name: auroraβtromso changed every cache key β world rebuild | expected one-time cost; caches repopulate |
| 2026-07-19 | Installer flatpak never launched in live session | ISO baked org.bootcinstaller.Installer but autostart/symlink pointed elsewhere | both sides now use org.tunaos.InstallerKde |
| 2026-07-19 | Multi-runner never went green since May; every run "cancelled" at ~6.5 h | chunk jobs killed by job-level timeout-minutes β a cancelled job never reaches the CAS-push step, so 6 h Γ 10 chunks of build work was discarded daily (β720 runner-hours; zero chunk cache packages ever existed on GHCR) | build bounded inside the step (timeout 270m), push steps if: always() β partial CAS salvaged, builds converge across days |
| 2026-07-19 | Failed chunks could publish their exact-cache-key tag and be skipped forever | for i in 1 2 3 β¦ done retry loop exits 0 on total failure (status of last sleep) | retry loop removed (bst retry-failed/network-retries already cover it); rc propagated |
| 2026-07-19 | LUKS e2e / ISO jobs die in seconds: "Unknown attribute group" at Justfile:5 | Ubuntu 24.04 apt ships just 1.21 (predates [group()]); old runs survived because ancient just silently picked the group-free lowercase justfile we removed | workflows install just via extractions/setup-just and invoke sudo "$(command -v just)" |
Keep appending to this table while iterating on CI (see the org ci-fix-loop
skill; format proven in tuna-os/tunaos docs/ci-troubleshooting.md).
Channels: nightly (main) and stableβ
- main is the nightly trunk: the daily scheduled multi-runner build
publishes
:latest,:nightly,:nightly-YYYYMMDD,:<sha>; the ISO lands at R2tromso/. - stable is a release bookmark branch:
promote-stable.yml(weekly cron- dispatch,
force=trueto override) verifies the newest nightly build and ISO both succeeded, force-pushes that commit tostable, and dispatches the stable build β:stable,:stable-YYYYMMDDtags and an ISO under R2tromso/stable/. The stable ISO embeds the:stablepayload (payload_ref is rewritten per-channel in build-iso.yml).
- dispatch,
- Tracking/renovate PRs only ever target main; stable moves exclusively via promotion.
Release-linked sourcesβ
Local elements track upstream release tags (globs like v[0-9]*), not
dev branches, so the daily bst source track lands releases. Exceptions
that intentionally track branches: rolling content repos (aurora common,
docs, wallpapers / xfwl4 dev repos) and junctions (pinned branch). Never pin
track: to one exact tag β tracking can then never move it.
Guard rails (what stops a bad commit)β
Pre-merge, required on main (branch protection; automerge fires on green):
shellcheck/yamllint/actionlint, unit suites (BATS + pytest incl. the
52-test luks-unlock suite and test_iso_invariants.py β every invariant
assertion encodes a bug class that actually shipped), the BuildStream
graph gate (bst show --deps all on the shipping target, junctions
resolved), and Just Parse. pr-build-changed.yml additionally builds
the elements a PR touches against the warm GHCR core CAS β informational
until the world rebuild converges, then promote to required (tromso#80).
Post-merge: salvage-enabled nightly world build β ISO boot gate
(ready-marker + screenshot artifact) β weekly LUKS install e2e
(screenshots on the ci-screenshots branch + PR comments). A cloud
routine ("tromso + xfce-linux CI babysitter", every 3 h) diagnoses
completed failures from logs and pushes fixes.
Rules that keep this healthy: never add paths: filters to workflows
whose jobs are required checks (a non-reporting required check deadlocks
automerge); if a required job is renamed, update the branch-protection
contexts in the same PR; never wrap a gate in || echo (that is how
bst-validate and pytest were silently dead for months).
Rollbackβ
rollback-stable.yml (dispatch-only, dry_run defaults to true) is the
inverse of promotion: verifies the target :<sha> image exists, then
skopeo copy --preserve-digests onto :stable (+ a dated
stable-rollback-* tag) and force-pushes the stable branch to the same
commit so branch and tag never diverge. Shares the promote concurrency
group so it cannot race a promotion. Dakota-pattern notes: once signing
lands, add a cosign-verify step before the retag.