Files
open-design/.github/workflows/docker-image.yml

234 lines
9.5 KiB
YAML

name: Docker image
# Standalone Docker packaging line (not part of core merge validation in ci.yml).
#
# - pull_request / standalone workflow_dispatch: smoke build only (push: false, amd64)
# - workflow_call from release-stable-docker: idempotent version-tag reconcile
on:
workflow_call:
inputs:
ref:
description: "Git ref to build when invoked as a reusable workflow."
required: false
type: string
default: ""
release_version:
description: "Stable Docker tag to publish when invoked from release automation."
required: false
type: string
default: ""
expected_revision:
description: "Authoritative stable source commit from versioned release metadata."
required: false
type: string
default: ""
outputs:
reconciliation:
description: "Version-tag reconciliation action: noop or published."
value: ${{ jobs.build-and-push.outputs.reconciliation }}
version_digest:
description: "Verified multi-arch digest for the immutable version tag."
value: ${{ jobs.build-and-push.outputs.version_digest }}
pull_request:
paths:
- "deploy/**"
- ".dockerignore"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- ".github/scripts/release/stable-docker.py"
- ".github/workflows/docker-image.yml"
- ".github/workflows/release-stable-docker.yml"
workflow_dispatch:
jobs:
build-and-push:
runs-on: ubuntu-latest
outputs:
reconciliation: ${{ steps.verify.outputs.reconciliation }}
version_digest: ${{ steps.verify.outputs.digest }}
permissions:
contents: read
packages: write
steps:
- name: Check out trusted Docker control plane
uses: actions/checkout@v4
with:
ref: ${{ github.event.repository.default_branch || 'main' }}
path: .docker-control
- name: Check out image source
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.sha }}
path: source
- name: Resolve publish mode
id: mode
shell: bash
env:
EXPECTED_REVISION: ${{ inputs.expected_revision }}
RELEASE_VERSION: ${{ inputs.release_version }}
run: |
set -euo pipefail
if { [ -n "${RELEASE_VERSION:-}" ] && [ -z "${EXPECTED_REVISION:-}" ]; } || \
{ [ -z "${RELEASE_VERSION:-}" ] && [ -n "${EXPECTED_REVISION:-}" ]; }; then
echo "::error::release_version and expected_revision must be supplied together"
exit 1
fi
if [ -n "${RELEASE_VERSION:-}" ]; then
echo "publish=true" >> "$GITHUB_OUTPUT"
echo "platforms=linux/amd64,linux/arm64" >> "$GITHUB_OUTPUT"
else
# pull_request and standalone workflow_dispatch: smoke only
echo "publish=false" >> "$GITHUB_OUTPUT"
echo "platforms=linux/amd64" >> "$GITHUB_OUTPUT"
fi
- name: Set up QEMU
if: ${{ steps.mode.outputs.publish == 'true' }}
uses: docker/setup-qemu-action@v3
with:
platforms: linux/amd64,linux/arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
# Prefer Google's public Docker Hub cache, with upstream fallback.
buildkitd-config-inline: |
[registry."docker.io"]
mirrors = ["mirror.gcr.io"]
- name: Log in to GitHub Container Registry
if: ${{ steps.mode.outputs.publish == 'true' }}
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Inspect immutable version tag
id: existing
if: ${{ steps.mode.outputs.publish == 'true' }}
shell: bash
env:
EXPECTED_REVISION: ${{ inputs.expected_revision }}
IMAGE: ghcr.io/${{ github.repository_owner }}/od:${{ inputs.release_version }}
RELEASE_VERSION: ${{ inputs.release_version }}
run: |
set -euo pipefail
inspection="$RUNNER_TEMP/stable-docker-existing.json"
error="$RUNNER_TEMP/stable-docker-existing.err"
if docker buildx imagetools inspect "$IMAGE" --format '{{json .}}' > "$inspection" 2> "$error"; then
python3 .docker-control/.github/scripts/release/stable-docker.py image-state \
--inspection "$inspection" \
--expected-revision "$EXPECTED_REVISION" \
--expected-version "$RELEASE_VERSION" \
--github-output "$GITHUB_OUTPUT"
elif grep -Eqi 'manifest unknown|not found|does not exist' "$error"; then
{
echo "state=missing"
echo "digest="
echo "missing_platforms=linux/amd64,linux/arm64"
} >> "$GITHUB_OUTPUT"
else
cat "$error" >&2
echo "::error::could not determine whether $IMAGE already exists"
exit 1
fi
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/od
# spec §15.1 tag scheme:
# - workflow_call (stable) → immutable :<release_version> only
# - PR / manual validate → local-only sha tags (only when not publishing)
tags: |
type=raw,value=${{ inputs.release_version }},enable=${{ inputs.release_version != '' }}
type=sha,prefix=pr-${{ github.event.pull_request.number }}-sha-,format=short,enable=${{ steps.mode.outputs.publish != 'true' && github.event_name == 'pull_request' }}
type=sha,prefix=manual-sha-,format=short,enable=${{ steps.mode.outputs.publish != 'true' && github.event_name == 'workflow_dispatch' }}
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ inputs.expected_revision || github.sha }}
org.opencontainers.image.version=${{ inputs.release_version || github.sha }}
- name: Build and push
if: ${{ steps.mode.outputs.publish != 'true' || steps.existing.outputs.state != 'complete' }}
uses: docker/build-push-action@v6
with:
context: source
file: source/deploy/Dockerfile
platforms: ${{ steps.mode.outputs.platforms }}
push: ${{ steps.mode.outputs.publish == 'true' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
annotations: |
index:org.opencontainers.image.revision=${{ inputs.expected_revision || github.sha }}
index:org.opencontainers.image.version=${{ inputs.release_version || github.sha }}
# The in-tree Dockerfile uses node:24-alpine + apk for build
# tooling; we keep that default so the workflow doesn't drift
# from local builds. Spec §15.1 nominates bookworm-slim as
# the canonical base; switching is a follow-up that needs
# the Dockerfile's apk lines re-cast for apt.
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Verify public GHCR pull access
id: verify
if: ${{ steps.mode.outputs.publish == 'true' }}
shell: bash
env:
EXPECTED_REVISION: ${{ inputs.expected_revision }}
EXISTING_STATE: ${{ steps.existing.outputs.state }}
IMAGE: ghcr.io/${{ github.repository_owner }}/od:${{ inputs.release_version }}
RELEASE_VERSION: ${{ inputs.release_version }}
run: |
set -euo pipefail
docker logout ghcr.io >/dev/null 2>&1 || true
inspection="$RUNNER_TEMP/stable-docker-verified.json"
verified=false
for attempt in 1 2 3 4 5; do
if docker buildx imagetools inspect "$IMAGE" --format '{{json .}}' > "$inspection"; then
verified=true
break
fi
echo "public GHCR verification attempt $attempt failed; retrying"
sleep 15
done
if [ "$verified" != "true" ]; then
printf '%s\n' \
"Published image is not anonymously pullable: $IMAGE" \
"" \
"Make the GHCR package public before cutting a public Docker release:" \
"GitHub organization -> Packages -> od -> Package settings -> Change visibility -> Public." >&2
exit 1
fi
python3 .docker-control/.github/scripts/release/stable-docker.py image-state \
--inspection "$inspection" \
--expected-revision "$EXPECTED_REVISION" \
--expected-version "$RELEASE_VERSION" \
--github-output "$GITHUB_OUTPUT"
if [ "${EXISTING_STATE:-}" = "complete" ]; then
echo "reconciliation=noop" >> "$GITHUB_OUTPUT"
else
echo "reconciliation=published" >> "$GITHUB_OUTPUT"
fi
- name: Publish reconciliation summary
if: ${{ steps.mode.outputs.publish == 'true' }}
env:
DIGEST: ${{ steps.verify.outputs.digest }}
IMAGE: ghcr.io/${{ github.repository_owner }}/od:${{ inputs.release_version }}
RECONCILIATION: ${{ steps.verify.outputs.reconciliation }}
run: |
{
echo "## Stable Docker version tag"
echo
echo "- Image: \`$IMAGE\`"
echo "- Action: \`$RECONCILIATION\`"
echo "- Digest: \`$DIGEST\`"
} >> "$GITHUB_STEP_SUMMARY"