Tags

Tags give the ability to mark specific points in history as being important
  • v17.2.1

    protected Release: v17.2.1
    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

    protected Release: v17.2.0
    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

    protected Release: v17.1.0
    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

    protected Release: v17.0.1
    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

    protected Release: v17.0.0
    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

    protected Release: v16.0.0
    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

    protected Release: v15.0.1
    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

    protected Release: v15.0.0
    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

    protected Release: v14.0.1
    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

    protected Release: v14.0.0
    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

    protected Release: v13.0.0
    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

    protected Release: v12.0.0
    **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

    protected Release: v11.1.0
    `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

    protected Release: v11.0.1
    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

    protected Release: v11.0.0
    **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

    protected
    44f6a5aa · release: prepare v10.5.0 ·
    Release: v10.5.0
    `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

    protected
    a0bcc4a3 · release: prepare v10.4.1 ·
    Release: v10.4.1
    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

    protected
    83845072 · release: prepare v10.4.0 ·
    Release: v10.4.0
    `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

    protected
    34108cf2 · release: prepare v10.3.1 ·
    Release: v10.3.1
    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

    protected
    98901c41 · release: prepare v10.3.0 ·
    `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.