Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions .github/workflows/build-staged-images.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
name: Build chunkah-staged bootc base images

on:
push:
branches: [main]
paths:
- 'staged-images/**'
- '.github/workflows/build-staged-images.yml'
schedule:
# Rebuild weekly to pick up upstream base image updates
- cron: '0 6 * * 1'
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
REGISTRY: ghcr.io

jobs:
# Read sources.json and generate matrices for downstream jobs.
generate-matrix:
name: Generate matrix
runs-on: ubuntu-24.04
outputs:
mirror: ${{ steps.matrix.outputs.mirror }}
build: ${{ steps.matrix.outputs.build }}
manifest: ${{ steps.matrix.outputs.manifest }}
steps:
- uses: actions/checkout@v6
- uses: extractions/setup-just@v3
- id: matrix
run: |
echo "mirror=$(just staged-images/ci-mirror-matrix)" >> "$GITHUB_OUTPUT"
echo "build=$(just staged-images/ci-matrix)" >> "$GITHUB_OUTPUT"
echo "manifest=$(just staged-images/ci-manifest-matrix)" >> "$GITHUB_OUTPUT"

# Mirror upstream source images to GHCR so we have our own copy.
# Upstream registries (quay.io) may delete old manifests, breaking
# digest-pinned pulls.
mirror:
name: Mirror ${{ matrix.name }}:${{ matrix.tag }}
needs: generate-matrix
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs.mirror) }}
steps:
- uses: actions/checkout@v6
- uses: bootc-dev/actions/bootc-ubuntu-setup@main
- name: Log in to GHCR
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | \
podman login -u "${{ github.actor }}" --password-stdin ${{ env.REGISTRY }}
- name: Mirror image
run: just staged-images/mirror ${{ matrix.name }}-${{ matrix.tag }}
env:
REGISTRY_OWNER: ${{ github.repository_owner }}

build:
name: Build ${{ matrix.name }}:${{ matrix.tag }} (${{ matrix.arch }})
needs: [generate-matrix, mirror]
if: ${{ !cancelled() }}
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs.build) }}
steps:
- uses: actions/checkout@v6
- uses: bootc-dev/actions/bootc-ubuntu-setup@main
- name: Log in to GHCR
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | \
podman login -u "${{ github.actor }}" --password-stdin ${{ env.REGISTRY }}
- name: Build staged image
run: just staged-images/build ${{ matrix.image_key }}
env:
SOURCE_FROM_MIRROR: "1"
REGISTRY_OWNER: ${{ github.repository_owner }}
- name: Push by digest
id: push
run: |
digest=$(just staged-images/push ${{ matrix.image_key }} ${{ matrix.arch }})
echo "digest=${digest}" >> "$GITHUB_OUTPUT"
env:
REGISTRY_OWNER: ${{ github.repository_owner }}
- name: Upload digest artifact
run: |
mkdir -p "${{ runner.temp }}/digests"
echo "${{ steps.push.outputs.digest }}" > "${{ runner.temp }}/digests/${{ matrix.arch }}"
- uses: actions/upload-artifact@v7
with:
name: staged-digests-${{ matrix.name }}-${{ matrix.tag }}-${{ matrix.arch }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1

manifest:
name: Manifest ${{ matrix.name }}:${{ matrix.tag }}
needs: [generate-matrix, build]
if: ${{ !cancelled() }}
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs.manifest) }}
steps:
- uses: bootc-dev/actions/bootc-ubuntu-setup@main
- uses: bootc-dev/actions/create-manifest@main
with:
image: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.name }}
tags: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.name }}:${{ matrix.tag }}
artifact-pattern: staged-digests-${{ matrix.name }}-${{ matrix.tag }}-*
registry-login-env: 'false'
28 changes: 28 additions & 0 deletions renovate-shared-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@
"# renovate: datasource=(?<datasource>[a-z-]+) depName=(?<depName>[^\\s]+)\\n\\s*(?:export )?\\w*VERSION=(?<currentValue>v?\\S+)"
]
},
// Container image digest pinning in JSON config files
// Matches patterns like:
// "_renovate": "datasource=docker depName=quay.io/fedora/fedora-bootc",
// ...
// "source": "quay.io/fedora/fedora-bootc:43@sha256:abc123..."
{
"customType": "regex",
"managerFilePatterns": ["**/sources.json"],
"matchStrings": [
"\"_renovate\":\\s*\"datasource=(?<datasource>docker) depName=(?<depName>[^\"]+)\"[^}]*\"source\":\\s*\"\\S+:(?<currentValue>[^@\\s\"]+)@(?<currentDigest>sha256:[a-f0-9]+)\""
]
},
// Git refs (commit SHA) tracking in Justfiles and YAML workflows
// Justfile example:
// # renovate: datasource=git-refs depName=https://github.com/org/repo branch=main
Expand Down Expand Up @@ -134,6 +146,22 @@
"groupName": "Docker",
"enabled": true
},
// Group staged bootc base image digest updates separately
//
// These are the upstream source images for chunkah-staged builds.
// Digest updates trigger a rebuild of the staged images, so they
// get their own PR. Must come after the Docker group rule so it
// takes precedence (Renovate applies all matching rules in order,
// later rules win).
{
"description": ["Staged bootc base image digest updates"],
"matchManagers": ["custom.regex"],
"matchDepNames": [
"quay.io/fedora/fedora-bootc",
"quay.io/centos-bootc/centos-bootc"
],
"groupName": "staged-images"
},
// bcvk gets its own group so it isn't blocked by the weekly schedule
// applied to other Docker group members. Without this, the Docker group
// PR can only be created on Sundays (when all deps are in-schedule),
Expand Down
2 changes: 2 additions & 0 deletions staged-images/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
source-config.json
out.ociarchive
28 changes: 28 additions & 0 deletions staged-images/Containerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Rechunk a bootc base image with chunkah
#
# Takes an upstream bootc image (fedora-bootc or centos-bootc), strips
# the /sysroot (ostree data), and rechunks using chunkah for optimal
# layer reuse across updates. The result is a "staged" base image
# suitable for use as a FROM base in bootc development.
#
# Usage: just staged-images/build fedora-bootc-44

