# ADR 9 — Artefact builder<no value>

<!-- Generated by website/sync-docs.py from docs/adr/0009-image-builder.md. Edit that file, not this one. -->

Status: proposed

## Context

[Issue #11](https://github.com/Corium-OS/Corium/issues/11) points out two facts.
Every installable artefact that Corium produces (the qcow2, the raw disk and the
installer ISO) goes through `bootc-image-builder`. And upstream archived that
project on 2026-06-18. These are the facts, checked against the registries and the
upstream source, not against the announcement:

- `mise.toml` and both workflows track
  `quay.io/centos-bootc/bootc-image-builder:latest`. Its last build was on
  2026-06-18 at 11:31 UTC, from commit `a686afe`, "readme: migration notice",
  which is the archive commit. Its digest is
  `sha256:2b52843ea2bfda73b0a08d97e76b734393b1d3a804681b9fabb26723bd3a2f0b`.
  It is built on Fedora 42, which reached end of life on 2026-05-13, a month
  before the image was built. The tag that we follow stopped moving three months
  ago. Nobody noticed, because nothing broke.
- The code did not stop. Upstream merged it into
  [`osbuild/image-builder`](https://github.com/osbuild/image-builder). That
  project releases weekly (`v78.0.0` through `v83.0.0` between mid-August and
  2026-09-16). It publishes three images from one workflow, all on Fedora 44.
  `ghcr.io/osbuild/image-builder` and `ghcr.io/osbuild/image-builder-cli` are
  the same digest under two names. `ghcr.io/osbuild/bootc-image-builder` holds
  the same binary, installed as `/usr/bin/bootc-image-builder`, with the old
  entry point, flags, and output layout. Each image has a tag per release. For
  the compatibility image, `v83.0.0` is
  `sha256:e7aadce6b3f5639cd47d83354791931ea219891a0d113c2fe74a0f0d352b165c`.
- The [deprecation notice](https://osbuild.org/docs/bootc/deprecation-notice/)
  commits to keep that entry point for the life of RHEL 10, and to drop it in
  RHEL 11. It says the same about the `anaconda-iso` image type: deprecated,
  removed in RHEL 11, with the container-based installers as the path forward.

The issue asked three questions. We answered each one from the `image-builder`
source at `v83.0.0` and the documentation in its tree. The published pages
describe the image types, but they do not say what the types do.

## Is `image-builder build --bootc-ref` a drop-in replacement?

**For the qcow2 and the raw disk, yes in substance and no in detail.** The
command becomes `image-builder build --bootc-ref <image> qcow2`. It runs from
`ghcr.io/osbuild/image-builder`, with the same privileged flags and the same
mount of the root container store. The container must already be in local
storage. The release workflow already does this today, because it pulls the
image first. The builder reads the root filesystem type from the bootc install
configuration of the container (`/usr/lib/bootc/install/00-corium.toml`, per
ADR 2). So `--bootc-default-fs` is not necessary. Two things differ:

- The output layout. `image-builder` writes
  `<distro>-<type>-<arch>/<distro>-<type>-<arch>.<ext>`. For a bootc input,
  this is `bootc-based-qcow2-x86_64/bootc-based-qcow2-x86_64.qcow2`.
  `bootc-image-builder` writes `qcow2/disk.qcow2`. `--output-dir` and
  `--output-name` restore today's paths. So `deploy/proxmox/` and the publish
  step, which only consume files, do not need to change.
- A warning during manifest generation stops the build, unless you pass
  `--ignore-warnings`. This is a feature, because the release path must fail
  loudly. But it is a change in behavior to expect on the first run.

**For the ISO, no.** If you give the CLI a bootc reference, it refuses the type
outright:

    image type bootc "anaconda-iso" is not supported with image-builder,
    please consider switching to "bootc-installer" or use bootc-image-builder

The comment above that check gives the reason. Without the mTLS plumbing that
the old tool set up, the behavior would differ in small ways. Upstream chose an
error over a quiet incompatibility. `anaconda-iso` is reachable only through the
compatibility entry point. In upstream's own image definitions, that code path
(`bootc_legacy_iso`) carries the note "we want to get rid of it".

## Can `bootc-generic-iso` carry a custom kickstart?

**Not the way `[customizations.installer.kickstart]` does today, because the
generic ISO reads no build configuration at all.** In the source,
`manifestForGenericISO` is the only image function that never receives the
blueprint. The ISO that it produces is the container itself, converted to a
squashfs and booted live. Its GRUB menu comes from
`/usr/lib/image-builder/bootc/iso.yaml` *inside the container*. When that ISO
boots, it does what the container does when it boots. Upstream's reference has
three parts: a `fedora-bootc` container with Anaconda installed by `dnf`, an
`iso.yaml` that boots it with `inst.stage2=`, and a kickstart at
`/usr/share/anaconda/interactive-defaults.ks`. The kickstart holds a single
`bootc --source-imgref … --target-imgref …` line.
`--bootc-installer-payload-ref` copies the image to install into the container
store of the squashfs. This is what makes the install work offline.

So the kickstart does not go away. It moves. It is no longer a build input that
the builder receives. It becomes a file in an installer container that Corium
must build and own: a second `Containerfile` beside the appliance and ZFS
variants. The RAID root procedure in [`docs/raid.md`](/docs/guides/raid/) is still an
Anaconda kickstart, with the same content and the same three manual steps. Only
its delivery changes. It must be tested again on two disks, because the earlier
test used the ISO type that this decision retires.

Two things follow that are easy to miss:

- Today's ISO is **unattended**, because `bootc-image-builder` ships a default
  kickstart for `anaconda-iso`. That kickstart installs to the first disk with
  no prompts. In the generic ISO, we must write that default ourselves. It is a
  few lines, and the quick start and `deploy/proxmox/create-vm-iso.sh` depend on
  it.
- `bootc-installer`, the other suggested type, *is* Anaconda-based and *does*
  honor the blueprint kickstart. It uses the same `kickstart.New(customizations)`
  call as the legacy path. But it also requires two containers. `--bootc-ref`
  must be an Anaconda installer environment, and the image to install goes in
  `--bootc-installer-payload-ref`. Upstream lists it under "Historical" and says
  to prefer the generic type. It needs the installer container in both cases, so
  it saves nothing.

## Does pinning buy useful time?

**Pinning the Quay digest gives reproducibility and nothing else.** The tag is
already frozen. A digest makes that explicit, and lets CI record which builder
produced an artefact. The release workflow cannot do this today. A digest does
not make the builder maintained, and the base of the builder is a Fedora release
that no longer receives updates.

**Pinning the compatibility image from `image-builder` gives the time we
need.** It has the same entry point, the same flags, and the same output paths.
The people who wrote it keep them working on purpose, on a current base, with a
version number to bump. It still builds `anaconda-iso`. This is exactly what we
need while we rebuild the ISO on the generic type: one change that stops the
dependency from decaying, and that does not touch the install path that a
release is verified against.

## Decision

1. **Now: build with `ghcr.io/osbuild/bootc-image-builder`, pinned to a
   release tag and its digest, everywhere the builder is named.** These places
   are `mise.toml`, the release workflow and the end-to-end workflow. Same
   command, same three artefact types, same output files. The `BIB`
   environment override stays. So if the new image misbehaves, the frozen Quay
   digest is a one-line fallback. A bump of the pin is a reviewable diff that
   contains the digest, as `build/k0s.lock` already does for the k0s binary.

2. **Next, as its own change: the qcow2 and the raw disk move to
   `image-builder build --bootc-ref`.** They run from
   `ghcr.io/osbuild/image-builder`, pinned the same way. `--output-dir` and
   `--output-name` keep `output/qcow2/disk.qcow2` and `output/image/disk.raw`.
   The end-to-end workflow builds a qcow2 and boots it, and that is the test.
   After this step, the compatibility image builds the ISO and nothing else.

3. **Then: an installer container, built as `bootc-generic-iso`.** It has three
   parts: a `Containerfile` that derives from the Fedora bootc base with
   Anaconda, an `iso.yaml`, and a default kickstart. The kickstart installs the
   embedded Corium image to the first disk, unattended.
   `--bootc-installer-payload-ref` passes the image to install, by digest, as
   today. The RAID root procedure becomes a variant of that container, with a
   different kickstart file at build time. We test it again by removing a disk,
   as we did for the first one. The published ISO stays on `anaconda-iso` until
   the generic ISO has installed a node through the release verification list,
   including the Proxmox script and the RAID procedure. Not before, because bare
   metal is a documented path with one artefact, and this ISO is the only thing
   that exercises that path.

Step 1 is in this change. Steps 2 and 3 wait for their tests to pass, not for a
date. RHEL 11 is the outer limit, not the schedule.

## Consequences

Nothing changes today for a person who builds or installs Corium. The commands,
the artefacts and their paths stay the same, and the published ISO installs the
same way.

The builder becomes a pinned dependency, like the others. This is a small
recurring cost: a digest to bump, ideally with each Corium release. In exchange,
we know which bytes produced an artefact, and the release job can say so. Today
the release job prints the digest of whatever `:latest` was at that time.

Step 2 changes the command in the same two places. It carries a risk: a warning
that `bootc-image-builder` tolerated can become an error. The end-to-end workflow
exists to catch this kind of failure.

Step 3 adds a second container image to build and a verification item to the
release list. It also makes the installer *ours*. A file in this repository
defines what the ISO does at boot, not the defaults of the builder. A project
that documents a RAID root needs it there. We rewrite the kickstart section of
`docs/raid.md` when step 3 lands, not before.

Upstream warns that a bootc system installed through Anaconda fails
`systemd-remount-fs.service` at boot
([bugzilla 2332319](https://bugzilla.redhat.com/show_bug.cgi?id=2332319)).
Today's ISO also installs through Anaconda. So either this already happens and
is harmless, or it does not reproduce here. In both cases, we test it when we
test the new ISO, and we do not assume it.

## Alternatives considered

**Keep tracking `quay.io/centos-bootc/bootc-image-builder:latest`.** It did not
move after the archive commit, and it will not move. A tag that is frozen today
and returns 404 tomorrow is the failure mode that the issue describes, found
during a release. Rejected.

**Pin the Quay digest and stop there.** This is reproducible and honest about
what built an artefact. Otherwise it is identical to the option above: an
end-of-life base and an unmaintained binary. We keep it as the fallback that the
`BIB` override uses, not as the decision.

**Move everything to `image-builder build --bootc-ref` in one step.** Not
possible. The CLI refuses `anaconda-iso`, and the replacement ISO needs an
installer container that does not exist yet. If we do the disks now and the ISO
later, we reach the same end state, with a test between the two steps.

**Use `bootc-installer` rather than `bootc-generic-iso` for the ISO**, because
it honors `[customizations.installer.kickstart]` and keeps `docs/raid.md` closer
to its current content. It still requires our own installer container, and
upstream marks it historical. It also ties the ISO to Anaconda, where the generic
type leaves the choice to the container. It saves nothing. Rejected.

**Drop the ISO and publish only disks.** The ISO is the documented path onto
bare metal, and the RAID root depends on it. `deploy/proxmox/create-vm-iso.sh`
and the appliance image both produce one. Rejected.
