Skip to main content

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:

  1. planning β€” bst-ci's scripts/ci-build-matrix.py runs bst show and splits uncached elements into a core set (first CORE_SPLIT) and NUM_CHUNKS round-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.
  2. build_core β€” builds the bootstrap set, pushes the CAS as ghcr.io/…/cache-tromso-core:latest (zstd tarball via oras).
  3. 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.
  4. build_final β€” merges all chunk CAS tarballs, builds the final target, just export (squash + OCI labels + chunkify), just lint (bootc container lint), pushes latest + 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):

  1. just container tromso β€” 3-stage tromso/Containerfile: ghcr payload (kernel modules) β†’ Debian stage builds a dmsquash-live initramfs (incl. the 95tromso-isofile Ventoy dracut module) β†’ final stage installs flatpaks (src/install-flatpaks.sh) and configures the live env (src/configure-live.sh).
  2. The payload image is squashed and imported into a VFS containers-storage inside the squashfs β€” that's what makes the offline self-install work.
  3. tromso/src/build-iso.sh assembles 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 runs bst source track daily. Local elements (elements/tromso, elements/gnomeos-deps) go into one automergeable PR; the kde-build-meta.bst junction gets a separate review-required PR (a junction bump can rebuild the world). PRs made with the default GITHUB_TOKEN don't trigger CI β€” set a BOT_TOKEN secret to fix that.

Troubleshooting log (symptom β†’ root cause β†’ fix)​

DateSymptomRoot causeFix
2026-07-19Every just call in CI fails: "multiple candidate justfiles"justfile + Justfile both at root; just β‰₯1.30 hard-errorsISO recipes moved to iso.justfile, imported from Justfile
2026-07-19just iso-sd-boot tromso in build-iso.yml never workedrecipe + tromso/Containerfile + dracut module never existed in this repoported from xfce-linux/dakota-iso (PR #74)
2026-07-19tromso/Containerfile missing from git after commit.gitignore had unanchored Containerfile ruleanchored to /Containerfile
2026-07-19All 10 chunk jobs building for 5+ hproject.conf name: aurora→tromso changed every cache key → world rebuildexpected one-time cost; caches repopulate
2026-07-19Installer flatpak never launched in live sessionISO baked org.bootcinstaller.Installer but autostart/symlink pointed elsewhereboth 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 R2 tromso/.
  • stable is a release bookmark branch: promote-stable.yml (weekly cron
    • dispatch, force=true to override) verifies the newest nightly build and ISO both succeeded, force-pushes that commit to stable, and dispatches the stable build β†’ :stable, :stable-YYYYMMDD tags and an ISO under R2 tromso/stable/. The stable ISO embeds the :stable payload (payload_ref is rewritten per-channel in build-iso.yml).
  • 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.