035e71010d
Every publish now also pushes an immutable per-publish tag alongside the mutable one, named after the snapshot date and commit (e.g. `22-bookworm-20260812-45444a7`), so previously published digests stay tag-referenced after republishes. Shipped CLI releases pin those digests, so they must remain resolvable indefinitely. Merging triggers a republish; the fresh tag-protected digests will then be pinned by #4602 before it merges.
273 lines
12 KiB
YAML
273 lines
12 KiB
YAML
name: 🐳 Deploy base images
|
|
|
|
# Publishes the deploy base images (see base-images/README.md) to Docker Hub.
|
|
# Tags are mutable and rebuilt in place; the CLI pins digests, so consumers
|
|
# only move when a release bumps its pins.
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
debian_snapshot:
|
|
description: "Debian snapshot timestamp (YYYYMMDDTHHMMSSZ). Defaults to yesterday 00:00 UTC."
|
|
required: false
|
|
type: string
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
- "base-images/**"
|
|
- ".github/workflows/base-images.yml"
|
|
pull_request:
|
|
paths:
|
|
- "base-images/**"
|
|
- ".github/workflows/base-images.yml"
|
|
|
|
concurrency:
|
|
group: base-images-${{ github.ref }}
|
|
cancel-in-progress: false
|
|
|
|
permissions: {}
|
|
|
|
jobs:
|
|
setup:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
permissions:
|
|
contents: read
|
|
outputs:
|
|
images: ${{ steps.config.outputs.images }}
|
|
packages: ${{ steps.config.outputs.packages }}
|
|
build_packages: ${{ steps.config.outputs.build_packages }}
|
|
suite: ${{ steps.config.outputs.suite }}
|
|
snapshot: ${{ steps.config.outputs.snapshot }}
|
|
publish_id: ${{ steps.config.outputs.publish_id }}
|
|
source_date_epoch: ${{ steps.config.outputs.source_date_epoch }}
|
|
push: ${{ steps.config.outputs.push }}
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Read image matrix and resolve snapshot
|
|
id: config
|
|
env:
|
|
SNAPSHOT_INPUT: ${{ inputs.debian_snapshot }}
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
REF: ${{ github.ref }}
|
|
SHA: ${{ github.sha }}
|
|
run: |
|
|
PACKAGES="$(jq -er '.packages' base-images/images.json)"
|
|
BUILD_PACKAGES="$(jq -er '.buildPackages' base-images/images.json)"
|
|
SUITE="$(jq -er '.suite' base-images/images.json)"
|
|
|
|
# Values land in build args and shell lines; keep them boring.
|
|
# NUL-delimited whole-record match so multi-line values can't sneak through
|
|
printf '%s\0' "$PACKAGES" | grep -zqxE '[a-z0-9][a-z0-9 .+:=~-]*' || { echo "invalid packages value"; exit 1; }
|
|
printf '%s\0' "$BUILD_PACKAGES" | grep -zqxE '[a-z0-9][a-z0-9 .+:=~-]*' || { echo "invalid buildPackages value"; exit 1; }
|
|
printf '%s\0' "$SUITE" | grep -zqxE '[a-z]+' || { echo "invalid suite value"; exit 1; }
|
|
jq -e '.images | length > 0 and all((.repo | test("^[a-z0-9-]+$")) and (.tag | test("^[a-z0-9.-]+$")) and (.base | test("^[a-zA-Z0-9./:@-]+$")))' base-images/images.json > /dev/null \
|
|
|| { echo "invalid images entries"; exit 1; }
|
|
|
|
SNAPSHOT="$SNAPSHOT_INPUT"
|
|
if [ -z "$SNAPSHOT" ]; then
|
|
SNAPSHOT="$(date -u -d yesterday +%Y%m%dT000000Z)"
|
|
fi
|
|
printf '%s\0' "$SNAPSHOT" | grep -zqxE '[0-9]{8}T[0-9]{6}Z' || { echo "invalid debian_snapshot: $SNAPSHOT"; exit 1; }
|
|
|
|
# Snapshot-derived timestamps: reproducible, with a real created date
|
|
EPOCH="$(date -u -d "${SNAPSHOT:0:4}-${SNAPSHOT:4:2}-${SNAPSHOT:6:2} ${SNAPSHOT:9:2}:${SNAPSHOT:11:2}:${SNAPSHOT:13:2}Z" +%s)"
|
|
# Future snapshots resolve to "latest" and break mtime normalization
|
|
[ "$EPOCH" -le "$(date -u +%s)" ] || { echo "debian_snapshot is in the future: $SNAPSHOT"; exit 1; }
|
|
|
|
# Pull requests and branch dispatches build without pushing
|
|
if [ "$EVENT_NAME" = "pull_request" ] || [ "$REF" != "refs/heads/main" ]; then
|
|
PUSH=false
|
|
else
|
|
PUSH=true
|
|
fi
|
|
|
|
{
|
|
echo "images=$(jq -c '.images' base-images/images.json)"
|
|
echo "packages=$PACKAGES"
|
|
echo "build_packages=$BUILD_PACKAGES"
|
|
echo "suite=$SUITE"
|
|
echo "snapshot=$SNAPSHOT"
|
|
echo "publish_id=${SNAPSHOT:0:8}-${SNAPSHOT:9:6}-${SHA:0:7}"
|
|
echo "source_date_epoch=$EPOCH"
|
|
echo "push=$PUSH"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
publish:
|
|
needs: setup
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 60
|
|
permissions:
|
|
contents: read
|
|
id-token: write
|
|
attestations: write
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
image: ${{ fromJSON(needs.setup.outputs.images) }}
|
|
env:
|
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
DOCKER_BUILD_SUMMARY: "false"
|
|
DOCKER_BUILD_RECORD_UPLOAD: "false"
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
persist-credentials: false
|
|
|
|
# Before any pull so rate limits are authenticated; fork PRs skip (no secrets)
|
|
- name: 🐳 Login to Docker Hub
|
|
if: env.DOCKERHUB_USERNAME != ''
|
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
|
with:
|
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
|
|
- name: 🐳 Set up QEMU
|
|
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
|
with:
|
|
image: docker.io/tonistiigi/binfmt:latest@sha256:400a4873b838d1b89194d982c45e5fb3cda4593fbfd7e08a02e76b03b21166f0
|
|
|
|
- name: 🐳 Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
|
|
|
# Build both targets before pushing either so the tag pair can't skew
|
|
- name: 🐳 Build both targets (no push)
|
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
|
with:
|
|
context: base-images
|
|
file: base-images/Dockerfile
|
|
target: build
|
|
platforms: linux/amd64,linux/arm64
|
|
provenance: false
|
|
outputs: type=image,push=false,rewrite-timestamp=true
|
|
tags: triggerdotdev/${{ matrix.image.repo }}:${{ matrix.image.tag }}-build
|
|
build-args: |
|
|
BASE_IMAGE=${{ matrix.image.base }}
|
|
DEBIAN_SNAPSHOT=${{ needs.setup.outputs.snapshot }}
|
|
DEBIAN_SUITE=${{ needs.setup.outputs.suite }}
|
|
PACKAGES=${{ needs.setup.outputs.packages }}
|
|
BUILD_PACKAGES=${{ needs.setup.outputs.build_packages }}
|
|
SOURCE_DATE_EPOCH=${{ needs.setup.outputs.source_date_epoch }}
|
|
labels: |
|
|
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
|
org.opencontainers.image.revision=${{ github.sha }}
|
|
dev.trigger.debian-snapshot=${{ needs.setup.outputs.snapshot }}
|
|
|
|
- name: 🐳 Push runtime image
|
|
id: build_runtime
|
|
if: needs.setup.outputs.push == 'true'
|
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
|
with:
|
|
context: base-images
|
|
file: base-images/Dockerfile
|
|
target: runtime
|
|
platforms: linux/amd64,linux/arm64
|
|
provenance: false
|
|
outputs: type=image,push=true,rewrite-timestamp=true
|
|
# The dated tag is immutable and keeps every published digest
|
|
# tag-referenced forever; shipped CLI releases pin these digests
|
|
tags: |
|
|
triggerdotdev/${{ matrix.image.repo }}:${{ matrix.image.tag }}
|
|
triggerdotdev/${{ matrix.image.repo }}:${{ matrix.image.tag }}-${{ needs.setup.outputs.publish_id }}
|
|
build-args: |
|
|
BASE_IMAGE=${{ matrix.image.base }}
|
|
DEBIAN_SNAPSHOT=${{ needs.setup.outputs.snapshot }}
|
|
DEBIAN_SUITE=${{ needs.setup.outputs.suite }}
|
|
PACKAGES=${{ needs.setup.outputs.packages }}
|
|
SOURCE_DATE_EPOCH=${{ needs.setup.outputs.source_date_epoch }}
|
|
labels: |
|
|
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
|
org.opencontainers.image.revision=${{ github.sha }}
|
|
dev.trigger.debian-snapshot=${{ needs.setup.outputs.snapshot }}
|
|
|
|
- name: 🐳 Push build-variant image
|
|
id: build_toolchain
|
|
if: needs.setup.outputs.push == 'true'
|
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
|
with:
|
|
context: base-images
|
|
file: base-images/Dockerfile
|
|
target: build
|
|
platforms: linux/amd64,linux/arm64
|
|
provenance: false
|
|
outputs: type=image,push=true,rewrite-timestamp=true
|
|
tags: |
|
|
triggerdotdev/${{ matrix.image.repo }}:${{ matrix.image.tag }}-build
|
|
triggerdotdev/${{ matrix.image.repo }}:${{ matrix.image.tag }}-build-${{ needs.setup.outputs.publish_id }}
|
|
build-args: |
|
|
BASE_IMAGE=${{ matrix.image.base }}
|
|
DEBIAN_SNAPSHOT=${{ needs.setup.outputs.snapshot }}
|
|
DEBIAN_SUITE=${{ needs.setup.outputs.suite }}
|
|
PACKAGES=${{ needs.setup.outputs.packages }}
|
|
BUILD_PACKAGES=${{ needs.setup.outputs.build_packages }}
|
|
SOURCE_DATE_EPOCH=${{ needs.setup.outputs.source_date_epoch }}
|
|
labels: |
|
|
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
|
org.opencontainers.image.revision=${{ github.sha }}
|
|
dev.trigger.debian-snapshot=${{ needs.setup.outputs.snapshot }}
|
|
|
|
# An auto-created private repo would publish green while customer pulls fail
|
|
- name: 🔎 Verify anonymous pullability
|
|
if: needs.setup.outputs.push == 'true'
|
|
env:
|
|
IMAGE_REPO: ${{ matrix.image.repo }}
|
|
RUNTIME_DIGEST: ${{ steps.build_runtime.outputs.digest }}
|
|
BUILD_DIGEST: ${{ steps.build_toolchain.outputs.digest }}
|
|
run: |
|
|
for digest in "$RUNTIME_DIGEST" "$BUILD_DIGEST"; do
|
|
TOKEN="$(curl -fsS --connect-timeout 10 --max-time 60 "https://auth.docker.io/token?service=registry.docker.io&scope=repository:triggerdotdev/$IMAGE_REPO:pull" | jq -r .token)"
|
|
curl -fsS --connect-timeout 10 --max-time 60 -o /dev/null -H "Authorization: Bearer $TOKEN" -H "Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json" "https://registry-1.docker.io/v2/triggerdotdev/$IMAGE_REPO/manifests/$digest" || { echo "triggerdotdev/$IMAGE_REPO@$digest is not anonymously pullable; is the repo private?"; exit 1; }
|
|
done
|
|
|
|
# Builds are reproducible, so re-running a red publish re-pushes the
|
|
# same digests and re-attests them
|
|
- name: 🔏 Attest runtime image provenance
|
|
if: needs.setup.outputs.push == 'true'
|
|
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
|
|
with:
|
|
subject-name: index.docker.io/triggerdotdev/${{ matrix.image.repo }}
|
|
subject-digest: ${{ steps.build_runtime.outputs.digest }}
|
|
push-to-registry: false
|
|
|
|
- name: 🔏 Attest build-variant image provenance
|
|
if: needs.setup.outputs.push == 'true'
|
|
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
|
|
with:
|
|
subject-name: index.docker.io/triggerdotdev/${{ matrix.image.repo }}
|
|
subject-digest: ${{ steps.build_toolchain.outputs.digest }}
|
|
push-to-registry: false
|
|
|
|
- name: 📋 Record digests
|
|
if: needs.setup.outputs.push == 'true'
|
|
env:
|
|
IMAGE_REPO: ${{ matrix.image.repo }}
|
|
IMAGE_TAG: ${{ matrix.image.tag }}
|
|
RUNTIME_DIGEST: ${{ steps.build_runtime.outputs.digest }}
|
|
BUILD_DIGEST: ${{ steps.build_toolchain.outputs.digest }}
|
|
SNAPSHOT: ${{ needs.setup.outputs.snapshot }}
|
|
run: |
|
|
{
|
|
echo "### triggerdotdev/$IMAGE_REPO:$IMAGE_TAG"
|
|
echo '```'
|
|
echo "runtime: $RUNTIME_DIGEST"
|
|
echo "build: $BUILD_DIGEST"
|
|
echo "debian snapshot: $SNAPSHOT"
|
|
echo '```'
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
results:
|
|
needs: [publish]
|
|
if: always()
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
permissions: {}
|
|
steps:
|
|
- name: Fail if any image build failed
|
|
env:
|
|
RESULT: ${{ needs.publish.result }}
|
|
run: |
|
|
[ "$RESULT" = "success" ] || { echo "one or more image builds failed: $RESULT"; exit 1; }
|