Skip to content

Commit 480b425

Browse files
committed
staged-images: Add chunkah-staged bootc base image builds
Add infrastructure to build rechunked bootc base images using chunkah. These 'staged' images mirror upstream fedora-bootc and centos-bootc, strip /sysroot (ostree data), and rechunk with content-based layers for optimal layer reuse across updates. The workflow has three phases: mirror — skopeo copy --all from quay.io to GHCR so we own the copies and aren't broken when upstream deletes old manifests on tag re-push. build — per-arch (amd64 + arm64) chunkah builds on native runners, pushed by digest. manifest — assemble per-arch digests into multi-arch manifest lists using bootc-dev/actions/create-manifest. Source images are pinned by @sha256 digest for reproducibility, with a Renovate custom regex manager to automatically bump digests when upstream tags are updated. Target images: - ghcr.io/bootc-dev/fedora-bootc-staged:43 - ghcr.io/bootc-dev/fedora-bootc-staged:44 - ghcr.io/bootc-dev/centos-bootc-staged:stream9 - ghcr.io/bootc-dev/centos-bootc-staged:stream10 Closes: #151 Assisted-by: OpenCode (Claude Opus 4)
1 parent 86f10ae commit 480b425

4 files changed

Lines changed: 292 additions & 0 deletions

File tree

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
name: Build chunkah-staged bootc base images
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'staged-images/**'
8+
- '.github/workflows/build-staged-images.yml'
9+
schedule:
10+
# Rebuild weekly to pick up upstream base image updates
11+
- cron: '0 6 * * 1'
12+
workflow_dispatch:
13+
14+
concurrency:
15+
group: ${{ github.workflow }}-${{ github.ref }}
16+
cancel-in-progress: true
17+
18+
env:
19+
REGISTRY: ghcr.io
20+
21+
jobs:
22+
# Mirror upstream source images to GHCR so we have our own copy.
23+
# Upstream registries (quay.io) may delete old manifests, breaking
24+
# digest-pinned pulls. skopeo copy --all preserves the full
25+
# multi-arch manifest list.
26+
mirror:
27+
name: Mirror ${{ matrix.upstream_name }}:${{ matrix.tag }}
28+
runs-on: ubuntu-24.04
29+
permissions:
30+
contents: read
31+
packages: write
32+
strategy:
33+
fail-fast: false
34+
matrix:
35+
include:
36+
- upstream_name: fedora/fedora-bootc
37+
tag: "43"
38+
mirror_name: fedora-bootc-source
39+
# renovate: datasource=docker depName=quay.io/fedora/fedora-bootc
40+
source: quay.io/fedora/fedora-bootc:43@sha256:60900cb506936af583704955830c72de1d7fc2622a92a9a3039442871a798260
41+
- upstream_name: fedora/fedora-bootc
42+
tag: "44"
43+
mirror_name: fedora-bootc-source
44+
# renovate: datasource=docker depName=quay.io/fedora/fedora-bootc
45+
source: quay.io/fedora/fedora-bootc:44@sha256:33811dcc3d56e69a810fab88e8af08a121db2897625eec859b004b869a2d3d4e
46+
- upstream_name: centos-bootc/centos-bootc
47+
tag: stream9
48+
mirror_name: centos-bootc-source
49+
# renovate: datasource=docker depName=quay.io/centos-bootc/centos-bootc
50+
source: quay.io/centos-bootc/centos-bootc:stream9@sha256:2b96ee6f7157ed79c0f3db4d2c56686444e61733cabddc6229711c6a6b5dd673
51+
- upstream_name: centos-bootc/centos-bootc
52+
tag: stream10
53+
mirror_name: centos-bootc-source
54+
# renovate: datasource=docker depName=quay.io/centos-bootc/centos-bootc
55+
source: quay.io/centos-bootc/centos-bootc:stream10@sha256:43d2199dc2b147905ff3970b77957ac4775554b5ee6973abb577243a3fa28614
56+
steps:
57+
- name: Set up podman
58+
uses: bootc-dev/actions/bootc-ubuntu-setup@main
59+
60+
- name: Log in to GHCR
61+
run: |
62+
echo "${{ secrets.GITHUB_TOKEN }}" | \
63+
podman login -u "${{ github.actor }}" --password-stdin ${{ env.REGISTRY }}
64+
65+
- name: Mirror image
66+
id: mirror
67+
run: |
68+
set -euo pipefail
69+
dest="${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.mirror_name }}:${{ matrix.tag }}"
70+
# skopeo doesn't support tag@digest references, so strip the
71+
# tag and use the digest-only form for the source.
72+
src="${{ matrix.source }}"
73+
src_by_digest="${src%%:*}@${src##*@}"
74+
echo "Mirroring ${src_by_digest} -> ${dest}"
75+
skopeo copy --all --retry-times 3 \
76+
"docker://${src_by_digest}" \
77+
"docker://${dest}"
78+
echo "Mirrored ${dest}"
79+
80+
build:
81+
name: Build ${{ matrix.name }}:${{ matrix.tag }} (${{ matrix.arch }})
82+
needs: mirror
83+
if: ${{ !cancelled() }}
84+
runs-on: ${{ matrix.runner }}
85+
permissions:
86+
contents: read
87+
packages: write
88+
strategy:
89+
fail-fast: false
90+
matrix:
91+
include:
92+
# fedora-bootc-staged:43
93+
- name: fedora-bootc-staged
94+
tag: "43"
95+
mirror_name: fedora-bootc-source
96+
arch: amd64
97+
runner: ubuntu-24.04
98+
- name: fedora-bootc-staged
99+
tag: "43"
100+
mirror_name: fedora-bootc-source
101+
arch: arm64
102+
runner: ubuntu-24.04-arm
103+
# fedora-bootc-staged:44
104+
- name: fedora-bootc-staged
105+
tag: "44"
106+
mirror_name: fedora-bootc-source
107+
arch: amd64
108+
runner: ubuntu-24.04
109+
- name: fedora-bootc-staged
110+
tag: "44"
111+
mirror_name: fedora-bootc-source
112+
arch: arm64
113+
runner: ubuntu-24.04-arm
114+
# centos-bootc-staged:stream9
115+
- name: centos-bootc-staged
116+
tag: stream9
117+
mirror_name: centos-bootc-source
118+
arch: amd64
119+
runner: ubuntu-24.04
120+
- name: centos-bootc-staged
121+
tag: stream9
122+
mirror_name: centos-bootc-source
123+
arch: arm64
124+
runner: ubuntu-24.04-arm
125+
# centos-bootc-staged:stream10
126+
- name: centos-bootc-staged
127+
tag: stream10
128+
mirror_name: centos-bootc-source
129+
arch: amd64
130+
runner: ubuntu-24.04
131+
- name: centos-bootc-staged
132+
tag: stream10
133+
mirror_name: centos-bootc-source
134+
arch: arm64
135+
runner: ubuntu-24.04-arm
136+
137+
steps:
138+
- name: Checkout
139+
uses: actions/checkout@v6
140+
141+
- name: Set up podman
142+
uses: bootc-dev/actions/bootc-ubuntu-setup@main
143+
144+
- name: Log in to GHCR
145+
run: |
146+
echo "${{ secrets.GITHUB_TOKEN }}" | \
147+
podman login -u "${{ github.actor }}" --password-stdin ${{ env.REGISTRY }}
148+
149+
- name: Pull source image from mirror
150+
id: source
151+
run: |
152+
set -euo pipefail
153+
source="${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.mirror_name }}:${{ matrix.tag }}"
154+
podman pull "${source}"
155+
echo "image=${source}" >> "$GITHUB_OUTPUT"
156+
157+
- name: Write source image config to build context
158+
working-directory: staged-images
159+
run: podman inspect ${{ steps.source.outputs.image }} > source-config.json
160+
161+
- name: Build staged image
162+
working-directory: staged-images
163+
run: |
164+
buildah build --skip-unused-stages=false \
165+
--build-arg SOURCE_IMAGE=${{ steps.source.outputs.image }} \
166+
--build-arg MAX_LAYERS=128 \
167+
-f Containerfile.staged \
168+
-t localhost/${{ matrix.name }}:latest \
169+
.
170+
171+
- name: Verify image
172+
run: |
173+
echo "=== Image labels ==="
174+
podman inspect localhost/${{ matrix.name }}:latest | jq '.[0].Config.Labels'
175+
echo "=== Layer count ==="
176+
podman inspect localhost/${{ matrix.name }}:latest | jq '.[0].RootFS.Layers | length'
177+
178+
- name: Push by digest
179+
id: push
180+
run: |
181+
set -euo pipefail
182+
image="${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.name }}"
183+
podman tag localhost/${{ matrix.name }}:latest "${image}:latest"
184+
podman push --retry 3 --digestfile "${{ runner.temp }}/digestfile" "${image}:latest"
185+
digest=$(cat "${{ runner.temp }}/digestfile")
186+
echo "digest=${digest}" >> "$GITHUB_OUTPUT"
187+
echo "Pushed ${image}@${digest} (${{ matrix.arch }})"
188+
189+
- name: Upload digest artifact
190+
run: |
191+
mkdir -p "${{ runner.temp }}/digests"
192+
echo "${{ steps.push.outputs.digest }}" > "${{ runner.temp }}/digests/${{ matrix.arch }}"
193+
- uses: actions/upload-artifact@v7
194+
with:
195+
name: staged-digests-${{ matrix.name }}-${{ matrix.tag }}-${{ matrix.arch }}
196+
path: ${{ runner.temp }}/digests/*
197+
if-no-files-found: error
198+
retention-days: 1
199+
200+
manifest:
201+
name: Manifest ${{ matrix.name }}:${{ matrix.tag }}
202+
needs: build
203+
if: ${{ !cancelled() }}
204+
runs-on: ubuntu-24.04
205+
permissions:
206+
contents: read
207+
packages: write
208+
strategy:
209+
fail-fast: false
210+
matrix:
211+
include:
212+
- name: fedora-bootc-staged
213+
tag: "43"
214+
- name: fedora-bootc-staged
215+
tag: "44"
216+
- name: centos-bootc-staged
217+
tag: stream9
218+
- name: centos-bootc-staged
219+
tag: stream10
220+
steps:
221+
- name: Set up podman
222+
uses: bootc-dev/actions/bootc-ubuntu-setup@main
223+
224+
- name: Create and push manifest
225+
uses: bootc-dev/actions/create-manifest@main
226+
with:
227+
image: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.name }}
228+
tags: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.name }}:${{ matrix.tag }}
229+
artifact-pattern: staged-digests-${{ matrix.name }}-${{ matrix.tag }}-*
230+
registry-login-env: 'false'

renovate-shared-config.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@
5757
"# renovate: datasource=(?<datasource>[a-z-]+) depName=(?<depName>[^\\s]+)\\n\\s*(?:export )?\\w*VERSION=(?<currentValue>v?\\S+)"
5858
]
5959
},
60+
// Container image digest pinning in YAML workflows
61+
// Matches patterns like:
62+
// # renovate: datasource=docker depName=quay.io/fedora/fedora-bootc
63+
// source: quay.io/fedora/fedora-bootc:43@sha256:abc123...
64+
{
65+
"customType": "regex",
66+
"managerFilePatterns": ["**/*.yml", "**/*.yaml"],
67+
"matchStrings": [
68+
"# renovate: datasource=(?<datasource>docker) depName=(?<depName>[^\\s]+)\\n\\s*\\w+:\\s*\\S+:(?<currentValue>[^@\\s]+)@(?<currentDigest>sha256:[a-f0-9]+)"
69+
]
70+
},
6071
// Git refs (commit SHA) tracking in Justfiles and YAML workflows
6172
// Justfile example:
6273
// # renovate: datasource=git-refs depName=https://github.com/org/repo branch=main
@@ -134,6 +145,22 @@
134145
"groupName": "Docker",
135146
"enabled": true
136147
},
148+
// Group staged bootc base image digest updates separately
149+
//
150+
// These are the upstream source images for chunkah-staged builds.
151+
// Digest updates trigger a rebuild of the staged images, so they
152+
// get their own PR. Must come after the Docker group rule so it
153+
// takes precedence (Renovate applies all matching rules in order,
154+
// later rules win).
155+
{
156+
"description": ["Staged bootc base image digest updates"],
157+
"matchManagers": ["custom.regex"],
158+
"matchDepNames": [
159+
"quay.io/fedora/fedora-bootc",
160+
"quay.io/centos-bootc/centos-bootc"
161+
],
162+
"groupName": "staged-images"
163+
},
137164
// bcvk gets its own group so it isn't blocked by the weekly schedule
138165
// applied to other Docker group members. Without this, the Docker group
139166
// PR can only be created on Sundays (when all deps are in-schedule),

staged-images/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
source-config.json
2+
out.ociarchive

staged-images/Containerfile.staged

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Containerfile.staged — Rechunk a bootc base image with chunkah
2+
#
3+
# Takes an upstream bootc image (fedora-bootc or centos-bootc), strips
4+
# the /sysroot (ostree data), and rechunks using chunkah for optimal
5+
# layer reuse across updates. The result is a "staged" base image
6+
# suitable for use as a FROM base in bootc development.
7+
#
8+
# Usage:
9+
# podman pull quay.io/fedora/fedora-bootc:44
10+
# podman inspect quay.io/fedora/fedora-bootc:44 > source-config.json
11+
# buildah build --skip-unused-stages=false \
12+
# --build-arg SOURCE_IMAGE=quay.io/fedora/fedora-bootc:44 \
13+
# -f Containerfile.staged .
14+
15+
ARG SOURCE_IMAGE
16+
ARG CHUNKAH=quay.io/jlebon/chunkah:latest
17+
ARG MAX_LAYERS=128
18+
19+
FROM ${SOURCE_IMAGE} AS source
20+
21+
FROM ${CHUNKAH} AS chunkah
22+
ARG MAX_LAYERS
23+
RUN --mount=type=bind,target=/run/src,rw \
24+
--mount=from=source,target=/chunkah,ro \
25+
chunkah build \
26+
--config /run/src/source-config.json \
27+
--prune /sysroot/ \
28+
--max-layers "${MAX_LAYERS}" \
29+
--label ostree.commit- \
30+
--label ostree.final-diffid- \
31+
> /run/src/out.ociarchive
32+
33+
FROM oci-archive:out.ociarchive

0 commit comments

Comments
 (0)