ci #539
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: ci | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| workflow_dispatch: | |
| schedule: | |
| # Daily live-ISO signal: base images drift under us (dracut, systemd, | |
| # bootc) — a broken live boot should page this repo, not a downstream. | |
| - cron: '20 4 * * *' | |
| # Without this, every push to a branch/PR (including Renovate's frequent | |
| # rebases) queues a brand-new run instead of superseding the last one for | |
| # that ref. Observed 2026-07-20: 26 queued runs piled up on this repo alone | |
| # — many of them stale duplicates from earlier rebases of the same PR — | |
| # saturating the shared macOS/Windows runner pool for hours and starving | |
| # genuinely new work. github.head_ref is empty on push/schedule, so this | |
| # still groups correctly (falls back to github.ref) for non-PR triggers. | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.head_ref || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # ── Stage 1+2: lint + unit + recipe schema parse ──────────────────────────── | |
| # Cheap, runs on every PR. ~2 min. | |
| lint-test: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 | |
| with: | |
| go-version-file: go.mod | |
| - name: go vet | |
| run: go vet ./... | |
| - name: go test | |
| run: go test ./... | |
| - name: go test with coverage summary | |
| run: go test -cover -covermode=set ./... | |
| # Integration-tag help checks: builds the binary and execs --help | |
| # for every subcommand. Confirms the CLI surface matches what CI depends on. | |
| - name: go test (integration tag, help-only) | |
| run: go test -tags=integration -run='Help' ./... | |
| - name: go build | |
| run: go build ./... | |
| # Stage 2: every example recipe must parse successfully. The build | |
| # tool itself enforces the schema; we just feed each one to a no-op | |
| # invocation and check exit status. --help short-circuits before any | |
| # actual I/O so this is fast. | |
| - name: recipe schema parses | |
| run: | | |
| go build -o tacklebox ./cmd/tacklebox | |
| fail=0 | |
| for r in examples/*.json fixtures/*.json; do | |
| [ -f "$r" ] || continue | |
| # Use a syntactic-only parse: jq if installed, else go's parser | |
| # via `tacklebox build --help` (rejects the recipe arg before | |
| # opening anything). | |
| if ! python3 -c "import json,sys; json.load(open('$r'))"; then | |
| echo "::error file=$r::JSON parse failed" | |
| fail=1 | |
| fi | |
| done | |
| exit $fail | |
| - name: shellcheck dracut modules | |
| uses: ludeeus/action-shellcheck@00b27aa7cb85167568cb48a3838b75f4265f2bca # master as of 2026-06-09 | |
| with: | |
| additional_files: src/dracut/95tbox-root/module-setup.sh src/dracut/95tbox-root/tbox-root-mount.sh src/dracut/90tbox-live/module-setup.sh src/dracut/90tbox-live/parse-tbox-live.sh src/dracut/90tbox-live/tbox-live-root.sh src/dracut/90tbox-live/tbox-live-generator.sh src/dracut/90tbox-live/tbox-live-mount.sh scripts/test-boot.sh | |
| severity: warning | |
| # ── Stage 3: two-env disk-build smoke, verify, update, and boot ──────────── | |
| # Builds a 10 GB loop image with two distinct upstream bootc images and | |
| # asserts `tacklebox verify` passes (each env must end up with a distinct | |
| # ostree commit hash — the regression check for the 2026-05-11 cross-env | |
| # collision bug). Then runs `tacklebox update` to re-install all envs | |
| # without reformatting, and verifies again — regression check for the | |
| # USB pre-flight unmount and the update command. | |
| # | |
| # Also exercises the UnmountDevice loop-device smoke at the integration | |
| # level: creates a loop partition, mounts it, calls UnmountDevice, asserts | |
| # the mount is gone. | |
| # | |
| # Uses /mnt (~75 GB NVMe on free runners); jlumbroso/free-disk-space | |
| # recovers ~30 GB on / first. | |
| verify-smoke: | |
| runs-on: ubuntu-latest | |
| needs: lint-test | |
| timeout-minutes: 25 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 | |
| with: | |
| go-version-file: go.mod | |
| - name: free disk space | |
| uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # main as of 2026-06-09 | |
| with: | |
| tool-cache: false | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: false | |
| docker-images: true | |
| swap-storage: false | |
| - name: install build deps | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends \ | |
| xorriso mtools squashfs-tools dosfstools \ | |
| systemd-boot systemd-boot-efi gdisk podman \ | |
| qemu-system-x86 ovmf | |
| # Runner image ubuntu24/20260726 bumped its Podman 4.9.3 -> 5.8.4 but | |
| # left crun at noble's 1.14.1. Podman >= 5 stamps `ociVersion: 1.2.0` | |
| # into config.json, and crun <= 1.14 rejects any version that is not | |
| # 1.0.x, so every `podman run` dies before the container starts with | |
| # "OCI runtime error: crun: unknown version specified" (exit 126). | |
| # crun 1.15 dropped that check and runc never had it, so point podman | |
| # at runc for exactly that pairing: a no-op on images whose crun and | |
| # podman already agree. | |
| - name: match podman's OCI runtime to its spec version | |
| run: | | |
| podman_ver=$(podman --version | awk '{print $3}') | |
| crun_ver=$(crun --version 2>/dev/null | awk 'NR==1{print $3}') | |
| echo ">>> podman=$podman_ver crun=${crun_ver:-absent}" | |
| if dpkg --compare-versions "$podman_ver" ge 5 && | |
| [ -n "$crun_ver" ] && dpkg --compare-versions "$crun_ver" lt 1.15; then | |
| command -v runc >/dev/null || { | |
| echo "::error::crun $crun_ver cannot run podman $podman_ver's spec and there is no runc to fall back to" | |
| exit 1 | |
| } | |
| sudo install -d /etc/containers/containers.conf.d | |
| printf '[engine]\nruntime = "runc"\n' | | |
| sudo tee /etc/containers/containers.conf.d/10-tbox-runc.conf | |
| podman info --format '>>> OCI runtime: {{.Host.OCIRuntime.Name}} {{.Host.OCIRuntime.Version}}' | |
| fi | |
| - name: build tacklebox | |
| run: go build -o tacklebox ./cmd/tacklebox | |
| # Persist the image-ID-keyed build caches across runs. tacklebox keys | |
| # cache entries by image ID internally, so restoring a stale snapshot | |
| # is harmless (changed images simply miss). Rolling key: every run | |
| # saves a fresh snapshot, restore picks the most recent. Saves the | |
| # ~2 min/env dracut initramfs rebuild on cache-warm runs. | |
| - name: prepare output base for cache restore | |
| run: sudo install -d -o "$(id -u)" -g "$(id -g)" /mnt/tbx | |
| - name: restore build caches | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: | | |
| /mnt/tbx/initramfs-cache | |
| /mnt/tbx/squashfs-cache | |
| key: tbox-build-cache-${{ github.run_id }} | |
| restore-keys: | | |
| tbox-build-cache- | |
| - name: pre-pull fixture images | |
| # Quay CDN occasionally drops blobs mid-transfer (unexpected EOF). | |
| # Retry up to 3 times with exponential backoff. | |
| run: | | |
| for img in quay.io/centos-bootc/centos-bootc:stream10 quay.io/fedora/fedora-bootc:44; do | |
| ok=0 | |
| for i in 1 2 3; do | |
| echo ">>> pull $img (attempt $i/3)" | |
| if sudo podman pull "$img"; then ok=1; break; fi | |
| sleep $((i * 10)) | |
| done | |
| if [ "$ok" -eq 0 ]; then | |
| echo "::error::failed to pull $img after 3 attempts" | |
| exit 1 | |
| fi | |
| done | |
| - name: tacklebox build (2-env block image) | |
| run: | | |
| sudo mkdir -p /mnt/tbx | |
| sudo ./tacklebox build fixtures/smoke-2env.json -b /mnt/tbx | |
| - name: tacklebox verify | |
| run: sudo ./tacklebox verify /mnt/tbx/tacklebox.img | |
| - name: tacklebox status check | |
| run: sudo ./tacklebox status /mnt/tbx/tacklebox.img | |
| # ── Stage 3b: update smoke ─────────────────────────────────────────── | |
| # Re-installs both envs on the already-built image using | |
| # `tacklebox update`. This exercises: | |
| # - loop-image attach (resolveDevice) | |
| # - pre-flight unmount (UnmountDevice) on a loop device with | |
| # no existing mounts — should be a clean no-op | |
| # - full bootc re-install → BLS rewrite pipeline | |
| # - verify that the result still passes after update | |
| # Shares -b /mnt/tbx with the build step on purpose: the update must | |
| # hit the initramfs cache the build populated minutes earlier (the | |
| # cache-reuse regression check). | |
| - name: tacklebox update (re-install same recipe) | |
| run: | | |
| sudo ./tacklebox update --yes fixtures/smoke-2env.json \ | |
| /mnt/tbx/tacklebox.img -b /mnt/tbx | |
| - name: tacklebox verify (post-update) | |
| run: sudo ./tacklebox verify /mnt/tbx/tacklebox.img | |
| # ── Stage 3c: UnmountDevice loop smoke (integration test) ──────────── | |
| # Runs the Go integration test that creates a loop device, mounts a | |
| # partition, and asserts UnmountDevice clears it. Faster than the full | |
| # build smoke and directly exercises the new pre-flight unmount code. | |
| - name: UnmountDevice loop smoke (integration) | |
| run: go test -tags=integration -v -run=TestUnmountDevice_LoopSmoke ./internal/blockdev/... | |
| # ── Stage 3d: add / remove smoke ──────────────────────────────────── | |
| # Adds a third env (using the already-pulled centos-bootc image) to | |
| # the 2-env image, verifies the 3-env result, then removes the added | |
| # env and verifies the image is back to 2-env. Exercises the full | |
| # add/remove mutation pipeline without reformatting. | |
| - name: tacklebox add (3rd env) | |
| run: | | |
| sudo ./tacklebox add fixtures/smoke-add.json \ | |
| /mnt/tbx/tacklebox.img --yes -b /mnt/tbx | |
| - name: tacklebox verify (post-add, 3 envs) | |
| run: sudo ./tacklebox verify /mnt/tbx/tacklebox.img | |
| - name: tacklebox status (post-add) | |
| run: sudo ./tacklebox status /mnt/tbx/tacklebox.img | |
| - name: tacklebox remove (the added env) | |
| run: | | |
| sudo ./tacklebox remove centos-add \ | |
| /mnt/tbx/tacklebox.img --yes -b /mnt/tbx | |
| - name: tacklebox verify (post-remove, back to 2 envs) | |
| run: sudo ./tacklebox verify /mnt/tbx/tacklebox.img | |
| # Stage 4: Boot smoke. We use TCG (no KVM on free runners) so it's | |
| # slow (~3 min to login). | |
| - name: tacklebox boot smoke (Stage 4) | |
| run: sudo ./scripts/test-boot.sh /mnt/tbx/tacklebox.img 600 | |
| - name: upload boot log | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: verify-smoke-boot-log | |
| path: qemu-boot.log | |
| if-no-files-found: ignore | |
| - name: artifact summary | |
| if: always() | |
| run: | | |
| echo '## tacklebox build artefacts' >> "$GITHUB_STEP_SUMMARY" | |
| ls -lh /mnt/tbx/ 2>/dev/null | awk 'NR>1 {print "- `" $NF "` (" $5 ")"}' >> "$GITHUB_STEP_SUMMARY" | |
| df -h / /mnt 2>/dev/null >> "$GITHUB_STEP_SUMMARY" | |
| if [ -f qemu-boot.log ]; then | |
| echo '### QEMU Boot Log (tail)' >> "$GITHUB_STEP_SUMMARY" | |
| echo '```' >> "$GITHUB_STEP_SUMMARY" | |
| tail -n 50 qemu-boot.log >> "$GITHUB_STEP_SUMMARY" | |
| echo '```' >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| # ── Stage 3-ISO: two-env ISO smoke (per-env + dedup layouts) ────────────── | |
| # Builds BOTH ISO layouts from one pair of fixture images (STOCK | |
| # fedora-bootc + a per-env marker, built in-job so the localhost/ | |
| # user-store path is exercised), verifies them, asserts that the dedup | |
| # layout actually shrinks the ISO, and QEMU-boots each layout. The stock | |
| # fixture (no dracut-live) is the regression test for the embedded | |
| # tbox-live module (#90). The two recipes pick different default_boot | |
| # envs on purpose so both envs get boot coverage: | |
| # - per-env layout boots beta (plain tbox-live path) | |
| # - dedup layout boots alpha and must log the tbox-root subtree pivot | |
| # No actions/cache here: the fixture images are rebuilt every run, so their | |
| # image IDs (and thus every tacklebox cache key) change every run. | |
| iso-smoke: | |
| # RunsOn build-amd64 (controlled ubuntu24-full-x64 image): the GitHub | |
| # runner's podman 5.8.4 wedges the customize commit and its crun/conmon | |
| # cannot be re-pinned (tuna-os/tunaOS#1893). The `runs-on=` marker + `runner=` | |
| # group is RunsOn's label format (see docs/runners/labels). | |
| runs-on: runs-on=${{ github.run_id }}/runner=build-amd64 | |
| needs: lint-test | |
| timeout-minutes: 60 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 | |
| with: | |
| go-version-file: go.mod | |
| - name: free disk space | |
| uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # main as of 2026-06-09 | |
| with: | |
| tool-cache: false | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: false | |
| docker-images: true | |
| swap-storage: false | |
| - name: install build deps | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends \ | |
| xorriso mtools squashfs-tools dosfstools \ | |
| systemd-boot systemd-boot-efi podman \ | |
| qemu-system-x86 ovmf | |
| # Same crun/podman spec-version mismatch as verify-smoke; see there. | |
| - name: match podman's OCI runtime to its spec version | |
| run: | | |
| podman_ver=$(podman --version | awk '{print $3}') | |
| crun_ver=$(crun --version 2>/dev/null | awk 'NR==1{print $3}') | |
| echo ">>> podman=$podman_ver crun=${crun_ver:-absent}" | |
| if dpkg --compare-versions "$podman_ver" ge 5 && | |
| [ -n "$crun_ver" ] && dpkg --compare-versions "$crun_ver" lt 1.15; then | |
| command -v runc >/dev/null || { | |
| echo "::error::crun $crun_ver cannot run podman $podman_ver's spec and there is no runc to fall back to" | |
| exit 1 | |
| } | |
| sudo install -d /etc/containers/containers.conf.d | |
| printf '[engine]\nruntime = "runc"\n' | | |
| sudo tee /etc/containers/containers.conf.d/10-tbox-runc.conf | |
| podman info --format '>>> OCI runtime: {{.Host.OCIRuntime.Name}} {{.Host.OCIRuntime.Version}}' | |
| fi | |
| # podman 5.8.4 (what both the GitHub runner and the ubuntu24 RunsOn image | |
| # ship) is broken for this workload: the customize commit wedges and | |
| # `podman build` fails with "cannot re-exec process". Install the Kubic | |
| # unstable podman (6.x) and drop the image's 5.8.4 shadow from | |
| # /usr/local/bin so the pinned podman wins on PATH. | |
| - name: install podman 6.x from Kubic unstable | |
| run: | | |
| set -euo pipefail | |
| sudo install -d /etc/apt/keyrings | |
| curl -fsSL https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/unstable/xUbuntu_24.04/Release.key \ | |
| | gpg --dearmor | sudo tee /etc/apt/keyrings/kubic.gpg >/dev/null | |
| echo 'deb [signed-by=/etc/apt/keyrings/kubic.gpg] https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/unstable/xUbuntu_24.04/ /' \ | |
| | sudo tee /etc/apt/sources.list.d/kubic.list | |
| sudo apt-get update -qq | |
| # Force the Kubic stack: apt kept the image's 4.9.3+conmon2.1.10 | |
| # (conmon journald failure). Remove the old podman/conmon so the | |
| # Kubic podman 6.x + matching conmon/crun come in fresh. | |
| sudo apt-get remove -y podman conmon crun || true | |
| sudo apt-get install -y podman | |
| # conmon 2.1.10 on this image lacks journald support and dies on the | |
| # customize run ("[conmon:e]: Include journald in compilation path"). | |
| # Force a file log driver so conmon never touches journald. | |
| sudo install -d /etc/containers/containers.conf.d | |
| printf '[engine]\nlog_driver = "k8s-file"\n' | \ | |
| sudo tee /etc/containers/containers.conf.d/10-tbox-log.conf | |
| for p in /usr/local/bin/podman /usr/local/sbin/podman; do | |
| if [ -e "$p" ] && ! dpkg -S "$p" >/dev/null 2>&1; then | |
| echo ">>> removing non-apt podman shadow: $p" | |
| sudo rm -f "$p" | |
| fi | |
| done | |
| hash -r | |
| echo ">>> podman now: $(podman --version) / conmon $(conmon --version 2>/dev/null | head -1 || echo none)" | |
| - name: build tacklebox | |
| run: go build -o tacklebox ./cmd/tacklebox | |
| # Plain podman (no sudo): the images must land in the runner user's | |
| # store, which is where the live pipeline reads from. | |
| # k8s-file log driver for all podman runs resolves the runner journald | |
| # gap (tuna-os/tunaOS#1893). | |
| - name: build fixture live images | |
| run: | | |
| # sudo (root) podman: the RunsOn image's rootless re-exec is broken | |
| # ("cannot re-exec process"); the tacklebox build below is root too. | |
| sudo podman build -f fixtures/iso-smoke.Containerfile \ | |
| --build-arg MARKER=alpha -t localhost/tbox-iso-alpha:latest fixtures | |
| sudo podman build -f fixtures/iso-smoke.Containerfile \ | |
| --build-arg MARKER=beta -t localhost/tbox-iso-beta:latest fixtures | |
| sudo podman build -f fixtures/iso-smoke.Containerfile \ | |
| --build-arg MARKER=gamma -t localhost/tbox-iso-gamma:latest fixtures | |
| sudo podman build -f fixtures/iso-smoke.Containerfile \ | |
| --build-arg MARKER=delta -t localhost/tbox-iso-delta:latest fixtures | |
| sudo podman build -f fixtures/iso-smoke.Containerfile \ | |
| --build-arg MARKER=epsilon -t localhost/tbox-iso-epsilon:latest fixtures | |
| sudo podman build -f fixtures/iso-smoke.Containerfile \ | |
| --build-arg MARKER=zeta -t localhost/tbox-iso-zeta:latest fixtures | |
| - name: build per-env ISO | |
| run: | | |
| sudo mkdir -p /mnt/tbx | |
| sudo ./tacklebox build fixtures/iso-2env.json --iso /mnt/tbx/smoke.iso -b /mnt/tbx | |
| - name: verify per-env ISO | |
| run: ./tacklebox verify /mnt/tbx/smoke.iso | |
| # Boot gate: verify checks structure; this checks the thing users | |
| # do — boot it. Catches non-executable generators, sysroot.mount | |
| # regressions, ESP/El Torito breakage, unmountable rootfs images. | |
| - name: live boot gate (QEMU/OVMF to login) | |
| run: sudo ./scripts/test-live-boot.sh /mnt/tbx/smoke.iso 900 | |
| - name: upload live boot log | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: iso-smoke-live-boot-log | |
| path: qemu-live-boot.log | |
| if-no-files-found: ignore | |
| # alpha carries live_customize (fixtures/live-customize-smoke.sh); beta | |
| # doesn't. The marker must be inside alpha's squashed rootfs and absent | |
| # from beta's — this exercises the full path: recipe parsing, script | |
| # resolution, container run, commit, derived-image squash. | |
| - name: assert live_customize applied | |
| run: | | |
| xorriso -osirrox on -indev /mnt/tbx/smoke.iso \ | |
| -extract /LiveOS/alpha.rootfs.sfs /tmp/alpha.sfs \ | |
| -extract /LiveOS/beta.rootfs.sfs /tmp/beta.sfs | |
| unsquashfs -l /tmp/alpha.sfs 'usr/share/tbox-live-customize-marker' \ | |
| | grep -q tbox-live-customize-marker \ | |
| || { echo "::error::alpha rootfs missing live_customize marker"; exit 1; } | |
| if unsquashfs -l /tmp/beta.sfs 'usr/share/tbox-live-customize-marker' 2>/dev/null \ | |
| | grep -q tbox-live-customize-marker; then | |
| echo "::error::beta (no live_customize) unexpectedly contains the marker" | |
| exit 1 | |
| fi | |
| echo "live_customize marker present in alpha, absent in beta" | |
| rm -f /tmp/alpha.sfs /tmp/beta.sfs | |
| - name: build dedup ISO | |
| run: sudo ./tacklebox build fixtures/iso-dedup-2env.json --iso /mnt/tbx/dedup.iso -b /mnt/tbx | |
| - name: verify dedup ISO | |
| run: ./tacklebox verify /mnt/tbx/dedup.iso | |
| # The fixture envs differ by one marker file, so the combined squashfs | |
| # should be roughly half the size of two per-env squashfs files. The | |
| # 90% bound is deliberately loose — it catches "dedup silently does | |
| # nothing" without being flaky about compression noise. | |
| - name: assert dedup shrinks the ISO | |
| run: | | |
| per_env=$(stat -c%s /mnt/tbx/smoke.iso) | |
| dedup=$(stat -c%s /mnt/tbx/dedup.iso) | |
| echo "per-env ISO: ${per_env} bytes; dedup ISO: ${dedup} bytes" | |
| if [ "$dedup" -ge $((per_env * 9 / 10)) ]; then | |
| echo "::error::dedup ISO (${dedup}) is not meaningfully smaller than per-env ISO (${per_env})" | |
| exit 1 | |
| fi | |
| # ── 6-env dedup smoke ──────────────────────────── | |
| - name: build 6-env dedup ISO | |
| run: sudo ./tacklebox build fixtures/iso-dedup-6env.json --iso /mnt/tbx/dedup-6env.iso -b /mnt/tbx | |
| - name: verify 6-env dedup ISO | |
| run: ./tacklebox verify /mnt/tbx/dedup-6env.iso | |
| # 6 nearly-identical envs should produce an ISO barely larger than 2. | |
| # Bound: 6-env < 2 × 2-env (very conservative, real ratio is much | |
| # tighter — each additional env adds only a marker file worth of | |
| # unique content). | |
| - name: assert 6-env dedup scales | |
| run: | | |
| dedup2=$(stat -c%s /mnt/tbx/dedup.iso) | |
| dedup6=$(stat -c%s /mnt/tbx/dedup-6env.iso) | |
| echo "2-env dedup ISO: ${dedup2} bytes; 6-env dedup ISO: ${dedup6} bytes" | |
| if [ "$dedup6" -ge $((dedup2 * 2)) ]; then | |
| echo "::error::6-env ISO (${dedup6}) is too large — expected ≤ 2× 2-env (${dedup2})" | |
| exit 1 | |
| fi | |
| echo "6-env/dedup2 ratio: $(echo "scale=2; $dedup6 / $dedup2" | bc)" | |
| # ── delta layout smoke ────────────────────────────────────────────── | |
| # dedup_layout=delta: alpha is the base squashfs, beta boots | |
| # base + beta.delta.sfs stacked as overlay lowerdirs. The delta must | |
| # be tiny (fixtures differ by one marker file) and must contain the | |
| # marker — proof the tree-diff caught the divergent file. | |
| - name: build delta ISO | |
| run: sudo ./tacklebox build fixtures/iso-delta-2env.json --iso /mnt/tbx/delta.iso -b /mnt/tbx | |
| - name: verify delta ISO | |
| run: ./tacklebox verify /mnt/tbx/delta.iso | |
| - name: assert delta is small and carries the marker | |
| run: | | |
| xorriso -osirrox on -indev /mnt/tbx/delta.iso \ | |
| -extract /LiveOS/beta.delta.sfs /tmp/beta.delta.sfs | |
| size=$(stat -c%s /tmp/beta.delta.sfs) | |
| echo "beta.delta.sfs: ${size} bytes" | |
| # The fixtures differ by one marker file (plus initramfs-adjacent | |
| # noise); anything over 64 MiB means the diff degenerated into a | |
| # near-full copy. | |
| if [ "$size" -ge $((64 * 1024 * 1024)) ]; then | |
| echo "::error::beta.delta.sfs (${size}B) is far too large for a one-file diff" | |
| exit 1 | |
| fi | |
| unsquashfs -l /tmp/beta.delta.sfs usr/share/tbox-env-marker \ | |
| | grep -q tbox-env-marker \ | |
| || { echo "::error::delta squashfs missing the env marker diff"; exit 1; } | |
| rm -f /tmp/beta.delta.sfs | |
| # ── offline store smoke ───────────────────────────────────────────── | |
| # Builds a dedup ISO with offline_payloads that embeds container images | |
| # as a separate store.squashfs.img in /LiveOS. The boot-time | |
| # tbox-offline-store.mount unit picks this up for air-gapped installs. | |
| - name: build offline-store ISO | |
| run: | | |
| sudo ./tacklebox build fixtures/iso-offline.json \ | |
| --iso /mnt/tbx/offline.iso -b /mnt/tbx | |
| - name: verify offline-store ISO | |
| run: ./tacklebox verify /mnt/tbx/offline.iso | |
| # Boot all three ISOs in parallel to keep CI time under the 60-min | |
| # budget. Each QEMU VM gets 2G RAM (enough for a non-interactive | |
| # boot to login prompt). Output tee'd to step log + per-ISO file. | |
| - name: boot smoke, all ISOs (parallel) | |
| run: | | |
| fail=0 | |
| # 6-env dedup ISO: gamma env with tbox-root pivot | |
| ( QEMU_LOG=qemu-iso-6env.log ./scripts/test-boot.sh /mnt/tbx/dedup-6env.iso 600 \ | |
| "tacklebox.env=gamma" "Tacklebox: rebased OK" \ | |
| 2>&1 | tee -a qemu-iso-6env.out ) & | |
| pid1=$! | |
| # per-env ISO: beta env (non-dedup path) | |
| ( QEMU_LOG=qemu-iso-perenv.log ./scripts/test-boot.sh /mnt/tbx/smoke.iso 600 \ | |
| "tacklebox.env=beta" \ | |
| 2>&1 | tee -a qemu-iso-perenv.out ) & | |
| pid2=$! | |
| # dedup ISO: alpha env with tbox-root pivot | |
| ( QEMU_LOG=qemu-iso-dedup.log ./scripts/test-boot.sh /mnt/tbx/dedup.iso 600 \ | |
| "tacklebox.env=alpha" "Tacklebox: rebased OK" \ | |
| 2>&1 | tee -a qemu-iso-dedup.out ) & | |
| pid3=$! | |
| # delta ISO: beta env boots base + beta.delta.sfs lowerdir stack | |
| ( QEMU_LOG=qemu-iso-delta.log ./scripts/test-boot.sh /mnt/tbx/delta.iso 600 \ | |
| "tacklebox.env=beta" "tacklebox.live.delta=beta.delta.sfs" \ | |
| 2>&1 | tee -a qemu-iso-delta.out ) & | |
| pid4=$! | |
| wait $pid1 || { echo "::error::6-env dedup ISO boot failed"; fail=1; } | |
| wait $pid2 || { echo "::error::per-env ISO boot failed"; fail=1; } | |
| wait $pid3 || { echo "::error::dedup ISO boot failed"; fail=1; } | |
| wait $pid4 || { echo "::error::delta ISO boot failed"; fail=1; } | |
| exit $fail | |
| - name: upload boot logs | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: iso-smoke-boot-logs | |
| path: qemu-iso-*.log | |
| if-no-files-found: ignore | |
| - name: artifact summary | |
| if: always() | |
| run: | | |
| echo '## iso-smoke artefacts' >> "$GITHUB_STEP_SUMMARY" | |
| ls -lh /mnt/tbx/*.iso 2>/dev/null | awk '{print "- `" $NF "` (" $5 ")"}' >> "$GITHUB_STEP_SUMMARY" | |
| for log in qemu-iso-perenv.log qemu-iso-dedup.log qemu-iso-6env.log; do | |
| if [ -f "$log" ]; then | |
| echo "### $log (tail)" >> "$GITHUB_STEP_SUMMARY" | |
| echo '```' >> "$GITHUB_STEP_SUMMARY" | |
| tail -n 30 "$log" >> "$GITHUB_STEP_SUMMARY" | |
| echo '```' >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| done |