ARG SOURCE_IMAGE
ARG CHUNKAH=quay.io/jlebon/chunkah:latest
Comment thread
cgwalters marked this conversation as resolved.
ARG MAX_LAYERS=128

FROM ${SOURCE_IMAGE} AS source

FROM ${CHUNKAH} AS chunkah
ARG MAX_LAYERS
RUN --mount=type=bind,target=/run/src,rw \
Comment thread
cgwalters marked this conversation as resolved.
--mount=from=source,target=/chunkah,ro \
chunkah build \
--config /run/src/source-config.json \
Comment thread
cgwalters marked this conversation as resolved.
--prune /sysroot/ \
--max-layers "${MAX_LAYERS}" \
--label ostree.commit- \
--label ostree.final-diffid- \
> /run/src/out.ociarchive

FROM oci-archive:out.ociarchive
Comment thread
cgwalters marked this conversation as resolved.
101 changes: 101 additions & 0 deletions staged-images/Justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
_sources := justfile_directory() / "sources.json"
registry := env("REGISTRY", "ghcr.io")
registry_owner := env("REGISTRY_OWNER", "bootc-dev")

# Look up a field from sources.json by image key (e.g. fedora-bootc-43)
[private]
_field image field:
@jq -re --arg n "{{image}}" '.[] | select(.name + "-" + .tag == $n) | .{{field}}' "{{_sources}}"

# List available staged images
list:
@jq -r '.[] | .name + "-" + .tag' "{{_sources}}"

# Mirror an upstream source image to our registry.
# Usage: just staged-images/mirror fedora-bootc-43
mirror image:
#!/bin/bash
set -euo pipefail
name=$(just {{justfile_directory()}}/_field {{image}} name)
tag=$(just {{justfile_directory()}}/_field {{image}} tag)
src=$(just {{justfile_directory()}}/_field {{image}} source)
dest="{{registry}}/{{registry_owner}}/${name}-source:${tag}"
# skopeo doesn't support tag@digest, use digest-only form
src_by_digest="${src%%:*}@${src##*@}"
echo "Mirroring ${src_by_digest} -> ${dest}"
skopeo copy --all --retry-times 3 "docker://${src_by_digest}" "docker://${dest}"
echo "Mirrored ${dest}"

