headroom/.github/workflows/docker.yml
dependabot[bot] 920a93bc2a
ci: bump actions/setup-python from 5 to 7 (#3300)
Bumps [actions/setup-python](https://github.com/actions/setup-python)
from 5 to 7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/setup-python/releases">actions/setup-python's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>What's Changed</h2>
<h3>Enhancements</h3>
<ul>
<li>Migrate to ESM and upgrade dependencies by <a
href="https://github.com/priyagupta108"><code>@​priyagupta108</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1330">actions/setup-python#1330</a></li>
<li>Pin SHA commits and update docs with latest versions by <a
href="https://github.com/HarithaVattikuti"><code>@​HarithaVattikuti</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1338">actions/setup-python#1338</a></li>
<li>Remove the pip-install input by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-python/pull/1336">actions/setup-python#1336</a></li>
</ul>
<h3>Bug Fix</h3>
<ul>
<li>Fix to Classify stderr warning messages as warnings instead of
errors in annotations by <a
href="https://github.com/lmvysakh"><code>@​lmvysakh</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1335">actions/setup-python#1335</a></li>
<li>Validate and retry manifest fetch to prevent silent failures by <a
href="https://github.com/priyagupta108"><code>@​priyagupta108</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1332">actions/setup-python#1332</a></li>
</ul>
<h3>Dependency Upgrade</h3>
<ul>
<li>Bump certifi from 2020.6.20 to 2024.7.4 in
/<strong>tests</strong>/data by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1328">actions/setup-python#1328</a></li>
<li>Remove EOL Python versions and Bumps numpy text fixture by <a
href="https://github.com/priya-kinthali"><code>@​priya-kinthali</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1333">actions/setup-python#1333</a></li>
<li>Upgrade <code>@​actions/cache</code> to 6.2.0 by <a
href="https://github.com/philip-gai"><code>@​philip-gai</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1337">actions/setup-python#1337</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/lmvysakh"><code>@​lmvysakh</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-python/pull/1335">actions/setup-python#1335</a></li>
<li><a
href="https://github.com/philip-gai"><code>@​philip-gai</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-python/pull/1337">actions/setup-python#1337</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-python/compare/v6...v7.0.0">https://github.com/actions/setup-python/compare/v6...v7.0.0</a></p>
<h2>v6.3.0</h2>
<h2>What's Changed</h2>
<h3>Enhancement</h3>
<ul>
<li>Add RHEL support and include Linux distro in cache keys by <a
href="https://github.com/priyagupta108"><code>@​priyagupta108</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1323">actions/setup-python#1323</a></li>
<li>Fix pip cache error handling on Windows by <a
href="https://github.com/priyagupta108"><code>@​priyagupta108</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1040">actions/setup-python#1040</a></li>
</ul>
<h3>Dependency update</h3>
<ul>
<li>Upgrade minimatch from 3.1.2 to 3.1.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1281">actions/setup-python#1281</a></li>
<li>Upgrade actions dependencies by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a>
with <a href="https://github.com/Copilot"><code>@​Copilot</code></a> in
<a
href="https://redirect.github.com/actions/setup-python/pull/1303">actions/setup-python#1303</a></li>
<li>Upgrade <code>@​actions/cache</code> to 5.1.0, log cache write
denied by <a
href="https://github.com/jasongin"><code>@​jasongin</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1324">actions/setup-python#1324</a></li>
<li>Upgrade dependency versions and test workflow configuration by <a
href="https://github.com/HarithaVattikuti"><code>@​HarithaVattikuti</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1322">actions/setup-python#1322</a></li>
</ul>
<h3>Documentation</h3>
<ul>
<li>Update advanced-usage.md by <a
href="https://github.com/Dunky-Z"><code>@​Dunky-Z</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/811">actions/setup-python#811</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a>
with <a href="https://github.com/Copilot"><code>@​Copilot</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-python/pull/1303">actions/setup-python#1303</a></li>
<li><a href="https://github.com/jasongin"><code>@​jasongin</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-python/pull/1324">actions/setup-python#1324</a></li>
<li><a href="https://github.com/Dunky-Z"><code>@​Dunky-Z</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-python/pull/811">actions/setup-python#811</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-python/compare/v6.2.0...v6.3.0">https://github.com/actions/setup-python/compare/v6.2.0...v6.3.0</a></p>
<h2>v6.2.0</h2>
<h2>What's Changed</h2>
<h3>Dependency Upgrades</h3>
<ul>
<li>Upgrade dependencies to Node 24 compatible versions by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1259">actions/setup-python#1259</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="5fda3b95a4"><code>5fda3b9</code></a>
Pin SHA commits and update docs with latest versions (<a
href="https://redirect.github.com/actions/setup-python/issues/1338">#1338</a>)</li>
<li><a
href="4ab7e95f05"><code>4ab7e95</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/setup-python/issues/1337">#1337</a>
from actions/philip-gai/bump-actions-cache-6-2-0</li>
<li><a
href="0f3a009f47"><code>0f3a009</code></a>
Remove the pip-install input (<a
href="https://redirect.github.com/actions/setup-python/issues/1336">#1336</a>)</li>
<li><a
href="f8cf4291c8"><code>f8cf429</code></a>
Migrate to ESM and upgrade dependencies (<a
href="https://redirect.github.com/actions/setup-python/issues/1330">#1330</a>)</li>
<li><a
href="54baeea5b3"><code>54baeea</code></a>
Validate and retry manifest fetch to prevent silent failures (<a
href="https://redirect.github.com/actions/setup-python/issues/1332">#1332</a>)</li>
<li><a
href="c7092773a3"><code>c709277</code></a>
Annotation code fix (<a
href="https://redirect.github.com/actions/setup-python/issues/1335">#1335</a>)</li>
<li><a
href="6849080452"><code>6849080</code></a>
remove EOL Python versions and Bumps numpy text fixture (<a
href="https://redirect.github.com/actions/setup-python/issues/1333">#1333</a>)</li>
<li><a
href="0903b469fb"><code>0903b46</code></a>
Bump certifi from 2020.6.20 to 2024.7.4 in /<strong>tests</strong>/data
(<a
href="https://redirect.github.com/actions/setup-python/issues/1328">#1328</a>)</li>
<li><a
href="ece7cb06ca"><code>ece7cb0</code></a>
Fix pip cache error handling on Windows. (<a
href="https://redirect.github.com/actions/setup-python/issues/1040">#1040</a>)</li>
<li><a
href="1d18d7af5f"><code>1d18d7a</code></a>
Update advanced-usage.md (<a
href="https://redirect.github.com/actions/setup-python/issues/811">#811</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/setup-python/compare/v5...v7">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-python&package-manager=github_actions&previous-version=5&new-version=7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-27 12:40:33 -05:00

432 lines
20 KiB
YAML

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]
# A merge spree pushes many commits to main; without this, each commit starts
# a full multi-arch image build and they pile up against the 20-job concurrency
# cap. Supersede all but the latest build for a given ref. cancel-in-progress is
# scoped to main only so a release tag's publish (its own ref) is never killed.
concurrency:
group: docker-${{ github.ref }}
cancel-in-progress: ${{ github.ref == 'refs/heads/main' }}
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@v7
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 uniquely identify the marker. The manifest job
# downloads both architecture artifacts by exact name; a glob such
# as `digests-code-*` would also match code-nonroot/code-slim.
# `root` substitutes the empty-string variant.
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
if: ${{ always() }}
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 amd64 digest for this variant
uses: actions/download-artifact@v8
with:
name: digests-${{ matrix.variant.name || 'root' }}-amd64
path: ${{ runner.temp }}/digests
- name: Download arm64 digest for this variant
uses: actions/download-artifact@v8
with:
name: digests-${{ matrix.variant.name || 'root' }}-arm64
path: ${{ runner.temp }}/digests
# 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 }}
# `latest=false` is load-bearing (#3150). The action defaults to
# `latest=auto`, which appends a bare `latest` for any semver
# release — and it logs `suffixLatest=false`, so the per-tag
# `suffix=` below never reaches it. Every one of the 8 variant
# cells therefore pushed `ghcr.io/.../headroom:latest`, and the
# last cell to finish won. At 0.36.0 that was `code-slim`, so
# `:latest` resolved to the distroless build, whose
# `import onnxruntime` segfaults on arm64 — `headroom deploy`
# crash-looped on Apple Silicon. `:latest` has exactly one
# writer: the root-cell promotion step at the end of this job.
flavor: |
latest=false
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
# Read by the bare-`latest` guard below. Via `env:` rather than
# inline `${{ }}` so the value is never spliced into the script.
VARIANT_NAME: ${{ matrix.variant.name }}
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_count="$(find "${DIGEST_DIR}" -maxdepth 1 -type f | wc -l)"
if [ "${digest_count}" -ne 2 ]; then
echo "ERROR: expected both architecture digests for variant '${{ matrix.variant.name || 'root' }}', found ${digest_count}" >&2
exit 1
fi
digest_refs=()
for f in "${DIGEST_DIR}"/*; do
digest="$(basename "$f")"
digest_refs+=("${IMAGE}@sha256:${digest}")
done
# Belt-and-braces for #3150: only the root cell may ever carry a
# bare `latest`. A suffixed variant reaching this point with one
# means the tag rules regressed, and shipping it would repoint
# `:latest` at a non-default image. Fail instead of publishing.
if [ -n "${VARIANT_NAME}" ] && jq -e '.tags[]? | select(endswith(":latest"))' \
<<< "${DOCKER_METADATA_OUTPUT_JSON}" >/dev/null 2>&1; then
echo "::error::variant '${VARIANT_NAME}' would publish a bare :latest tag" >&2
exit 1
fi
# 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
- name: Re-tag root image as :latest
if: steps.manifest.outputs.index_digest != '' && matrix.variant.name == '' && 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 GHCR records a fresh root package version.
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}"