The pipeline composes a Fedora bootc image from scratch, signs its kernel modules, seals it, boots the sealed result under enforcing Secure Boot, and only then moves :stable. Most of that mechanism lives somewhere other than the repository that runs it: base includes base-build-scratch and seal from the public catalog at pipeline and writes only what the catalog leaves blank, the gates that decide whether a candidate may move. The catalog owns how an image is built, base owns what verified means.

Build and sign

COMPOSE, SIGN, LAYER NO PARENT IMAGE compose rpm-ostree compose rootfs BUILT AND SIGNED module builders kmod-zfs · kmod-nvidia LAYERED the candidate sha tag · OCI media types MASKED CI VARIABLE mount-only build secret present for one RUN, in no layer

The signing key is readable while the modules are built and absent from everything that ships.

build-base composes the rootfs with no upstream bootc parent, then derives the kernel version from what it just composed so the module builders compile against exactly that kernel. Every build point emits OCI media types, because bootc’s composefs backend re-reads the image config blob at install and at bootc upgrade, and a Docker schema2 image fails that read. What the layers contain is the image.

One path list decides whether any of this runs. A push touching only documentation, CI configuration or the scripts harness matches none of it, so nothing builds, promote finds no candidate, and no cascade fires; an operator-fired rebuild, a schedule, an API run or a downstream trigger builds regardless of paths. Ahead of all of it, preflight-alignment stands the run down when Fedora has published a kernel whose development and debug packages have not propagated yet, which otherwise surfaces three stages later as an empty version tag (justfile).

The gates

EVERY GATE, THEN THE TAG BOOT GATE SUITE THE CANDIDATE one digest, every gate sha-tagged base-zfs-nvidia STATIC GATE, NO BOOT manifest-gate module paths vs goldens SEALED LANE seal db-signed UKI stack sealed-boot installs and boots it pcr11-anchor-gate without a boot autoenroll keyless firmware IF A GATE FAILS :stable stays on the last image promote :latest · :stable promote-sealed :stable-sealed

Both promotions hang off the same sealed candidate, and every gate is a hard requirement of the tag it guards.

manifest-gate is the cheap half. It reads the candidate’s module manifests out of the public registry, normalises the file paths, and diffs them against committed goldens, printing both drift buckets on a mismatch (manifest-lib.sh). Needing only curl, jq and python3, it is the one image-aware gate a merge request gets; the boot gates need a protected runner with nested virtualisation.

The sealed suite is the estate’s only boot gate, because the sealed image is the only configuration an estate host runs (D27). It installs the sealed candidate, boots it under enforcing Secure Boot with no shim in the chain, and reads its receipts out of the running guest (sealed-boot.sh). What those receipts prove is the sealing chain, and the keys behind them are the trust chain.

The flagship’s :stable moves only after every gate has passed, and base:stable only after its own sealed suite. promote re-pulls the candidate, re-runs cairn-verify on the far side of the registry round trip, asserts the media types once more, and retags each tested digest to :latest and :stable, skipping the copy when the digests already match. A gate that fails skips promote, and the last good image stays where it is. That contract is the estate’s central rule.

After a promotion

A promotion posts a trigger to each instance and never waits on it, because a job token can ask for a pipeline and cannot read one back (D30). The full TPM2-LUKS install rehearsal runs on its own schedule and is allowed to fail: it gates no promotion, so it can carry its whole asserting belt without a red run holding a release (D17). What the instances are is running it. Every image project sets its own registry cleanup policy instead of taking the framework default, so :latest, :stable and anything tagged keep- are never in scope (D32).

The catalog

TWO LEVELS LEVEL 2, THE CONSUMERS @MAJOR pin LEVEL 1 pipeline public components catalog base base-build-scratch · seal example instance · supply-chain whetstone instance · supply-chain

One catalog holds the mechanism; each consumer decides what it includes and what verified means for its own images.

A component is one spec.inputs unit under templates/, released on annotated semver tags. Consumers pin the major range and never an exact tag, so a patch or a minor reaches every consumer with no commit in any of them and a major arrives as one reviewable change (CONTRIBUTING.md). That range resolves at parse time against published Releases only, which is why a mutable tag stays banned: it would move under a consumer with nothing released behind it.