Skip to content

Docker base images

Docker base images #3

# Build the base/system images declared in etc/docker/docker-bake.hcl.
#
# Cells (distro, version) come from the bake matrix; each is built natively on
# amd64 AND arm64 (no QEMU), pushed by digest, then the two arch digests are
# merged into one multi-arch manifest list per cell.
#
# The cell surface is owned entirely by docker-bake.hcl. Adding a distro/version
# or arch is a change to that file (and the `arch` list below for a new arch);
# the job graph here is generic and stays untouched.
name: Docker base images
on:
workflow_dispatch:
inputs:
push:
description: "push images + create manifest lists (off = build-only dry run)"
type: boolean
default: false
# Rebuild when the image definition changes.
push:
branches: [main]
paths:
- etc/docker/**
- .github/workflows/docker-base-images.yml
env:
REGISTRY: ghcr.io/viamrobotics
BAKE_FILE: etc/docker/docker-bake.hcl
# workflow_dispatch carries inputs.push; branch pushes always publish.
PUSH: ${{ github.event_name != 'workflow_dispatch' || inputs.push }}
jobs:
# ---------------------------------------------------------------------------
# Read the cell surface straight out of the bake file. `bake --print` is the
# documented contract; we never hand-maintain the cell list in YAML.
# ---------------------------------------------------------------------------
matrix:
runs-on: ubuntu-latest
outputs:
cells: ${{ steps.gen.outputs.cells }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- id: gen
name: Derive cell matrix from bake --print
working-directory: etc/docker
run: |
# Each system-* target -> { target, tag, image }. `tag` is the published
# multi-arch reference the merge job creates; `image` is that tag with
# the version stripped (the per-cell repo digests are pushed under).
cells=$(docker buildx bake -f docker-bake.hcl system --print \
| jq -c '[
.target
| to_entries[]
| select(.key | startswith("system-"))
| { target: .key, tag: (.value.tags[0]), image: (.value.tags[0] | split(":")[0]) }
]')
echo "cells=$cells" >> "$GITHUB_OUTPUT"
echo "$cells" | jq .
# ---------------------------------------------------------------------------
# One job per (cell, arch). Native runner per arch, build a single named bake
# target, push by digest only (no tag). Digests are collected by the merge job.
# ---------------------------------------------------------------------------
build:
needs: matrix
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
cell: ${{ fromJson(needs.matrix.outputs.cells) }}
arch: [amd64, arm64]
include:
- arch: amd64
runner: ubuntu-24.04
- arch: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
if: env.PUSH == 'true'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build ${{ matrix.cell.target }} (${{ matrix.arch }})
working-directory: etc/docker
run: |
# Slice the bake matrix to this runner's native arch (no QEMU); the
# bake file defaults to the full amd64+arm64 matrix.
# Push by digest: strip the tag and emit a name-canonical image so the
# arch builds don't race on a shared tag. Merge job stitches them.
docker buildx bake -f docker-bake.hcl \
--set "${{ matrix.cell.target }}.platform=linux/${{ matrix.arch }}" \
--set "${{ matrix.cell.target }}.tags=" \
--set "${{ matrix.cell.target }}.output=type=image,name=${{ matrix.cell.image }},push-by-digest=true,name-canonical=true,push=${PUSH}" \
--metadata-file metadata.json \
"${{ matrix.cell.target }}"
- name: Export digest
if: env.PUSH == 'true'
working-directory: etc/docker
run: |
digest=$(jq -r '.["${{ matrix.cell.target }}"]."containerimage.digest"' metadata.json)
test -n "$digest" && test "$digest" != null
mkdir -p /tmp/digests
# Filename = sanitized digest; content unused, name carries the data.
echo -n > "/tmp/digests/${digest#sha256:}"
echo "$digest"
- name: Upload digest
if: env.PUSH == 'true'
uses: actions/upload-artifact@v4
with:
# One artifact per cell; arch jobs append their digest into it.
name: digest-${{ matrix.cell.target }}-${{ matrix.arch }}
path: /tmp/digests/*
retention-days: 1
if-no-files-found: error
# ---------------------------------------------------------------------------
# Per cell, join the arch digests into one manifest list under the cell tag.
# ---------------------------------------------------------------------------
merge:
needs: [matrix, build]
if: needs.build.result == 'success' && (github.event_name != 'workflow_dispatch' || inputs.push)
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
cell: ${{ fromJson(needs.matrix.outputs.cells) }}
runs-on: ubuntu-latest
steps:
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Collect digests for ${{ matrix.cell.target }}
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digest-${{ matrix.cell.target }}-*
merge-multiple: true
- name: Create multi-arch manifest
run: |
# imagetools create -t <tag> <image>@sha256:<d1> <image>@sha256:<d2>
docker buildx imagetools create -t "${{ matrix.cell.tag }}" \
$(for d in /tmp/digests/*; do printf '%s@sha256:%s ' "${{ matrix.cell.image }}" "$(basename "$d")"; done)
- name: Inspect
run: docker buildx imagetools inspect "${{ matrix.cell.tag }}"