If a term is unfamiliar, check the shared glossary first:
docs/glossary.md
This repository builds the production ZFS-on-Aurora image using a native Containerfile build.
The pipeline is built to safely:
- track the current Aurora/Fedora kernel stream
- build ZFS kernel modules against the primary kernel the image is expected to boot first
- install those modules into the final bootc image
- fail in the GitHub Actions workflow run before a broken image replaces
latest
- Aurora is an ostree/bootc-style image, so ZFS integration must happen during image build.
- ZFS compatibility can lag new Fedora kernels.
- Branch testing must not overwrite
latest. - pull request (PR) validation should exercise the real build logic but should not push anything.
- pull request validation stays read-only, and branch builds are read-only against shared production state too: they may reuse the shared akmods cache but never rebuild or republish it. Seeding a missing cache is a
mainworkflow action (workflow_dispatchwithrebuild_akmods=true).
- candidate OS image:
ghcr.io/danathar/zfs-aurora-complex:candidate-<sha>-<fedora> - stable OS image:
ghcr.io/danathar/zfs-aurora-complex:latest - stable audit tag:
ghcr.io/danathar/zfs-aurora-complex:stable-<run>-<sha> - shared akmods cache image:
ghcr.io/danathar/zfs-aurora-complex-akmods:main-<fedora>- final image builds consume the digest-pinned form:
ghcr.io/danathar/zfs-aurora-complex-akmods@sha256:<digest>
- final image builds consume the digest-pinned form:
- human-authored branch image:
ghcr.io/danathar/zfs-aurora-complex:br-<branch>-<fedora> - bot-authored branch runs stop after local validation and do not push any public tag
- shared akmods cache stays the same shared source image; branch builds never publish branch-specific cache tags and never refresh the shared one. When it does not cover the current primary kernel, the branch's akmods job fails fast with instructions to seed it from the
mainworkflow
The main workflow resolves build inputs in one of two modes:
- default mode: resolve floating refs to immutable digests and immutable stream tags at run time
- replay mode: read pinned inputs from
ci/inputs.lock.json
After resolving the base image, the workflow inspects /lib/modules inside the pinned base image so it knows every installed kernel, not just one metadata label.
The repo then makes one explicit policy choice:
- record all detected kernels in logs and the saved input file
- choose the newest detected kernel as the supported primary kernel
- require ZFS support only for that supported kernel
- use image rollback, not an older bundled kernel inside the same image, as the recovery path
Maintenance note:
- as of April 23, 2026, kernel-release ordering is implemented in one shared helper at
shared/kernel_release.py - both CI input resolution and the in-image ZFS install helper use that same helper now
- this was a refactor to remove drift between two equivalent implementations, not a change to the primary-kernel policy described above
Before rebuilding akmods, the GitHub Actions workflow run checks whether the shared cache image can be reused.
That check now uses one direct inspection path:
- copy the shared cache image into a local Open Container Initiative (OCI) layout
- unpack the filesystem layers from that local copy
- inspect the extracted RPM filenames directly, requiring a
kmod-zfsRPM matching both the supported primary kernel and the exact OpenZFS patch version this run resolved — matching the minor line alone would let an older patch satisfy a newer one - only once that content matches, verify the cache image's cosign signature against the
committed
cosign.pub
Any of those failing forces a rebuild. The signature check means a cache that looks correct but was not produced by this repo's own pipeline is treated as a cache miss rather than trusted.
Separate from cache reuse, every workflow path also clones the resolved
Danathar/akmods commit once.
That check exists because:
- an out-of-date shared cache can hide a broken source ref for a while
- branch and pull request validation should still prove that the resolved akmods commit SHA is fetchable, even when they do not end up rebuilding the cache
What that resolved commit actually is:
- this repo uses the configured fork, not upstream directly
- it resolves one exact commit per run, and clones only that SHA — but by default that
commit is discovered by resolving the moving
maintip at the start of each run, not read from a checked-in pin.AKMODS_UPSTREAM_REFinci/defaults.jsonexists to freeze it and is empty by default - the GitHub Actions workflow run clones that exact commit into
/tmp/akmodsfor the current run only - so updating that fork does change what this repo builds, with no edit here — at the next cache rebuild. A run that reuses the cache does not rebuild the modules, so fork changes reach the image on the next rebuild rather than immediately
See docs/akmods-fork-maintenance.md for the full cascade
and for why the akmods-ref image label is only reliable on a run that rebuilt.
If the cache is missing, out of date, or a manual rebuild is requested, the workflow run:
- clones the resolved
Danathar/akmodscommit for this run - points its target output to
zfs-aurora-complex-akmods - writes the upstream
cache.jsonfile for the supported primary kernel - builds the shared cache image for that supported kernel
Branch note:
- branch builds cannot run this refresh path -- it is exclusive to the
mainworkflow. A branch targeting a kernel or ZFS version the shared cache does not cover fails fast in its akmods job with instructions to run themainworkflow withrebuild_akmods=true, then re-run the branch - pull request validation is likewise read-only and fails fast instead of publishing cache changes
The final image build is standard OCI composition now.
The workflow passes build arguments directly into Containerfile:
BASE_IMAGEAKMODS_IMAGEIMAGE_REPOSIGNING_KEY_FILENAME
That means there is no generated workspace and no per-run file mutation layer.
AKMODS_IMAGE_TEMPLATE is still available as a Containerfile fallback for
local builds that do not pass an exact cache image ref. CI passes the digest-pinned
cache ref resolved by the earlier akmods job.
The image also installs files/usr/lib/modules-load.d/zfs.conf
so systemd-modules-load loads the ZFS kernel module during boot. That makes
post-boot validation report both the userspace and kernel-module versions
without requiring a manual modprobe zfs.
Candidate tags are signed after push by resolving the pushed tag to a digest and
then signing that digest, inside the production-signing environment that only
main refs can reach.
Stable latest is promoted by copying the already-signed candidate digest, not
by signing a second time.
Branch note:
- branch runs cannot sign: the key is environment-scoped to
main - human-authored branch runs push an UNSIGNED
br-*test image via an explicitallow_unsignedopt-in -- usable only on fresh, never-enforced throwaway VMs, since enforced machines refuse unsigned tags under this repository's policy - automation accounts such as Renovate stop before the push entirely
Promotion only copies the tested candidate digest to:
lateststable-<run>-<sha>
The candidate signature carries over because both promoted tags resolve to the
same digest. The signer uses legacy cosign attachment storage so bootc's current
containers/image policy path can discover the signature during bootc upgrade.
- no generated workspace layer
- no recipe mutation
- no second image repository for candidate
- no candidate/stable repo-policy normalization inside the image
- no host repair script for dual repository trust drift
- Fedora kernel timing vs OpenZFS release timing
- shared akmods cache rebuild rules
- deciding when the primary-kernel-only contract is acceptable
Those are the real complexity drivers that remain.