chore: import upstream snapshot with attribution
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,428 @@
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to stamp into the image contents and exact image tag"
|
||||
required: false
|
||||
type: string
|
||||
enable_ref_tags:
|
||||
description: "Whether to emit branch/PR ref tags"
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to stamp into the image contents and exact image tag"
|
||||
required: false
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write # For cosign keyless signing via Sigstore OIDC
|
||||
|
||||
jobs:
|
||||
# ─── Per-arch fan-out ──────────────────────────────────────────────────────
|
||||
# Build each variant on its native architecture in parallel:
|
||||
# linux/amd64 → ubuntu-24.04 (native x86_64)
|
||||
# linux/arm64 → ubuntu-24.04-arm (native aarch64, GA Jan 2025)
|
||||
#
|
||||
# Pre-#377 we ran a single matrix job per variant on `ubuntu-latest` and
|
||||
# let bake's `platforms = ["linux/amd64", "linux/arm64"]` do multi-arch
|
||||
# via QEMU emulation — ~1h per variant. Native arm64 runners drop QEMU
|
||||
# entirely and cut each variant to ~10 min on each arch in parallel.
|
||||
#
|
||||
# Each per-arch build pushes by digest only (no tags). The
|
||||
# `docker-manifest` job below combines the per-arch digests into the
|
||||
# final multi-arch tagged manifest, which is what users pull by tag.
|
||||
docker-build:
|
||||
runs-on: ${{ matrix.arch.runs_on }}
|
||||
timeout-minutes: 75
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
variant:
|
||||
- { name: "", bake_target: runtime }
|
||||
- { name: nonroot, bake_target: runtime-nonroot }
|
||||
- { name: code, bake_target: runtime-code }
|
||||
- { name: code-nonroot, bake_target: runtime-code-nonroot }
|
||||
- { name: slim, bake_target: runtime-slim }
|
||||
- { name: slim-nonroot, bake_target: runtime-slim-nonroot }
|
||||
- { name: code-slim, bake_target: runtime-code-slim }
|
||||
- { name: code-slim-nonroot, bake_target: runtime-code-slim-nonroot }
|
||||
arch:
|
||||
- { name: amd64, runs_on: ubuntu-24.04, platform: linux/amd64 }
|
||||
- { name: arm64, runs_on: ubuntu-24.04-arm, platform: linux/arm64 }
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Normalize image name
|
||||
id: image-name
|
||||
run: |
|
||||
image_name="$(printf '%s' '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')"
|
||||
printf 'image_name=%s\n' "$image_name" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Determine image version
|
||||
id: version
|
||||
env:
|
||||
MANUAL_VERSION: ${{ inputs.version || github.event.inputs.version }}
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
version="${MANUAL_VERSION#v}"
|
||||
if [ -z "$version" ] && [ -n "$RELEASE_TAG" ]; then
|
||||
version="${RELEASE_TAG#v}"
|
||||
fi
|
||||
printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Python
|
||||
if: steps.version.outputs.version != ''
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Sync versioned files for image build
|
||||
if: steps.version.outputs.version != ''
|
||||
run: |
|
||||
python scripts/version-sync.py --version ${{ steps.version.outputs.version }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Labels (not tags) for the per-arch image. Tags belong on the
|
||||
# multi-arch index manifest and are applied in docker-manifest.
|
||||
- name: Extract image labels
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ steps.image-name.outputs.image_name }}
|
||||
|
||||
- name: Build and push by digest (single platform)
|
||||
id: bake
|
||||
uses: docker/bake-action@v7
|
||||
with:
|
||||
files: |
|
||||
./docker-bake.hcl
|
||||
cwd://${{ steps.meta.outputs.bake-file-labels }}
|
||||
targets: ${{ matrix.variant.bake_target }}
|
||||
push: true
|
||||
# `*.platform` overrides the [amd64,arm64] default in
|
||||
# docker-bake.hcl. `push-by-digest=true,name-canonical=true`
|
||||
# tells buildx to push the per-platform manifest with no tags
|
||||
# — only the digest is recorded — so multiple per-arch builds
|
||||
# can coexist in the registry until the manifest job stitches
|
||||
# them. `name=<registry>/<image>` is REQUIRED here: with no
|
||||
# `bake-file-tags` in scope (tags belong on the manifest, not
|
||||
# per-arch), bake has no way to know the push target without
|
||||
# the explicit `name=`. Removing it surfaces as the
|
||||
# misleading "ERROR: tag is needed when pushing to registry"
|
||||
# — see PR #378 (regression from #376). GHA cache is scoped
|
||||
# per (variant, arch) so the two arches don't fight over the
|
||||
# same cache key.
|
||||
set: |
|
||||
*.platform=${{ matrix.arch.platform }}
|
||||
*.output=type=image,name=${{ env.REGISTRY }}/${{ steps.image-name.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
||||
*.cache-from=type=gha,scope=${{ matrix.variant.name || 'root' }}-${{ matrix.arch.name }}
|
||||
*.cache-to=type=gha,mode=max,scope=${{ matrix.variant.name || 'root' }}-${{ matrix.arch.name }}
|
||||
|
||||
- name: Export digest
|
||||
id: digest
|
||||
env:
|
||||
BAKE_METADATA: ${{ steps.bake.outputs.metadata }}
|
||||
run: |
|
||||
# Bake's metadata is one entry per target; for a single-target
|
||||
# single-platform build it has exactly one digest. Pipe the
|
||||
# JSON through a file (same ARG_MAX rationale as before) and
|
||||
# extract that digest.
|
||||
cat > "${RUNNER_TEMP}/bake_meta.json" <<'__HEADROOM_BAKE_META_EOF__'
|
||||
${{ steps.bake.outputs.metadata }}
|
||||
__HEADROOM_BAKE_META_EOF__
|
||||
digest="$(jq -r 'to_entries[0].value."containerimage.digest" // empty' \
|
||||
"${RUNNER_TEMP}/bake_meta.json")"
|
||||
if [ -z "$digest" ]; then
|
||||
echo "ERROR: no digest in bake metadata" >&2
|
||||
cat "${RUNNER_TEMP}/bake_meta.json" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'digest=%s\n' "$digest" >> "$GITHUB_OUTPUT"
|
||||
# Stage a marker file named after the bare hex digest. The
|
||||
# manifest job downloads all per-arch markers for a variant
|
||||
# and reconstructs `IMAGE@sha256:<digest>` references from
|
||||
# the filenames.
|
||||
mkdir -p "${RUNNER_TEMP}/digests"
|
||||
touch "${RUNNER_TEMP}/digests/${digest#sha256:}"
|
||||
|
||||
# Smoke-test the built image before recording its digest. If the
|
||||
# Python ABI is wrong (e.g. builder Python 3.11 vs distroless
|
||||
# Python 3.13) pydantic_core._pydantic_core fails to dlopen and
|
||||
# the import raises ModuleNotFoundError. Catching it here prevents
|
||||
# a broken digest from reaching the manifest merge job and being
|
||||
# tagged and published. Both python-slim and distroless variants
|
||||
# expose python3 in PATH and honour the image's PYTHONPATH env.
|
||||
- name: Smoke-test image (pydantic_core + headroom._core)
|
||||
env:
|
||||
IMAGE: ${{ env.REGISTRY }}/${{ steps.image-name.outputs.image_name }}
|
||||
DIGEST: ${{ steps.digest.outputs.digest }}
|
||||
PLATFORM: ${{ matrix.arch.platform }}
|
||||
run: |
|
||||
docker run --rm \
|
||||
--platform "$PLATFORM" \
|
||||
--entrypoint python3 \
|
||||
"${IMAGE}@${DIGEST}" \
|
||||
-c "
|
||||
import pydantic_core
|
||||
from headroom._core import DiffCompressor, SmartCrusher
|
||||
print('smoke-test OK: pydantic_core', pydantic_core.__version__,
|
||||
'| DiffCompressor', DiffCompressor.__name__,
|
||||
'| SmartCrusher', SmartCrusher.__name__)
|
||||
"
|
||||
|
||||
- name: Upload digest marker
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
# Variant + arch in the artifact name so the manifest job can
|
||||
# download with `pattern: digests-<variant>-*` to gather all
|
||||
# arches for one variant. `root` substitutes the empty-string
|
||||
# variant since GHA artifact names can't end in a hyphen.
|
||||
name: digests-${{ matrix.variant.name || 'root' }}-${{ matrix.arch.name }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
# ─── Per-variant manifest merge ────────────────────────────────────────────
|
||||
# One job per variant, after both arch builds for that variant complete.
|
||||
# `docker buildx imagetools create` stitches the two per-arch digests
|
||||
# into a single multi-arch index manifest, applies the metadata-action
|
||||
# tags, and that manifest is what users pull by `:tag`.
|
||||
docker-manifest:
|
||||
needs: docker-build
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
variant:
|
||||
- { name: "", bake_target: runtime }
|
||||
- { name: nonroot, bake_target: runtime-nonroot }
|
||||
- { name: code, bake_target: runtime-code }
|
||||
- { name: code-nonroot, bake_target: runtime-code-nonroot }
|
||||
- { name: slim, bake_target: runtime-slim }
|
||||
- { name: slim-nonroot, bake_target: runtime-slim-nonroot }
|
||||
- { name: code-slim, bake_target: runtime-code-slim }
|
||||
- { name: code-slim-nonroot, bake_target: runtime-code-slim-nonroot }
|
||||
|
||||
steps:
|
||||
# No `actions/checkout` here: the manifest job only calls
|
||||
# `docker buildx imagetools` against the registry and runs
|
||||
# cosign — neither needs the repo on disk. Skipping checkout
|
||||
# saves a few seconds across 8 parallel manifest jobs.
|
||||
- name: Normalize image name
|
||||
id: image-name
|
||||
run: |
|
||||
image_name="$(printf '%s' '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')"
|
||||
printf 'image_name=%s\n' "$image_name" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Determine image version
|
||||
id: version
|
||||
env:
|
||||
MANUAL_VERSION: ${{ inputs.version || github.event.inputs.version }}
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
version="${MANUAL_VERSION#v}"
|
||||
if [ -z "$version" ] && [ -n "$RELEASE_TAG" ]; then
|
||||
version="${RELEASE_TAG#v}"
|
||||
fi
|
||||
printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Compute short SHA
|
||||
id: short-sha
|
||||
run: printf 'sha=%s\n' "${GITHUB_SHA:0:7}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Download per-arch digests for this variant
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: digests-${{ matrix.variant.name || 'root' }}-*
|
||||
path: ${{ runner.temp }}/digests
|
||||
merge-multiple: true
|
||||
|
||||
# Same tag rules as the pre-fan-out workflow — preserve every
|
||||
# tag flavor (semver, ref, sha-prefixed, version-suffixed,
|
||||
# bare variant) so existing pull URLs keep working.
|
||||
- name: Extract metadata (variant)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ steps.image-name.outputs.image_name }}
|
||||
tags: |
|
||||
type=ref,event=branch,enable=${{ inputs.enable_ref_tags != 'false' && github.event_name != 'release' }},suffix=${{ matrix.variant.name != '' && format('-{0}', matrix.variant.name) || '' }}
|
||||
type=ref,event=pr,enable=${{ inputs.enable_ref_tags != 'false' && github.event_name != 'release' }},suffix=${{ matrix.variant.name != '' && format('-{0}', matrix.variant.name) || '' }}
|
||||
type=raw,value=dev,enable=${{ inputs.enable_ref_tags != 'false' && github.event_name == 'push' }},suffix=${{ matrix.variant.name != '' && format('-{0}', matrix.variant.name) || '' }}
|
||||
type=raw,value=${{ steps.version.outputs.version }},enable=${{ steps.version.outputs.version != '' }},suffix=${{ matrix.variant.name != '' && format('-{0}', matrix.variant.name) || '' }}
|
||||
type=raw,value=${{ steps.version.outputs.version }}-${{ steps.short-sha.outputs.sha }},enable=${{ steps.version.outputs.version != '' && matrix.variant.name == '' }}
|
||||
type=raw,value=${{ steps.version.outputs.version }}-${{ matrix.variant.name }}-${{ steps.short-sha.outputs.sha }},enable=${{ steps.version.outputs.version != '' && matrix.variant.name != '' }}
|
||||
type=semver,pattern={{version}},suffix=${{ matrix.variant.name != '' && format('-{0}', matrix.variant.name) || '' }}
|
||||
type=semver,pattern={{major}}.{{minor}},suffix=${{ matrix.variant.name != '' && format('-{0}', matrix.variant.name) || '' }}
|
||||
type=semver,pattern={{major}},suffix=${{ matrix.variant.name != '' && format('-{0}', matrix.variant.name) || '' }}
|
||||
type=sha,format=short,prefix=${{ matrix.variant.name != '' && format('{0}-', matrix.variant.name) || 'sha-' }}
|
||||
type=raw,value=${{ matrix.variant.name }},enable=${{ matrix.variant.name != '' }}
|
||||
|
||||
- name: Create multi-arch manifest
|
||||
id: manifest
|
||||
env:
|
||||
IMAGE: ${{ env.REGISTRY }}/${{ steps.image-name.outputs.image_name }}
|
||||
DIGEST_DIR: ${{ runner.temp }}/digests
|
||||
run: |
|
||||
# Reconstruct full image references from the digest marker
|
||||
# filenames (each file is named after the bare hex digest
|
||||
# of one per-arch manifest).
|
||||
if ! ls "${DIGEST_DIR}"/* >/dev/null 2>&1; then
|
||||
echo "ERROR: no digests downloaded for variant '${{ matrix.variant.name || 'root' }}'" >&2
|
||||
exit 1
|
||||
fi
|
||||
digest_refs=()
|
||||
for f in "${DIGEST_DIR}"/*; do
|
||||
digest="$(basename "$f")"
|
||||
digest_refs+=("${IMAGE}@sha256:${digest}")
|
||||
done
|
||||
|
||||
# Build `--tag` args from the metadata-action JSON output.
|
||||
# Empty tags array is valid (PR builds without ref-tags
|
||||
# enabled emit nothing); skip manifest creation in that case.
|
||||
tag_args=()
|
||||
while IFS= read -r tag; do
|
||||
[ -n "$tag" ] && tag_args+=("--tag" "$tag")
|
||||
done < <(jq -r '.tags[]?' <<< '${{ steps.meta.outputs.json }}')
|
||||
|
||||
if [ "${#tag_args[@]}" -eq 0 ]; then
|
||||
echo "No tags to apply for variant '${{ matrix.variant.name || 'root' }}'; skipping manifest."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
docker buildx imagetools create \
|
||||
"${tag_args[@]}" \
|
||||
"${digest_refs[@]}"
|
||||
|
||||
# Resolve the index manifest digest of the freshly pushed
|
||||
# multi-arch manifest so cosign can sign it directly. We
|
||||
# ask the registry via `imagetools inspect` and read the
|
||||
# `.manifest.digest` field — that's the registry's own
|
||||
# record of the index digest (no client-side hashing).
|
||||
first_tag="$(jq -r '.tags[0]' <<< '${{ steps.meta.outputs.json }}')"
|
||||
index_digest="$(docker buildx imagetools inspect "${first_tag}" \
|
||||
--format '{{ json . }}' | jq -r '.manifest.digest')"
|
||||
if [ -z "$index_digest" ] || [ "$index_digest" = "null" ]; then
|
||||
echo "ERROR: could not resolve index digest for ${first_tag}" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'index_digest=%s\n' "$index_digest" >> "$GITHUB_OUTPUT"
|
||||
printf 'first_tag=%s\n' "$first_tag" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Install cosign
|
||||
if: steps.manifest.outputs.index_digest != ''
|
||||
uses: sigstore/cosign-installer@v3
|
||||
|
||||
- name: Sign multi-arch index manifest with cosign
|
||||
if: steps.manifest.outputs.index_digest != ''
|
||||
env:
|
||||
# Same routing as before: keep signature artifacts in a
|
||||
# sibling GHCR package so the main image's version listing
|
||||
# stays clean. Verifiers must export the same
|
||||
# COSIGN_REPOSITORY when running 'cosign verify'. See the
|
||||
# pre-#377 workflow for the GHCR/OCI-1.1 referrers context.
|
||||
COSIGN_REPOSITORY: ${{ env.REGISTRY }}/${{ steps.image-name.outputs.image_name }}-signatures
|
||||
IMAGE: ${{ env.REGISTRY }}/${{ steps.image-name.outputs.image_name }}
|
||||
INDEX_DIGEST: ${{ steps.manifest.outputs.index_digest }}
|
||||
run: |
|
||||
target="${IMAGE}@${INDEX_DIGEST}"
|
||||
echo "Signing ${target} (signatures -> ${COSIGN_REPOSITORY})"
|
||||
for attempt in 1 2 3; do
|
||||
if cosign sign --yes "${target}"; then
|
||||
exit 0
|
||||
fi
|
||||
if [ "$attempt" -eq 3 ]; then
|
||||
echo "ERROR: cosign signing failed after ${attempt} attempts" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep_for=$((attempt * 10))
|
||||
echo "cosign signing failed on attempt ${attempt}; retrying in ${sleep_for}s" >&2
|
||||
sleep "$sleep_for"
|
||||
done
|
||||
|
||||
promote-latest:
|
||||
# Re-push the :latest tag pointing at the root variant *after* every
|
||||
# variant manifest job has finished, so GHCR's package version
|
||||
# listing (sorted by created_at) shows the root image with :latest
|
||||
# at the top instead of whichever variant happened to finish last.
|
||||
needs: docker-manifest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Normalize image name
|
||||
id: image-name
|
||||
run: |
|
||||
image_name="$(printf '%s' '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')"
|
||||
printf 'image_name=%s\n' "$image_name" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Determine image version
|
||||
id: version
|
||||
env:
|
||||
MANUAL_VERSION: ${{ inputs.version || github.event.inputs.version }}
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
version="${MANUAL_VERSION#v}"
|
||||
if [ -z "$version" ] && [ -n "$RELEASE_TAG" ]; then
|
||||
version="${RELEASE_TAG#v}"
|
||||
fi
|
||||
printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Re-tag root image as :latest
|
||||
if: steps.version.outputs.version != ''
|
||||
env:
|
||||
IMAGE: ${{ env.REGISTRY }}/${{ steps.image-name.outputs.image_name }}
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
# Add a unique annotation so the resulting image index manifest gets
|
||||
# a new digest, which makes GHCR record a fresh package version with
|
||||
# current timestamp (otherwise the existing root manifest is reused
|
||||
# and stays where it was in the version listing).
|
||||
promoted_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
docker buildx imagetools create \
|
||||
--annotation "index:io.headroom.promoted-at=${promoted_at}" \
|
||||
--tag "${IMAGE}:latest" \
|
||||
"${IMAGE}:${VERSION}"
|
||||
Reference in New Issue
Block a user