Tags

Tags give the ability to mark specific points in history as being important
  • 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.
  • v10.2.0

    protected Release: v10.2.0
    `instance` gains opt-in cosign attestation at promote, alongside the
    existing opt-in signing. When a consumer sets `attest_on_promote:
    "true"`, the promote job attests the promoted image digest with the
    estate cosign key: a CycloneDX SBOM predicate (reuses `sbom.json`, the
    same build artifact promote's SBOM-reuse check already validates) and
    a minimal SLSA v1 provenance predicate built inline from CI env vars
    (ref, sha, project, builder id, pipeline URL). Same own-key, no-Rekor
    posture as `sign_on_promote` (`--tlog-upload=false`, cosign v2
    pinned), and shares its cosign install + decoded key -- the binary
    installs once when either input is opted in. Off by default; no
    behavior change for existing consumers.
    
    Additive, opt-in, no behavior change for existing consumers. Minor bump.
    
    - `instance` input `attest_on_promote` (default `"false"`).
    - Promote attests the digest once per predicate type; `:latest` and
      `:stable` both point at it, so one SBOM attestation and one
      provenance attestation cover every tag in the repo.
    
    - The attester fails closed if opted in but the protected group vars
      (`COSIGN_PRIVATE_KEY_B64`/`COSIGN_PASSWORD`) are absent, same as
      `sign_on_promote`.
    - `cosign attest --type slsaprovenance1` confirmed accepted by cosign
      2.6.3.
    - `container-build.yml`'s header comment claimed
      build→push→sign→SBOM→attest; corrected to build→push→SBOM, matching
      what the script actually does (it has neither a sign nor an attest
      step).
    - README.md: dropped the stale "Current series: `@v9.x`" line and
      updated the Related section to point at `cairn/base` and the four
      `example-*` vignettes instead of the retired `immutable/basef` and
      `immutable/instance`.
    - Template doc-comment version literals in `supply-chain.yml`,
      `base-build-scratch.yml`, `seal/template.yml`,
      `installer-anaconda-iso/template.yml`, and `qcow2-bake/template.yml`
      replaced with a `@vX.Y.Z` placeholder plus a pointer to this
      CHANGELOG, so a usage-example pin can no longer go stale in place.
      `instance.yml`'s dated `immutable/pipeline` postmortem citation is a
      historical reference, not a usage pin, and was left as-is.
    - `justfile`'s `release-check` recipe now also fails on series-style
      literals (`@v9.x`) in README.md, the class that let the stale
      "Current series" line above survive three releases unnoticed.
  • v10.1.1

    protected
    94ff9d10 · release: prepare v10.1.1 ·
    Release: v10.1.1
    Unpin quay.io buildah/skopeo images to version tags
    
    quay rebuilds its rolling tags (including plain vX.Y.Z) and garbage-collects
    the superseded manifests, so any digest pin on these images rots within days.
    The v10.1.0 catalog baked such a pin into supply-chain's detect-changes,
    which took every consumer's daily cascade red from Aug 19 (lodestone,
    hagstone). All buildah/skopeo refs now track version tags only, and the
    catalog's renovate config carries pinDigests:false for both images so the
    pins cannot regrow. See CHANGELOG v10.1.1.
  • v10.1.0

    protected Release: v10.1.0
    v10.1.0 — stop lint-containerfile + detect-changes contending for anvil
    
    MINOR: no input added, removed, or renamed. Two instance jobs move off the
    runner_tag input onto a hardcoded SaaS tag; every consumer's own
    .gitlab-ci.yml needs no change to pick this up on its next pin bump.
    
    instance's lint-containerfile and detect-changes jobs shared the
    runner_tag input with the hardware-bound build job. All 8 instance
    consumers set runner_tag: anvil (the estate's single self-hosted qemu
    host), so both lightweight jobs queued behind real image builds on the
    same single machine. Measured over 30 days: lint-containerfile +
    detect-changes were 2.5% of anvil's compute but 49% of its total queue
    wait across those 8 consumers. Worst case: roon-bootc's lint queued 52.3
    minutes for 1 minute of work, and because stages run sequentially that
    dragged build/detect-changes behind it too. Neither job needs anvil:
    hadolint is a static lint and detect-changes is a skopeo inspect registry
    digest check -- no privilege, no host state, no credential beyond the job
    token.
    
    Hardcoded both jobs' tags to saas-linux-small-amd64 rather than dropping
    tags entirely or adding a new spec:inputs override. Confirmed via the
    runners API that the self-hosted storr runner (id 50689664) registers
    itself under every GitLab SaaS size tag AND has run_untagged: true, so a
    bare/untagged job is not guaranteed to land on genuine SaaS either -- an
    explicit tag is no more or less ambiguous on that axis, but it is
    self-documenting and (also runners-API-confirmed) structurally guaranteed
    to never touch anvil: anvil's tag_list is [lodestone, qemu, anvil] with
    run_untagged: false. small, not runner_tag's large default: both jobs are
    sub-minute work, right-sized the way vm_size is for build. No new input
    added: neither job has a real per-consumer reason to diverge, so a
    lint_runner_tag-style knob would be a moving part no consumer would ever
    set. build and promote are untouched.
    
    Verified via POST ci/lint against a real consumer's content
    (example-plain), before vs. after: only lint-containerfile and
    detect-changes tags changed (anvil -> saas-linux-small-amd64); build
    stayed anvil, promote stayed crucible. This catalog's own
    consumer-shape-gate child pipeline -- a real pipeline-creation +
    detect-changes execution -- passed against the fix branch, and both the
    branch pipeline and post-merge main pipeline (seal-self-test,
    consumer-test-build/verify/cleanup, release-check,
    verify-release-image-toolchain, consumer-shape-gate) were job-level green.
    
    Also folds in the prior Unreleased content: verify-release-image-toolchain
    (always-on toolchain check against create-release's image, closing the gap
    that let a bad gitlab-org/cli digest bump land unverified until the next
    tag), the .release-image/&release-image pin refactor, and the
    qcow2-bake/installer-anaconda-iso rootfs default change (xfs -> ext4,
    fs-verity requirement). See CHANGELOG.md for the full entry.
  • v10.0.1

    protected
    c3085470 · release: prepare v10.0.1 ·
    Release: v10.0.1
    v10.0.1 — fix the stage v10.0.0 shipped broken, close the gap that let it through
    
    PATCH: v10.0.0's public contract (inputs, job names, the intended
    default-behavior change) is unchanged. Only the broken implementation is
    fixed. v10.0.0 as tagged was unusable by all seven of its declared
    consumers -- none had merged a pin bump to it.
    
    instance's lint-containerfile job (added v10.0.0) landed in a lint stage
    declared only in the component's own stages:. A consumer's own top-level
    stages: REPLACES the component's wholesale rather than merging with it,
    and every real instance consumer declares its own stages:
    [detect-changes, build, promote] with no lint -- so lint vanished from
    all seven and lint-containerfile referenced a stage that did not exist.
    Pipeline CREATION failed outright: 0 jobs, started_at: null, nothing
    resembling a lint failure or a YAML error on the pipeline object.
    
    Fixed by moving the job to .pre rather than adding "- lint" to all seven
    consumers' stages:, which would have recreated exactly the seven
    near-identical deviations this component exists to eliminate. GitLab
    injects .pre/.post into the merged stage list unconditionally regardless
    of what any consumer's stages: declares, so no consumer override can make
    it disappear -- confirmed by ci/lint against a consumer-shaped synthetic
    config both before the fix (invalid: chosen stage lint does not exist)
    and after (valid, all jobs present in the merged result). instance's own
    stages: no longer lists lint.
    
    Three separate pre-landing checks all passed without exercising the
    actual failure mode: the hadolint pre-audit validated hadolint's verdict
    on real Containerfiles, never the component's own resolvability; a
    synthetic-consumer ci/lint run passed because that synthetic consumer did
    not declare its own stages: unlike every real one; and this repo's own
    consumer-test-build/consumer-test-verify never instantiate a consumer
    .gitlab-ci.yml that includes the component alongside its own stages:.
    
    A POST ci/lint-based regression test was built and abandoned before
    landing -- it needs the api OAuth scope, and the estate's existing
    read_api-scoped token 403s insufficient_scope on that call. No token was
    minted or widened to route around this. Closed instead with
    consumer-shape-gate: a GitLab child pipeline for a synthetic instance
    consumer shaped exactly like the seven real ones, including
    instance/supply-chain at $CI_COMMIT_SHA, with strategy: depend
    propagating a failed pipeline CREATION back as a red job. Needs no API
    call or credential of any kind. Mutation-probed both directions on a real
    pipeline: pointed at the broken commit the child pipeline came back
    failed/0 jobs with GitLab's own stage-does-not-exist diagnostic; pointed
    at the fixed commit it came back success. Runs on every push and tag
    pipeline.
    
    release-check -- the guard for kickstart/summary/seal clone- and
    ref-literals plus README's component-pin examples -- was manual-only and
    CI-unwired; it failed when finally run by hand ahead of the v10.0.0 tag
    (all three template literals and four of five README pins were still
    v9.4.0, three releases stale) and one README pin stayed stale even in the
    v10.0.0 fix commit because the recipe never checked README.md at all.
    Wired into every pipeline now: tag pipelines check pins against
    CI_COMMIT_TAG exactly, push pipelines check self-referential coherence
    against kickstart.yml since there is no tag yet to compare against.
    
    Also fixes two qcow2-bake defects found on the lane's first real CI run
    (it had never executed before this release): the bib_image input default
    could not resolve the component.version directive inside a default value
    (GitLab does not re-interpolate there), now resolved in the job body; and
    BIB's devtmpfs mount failed EPERM under SaaS nested DinD, now bound to
    the dind daemon's already-populated /dev instead.
    
    Release-prep: bumped the kickstart/summary catalog clone literals, the
    seal catalog_ref default, and the README component usage pins v10.0.0 ->
    v10.0.1.
  • v10.0.0

    protected Release: v10.0.0
    v10.0.0 — instance gains an unconditional lint-containerfile job
    
    MAJOR: every instance consumer's pipeline gains a new, always-run,
    merge-gating job with no input or config change on their part -- a
    default-behavior change per this catalog's own versioning policy, not
    an additive no-op.
    
    A vacuity audit found six cairn instance consumers (example-plain,
    example-attended, example-edge, example-evidence, corbel, whetstone --
    lodestone is a seventh) whose sole merge-gating job was
    detect-changes, which asserts nothing: build/promote both gate on
    build_change_paths, so a docs/CI-only MR matches none of them and,
    with only_allow_merge_if_pipeline_succeeds=true, an empty-and-failed
    pipeline permanently blocks the merge on nothing being asserted at
    all. roon-bootc hit this exact failure mode and hand-carried a
    lint-containerfile job directly in its own .gitlab-ci.yml as the fix.
    Ported here once instead of copying it into six more repos: hadolint
    against the Containerfile, no changes: filter so it runs on every
    MR/main push regardless of path, --failure-threshold error. Inert
    until a consumer moves its instance pin past v9.6.x.
    
    Also carries everything merged since v9.6.1: the instance promote
    SBOM-reuse + missing-SBOM-fail fix (MR !8), retry coverage for the
    buildah login auth-flake class across base-build-scratch, image-verify,
    and seal, and the create-release job's move off GitLab's release:
    keyword (which shells to glab release create and is shadowable by an
    ambient GITLAB_TOKEN) to an explicit curl + JOB-TOKEN recipe -- this
    tag is that recipe's first real exercise.
    
    Release-prep: bumped the kickstart/summary catalog clone literals and
    the seal catalog_ref default v9.4.0 -> v10.0.0 (the release-check
    guard, stale since v9.4.0 across three releases), and the README
    component usage pins to match.
  • v9.6.1

    protected Release: v9.6.1
    v9.6.1 — digest-pin floating buildah/skopeo tags
    
    quay.io/buildah/stable and quay.io/skopeo/stable rebuild their vX, vX.Y,
    vX.Y.Z, and `latest` tags IN PLACE (proven 2026-08-03: v1.43.1, v1.43, v1,
    and `latest` on buildah/stable all resolved to one digest pushed 31 May).
    Every job in this catalog that pinned one of those tags was silently
    floating -- including `seal`, which signs boot artifacts.
    
    Digest-pinned all six occurrences to what each named tag currently resolves
    to, matching the `tag@sha256` style this repo's own `.gitlab-ci.yml` already
    uses for the same two images:
      - `templates/seal/template.yml` (highest consequence: signs boot artifacts)
      - `templates/base-build-scratch.yml`
      - `templates/container-build.yml`
      - `templates/image-verify/template.yml` (kept at its existing v1.41.5 --
        only the floating-pin defect was in scope here)
      - `templates/supply-chain.yml` (buildah + skopeo)
    
    skopeo/stable's `-immutable` variant tag is distinct content
    (sha256:4a16d57b...) from what v1.22.2/`latest` currently resolve to
    (sha256:c7d3c512...) -- not a drop-in swap, so pinned to the latter to match
    this catalog's own existing usage rather than silently substituting
    different image content into a supply-chain component.
    
    renovate.json's `templates/*.yml` customManager now also matches a job's
    `image:`/`image.name:` key (previously it only saw `spec.inputs.*.default`
    values, and only when quoted), so none of these six pins were tracked going
    forward before this release. Added `versioningTemplate: docker` for correct
    tag-with-suffix ordering. Two of the six floating pins (container-build.yml,
    image-verify/template.yml) were found via a full-catalog grep beyond the
    four originally reported, and fixed the same way.
    
    Also carries two routine Renovate bumps merged since v9.6.0:
    registry.gitlab.com/gitlab-org/cli:latest docker digest, and anchore/syft to
    v1.49.0.
    
    Patch bump: fixes a floating-pin defect (jobs resolving a mutable upstream
    tag at parse time instead of a fixed digest). No input or interface change;
    consumers pick it up by moving the component pin.
  • v9.6.0

    protected Release: v9.6.0
    v9.6.0 — the OCI media-type gate now covers the instance lane
    
    v9.4.0 added a permanent OCI creep-detector for bootc-dev/bootc#1703 at two
    image-promotion points: base-build-scratch's promote (each of base, base-zfs,
    base-zfs-nvidia, at the exact digest the boot smoke tested) and seal (the local
    artifact, before push). The plain instance lane was the one remaining promotion
    point with no assertion — promote moved :latest/:stable without checking the
    candidate's media types.
    
    instance's promote now asserts the candidate is OCI immediately after
    SRC_DIGEST resolves and before either skopeo copy, using base-build-scratch's
    condition verbatim: .config.mediaType must be the OCI config type (the
    load-bearing signal — composefs re-reads the config blob, so that is the field
    ABSENT (chunkah-rechunked images omit it) but never a present non-OCI type.
    .install_skopeo already provides skopeo and jq, so the job gains no new
    install step.
    
    This closes a gap rather than fixing a live defect: .buildah_job pins
    BUILDAH_FORMAT=oci and a job-level variables: deep-merges rather than replaces
    it, so instance builds already emit OCI. The gate is what keeps a future edit
    that drops or overrides that variable from promoting a v2s2 image to a tag a
    host performs a day-2 bootc upgrade from — the failure mode with no
    workaround, since bootc upgrade/switch has no --source-imgref escape.
    
    Verified before tagging: the condition was probed over a seven-case truth
    table (OCI, absent-top-level, v2s2, the three mixed forms, and empty inspect
    output) and run against the live registry — base-zfs-nvidia:stable is
    chunkah-rechunked and really does report an absent top-level .mediaType with
    an OCI config, so the absent-tolerance is load-bearing on a real artifact
    rather than hypothetical; base:stable reports both OCI. Both pass; every
    non-OCI config fails.
    
    Minor bump: a consumer whose candidate is somehow not OCI now fails promote
    instead of publishing. No input or interface change; consumers pick it up by
    moving the instance component pin.
  • v9.5.0

    protected Release: v9.5.0
    v9.5.0 — chunkah rechunk on base-zfs-nvidia + reproducible input
    
    base-build-scratch rechunks the final base-zfs-nvidia candidate through
    coreos/chunkah and republishes it to the same sha tag, so the boot-gated and
    promoted artifact is content-addressed by rpmdb component and a day-2 update
    moves only the changed layers. SOURCE_DATE_EPOCH is pinned on the compose and
    the rechunk, and (with the base treefile's __pycache__ postprocess strip) two
    identical-source composes chunk to byte-identical layers and seal to an
    identical composefs digest. promote's OCI assertion tolerates chunkah's absent
    top-level mediaType while still rejecting present-but-non-OCI. New catalog_ref
    input (default v9.5.0) fetches the Containerfile.splitter recipe.
    
    First chunked release is a one-time full re-pull per host, then smaller pulls.
  • v9.4.0

    protected Release: v9.4.0
    v9.4.0 — OCI media types end to end (composefs/bootc#1703 fix)
    
    Every image the catalog builds is now OCI
    (application/vnd.oci.image.manifest.v1+json + application/vnd.oci.image.config.v1+json)
    instead of Docker schema2 (v2s2).
    
    bootc's composefs backend re-reads the image config blob at `bootc install`
    and again on day-2 `bootc upgrade`/`switch`; a v2s2 image fails that read
    (bootc-dev/bootc#1703, and the adjacent #2318) — "GetBlob ... file does not
    exist" at install, "blob unknown to registry" on day-2. Install had a
    local-OCI-staging workaround (--source-imgref oci:...); day-2 had none, so
    registry-direct upgrades were blocked outright. Native OCI media types remove
    the failing read at the source: the config blob is a proper OCI config the
    composefs backend can materialize, so install and day-2 both work
    registry-direct with a bare image ref — no local OCI staging, no
    --source-imgref.
    
    Changed:
    - base-build-scratch, seal, container-build, and supply-chain's shared
      .buildah_job template (which instance's build extends) set
      BUILDAH_FORMAT=oci (was docker). buildah's own upstream default is OCI. The
      dialect is set at BUILD and never converted after gating, so the digest the
      gates test is the digest promoted. Copies (promote, skopeo copy) preserve.
    
    Added:
    - Permanent OCI media-type assertion (creep-detector) so a v2s2 image can never
      regress into a promoted tag. base-build-scratch promote asserts each candidate
      layer is OCI (manifest + config) before moving :stable/:latest, on the exact
      digest smoke/smoke-sb tested; seal asserts the sealed artifact is OCI, read
      locally from containers-storage before push.
    
    Minor bump: published images change media type v2s2 -> OCI; layer contents and
    layer digests are unchanged.
  • v9.3.1

    protected Release: v9.3.1
    v9.3.1 -- fix installer-anaconda-iso/qcow2-bake digest-pinning bug
    
    Both components could not actually carry a digest-qualified `image`
    input (repo@sha256:...): the docker pull + docker-daemon skopeo bridge
    failed closed on every digest-qualified reference (Docker Engine's
    classic local image store does not preserve original registry manifest
    bytes). Fixed by fetching directly via skopeo's docker:// transport into
    containers-storage, bypassing docker pull and the docker-daemon bridge
    entirely. Also corrects both components' header comments (and the
    catalog README) about triggering a standalone config file: a
    CI_CONFIG_PATH pipeline variable does not work, per confirmed GitLab
    limitations gitlab-org/gitlab-foss#60331 and gitlab-org/gitlab#27987.
    
    Patch bump: no existing consumer passes a digest-qualified image today,
    so nothing working changes behavior.
  • v9.3.0

    protected Release: v9.3.0
    v9.3.0 — opt-in own-key cosign sign-at-promote (instance)
    
    The `instance` promote job can now sign the promoted image digest with
    an own cosign key when a consumer sets `sign_on_promote: "true"`
    (cosign v2, `--tlog-upload=false`, key from protected+masked group
    vars). Own-key signing verifies at pull time against a committed public
    key, unlike the keyless flow dropped in v4.3.0, so a reject-by-default
    `sigstoreSigned` policy.json (cairn/example-evidence) can finally
    enforce. Off by default; existing instance consumers are unaffected.
    
    - inputs `sign_on_promote` (default "false") and `cosign_version` ("2.6.3").
    - Digest signed once; matchRepository covers :latest and :stable.
    - cosign pinned to v2.x (v3 refuses --tlog-upload=false; example-evidence D4).
    - Validated sim-first: sign + verify + reject-by-default policy matrix
      against an ephemeral registry with cosign v2.
  • v9.2.0

    protected Release: v9.2.0
    v9.2.0 — qcow2-bake component + supply-chain/brand housekeeping
    
    New `qcow2-bake` component: converts a promoted bootc image to a qcow2
    disk image via bootc-image-builder, in the same standalone-invocation
    shape as `installer-anaconda-iso` (separate CI config, web/API/manual
    trigger, Package Registry publish). Produces the disk image only;
    boot-verifying it needs /dev/kvm and stays a consumer-owned gate on a
    self-managed nested-KVM lane. Additive, no consumer behavior change.
    
    Housekeeping in the same release:
    - Dropped the dead cosign install (and COSIGN_VERSION/COSIGN_YES) from
      the buildah-fedora builder image; signing left the catalog in v4.3.0
      and cairn's real signing path is the seal component.
    - Pinned syft via a syft_version component input instead of fetching
      releases/latest at CI runtime, with a matching Renovate customManager.
    - Pinned the remaining floating third-party :latest image refs
      (buildah, skopeo, docker/dind, bootc-image-builder) to version/digest.
    - Dropped two unreferenced pre-cairn brand PNGs.
  • v9.1.0

    protected Release: v9.1.0
    feat: seal component for the sealed UKI rung
    
    Adds `seal`: convert a built bootc base image into the sealed stack. It
    produces db-signed systemd-boot (bootupd removed), a db-signed Unified
    Kernel Image carrying the computed composefs= digest on its cmdline, and
    a signed PCR11 policy embedded in the UKI, so a TPM2 LUKS slot keeps
    auto-unlocking across OS updates. Minor bump: purely additive, with no
    change to existing components or their include paths.
    
    - templates/seal/ ships the sealing Containerfile plus the component. The
      job runs `bootc container ukify` with systemd-sbsign (Secure Boot) and
      systemd-measure sign (PCR11), decoding two group-level masked and
      protected variables (UKI_SIGNING_KEY_B64, PCR11_PRIVATE_PEM_B64) to
      build secrets. It fails closed if either is absent and, after the
      build, verifies composefs= present, .pcrsig present, and the UKI
      verifies against the db certificate.
    - Self-test seals quay.io/fedora/fedora-bootc:44 at the commit under test
      on saas-linux-xlarge-amd64 (push disabled).
    - release-check now guards the seal catalog_ref pin; the kickstart and
      summary clone literals and the README pins bump to v9.1.0.
    - Boot-gating (enforcing-Secure-Boot KVM boot and install-time composefs
      digest match) is the planned next rung.
    
    Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
  • v9.0.1

    protected Release: v9.0.1
    fix: invoke cairn-verify in the promote re-verify
    
    Patch fix for a v9.0.0 carry-forward rename miss. The base-build-scratch
    promote lane re-verifies the image after its registry round-trip by
    running the verify binary baked into the final layer. It invoked the
    immutable-era `/usr/bin/basef-verify`, which cairn base images do not
    ship — they bake `/usr/bin/cairn-verify`. Cairn base builds passed
    boot-smoke, then failed at promote with
    `fork/exec /usr/bin/basef-verify: no such file or directory`.
    
    - `base-build-scratch` promote now runs `/usr/bin/cairn-verify`; the two
      describing comments updated to match. No other component affected.
  • v9.0.0

    protected Release: v9.0.0
    feat: carry the catalog forward into the cairn estate
    
    First release under dunn.dev/cairn/pipeline. Carried forward from
    dunn.dev/immutable/pipeline at v8.4.0 with full git history and every
    release tag intact — the tags below v9 document the design arc that
    produced this catalog.
    
    No component was redesigned; this is a home move plus an identity
    repoint. The major bump reflects the one breaking change: component
    include paths move namespace. Consumers pinning
    `dunn.dev/immutable/pipeline/<component>@<tag>` must repoint to
    `dunn.dev/cairn/pipeline/<component>@<tag>`.
    
    - Registry image references, component include examples, pipeline and
      release badges, the kickstart/summary clone-URL literals, CI comments,
      and container LABELs repointed to the cairn namespace.
    - Estate-identity prose (README, CONTRIBUTING, the builder-image header)
      and the brand SVGs now name the cairn estate.
    - README records the carry-forward lineage from immutable/pipeline.
    
    - All dated CHANGELOG entries and annotated-tag release notes below v9.
    - Cross-estate references to the still-live immutable basef/instance
      projects and the immutable.dunn.dev estate site.
    - The just-recipe (catalog CI) / inline-YAML (component templates) /
      POSIX-sh (release scripts) boundaries.