# Build a staged image locally.
# Usage: just staged-images/build fedora-bootc-43
# Set SOURCE_FROM_MIRROR=1 to pull from registry mirror instead of upstream.
build image:
#!/bin/bash
set -euo pipefail
name=$(just {{justfile_directory()}}/_field {{image}} name)
tag=$(just {{justfile_directory()}}/_field {{image}} tag)
src=$(just {{justfile_directory()}}/_field {{image}} source)
staged_name="${name}-staged"
if [ "${SOURCE_FROM_MIRROR:-}" = "1" ]; then
src="{{registry}}/{{registry_owner}}/${name}-source:${tag}"
fi
echo "=== Pulling source image ==="
podman pull "${src}"
echo "=== Writing source config ==="
podman inspect "${src}" > "{{justfile_directory()}}/source-config.json"
echo "=== Building ${staged_name}:${tag} ==="
# -v is needed for buildah < 1.44 (see containers/buildah#5952)
buildah build --skip-unused-stages=false \
-v "{{justfile_directory()}}:/run/src" --security-opt=label=disable \
--build-arg SOURCE_IMAGE="${src}" \
--build-arg MAX_LAYERS=128 \
-f "{{justfile_directory()}}/Containerfile" \
-t "localhost/${staged_name}:${tag}" \
"{{justfile_directory()}}"
echo "=== Verifying ==="
echo "Labels:"
podman inspect "localhost/${staged_name}:${tag}" | jq '.[0].Config.Labels'
echo "Layer count:"
podman inspect "localhost/${staged_name}:${tag}" | jq '.[0].RootFS.Layers | length'
echo "Built localhost/${staged_name}:${tag}"

# Build all staged images
build-all:
#!/bin/bash
set -euo pipefail
for image in $(jq -r '.[] | .name + "-" + .tag' "{{_sources}}"); do
just {{justfile_directory()}}/build "$image"
done

# Push a built staged image by digest, print only the digest to stdout.
# Usage: just staged-images/push fedora-bootc-43 amd64
push image arch="":
#!/bin/bash
set -euo pipefail
name=$(just {{justfile_directory()}}/_field {{image}} name)
tag=$(just {{justfile_directory()}}/_field {{image}} tag)
staged_name="${name}-staged"
arch="{{arch}}"
if [ -z "$arch" ]; then
arch=$(podman info --format '{{{{.Host.Arch}}')
fi
dest="{{registry}}/{{registry_owner}}/${staged_name}"
# Use a per-arch tag to avoid collisions when pushing in parallel
push_tag="${tag}-${arch}"
podman tag "localhost/${staged_name}:${tag}" "${dest}:${push_tag}" >&2
digestfile=$(mktemp)
podman push --retry 3 --digestfile "${digestfile}" "${dest}:${push_tag}" >&2
digest=$(cat "${digestfile}")
rm -f "${digestfile}"
echo "${digest}"

# Generate GHA matrices from sources.json (used by CI workflow)
[private]
ci-matrix:
@jq -c '[.[] | . as $img | {name: ($img.name + "-staged"), tag: $img.tag, image_key: ($img.name + "-" + $img.tag), arch: "amd64", runner: "ubuntu-24.04"}, {name: ($img.name + "-staged"), tag: $img.tag, image_key: ($img.name + "-" + $img.tag), arch: "arm64", runner: "ubuntu-24.04-arm"}] | {include: .}' "{{_sources}}"
[private]
ci-mirror-matrix:
@jq -c '[.[] | {name: .name, tag: .tag, source: .source, mirror_name: (.name + "-source")}] | {include: .}' "{{_sources}}"
[private]
ci-manifest-matrix:
@jq -c '[.[] | {name: (.name + "-staged"), tag: .tag}] | {include: .}' "{{_sources}}"
26 changes: 26 additions & 0 deletions staged-images/sources.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[
{
"_renovate": "datasource=docker depName=quay.io/fedora/fedora-bootc",
"name": "fedora-bootc",
"tag": "43",
"source": "quay.io/fedora/fedora-bootc:43@sha256:dca83fb0b030b529394a129e82f0f75913d0b21f9da304b7ac048b1f12e48932"
},
{
"_renovate": "datasource=docker depName=quay.io/fedora/fedora-bootc",
"name": "fedora-bootc",
"tag": "44",
"source": "quay.io/fedora/fedora-bootc:44@sha256:4abd97abb04f600ba7a0eda950fd1d241d43bf472fd9fdd85e87a6be6cd5b1d1"
},
{
"_renovate": "datasource=docker depName=quay.io/centos-bootc/centos-bootc",
"name": "centos-bootc",
"tag": "stream9",
"source": "quay.io/centos-bootc/centos-bootc:stream9@sha256:2b96ee6f7157ed79c0f3db4d2c56686444e61733cabddc6229711c6a6b5dd673"
},
{
"_renovate": "datasource=docker depName=quay.io/centos-bootc/centos-bootc",
"name": "centos-bootc",
"tag": "stream10",
"source": "quay.io/centos-bootc/centos-bootc:stream10@sha256:43d2199dc2b147905ff3970b77957ac4775554b5ee6973abb577243a3fa28614"
}
]