Skip to main content

ci boot gate

Building a bootc OS image proves it assembles. It does not prove it boots β€” bootloader installs, initramfs contents, display-manager wiring, and compression formats all fail in ways podman build can't see. Corral turns "does this image actually boot?" into one command with an exit code, which makes it a publish gate: image builds β†’ corral boots it β†’ only then do tags get promoted or ISOs uploaded.

This page documents that use case on both backends. The reference consumer is tuna-os/tunaOS, which gates GHCR tag promotion on a QEMU boot of every image (see its docs/PIPELINE.md), with the same checks runnable against a KubeVirt cluster for local development.

The one-liner (QEMU backend, any KVM machine or CI runner)​

corral create gate --bootc ghcr.io/tuna-os/yellowfin:gnome-testing \
--wait-ssh --timeout 900
# exit 0 β†’ disk built via `bootc install to-disk`, VM booted, root SSH answers
# exit β‰ 0 β†’ it didn't; fail the pipeline
corral delete gate

--wait-ssh implies starting the VM and blocks until a root SSH probe over the forwarded port succeeds. The SSH key is injected at install time with --root-ssh-authorized-keys β€” the published image is never modified.

Locally built images​

Images in root podman storage work directly β€” no registry round-trip:

sudo podman build -t localhost/myos:test .
corral create gate --bootc localhost/myos:test --wait-ssh

(The install runs from containers-storage: when the ref is present locally; localhost/ refs error early with a podman save | sudo podman load hint if the image is only in your rootless store.)

Declarative form (Lima-style YAML)​

Corral reads Lima YAML natively; bootc: plus provision: covers the common CI need β€” enable sshd or drop test hooks chrooted into the installed disk before first boot, without touching the published image:

# verify.yaml
bootc: ghcr.io/tuna-os/yellowfin:gnome-testing
cpus: 4
memory: 4GiB
disk: 32GiB
provision:
- mode: system
script: |
#!/bin/sh
systemctl enable sshd
corral create gate -f verify.yaml --wait-ssh --timeout 900

GitHub Actions recipe​

jobs:
boot-gate:
runs-on: ubuntu-latest # hosted runners have KVM
steps:
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules && sudo udevadm trigger --name-match=kvm

- name: Install corral
run: go install github.com/tuna-os/corral@latest

- name: Boot gate
run: |
corral create gate --bootc "$IMAGE" --wait-ssh --timeout 900
corral delete gate

Beyond "it boots": health checks over SSH​

Once --wait-ssh returns, the VM is a normal SSH target β€” assert whatever "working" means for your image:

check() { corral ssh gate -u root -c "$1"; }
check "systemctl is-active graphical.target" # desktop reached
check "systemctl is-active gdm" # right display manager
check "systemctl --failed --no-legend" # empty, or a known allowlist
check "bootc status --format json | jq -r '.status.booted.image.image.image'"

KubeVirt backend (clusters, heavy images, local dev parity)​

corral bootc create gate --image ghcr.io/tuna-os/yellowfin:gnome -n corral-vms
# or: corral create gate --kubevirt --bootc <image> [-s <storage-class>]
corral start gate # bootc creates the VM stopped

The build runs in a builder VM on the cluster (bootc install to-disk onto a PVC), which is the only way to install images whose filesystems the node kernel can't handle (btrfs/composefs on Talos, for example). Notes that matter in practice:

  • Storage: disk PVCs are Filesystem-mode file-backed disks, so any provisioner works β€” including local-path. Block-mode provisioners are not required.
  • Registry cache: deploy deploy/registry-cache.yaml (a ghcr.io pull-through cache) and builders use it automatically β€” multi-GB desktop images pull at LAN speed after the first fetch. CORRAL_REGISTRY_MIRROR=off disables detection; note the cache is one-upstream-per-instance, so a quay-pointed registry:2 cannot serve ghcr content.
  • Interrupted builds: if the builder finished but the final VM step was lost, corral bootc create --resume <name> reuses the completed disk PVC instead of rebuilding.

Troubleshooting the gate​

SymptomCause / fix
VM lands in the UEFI setup menu (UiApp)disk has no portable bootloader β€” installs must use --generic-image; NVRAM entries written in a builder VM never reach the final VM
501 Unsupported client range during pullzstd:chunked partial pulls need multi-range HTTP; GHCR's CDN refuses them and some podman versions don't fall back. Builders set enable_partial_images = "false"; if you hit this elsewhere, do the same in storage.conf
no space left on device in /var/tmp mid-pullfull pulls stage blobs in $TMPDIR before committing to storage β€” point TMPDIR at a big disk (builders stage on the scratch disk)
ostree: min-free-space-percent '3%' would be exceededtarget disk too small for the extracted image + reserve β€” desktop images generally want β‰₯ 32G
SSH never answers but the build reported OKKubeVirt VMs are created stopped β€” corral start <name> first; then check the DM/sshd actually exist in the image
Cluster ssh works, CI ssh refused at 127.0.0.1expected: without a tailnet the hostfwd binds loopback, which is where --wait-ssh probes; interactive corral ssh needs the tailnet

Every row above was hit for real while gating TunaOS images β€” this table is field notes, not speculation.