Tags give the ability to mark specific points in history as being important
-
v17.2.1
protectedRelease: v17.2.18ae76a5f · ·cairn/pipeline v17.2.1 **Fixed**: with `rechunk: true`, a rebuild of an unchanged commit rechunked the registry's copy of `<image>:<sha>` instead of the image the job had just built, so the scheduled rebuild re-shipped old content and promoted it. The rechunk now reads its source by the image ID the build step recorded, never by tag. **Added**: the rechunk step fails unless the rechunked image's rpmdb hashes equal to the rpmdb of the image the job built. Two hashes in the existing job; no new jobs. Migration: none; consumers on `@17` pick this up on their next pipeline.
-
v17.2.0
protectedRelease: v17.2.0ddb2ef38 · ·cairn/pipeline v17.2.0 **Added**: `sealed-instance` input `rehearsal_vm_size` (default `large`) sizes `install-rehearsal`, which was fixed at `large`. `xlarge` joins the `boot_vm_size` options. **Changed**: the `runner_tag` and `vm_size` descriptions say that a `saas-linux-*` tag runs on GitLab SaaS, where `vm_size` is ignored, and that `vm_size` sizes the self-hosted custom executor (`xlarge` only where a runner offers it). Defaults are unchanged. Migration: none; consumers on `@17` pick this up on their next pipeline.
-
v17.1.0
protectedRelease: v17.1.0431beb68 · ·cairn/pipeline v17.1.0 **Added**: `seal` inputs `base_digest_file` and `digest_file`. `layer` and `base-build-scratch` write `image-<image>.digest`; the kmod jobs write `image-kmod-<layer>.digest`; `seal` writes `sealed.digest`. **Changed**: `promote`, `instance` promote, `seal`, `sealed-boot`, `pcr11-anchor-gate` and `promote-sealed` take `<image>@<digest>` from the build artifact instead of `<image>:<sha>`, and read the served digest back against it. `layer` builds over the parent digest, and over the kmod digest when the kmod job emitted one. Behaviour changes: - `layer` and `promote` need their builds' digest artifacts: keep `build-<parent>` and `build-<image>` in `needs:` with `artifacts` on (the default). A missing artifact fails the job; `promote` skips only when no build ran and no tag exists at the sha. - `sealed-instance` no longer runs on `web` pipelines, which have no build digest. Re-seal by pushing or through the cascade. Migration: no input changes; consumers on `@17` pick this up on their next pipeline.
-
v17.0.1
protectedRelease: v17.0.12de34d1a · ·cairn/pipeline v17.0.1 ## v17.0.1 — 2026-10-05 No interface change. **Fixed**: the `instance` build writes `image.digest` world-readable. It copied mktemp's 0600 file, so the runner could not upload the artifact and every v17.0.0 instance build failed after a successful push. Migration: none; consumers on `@17` pick this up on their next pipeline.
-
v17.0.0
protectedRelease: v17.0.07025c9f1 · ·cairn/pipeline v17.0.0 ## v17.0.0 — 2026-10-05 **BREAKING.** Every catalog push is plain zstd, and a push fails unless every layer of the pushed digest is. Move every cairn/pipeline ref to `@17` in one transaction. **Added**: `instance` input `rechunk` (boolean, default false) re-layers the built image with chunkah before the SBOM and the push, carrying its image config and labels over. It refuses an image labelled `ostree.commit`, and needs `--cap-add=all`, `/dev/fuse` and about twice the image size in free disk on the build runner. **Changed**: `base-build-scratch` (base and kmod builders), `layer` and `instance` push with `--compression-format zstd`. The base, layer, instance and seal pushes read back the manifest of the pushed digest and fail on any layer that is not `tar+zstd` or carries a zstd:chunked TOC, which replaces seal's last-layer check. `layer` rechunks before its single push, so no gzip image sits under the sha tag, and hands chunkah the full image config instead of a fixed label list; a label the rechunk drops or changes fails the job. `instance` promote copies the build's digest to `:latest` and `:stable` and fails unless both serve that digest; `sealed-instance` promote does the same for `:stable-sealed` and checks every layer. `instance` promote also fails when the sha tag no longer serves the digest its own build pushed, the one its SBOM and signatures describe. **Removed**: `templates/layer/Containerfile.splitter` and `layer`'s fetch of the catalog; the recipe is inline in `layer` and `instance`. Migration: - Bump every ref to `@17`; no inputs change. Hosts and tools that pull these images must decode zstd layers, as sealed hosts already do. - Every layer digest changes, so the first upgrade after the bump pulls the whole image once. - A consumer's own compression check on the last layer should check every layer. - To rechunk an instance, set `rechunk: true` on a runner that allows `--cap-add=all` and `/dev/fuse`.
-
v16.0.0
protectedRelease: v16.0.02e3535ac · ·cairn/pipeline v16.0.0 **BREAKING.** `base-build-scratch` builds the base image and the kmod builders only; layered images and promotion move to two new components. Move every cairn/pipeline ref to `@16` in one transaction. **Added**: `layer` builds one image as a layer over a parent and emits `build-<image>`: inputs `image`, `parent`, `layer`, `rechunk`, `runner_tag`, `vm_size`, `build_change_paths`. It builds `images/<layer>` over `<parent>:<sha>` with `kmod-<layer>:<sha>`, needs `build-<parent>` and optionally `kmod-<layer>` and `kmod-<layer>-mr`, and runs chunkah only when `rechunk` is true. `promote` verifies and promotes one image and emits `promote-<image>`: inputs `image`, `needs`, `runner_tag`, `build_change_paths`. Image names are relative to `${CI_REGISTRY_IMAGE}`. **Removed**: from `base-build-scratch`, the jobs `build-base-zfs`, `build-base-zfs-nvidia` and `promote`, and the inputs `base_zfs_image`, `base_zfs_nvidia_image`, `promote_needs` and `promote_runner_tag`. The chunkah recipe moved to `templates/layer/Containerfile.splitter`. **Security**: `layer` and `promote` write their inputs into the job script instead of passing them through job variables, so pipeline, project and group CI/CD variables cannot override an image, parent, layer or rechunk setting. Every default `promote` rule additionally requires `$CI_COMMIT_REF_PROTECTED == "true"`: a promote pipeline on an unprotected ref no longer runs. Consumers whose default branch is not protected must protect it. Migration: - Drop the removed inputs. Include `layer` once per layered image (`build-base-zfs` becomes `image: base-zfs, parent: base, layer: zfs`; `build-base-zfs-nvidia` becomes `image: base-zfs-nvidia, parent: base-zfs, layer: nvidia, rechunk: true`) and `promote` once per promoted image, with that image's gates in `needs`. - `layer` and `promote` address images at `${CI_REGISTRY_IMAGE}/<image>` and kmod builders at `${CI_REGISTRY_IMAGE}/kmod-<layer>`; keep `base_image`, `zfs_image` and `nvidia_image` on those paths. - The Containerfile directory is `images/<layer>` and its builder build-arg is `<LAYER>_BUILDER_IMAGE`; rename the directories if they differ. - Job names are `<job>-<image>`: update every `needs`, rule and cascade that names `promote`. -
v15.0.1
protectedRelease: v15.0.191b7da23 · ·cairn/pipeline v15.0.1 No interface change. **Changed**: `bootc container lint` is fatal on every build and seal path, including the rechunked base image before it is pushed; a warning now fails the job. `seal` normalizes file modes across every top-level directory and logs what it changed, and writes the ext4 install drop-in only when the base image lacks it. Checkout modes are set in each build job's script. The instance build fails when `BASE_REF` is empty. **Removed**: `seal`'s post-normalize fail gate. Normalization already fails closed under `set -e`, so the gate could never fire. `promote`'s lint of the pulled image, since the exact artifact is now linted before it is pushed. Migration: consumers on `@15` pick this up on their next pipeline, so make the base image lint-clean first and promote it; a consumer whose base still warns will fail its build and seal.
-
v15.0.0
protectedRelease: v15.0.007a4d0f6 · ·cairn/pipeline v15.0.0 **BREAKING.** Move every cairn/pipeline ref to `@15` in one transaction. **Added**: `sealed-instance`, the protected sealed lane for an instance. Base and instance images carry `org.opencontainers.image.revision` and `.source` labels; instance builds `FROM` the base digest and adds `.base.name`, `.base.digest`. `install-rehearsal` runs on default-branch schedules with `INSTALL_REHEARSAL=true`, which skip `detect-changes`, `build` and `promote`. **Changed**: protected seal, gate and promote jobs are non-interruptible; `promote-sealed` serializes per `<prefix>stable-sealed` and skips when it already descends from the pipeline commit; `pcr11-anchor-gate` runs untagged; default-branch push rules compare `$CI_DEFAULT_BRANCH`; kmod jobs time out at 30m; base `rootfs` label follows the image (`ext4`); `sign_on_promote`, `attest_on_promote` and `push` are `type: boolean`; `builder_base` is required; builds strip group/world write from the checkout; `seal` normalizes modes and fails on group/world-writable files. **Removed**: `catalog_ref` and `extra_kargs` (`seal`, and the `EXTRA_KARGS` arg); `catalog_ref` and the five Containerfile-path inputs of `base-build-scratch`. Migration: - Replace the local `seal` include, `.sealed-lane-rules`, the `seal` needs stanza and the local sealed jobs with `sealed-instance@15`. - Add a first `workflow:rules` entry `- if: '$CI_COMMIT_BRANCH && $CI_COMMIT_REF_PROTECTED == "true"'`, limited to the pipeline sources your later rules run, with `auto_cancel: {on_new_commit: none}`. - Unquote the boolean inputs, drop removed inputs, set `builder_base`. -
v14.0.1
protectedRelease: v14.0.10a95d001 · ·cairn/pipeline v14.0.1 **Fixed**: `instance`'s `promote` rules for acute, api, trigger and schedule pipelines now also require `$CI_COMMIT_REF_PROTECTED == "true"`, matching `base-build-scratch`'s protected-ref rules. On an unprotected ref the job used to sit pending forever, since `promote_runner_tag` names a runner that only takes protected refs, and it must never move `:stable`/`:latest` from one. Non-breaking: protected-ref pipelines and the main-push rule are unchanged, and no input or job name changes.
-
v14.0.0
protectedRelease: v14.0.0487ed296 · ·cairn/pipeline v14.0.0 **BREAKING.** `kmod-zfs` and `kmod-nvidia` now run on protected refs only, with `-mr` twins for merge-request pipelines. **Added**: `kmod-zfs`, `kmod-nvidia`, `seal` and `instance`'s `promote` declare `environment: {name: signing, action: access}`, so the four signing variables (`UKI_SIGNING_KEY_B64`, `PCR11_PRIVATE_PEM_B64`, `COSIGN_PRIVATE_KEY_B64`, `COSIGN_PASSWORD`) can be scoped to a `signing` environment. Inert while the variables carry environment scope `*`. **Added**: `base-build-scratch` input `signing_runner_tag` tags the protected-ref `kmod-zfs` and `kmod-nvidia` and is required (no default); new `kmod-zfs-mr` and `kmod-nvidia-mr` run on unprotected refs (merge-request pipelines, and acute/api/trigger/schedule runs) under `runner_tag`, the same canonical/`-mr` split cairn/base uses for `sealed-boot`. `build-base`, `build-base-zfs` and `build-base-zfs-nvidia` are unchanged and never see the signing variables. `seal` gains a `vm_size` input (default empty, exported as `VM_SIZE`). **Changed**: `build-base` no longer checks `UKI_SIGNING_KEY_B64` in mint mode; the check runs in the kmod jobs, which decode the key. **Migration:** every `base-build-scratch` consumer sets `signing_runner_tag` (the estate uses `crucible`). A hard `needs:` on `kmod-zfs` or `kmod-nvidia` from a job that also runs on merge-request pipelines needs the matching `-mr` job added with `optional: true`. -
v13.0.0
protectedRelease: v13.0.04b59dc20 · ·cairn/pipeline v13.0.0 ## v13.0.0 — 2026-09-21 **BREAKING.** `image-verify` and its self-test scaffold are removed; it had zero consumers in the group. Consumers change only the major pin. **Changed**: `seal` and `base-build-scratch` fetch their recipe at the commit the include resolved to; `catalog_ref` is an optional override. `release-check` scans every component README pin. **Added**: a parse check composes `base-build-scratch` and `supply-chain` on push and tag pipelines.
-
v12.0.0
protectedRelease: v12.0.055c60521 · ·**MAJOR (v12.0.0) — input removed, default behavior change, migration notes below.** `base-build-scratch` drops the `signing_key_var` input. `vars` mode now reads the group protected variable `UKI_SIGNING_KEY_B64` directly -- the same name `seal` already hardcodes -- and fails closed with a FATAL naming it when the ref does not carry it. There is no indirection and no fallback to any other variable name; a consumer that still sets `MOK_SIGNING_KEY_B64` and nothing else gets exactly that FATAL. Every internal name still carrying the retired module-key identity is renamed to the `kmod-signer`/`kmod-signing` role it actually plays, now that a `base` module is signed with the same key that signs the sealed UKI rather than a separate Machine Owner Key: the `.mok-material` anchor becomes `.kmod-signing`; its `MOK_ROLE` variable becomes `KMOD_SIGNING_ROLE`; the buildah secret id `mok` becomes `kmod-signing`; the ephemeral build-arg `MOK_CERT_SRC` becomes `KMOD_SIGNER_CERT_SRC`; the staged/artifact files `mok.pem`/`mok.der` become `kmod-signer.pem`/`kmod-signer.der`; and the catalog's own negative-control job `mok-fail-closed` becomes `kmod-signing-fail-closed`. `kmod-signing-fail-closed`'s negative control drops from five cases (plus the name-guard loop) to four: the `signing_key_var`-repoint case and the injected-name guard are gone with the input they tested. The remaining four are unset -> red naming `UKI_SIGNING_KEY_B64`, set -> green, the retired `MOK_SIGNING_KEY_B64` carrying a key while `UKI_SIGNING_KEY_B64` stays unset -> red still naming `UKI_SIGNING_KEY_B64` (no silent fallback to the retired name), and the ephemeral mint producing a CA:TRUE / no-keyUsage cert at `644`. - `base-build-scratch` input `signing_key_var` and its `SIGNING_KEY_VAR` wiring in `build-base`, `kmod-zfs`, and `kmod-nvidia`. - The `eval`-based indirect read, its name-format guard, and the `signing_key_var`/wiring assertions in `kmod-signing-fail-closed`. - `base-build-scratch` `vars` mode reads `UKI_SIGNING_KEY_B64` directly (was: whichever variable `signing_key_var` named, default `MOK_SIGNING_KEY_B64`). - `.mok-material` anchor -> `.kmod-signing`; `MOK_ROLE` -> `KMOD_SIGNING_ROLE`. - buildah secret id `mok` -> `kmod-signing`; ephemeral build-arg `MOK_CERT_SRC` -> `KMOD_SIGNER_CERT_SRC`. - Staged/artifact files `mok.pem`/`mok.der` -> `kmod-signer.pem`/`kmod-signer.der`. - Catalog job `mok-fail-closed` -> `kmod-signing-fail-closed`. 1. **Drop `signing_key_var:` from every include.** An include still passing it fails pipeline creation with "unknown input"; a consumer must remove its line in the same MR that bumps to `@12`. 2. **A consumer whose refs all carry `UKI_SIGNING_KEY_B64` needs no further change.** `vars` mode reads it directly now, with no variable name left to configure. 3. **A consumer whose protected refs only ever set `MOK_SIGNING_KEY_B64` must set `UKI_SIGNING_KEY_B64` instead**, or the build goes red where it used to build signed, and the committed cert your Containerfile bakes (the `KMOD_SIGNER_CERT_SRC` default) must be that key's cert. 4. **Update the build secret id and build-arg in every consumer Containerfile.** `RUN --mount=type=secret,id=mok` becomes `id=kmod-signing`; `ARG MOK_CERT_SRC=...` and its `COPY ${MOK_CERT_SRC}` become `ARG KMOD_SIGNER_CERT_SRC=...` / `COPY ${KMOD_SIGNER_CERT_SRC}`. 5. **Update any script or path that names the committed cert file.** `mok.der`/`mok.pem` no longer exist under that name; the baked path and build artifacts are `kmod-signer.der`/`kmod-signer.pem`. 6. **`mok-fail-closed` -> `kmod-signing-fail-closed`.** Update any required-check, badge, or API query that names the job. -
v11.1.0
protectedRelease: v11.1.0a2f25528 · ·`base-build-scratch` gains a `signing_key_var` input: `.mok-material`'s "vars" mode now decodes whichever CI/CD variable the consumer names, rather than always `MOK_SIGNING_KEY_B64`. cairn/base can point kmod signing at `UKI_SIGNING_KEY_B64` -- the same key that signs the sealed UKI -- and sign kmods with it instead of minting a separate MOK. The name is read indirectly and validated first (empty or any character outside `A-Z0-9_` is fatal); there is no fallback to `MOK_SIGNING_KEY_B64` once a consumer names a different variable. `mok-fail-closed`'s negative control grows from three cases to six: the two existing (unset -> red naming the variable, set -> green) now pass `SIGNING_KEY_VAR=MOK_SIGNING_KEY_B64` explicitly, plus two new cases proving `signing_key_var=UKI_SIGNING_KEY_B64` goes green when only that variable carries the key, and stays red naming `UKI_SIGNING_KEY_B64` -- never falling back to `MOK_SIGNING_KEY_B64` -- when only the old variable is set, and one more proving the name guard rejects an injected name and a positional-parameter name before the indirect read, without running the injection. A new assertion parses the template and checks `signing_key_var`'s default and that `build-base`, `kmod-zfs`, and `kmod-nvidia` all wire `SIGNING_KEY_VAR` from the input. Additive, backward compatible, default unchanged (`MOK_SIGNING_KEY_B64`). Minor bump. - `base-build-scratch` input `signing_key_var` (default `MOK_SIGNING_KEY_B64`, regex `^[A-Z_][A-Z0-9_]*$`). Wired into `build-base`, `kmod-zfs`, and `kmod-nvidia` as `SIGNING_KEY_VAR`.
-
v11.0.1
protectedRelease: v11.0.15d89932a · ·Fixes the ephemeral MOK artifact's mode. `base-build-scratch`'s `.mok-material` anchor minted the ephemeral key as root and wrote `.cairn-signing/` `700` and `mok.pem`/`mok.der` `600`; on the lodestone qemu-vm executor the artifact uploader runs as the unprivileged `build` user, so it saw an empty directory ("found 1 matching artifact files and directories") and both kmod jobs died on the anchor's own fail-closed guard because `.cairn-signing/mok.pem` was never fetched. SaaS docker executors upload as root, so the catalog's own self-test never saw it. `base` worked around it consumer-side with an `after_script` chmod; that workaround is no longer needed. ### Fixed - `base-build-scratch`'s ephemeral mint role (`MOK_ROLE=mint`, `signing_source=ephemeral`) now leaves `.cairn-signing/` `755` and `mok.pem`/`mok.der` `644` after minting -- world-readable is acceptable for a one-day do-not-trust throwaway with `access: none`. The canonical key (`vars` mode, and the ephemeral consume role's fetched copy) is unaffected and stays `700`/`600`. -
v11.0.0
protectedRelease: v11.0.0b43d00f3 · ·**MAJOR (v11.0.0) — default behavior change, migration notes below.** `base-build-scratch` gains a `signing_source` input and module signing fails closed. Before this release, kmod signing in `base-build-scratch` was a convention rather than an input: `kmod-zfs` and `kmod-nvidia` each carried a byte-identical block that signed when `MOK_SIGNING_KEY_B64` was present and, when it was absent, printed `building unsigned` and built green anyway. `build-base` baked the committed canonical signer cert into the image either way. A pipeline on a ref that does not carry the protected variable — every merge request, every fork, every unprotected branch — therefore produced an image asserting a MOK its modules were not signed with. Nothing in CI failed; the discovery point was a machine refusing to load the module under lockdown. `signing_source` mirrors seal's input of the same name and takes the same two values. `vars` (the default) decodes `MOK_SIGNING_KEY_B64` and **exits 1 naming the variable and the ref** when it is absent. There is no unsigned mode left. `ephemeral` mints one throwaway MOK per pipeline in `build-base` — RSA-2048, one day, `CN=cairn ephemeral module signing (do not trust)`, minted in the canonical cert's exact shape (CA:TRUE, no keyUsage, so it lands in `.platform` as the ceremony cert does) — publishes it on `build-base`'s artifacts as `.cairn-signing/`, and both kmod jobs sign against that one key. The build bakes THAT cert via a new `MOK_CERT_SRC` build-arg, so signer and baked cert always correspond. The two duplicated signing blocks collapse into one `.mok-material` anchor shared by all three jobs, so the mint and consume paths cannot drift apart. The component stays protection-blind: it reads `$SIGNING_SOURCE` and never `CI_COMMIT_REF_PROTECTED`, leaving the consumer to choose the mode from its own `workflow:rules`. The value is validated at runtime rather than by `options:`, so a consumer may pass a variable reference and decide per pipeline; empty or unknown is fatal. `build-base`'s artifacts drop from `expire_in: 1 week` to `1 day` with `access: none`, bounding the ephemeral key to its cert's lifetime. Cost: a manual `kmod-*` retry more than a day after the build loses the `KVER` dotenv and fails loudly instead of building against a stale kernel. A `mok-fail-closed` job in the catalog's own CI extracts the component's `.mok-material` preamble from the template and runs it three ways — unset must be red and must name the variable, set must be green, ephemeral must mint a CA:TRUE cert with no keyUsage. `base-build-scratch` cannot be included in this pipeline (canonical job names collide), so without it the guard would be a claim nothing in CI ever executes. ### Removed Six components retire. Each was surveyed by every consumption route — `component:`, `include: project:/remote:/local:`, `trigger: include:`, raw/archive URL, `git clone`, kickstart `%post`, cloud-init, systemd unit, justfile, Containerfile, `needs: project:` and jobs/artifacts API — across every dunn.dev and nomograph project, and each has no estate consumer by any of them. GitLab's own catalog telemetry agrees: `last30DayUsageCount` is 0 for all six on every published version. - **`installer-anaconda-iso`** — no estate consumer by any route; catalog usage 0. Its only consumer ever, the archived `immutable/carmine`, dropped it for an inline job before being archived. The estate has not cut an ISO in the cairn era: every install path is `bootc install to-disk` from an OCI source. - **`kickstart`** — no estate consumer by any route; catalog usage 0. Extracted from carmine in v4.6.0, never adopted by a cairn-era repo. - **`summary`** — no estate consumer by any route; catalog usage 0. Same v4.6.0 carmine extraction, same never-adopted history; its `--branch` literal was bumped every release since as pure upkeep. - **`qcow2-bake`** — no estate consumer by any route; catalog usage 0. whetstone's golden-qcow2 lane, its one consumer, replaced it with a consumer-owned `bootc install to-disk` job on 2026-08-06 and has been off the component since. - **`badges`** — no estate consumer by any route; catalog usage 0. Its one consumer, `immutable/basec`, was archived with the `immutable/*` generation; nothing in cairn publishes Pages badges. - **`container-build`** — no estate consumer by any route; catalog usage 0, and never had one. The generic-container role it was written for is served estate-wide by the L1 catalog's `dunn.dev/pipeline/container-image`. Going with them: `scripts/kickstart.py`, `scripts/validate-kickstart.sh`, `scripts/summary.py` and the `validate-kickstart` justfile recipe (only those components ran them); `containers/bib.Containerfile` and `bib` from `publish-images` (the two BIB components were its only consumers — the one estate repo that still bakes with BIB, cairn/hagstone, pulls `quay.io/centos-bootc/bootc-image-builder` from upstream directly, not this catalog's re-tag); and the `quay.io/centos-bootc/bootc-image-builder` half of the `pinDigests: false` Renovate rule, which tracked only that Containerfile's pin. The `chunkah` half stays — `base-build-scratch` still consumes it. `release-check` loses its `kickstart.yml`/`summary.yml` `--branch` assertions; `templates/seal/template.yml`'s `catalog_ref` default is now the sole clone-literal anchor, and the pre-tag coherence branch of the CI job reads its declared version instead of `kickstart.yml`'s. README drops from seven component pins to five; `release-check` counts them, so the two stay consistent with no number to maintain. `image-verify` is NOT retired despite reading 0: the catalog self-includes it at `$CI_COMMIT_SHA` as its own end-to-end image self-test, so a 0 there means "no version-pinned external include", not "unused". ### Migration 1. **A consumer whose refs all carry `MOK_SIGNING_KEY_B64` needs no change.** The default is `vars` and that is the behavior it already had on those refs. 2. **A consumer that builds on refs without the protected variable must pass `signing_source: "ephemeral"` on those pipelines**, or the build turns red where it used to be green-and-unsigned. The intended shape is a consumer-side variable defaulted to `vars` and overridden to `ephemeral` in the consumer's own `workflow:rules` for merge-request pipelines, passed through as `signing_source: "$MOK_SIGNING_SOURCE"`. 3. **A consumer using `ephemeral` must take the cert from the build-arg.** Add `ARG MOK_CERT_SRC=files/usr/share/cairn/mok.der` above the `COPY` that stages the signer cert and copy `${MOK_CERT_SRC}` instead of the literal path. `vars` mode passes no build-arg, so the committed cert is baked byte-for-byte and the image is unchanged. 4. Images built under `ephemeral` carry a real signed chain that is not the canonical one. Tag them so they can never be mistaken for a canonical artifact, and keep boot gates that assert canonical-key provenance on protected refs. 5. **A consumer of a removed component: none exist in the estate.** External consumers pin `@10` and are unaffected until they move — `@10` keeps resolving to the v10 series, which still ships all six. Moving to `@11` means the component is gone: `installer-anaconda-iso` and `qcow2-bake` are replaced by a consumer-owned `bootc install to-disk` job (whetstone's `.gitlab-ci-qcow2.yml` is the worked example), `container-build` by `dunn.dev/pipeline/container-image`, and `kickstart`, `summary` and `badges` have no successor here. -
v10.5.0
protectedRelease: v10.5.044f6a5aa · ·`instance` takes three new inputs so one project can include the component once per build lane. `job_name_prefix` renames that lane's four job keys (lint-containerfile, detect-changes, build, promote) and the three `needs:` edges between them; `containerfile` is hadolint's target and buildah's `-f`; `context` is buildah's trailing context argument, so a lane builds from its own subtree and anchors `build_change_paths` there. A second inclusion used to deep-merge silently onto the first's four job keys, later include winning, and every image built from the repo root. All three default to the previous behaviour: existing consumers get byte-for-byte identical merged jobs. `consumer-shape-test` runs two lanes: `instance` included twice, the second prefixed with its own containerfile and context, so child-pipeline creation reconciles eight job keys and two `needs:` chains. A child pipeline's source is `parent_pipeline`, matching neither build's nor promote's rules, so those two resolve structurally and never instantiate; their prefixed names and edges are graded by a real multi-lane consumer's pipeline, not here. `check-change-paths` reads every instance include, not only the first, diffs each against the Containerfile its own include names, and fails a repo whose includes share a prefix. Coverage now spans the Containerfile path and the context subtree as well as the COPY/ADD sources, so a lane that moves its Containerfile under `lanes/` while leaving the anchor at the template default reads DRIFT, not OK; a `containerfile` outside its own include's `context` is DRIFT too. An ancestor anchor reaches in, so one `lanes/**/*` covers every lane, and a single-mapping `include:` is checked, not skipped. Every command-level registry and network call retries. skopeo copy/inspect/delete and buildah push/pull against docker:// carry --retry-times/--retry-delay or --retry/--retry-delay (verified against skopeo v1.22.2, buildah v1.43.2). Every curl call gains --retry-connrefused and --connect-timeout 15 alongside its existing --retry flavor: the five binary downloads keep --retry-all-errors/--retry-max-time, API reads use --retry 3 --retry-delay 5 --max-time 120, release-create's release-metadata calls use --max-time 60. kickstart.yml and summary.yml's apt-get calls gain -o Acquire::Retries=3 on update and install. check-change-paths.py's fetch_url wraps urllib in a 3-attempt loop: a transport error (URLError, including a bare read timeout) retries after 5s; an HTTPError re-raises immediately as a verdict, not a flake. Job-level retry adds api_failure everywhere an outbound call is made, and script_failure only where a job's body is idempotent: a digest-compare guarded copy/promote, a read-only lint, a deterministic drift assertion. Four untimed jobs gain a timeout: seal 45m, image-verify 20m, installer-anaconda-iso and qcow2-bake 60m each, and the two long bakes gain `interruptible: true`, so a second push to the same branch cancels a running bake. `instance`'s build and promote gain 30m each; detect-changes stays untimed by design. base-build-scratch's `promote` no longer treats a failed `buildah pull` as a uniform no-op: only a captured stderr matching manifest-unknown/404 (no build at this sha) logs and exits 0, and every other pull failure, a registry flake included, exits 1 instead of silently skipping :latest/:stable promotion on a pipeline that stays green. installer-anaconda-iso and qcow2-bake's `--upload-file` calls raise `--max-time` from 900 to 1800 and add `--speed-limit 1024 --speed-time 120 --retry-max-time 3000`, so a transfer stalled below 1 KB/s for two minutes aborts and retries. `--max-time` bounds one attempt, `--retry-max-time` bounds how long the loop keeps starting them, and the loop closes at 3600s: the job's own 60m ceiling, not merely inside it. `consumers-aligned` and `release-check` accept a major-version range pin (`@10`) alongside an exact tag. `consumers-aligned` takes the literal after `@` verbatim instead of demanding a full `vX.Y.Z`, and compares it against the major of the catalog's newest release. `release-check` requires every catalog component pin in README.md to read `@<major-of-the-tag-being-checked>`; an exact `vX.Y.Z` pin, or a bare pin count of zero, fails. README.md's own pins move to `@10`: a range resolves at pipeline-parse time to the newest published release on that major, so a patch or minor reaches a consumer with no pin bump, leaving a major bump as the one fan-out step. `create-release` now carries `needs: [publish-images]`. Without the edge the two `release`-stage jobs ran in parallel, so a red `publish-images` did not stop the GitLab Release (and the Catalog release riding on it) being created. Survivable under an exact pin, but a range pin makes the Release itself the propagation event, so it must not register while the images it names are failing to publish. `seal`'s `buildah build` gains `--label catalog-ref=<catalog_ref>` on downstream runs, where the sealing recipe is cloned at the pinned `catalog_ref`. The label is conditional: a catalog self-test build uses the checked-out recipe and stamps none. Once a consumer cites `@10` instead of an exact tag, that label is the only place the resolved catalog release survives on a downstream image, readable with `skopeo inspect`. Minor bump: every new input keeps its previous default, and every other edit is additive (the label, the `needs:` edge) or a check accepting a second, already-valid pin shape.
-
v10.4.1
protectedRelease: v10.4.1a0bcc4a3 · ·seal pushes plain zstd instead of zstd:chunked, reverting v10.4.0's format: chunked images are unpullable by the composefs-native bootc backend this estate boots (composefs-rs decompresses layers with a plain zstd decoder that truncates a chunked stream at its first frame boundary; unimplemented upstream, composefs/composefs-rs#137; proven on metal 2026-08-24 against bootc 1.16.9). The post-push gate inverts with the format: layer mediaType must be tar+zstd AND the chunked TOC annotation must be ABSENT. Flip back only when composefs-rs#137 lands in a bootc the hosts run. Patch bump: same interface, unbreaks every sealed-image consumer. Also riding from main: consumer-change-paths-aligned catalog-CI job, fail on reintroduced catalog_ref overrides, fedora-release invariant (catalog-CI internal).
-
v10.4.0
protectedRelease: v10.4.083845072 · ·`seal` pushes sealed images as `zstd:chunked` instead of the previous default `gzip`, for faster decompression on install/upgrade -- unconditionally, not opt-in. Partial/delta pulls are NOT claimed: composefs, the backend this estate boots (per docs/decisions.md), has no confirmed chunked-aware partial-pull support upstream as of bootc 1.16.x (containers/storage#2095 still open). A post-push gate -- `skopeo inspect --raw` against the just-pushed manifest -- asserts the top layer's mediaType is `tar+zstd` AND carries the `io.github.containers.zstd-chunked.manifest-checksum` annotation, guarding against skopeo#2627's known behavior of mutating the compression annotation without real chunked TOC data. Driven by `cairn/base#14`. No new input; every consumer picks up the new push format by moving the component pin. Minor bump: new capability, no interface change. Also lands a quay pin-rot cleanup: `chunkah` (base-build-scratch's splitter) and `bootc-image-builder` convert from digest to tag-track, same class as buildah/skopeo in v10.1.1 -- a live digest at audit time doesn't refute the GC risk. `chunkah` tracks its resolved tag (`v0.6.0`); `bootc-image-builder` tracks `latest` by design, matching this repo's own v3.0.4 precedent for the same image (upstream ships no content tag at all). `hadolint` (ghcr.io) and `gitlab-org/cli` (registry.gitlab.com) were audited against the same question and stay digest-pinned: neither registry garbage-collects referenced manifests the way quay does. `renovate.json`'s `templates/*.yml` customManager gained a fourth matchString so it tracks arbitrary CI/CD variable-key pins, not just `image:`/`name:` (issue #7). - `seal`: pushes with `buildah push --compression-format zstd:chunked`; a post-push gate asserts the pushed manifest's top layer is `tar+zstd` with a real chunked TOC annotation, not just a mutated media type (cairn/base#14). - `templates/base-build-scratch.yml` (`CHUNKAH_REF`) and `installer-anaconda-iso/template.yml`/`containers/bib.Containerfile` (`bootc-image-builder`): digest pins converted to tag-track (`v0.6.0`, `latest` respectively); both get `pinDigests: false` in `renovate.json` so Renovate never re-pins a digest onto either. - `renovate.json`: `templates/*.yml` customManager's fourth matchString tracks arbitrary CI/CD variable-key pins (e.g. `CHUNKAH_REF`), not just `image:`/`name:`. - `hadolint` and `gitlab-org/cli` were audited under the same digest-vs-tag question and left digest-pinned: both registries retain referenced manifests, unlike quay. - Consumer pin bumps (base, lodestone) and promote-side idempotency under the new compression format ride the upcoming fleet transaction, not this tag -- `zstd:chunked`'s encoder determinism across pushes of unchanged content is undocumented upstream, so promote's digest-compare fast path may stop firing (an extra `skopeo copy --all`, not a false promotion). - `.gitlab-ci.yml`'s `consumer-change-paths-aligned` job (catalog CI, not shipped to consumers) lands ahead of this tag: enforces example-edge's D3 (an instance consumer's `build_change_paths` anchor must mirror its Containerfile's COPY/ADD sources) across all eight real instance consumers, real PyYAML + Containerfile parsing rather than a grep/awk approximation (issue #5).
-
v10.3.1
protectedRelease: v10.3.134108cf2 · ·Fixes a broken release. v10.3.0's tag pipeline failed in `publish-images`: `publish-one`'s digest-echo called `skopeo inspect`, and the publish image (`quay.io/buildah/stable:v1.43.2`) carries no skopeo. `buildah-fedora:10.3.0` was pushed before the job died; `skopeo:10.3.0` and `bib:10.3.0` were never published. v10.3.0's GitLab release is marked superseded -- do not pin it. - `justfile` `publish-one`'s digest-echo now reads the digest from `buildah push --digestfile` on the push it already performs -- buildah-native truth, no second binary, no post-hoc registry query that could in principle disagree with what was actually pushed. - `.gitlab-ci.yml`'s `verify-release-image-toolchain` job asserted the wrong image's toolchain (`registry.gitlab.com/gitlab-org/cli`, create-release's image, not publish-images'), so it stayed green while `publish-one`'s real skopeo dependency went completely unverified. Realigned onto the same image `publish-images` uses (single source via a shared `&publish-image` YAML anchor), asserting every external binary the publish recipe chain invokes (`buildah`, `just`) -- a missing tool now REDs every push pipeline instead of only the next tag. - Supersedes v10.3.0. That release's tag pipeline never completed `publish-images`; `buildah-fedora` is the only one of the three builder images actually published at that tag.
-
v10.3.0
protected98901c41 · ·`instance` gains opt-in verification of a signed/attested promote digest against a consumer-committed cosign public key. When a consumer sets `verify_pubkey_path` to a repo-relative path and `sign_on_promote`/`attest_on_promote` ran, promote runs `cosign verify` and/or `cosign verify-attestation` against that key immediately after signing/attesting, and fails the job on mismatch -- turning signing-key vs committed-pubkey drift into a red job instead of a silently unverifiable artifact (the incident class `cairn/example-evidence`'s D6 documents). Off by default; no behavior change for existing consumers. Also lands a loudness/verbosity hardening pass across `promote`, `detect-changes`, and the release tooling, closing several silent-skip gaps a gate-vacuity audit found: a masked-vs-genuine registry failure in `promote`'s digest resolution, an unparseable-vs-absent SBOM in the same job, undocumented decision inputs in `detect-changes`, and pass-path silence in `release-check`/`release-create`/`publish-one` that made a green run harder to audit than a red one. Additive, opt-in, no behavior change for existing consumers. Minor bump. - `instance` input `verify_pubkey_path` (default `""`). Fails closed if set to a path that doesn't exist. - `promote`'s `SRC_DIGEST` resolution now distinguishes a benign manifest-unknown/404 (documented "nothing to promote" skip) from any other registry/auth failure, which now fails loud with the captured stderr instead of silently resolving to "no digest". - `promote`'s SBOM component count is now a checked assignment; an unparseable (but present) `sbom.json` fails the job instead of surfacing as an empty count inline in the echo. - `promote`'s attest block echoes a `cosign verify-attestation` reproduction line per predicate type after each successful attest. - `detect-changes` echoes `BASE_CONFIG`/`PREVIOUS_BASE_CONFIG`/ `BASE_IMAGE_CHANGED` before writing `changes.env`, and notes when an unreadable `:latest` is being treated as changed. - `justfile` `release-check`'s pass path now names each matched file:line (kickstart/summary clone refs, seal `catalog_ref`, README pin count), matching the STALE path's existing offender-naming. - `justfile` `release-create` echoes its notes source (tag message vs fallback), the previous tag, and the commit count before calling the Releases API. - `justfile` `publish-one` resolves and echoes the pushed digest after push, so the log states what `:vX.Y.Z`/`:latest` now resolve to. - `.gitlab-ci.yml`'s `consumers-aligned` job (repo-CI, not shipped to consumers) got the same hardening ahead of this tag: the group-listing API call and both per-consumer file-read calls now fail loud instead of silently reading as "no refs", and the per-ref loop prints every consumer's actual pinned ref. - `templates/summary.yml`'s `allow_failure: true` now carries its rationale comment (summary is reporting-only and must never block the lane it reports on), matching the documentation discipline elsewhere in the catalog.