Skip to content

Docker

Docker #722

Workflow file for this run

name: Docker
on:
push:
branches: [main]
tags: ['v*']
workflow_dispatch:
inputs:
sha:
description: "Commit SHA or ref to build (e.g. a release tag's commit). Lets you re-release a signed image from an exact source commit without moving the git tag. Leave blank to build the branch you dispatched from."
required: false
default: ''
image_tag:
description: 'Image tag to publish as (e.g. 0.10.5). Leave blank to tag with the branch name (or sha-<short> when a sha is given).'
required: false
default: ''
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
concurrency:
group: docker-${{ github.ref }}-${{ inputs.image_tag || inputs.sha || github.sha }}
cancel-in-progress: true
jobs:
# Build each architecture on its OWN native runner — free arm64 hosted
# runners (ubuntu-24.04-arm) are GA for public repos — in parallel, pushing
# each as an untagged per-arch image BY DIGEST. Native beats QEMU here: the
# image runs `bun install` plus two Vite/TanStack production builds, which are
# far too slow under arm64 emulation (and risk job timeouts).
build:
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
steps:
- name: Prepare platform pair
env:
PLATFORM: ${{ matrix.platform }}
run: echo "PLATFORM_PAIR=${PLATFORM//\//-}" >> "$GITHUB_ENV"
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
# On workflow_dispatch, build the requested sha/ref (re-release of an
# exact source commit); otherwise build the pushed ref as usual.
ref: ${{ github.event.inputs.sha || github.ref }}
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
- name: Resolve lowercase image ref
run: echo "IMAGE=${REGISTRY}/$(printf '%s' "$IMAGE_NAME" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: .
file: ./apps/web/Dockerfile
platforms: ${{ matrix.platform }}
# No tags here — the manifest list (and its tags) is assembled in the
# merge job; each leg pushes an untagged per-arch image by digest.
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
# Per-arch cache scope so the two runners don't clobber each other.
cache-from: type=gha,scope=${{ env.PLATFORM_PAIR }}
cache-to: type=gha,mode=max,scope=${{ env.PLATFORM_PAIR }}
- name: Export digest
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
mkdir -p /tmp/digests
touch "/tmp/digests/${DIGEST#sha256:}"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
# Assemble the multi-arch manifest list (OCI image index) from the per-arch
# digests, apply the real tags, then keyless-sign the index digest.
merge:
runs-on: ubuntu-latest
needs: [build]
permissions:
contents: read
packages: write
id-token: write # keyless cosign signing via GitHub OIDC
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
- name: Resolve lowercase image ref
run: echo "IMAGE=${REGISTRY}/$(printf '%s' "$IMAGE_NAME" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
id: meta
with:
images: ${{ env.IMAGE }}
# The branch-name tag covers branch pushes AND manual dispatches that
# build the dispatched branch itself (no sha/image_tag override).
tags: |
type=ref,event=branch,enable=${{ github.event.inputs.sha == '' && github.event.inputs.image_tag == '' }}
type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=raw,value=${{ github.event.inputs.image_tag }},enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.image_tag != '' }}
type=sha,enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.image_tag == '' && github.event.inputs.sha != '' }}
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf "${IMAGE}@sha256:%s " *)
- name: Capture image index digest
id: index
env:
VERSION: ${{ steps.meta.outputs.version }}
run: |
DIGEST=$(docker buildx imagetools inspect "${IMAGE}:${VERSION}" --format '{{json .Manifest.Digest}}' | tr -d '"')
echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"
- name: Install cosign
uses: sigstore/cosign-installer@f713795cb21599bc4e5c4b58cbad1da852d7eeb9 # v3
# Keyless cosign signature over the pushed index digest, required by the
# cluster's Kyverno verify-images ClusterPolicy (keyless, GitHub OIDC
# issuer + QuackbackIO workflow subject). --recursive signs the OCI index
# AND each per-arch child manifest, so policy can verify either the index
# or a resolved per-arch digest.
- name: Sign image index (keyless)
env:
IMAGE_DIGEST: ${{ steps.index.outputs.digest }}
run: cosign sign --yes --recursive "${IMAGE}@${IMAGE_DIGEST}"