Skip to content

🐳 Docker Multi-Platform Build #17

🐳 Docker Multi-Platform Build

🐳 Docker Multi-Platform Build #17

Workflow file for this run

---
# Continuous Delivery (Multi-Platform Docker Build)
# Purpose: Builds and pushes multi-platform Docker images for Alpine, Debian, and Rocky variants.
# Trigger: Pushes to core branches, release tags, daily schedule, and manual dispatch.
# Permissions:
# - contents: read (Required for repository checkout).
# - packages: write (Required for GHCR push).
# - id-token: write (Required for secure OIDC authentication).
# - security-events: write (Required for SARIF upload from Trivy scans).
name: "🐳 Docker Multi-Platform Build"
"on":
push:
branches:
- "dev"
tags:
# Release-Please generated tags for Alpine (primary format)
- "alpine-v*"
# Release-Please generated tags for Debian (primary format)
- "debian-v*"
# Release-Please generated tags for Rocky (primary format)
- "rocky-v*"
# Legacy semantic version tags (backward compatibility)
- "[0-9]+.[0-9]+.[0-9]+"
- "v[0-9]+.[0-9]+.[0-9]+"
- "V[0-9]+.[0-9]+.[0-9]+"
- "alpine-*.*.*"
- "debian-*.*.*"
- "rocky-*.*.*"
- "[0-9]+.[0-9]+"
- "v[0-9]+.[0-9]+"
- "V[0-9]+.[0-9]+"
- "alpine-*.*"
- "debian-*.*"
- "rocky-*.*"
- "[0-9]+"
- "v[0-9]+"
- "V[0-9]+"
- "alpine-*"
- "debian-*"
- "rocky-*"
schedule:
# Automatically run every day at 17:00 UTC
- cron: "0 17 * * *"
workflow_dispatch:
inputs:
variant:
description: "Distribution variant to build"
required: false
type: choice
options:
- "all"
- "alpine"
- "debian"
- "rocky"
default: "all"
push_images:
description: "Push images to registries"
required: false
type: boolean
default: true
permissions:
contents: read
jobs:
buildx:
name: "πŸš€ Build & Deliver (${{ matrix.variant }})"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- variant: alpine
version: "3.24.0"
context: docker/alpine
platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/riscv64,linux/s390x
is_latest: false
package_manager: apk
- variant: debian
version: "13.5.0"
context: docker/debian
platforms: linux/386,linux/amd64,linux/arm/v5,linux/arm/v7,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x
is_latest: true
package_manager: apt-get
- variant: rocky
version: "10.2.0"
context: docker/rocky
platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x
is_latest: false
package_manager: dnf
concurrency:
group: docker-${{ github.workflow }}-${{ github.event_name == 'schedule' && 'nightly' || github.ref }}-${{ matrix.variant }}
cancel-in-progress: ${{ github.event_name != 'schedule' }}
permissions:
contents: read
packages: write
id-token: write
security-events: write
timeout-minutes: 60
env:
TRIVY_CACHE_DIR: .trivycache
BUILD_START_TIME: ${{ github.event.repository.updated_at }}
steps:
- name: "🎯 Check Variant Filter (workflow_dispatch)"
id: variant-filter
run: |
# For workflow_dispatch, check if this matrix variant should run
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
if [ "${{ inputs.variant }}" != "all" ] && [ "${{ inputs.variant }}" != "${{ matrix.variant }}" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "β†’ Skipping ${{ matrix.variant }} (user selected: ${{ inputs.variant }})"
exit 0
fi
fi
# For tag pushes, check if tag matches this variant
if [ "${{ github.event_name }}" = "push" ]; then
case "${{ github.ref }}" in
refs/tags/*)
tag_name="${{ github.ref }}"
tag_name="${tag_name#refs/tags/}"
case "${tag_name}" in
${{ matrix.variant }}-*)
# Tag matches variant, continue
;;
*)
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "β†’ Skipping ${{ matrix.variant }} (tag ${tag_name} does not match)"
exit 0
;;
esac
;;
esac
fi
# For release events, check if release tag matches this variant
if [ "${{ github.event_name }}" = "release" ]; then
tag_name="${{ github.event.release.tag_name }}"
case "${tag_name}" in
${{ matrix.variant }}-*)
# Release tag matches variant, continue
;;
*)
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "β†’ Skipping ${{ matrix.variant }} (release tag ${tag_name} does not match)"
exit 0
;;
esac
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "β†’ Building ${{ matrix.variant }} ${{ matrix.version }}"
- name: "⏱️ Record Build Start Time"
if: steps.variant-filter.outputs.skip != 'true'
id: start-time
run: |
echo "timestamp=$(date -u +%s)" >> "$GITHUB_OUTPUT"
echo "datetime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
- name: "πŸ”’ Harden Runner (Security Egress Audit)"
if: steps.variant-filter.outputs.skip != 'true'
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: block
allowed-endpoints: >
api.github.com:443
*.githubusercontent.com:443
github.com:443
mise.run:443
install.mise.jdx.dev:443
*.mise.jdx.dev:443
packages.microsoft.com:443
*.ubuntu.com:80
*.ubuntu.com:443
*.debian.org:80
*.debian.org:443
*.rockylinux.org:443
*.centos.org:443
*.redhat.com:443
dl-cdn.alpinelinux.org:443
registry.npmjs.org:443
registry.yarnpkg.com:443
pypi.org:443
files.pythonhosted.org:443
proxy.golang.org:443
sum.golang.org:443
index.crates.io:443
static.rust-lang.org:443
packagist.org:443
repo.maven.apache.org:443
rubygems.org:443
registry.terraform.io:443
formulae.brew.sh:443
repo.yarnpkg.com:443
mise.jdx.dev:443
ghcr.io:443
pkg-containers.githubusercontent.com:443
public.ecr.aws:443
production.cloudflare.docker.com:443
*.gcr.io:443
*.pkg.dev:443
*.quay.io:443
*.dkr.ecr.*.amazonaws.com:443
*.azurecr.io:443
osv-vulnerabilities.storage.googleapis.com:443
api.osv.dev:443
get.trivy.dev:443
*.aquasecurity.github.io:443
*.sigstore.dev:443
- name: "🧹 Optimize Runner Disk Space"
if: steps.variant-filter.outputs.skip != 'true'
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: false
- name: "πŸ“‚ Checkout Repository Code"
if: steps.variant-filter.outputs.skip != 'true'
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
token: ${{ secrets.WORKFLOW_SECRET || secrets.GITHUB_TOKEN }}
persist-credentials: false
- name: "⚑ Cache Trivy Database"
if: steps.variant-filter.outputs.skip != 'true'
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: .trivycache
key: ${{ runner.os }}-trivy-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-trivy-
- name: "πŸ—οΈ Configure QEMU Emulation"
if: steps.variant-filter.outputs.skip != 'true'
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
- name: "πŸ› οΈ Configure Docker Buildx"
if: steps.variant-filter.outputs.skip != 'true'
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
with:
buildkitd-config: .github/buildkitd.toml
- name: "πŸ”‘ Initialize DockerHub Registry Session"
if: steps.variant-filter.outputs.skip != 'true'
id: login-dockerhub
continue-on-error: true
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: "πŸ”‘ Initialize Quay.io Registry Session"
if: steps.variant-filter.outputs.skip != 'true'
id: login-quay
continue-on-error: true
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_ROBOT_TOKEN }}
- name: "πŸ”‘ Initialize GitHub Container Registry Session"
if: steps.variant-filter.outputs.skip != 'true'
id: login-ghcr
continue-on-error: true
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: "⚠️ Check Registry Login Status"
if: steps.variant-filter.outputs.skip != 'true'
run: |
echo "β†’ Registry login status:"
echo " DockerHub: ${{ steps.login-dockerhub.outcome }}"
echo " Quay.io: ${{ steps.login-quay.outcome }}"
echo " GHCR: ${{ steps.login-ghcr.outcome }}"
if [ "${{ steps.login-dockerhub.outcome }}" != "success" ] && \
[ "${{ steps.login-quay.outcome }}" != "success" ] && \
[ "${{ steps.login-ghcr.outcome }}" != "success" ]; then
echo "::error::All registry logins failed. Cannot proceed."
exit 1
fi
- name: "🏷️ Generate Docker Metadata"
if: steps.variant-filter.outputs.skip != 'true'
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
with:
images: |
name=snowdreamtech/python,enable=true
name=ghcr.io/snowdreamtech/python,enable=true
name=quay.io/snowdreamtech/python,enable=true
flavor: |
latest=false
prefix=
suffix=
tags: |
# Branch builds (non-tag pushes) - variant-suffixed branch tag
type=ref,enable=${{ github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/') }},priority=600,prefix=,suffix=-${{ matrix.variant }},event=branch
# Main/master branch - variant-suffixed tag
type=raw,enable=${{ contains(fromJSON('["refs/heads/main", "refs/heads/master"]'), github.ref) && github.event_name != 'schedule' }},priority=200,prefix=,suffix=-${{ matrix.variant }},value=latest
# Main/master branch - global latest tag (only for is_latest variant, no suffix)
type=raw,enable=${{ matrix.is_latest && contains(fromJSON('["refs/heads/main", "refs/heads/master"]'), github.ref) && github.event_name != 'schedule' }},priority=200,prefix=,suffix=,value=latest
# Tag builds - variant-suffixed tag
type=raw,enable=${{ startsWith(github.ref, format('refs/tags/{0}-', matrix.variant)) }},priority=200,prefix=,suffix=-${{ matrix.variant }},value=latest
# Tag builds - global latest tag (only for is_latest variant, no suffix)
type=raw,enable=${{ matrix.is_latest && startsWith(github.ref, format('refs/tags/{0}-', matrix.variant)) }},priority=200,prefix=,suffix=,value=latest
# Nightly builds - variant-suffixed
type=schedule,enable=true,priority=1000,prefix=,suffix=-${{ matrix.variant }},pattern=nightly
# Nightly builds - global nightly tag (only for is_latest variant, no suffix)
type=schedule,enable=${{ matrix.is_latest }},priority=1000,prefix=,suffix=,pattern=nightly
# Date-based tags for traceability (YYYYMMDD) - variant-suffixed
type=schedule,enable=true,priority=900,prefix=,suffix=-${{ matrix.variant }},pattern={{date 'YYYYMMDD'}}
# Date-based tags - global tag (only for is_latest variant, no suffix)
type=schedule,enable=${{ matrix.is_latest }},priority=900,prefix=,suffix=,pattern={{date 'YYYYMMDD'}}
# Semantic version tags from Release Please (variant-x.y.z) - variant-suffixed
type=match,enable=${{ startsWith(github.ref, format('refs/tags/{0}-', matrix.variant)) }},priority=800,prefix=,suffix=-${{ matrix.variant }},pattern=${{ matrix.variant }}-v?(\d+\.\d+\.\d+),group=1
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
- name: "πŸš€ Orchestrate Build & Delivery Suite"
id: build
if: steps.variant-filter.outputs.skip != 'true'
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: ${{ matrix.context }}
build-args: |
BUILDTIME=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
platforms: ${{ matrix.platforms }}
provenance: true
sbom: true
push: ${{ (github.event_name == 'workflow_dispatch' && inputs.push_images) || (github.event_name != 'workflow_dispatch' && (contains(fromJSON('["refs/heads/main", "refs/heads/dev"]'), github.ref) || startsWith(github.ref, 'refs/tags/') || github.event_name == 'schedule')) }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
annotations: ${{ steps.meta.outputs.annotations }}
cache-from: |
type=gha,scope=${{ github.ref_name }}-${{ matrix.variant }}
type=gha,scope=main-${{ matrix.variant }}
type=gha,scope=dev-${{ matrix.variant }}
cache-to: type=gha,mode=max,scope=${{ github.ref_name }}-${{ matrix.variant }}
- name: "πŸ“ˆ Analyze Build Cache Performance"
id: cache-stats
if: steps.variant-filter.outputs.skip != 'true' && always()
run: |
# Calculate build duration
start_time=${{ steps.start-time.outputs.timestamp }}
end_time=$(date -u +%s)
duration=$((end_time - start_time))
echo "duration_seconds=${duration}" >> "$GITHUB_OUTPUT"
echo "duration_minutes=$((duration / 60))" >> "$GITHUB_OUTPUT"
# Estimate cache efficiency (based on build time)
if [ "${duration}" -lt 300 ]; then
echo "cache_efficiency=High (< 5 min)" >> "$GITHUB_OUTPUT"
elif [ "${duration}" -lt 600 ]; then
echo "cache_efficiency=Medium (5-10 min)" >> "$GITHUB_OUTPUT"
else
echo "cache_efficiency=Low (> 10 min)" >> "$GITHUB_OUTPUT"
fi
echo "β†’ Build completed in ${duration} seconds ($((duration / 60)) minutes)"
- name: "πŸ” Verify Docker Manifest (Multi-Platform)"
id: manifest-check
if: steps.variant-filter.outputs.skip != 'true' && steps.build.conclusion == 'success' && steps.build.outputs.digest != ''
env:
IMAGE_DIGEST: ${{ steps.build.outputs.digest }}
run: |
# Get the first tag for inspection
first_tag=$(echo "${{ steps.meta.outputs.tags }}" | head -n 1)
echo "β†’ Inspecting manifest for: ${first_tag}"
# Inspect the manifest
if manifest=$(docker buildx imagetools inspect "${first_tag}" 2>&1); then
echo "${manifest}"
# Count platforms
platform_count=$(echo "${manifest}" | grep -c "Platform:" || echo "0")
echo "platform_count=${platform_count}" >> "$GITHUB_OUTPUT"
# Set expected count based on variant
case "${{ matrix.variant }}" in
alpine|debian)
expected_count=8
;;
rocky)
expected_count=4
;;
*)
expected_count=8
;;
esac
if [ "${platform_count}" -eq "${expected_count}" ]; then
echo "βœ“ All ${expected_count} platforms verified in manifest"
echo "status=success" >> "$GITHUB_OUTPUT"
else
echo "::warning title=Incomplete Manifest::Expected ${expected_count} platforms, found ${platform_count}"
echo "status=partial" >> "$GITHUB_OUTPUT"
fi
else
echo "::error title=Manifest Inspection Failed::Could not inspect image manifest"
echo "status=failed" >> "$GITHUB_OUTPUT"
fi
- name: "πŸš€ Smoke Test (Verify Image Functionality)"
if: steps.variant-filter.outputs.skip != 'true' && steps.build.conclusion == 'success'
env:
TAGS: ${{ steps.meta.outputs.tags }}
PACKAGE_MANAGER: ${{ matrix.package_manager }}
run: |
echo "β†’ Verifying multi-registry image availability..."
# Retry function
retry_pull() {
local tag="$1"
local max_attempts=3
local attempt=1
while [ $attempt -le $max_attempts ]; do
if docker pull "${tag}" 2>/dev/null; then
return 0
fi
echo " Attempt $attempt/$max_attempts failed, retrying..."
attempt=$((attempt + 1))
sleep 2
done
return 1
}
# Comprehensive smoke test function
run_smoke_test() {
local tag="$1"
local registry_name="$2"
echo " β†’ Running comprehensive smoke test..."
# Build package manager check based on variant
case "${PACKAGE_MANAGER}" in
apk)
pkg_check='command -v apk >/dev/null && apk info busybox >/dev/null 2>&1'
;;
apt-get)
pkg_check='command -v apt-get >/dev/null && dpkg -l bash >/dev/null 2>&1'
;;
dnf)
pkg_check='command -v dnf >/dev/null && rpm -q bash >/dev/null 2>&1'
;;
*)
pkg_check='echo "unknown"'
;;
esac
# Execute comprehensive test suite
if docker run --rm \
-e DEBUG=false \
-e TZ=Asia/Shanghai \
-e PUID=1000 \
-e PGID=1000 \
"${tag}" \
sh -c "
echo ' βœ“ Entrypoint execution: OK' && \
printf ' βœ“ Timezone: ' && date '+%Z %z' && \
printf ' βœ“ User context: ' && id -un && \
printf ' βœ“ Package manager: ' && \
${pkg_check} && echo 'functional' && \
printf ' βœ“ App version: ' && python3 --version && \
echo ' βœ“ All checks passed'
"; then
return 0
else
echo " βœ— Smoke test failed for ${registry_name}"
return 1
fi
}
# Test images from each registry
# DockerHub (no prefix)
if [ "${{ steps.login-dockerhub.outcome }}" = "success" ]; then
dockerhub_tag=$(echo "${TAGS}" | grep -v '/' | head -n 1)
if [ -n "${dockerhub_tag}" ]; then
echo " Testing DockerHub: ${dockerhub_tag}"
if retry_pull "${dockerhub_tag}"; then
if run_smoke_test "${dockerhub_tag}" "DockerHub"; then
size=$(docker image inspect "${dockerhub_tag}" --format='{{.Size}}' | awk '{printf "%.2f MB", $1/1024/1024}')
echo " βœ“ DockerHub image verified (Size: ${size})"
fi
else
echo " βœ— DockerHub image pull failed after retries"
fi
fi
fi
# GHCR
if [ "${{ steps.login-ghcr.outcome }}" = "success" ]; then
ghcr_tag=$(echo "${TAGS}" | grep '^ghcr.io/' | head -n 1)
if [ -n "${ghcr_tag}" ]; then
echo " Testing GHCR: ${ghcr_tag}"
if retry_pull "${ghcr_tag}"; then
if run_smoke_test "${ghcr_tag}" "GHCR"; then
size=$(docker image inspect "${ghcr_tag}" --format='{{.Size}}' | awk '{printf "%.2f MB", $1/1024/1024}')
echo " βœ“ GHCR image verified (Size: ${size})"
fi
else
echo " βœ— GHCR image pull failed after retries"
fi
fi
fi
# Quay.io
if [ "${{ steps.login-quay.outcome }}" = "success" ]; then
quay_tag=$(echo "${TAGS}" | grep '^quay.io/' | head -n 1)
if [ -n "${quay_tag}" ]; then
echo " Testing Quay.io: ${quay_tag}"
if retry_pull "${quay_tag}"; then
if run_smoke_test "${quay_tag}" "Quay.io"; then
size=$(docker image inspect "${quay_tag}" --format='{{.Size}}' | awk '{printf "%.2f MB", $1/1024/1024}')
echo " βœ“ Quay.io image verified (Size: ${size})"
fi
else
echo " βœ— Quay.io image pull failed after retries"
fi
fi
fi
echo "βœ“ Registry verification completed"
- name: "πŸ•΅οΈ Scan Image for Vulnerabilities (Trivy)"
id: trivy
if: steps.variant-filter.outputs.skip != 'true' && steps.build.conclusion == 'success'
continue-on-error: true
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
env:
TRIVY_DB_REPOSITORY: "public.ecr.aws/aquasecurity/trivy-db"
TRIVY_CHECKS_BUNDLE_REPOSITORY: "public.ecr.aws/aquasecurity/trivy-checks"
with:
scan-type: "image"
image-ref: ${{ fromJSON(steps.meta.outputs.json).tags[0] }}
format: "sarif"
output: "trivy-image-${{ matrix.variant }}.sarif"
severity: "CRITICAL,HIGH,MEDIUM,LOW"
ignore-unfixed: true
exit-code: "0"
skip-dirs: "/tmp,/var/tmp,/var/cache"
skip-files: "/usr/local/bin/docker-entrypoint.sh"
- name: "πŸ“Š Parse Trivy Results"
id: trivy-summary
if: steps.variant-filter.outputs.skip != 'true' && steps.build.conclusion == 'success' && steps.trivy.outcome == 'success'
continue-on-error: true
run: |
if [ -f "trivy-image-${{ matrix.variant }}.sarif" ]; then
critical=$(jq '[.runs[].results[] | select(.level=="error")] | length' trivy-image-${{ matrix.variant }}.sarif)
high=$(jq '[.runs[].results[] | select(.level=="warning")] | length' trivy-image-${{ matrix.variant }}.sarif)
medium=$(jq '[.runs[].results[] | select(.level=="note")] | length' trivy-image-${{ matrix.variant }}.sarif)
{
echo "critical=${critical}"
echo "high=${high}"
echo "medium=${medium}"
} >> "$GITHUB_OUTPUT"
echo "βœ“ Parsed Trivy results: Critical=${critical}, High=${high}, Medium=${medium}"
else
echo "⚠️ SARIF file not found, skipping parse"
{
echo "critical=0"
echo "high=0"
echo "medium=0"
} >> "$GITHUB_OUTPUT"
fi
- name: "πŸ“€ Upload Image Security Scan (SARIF)"
if: steps.variant-filter.outputs.skip != 'true' && steps.build.conclusion == 'success' && steps.trivy.outcome == 'success'
continue-on-error: true
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
sarif_file: "trivy-image-${{ matrix.variant }}.sarif"
category: "trivy-image-python-${{ matrix.variant }}"
- name: "πŸ—‘οΈ Clean Up Build Artifacts"
if: steps.variant-filter.outputs.skip != 'true' && always()
run: |
# Remove SARIF files to save space
rm -f trivy-image-*.sarif
echo "βœ“ Cleaned up build artifacts"
- name: "⚠️ Check Vulnerability Threshold"
id: vuln-check
if: steps.variant-filter.outputs.skip != 'true' && steps.build.conclusion == 'success'
run: |
critical=${{ steps.trivy-summary.outputs.critical }}
high=${{ steps.trivy-summary.outputs.high }}
if [ "${critical}" -gt 0 ]; then
echo "::warning title=Critical Vulnerabilities Detected::Found ${critical} critical vulnerabilities in ${{ matrix.variant }} ${{ matrix.version }} image"
fi
if [ "${high}" -gt 5 ]; then
echo "::warning title=High Vulnerabilities Detected::Found ${high} high-severity vulnerabilities in ${{ matrix.variant }} ${{ matrix.version }} image"
fi
- name: "πŸ” Install Cosign Tool"
if: steps.variant-filter.outputs.skip != 'true' && steps.build.conclusion == 'success'
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: "πŸ–ŠοΈ Sign the Docker Image"
if: steps.variant-filter.outputs.skip != 'true' && steps.build.conclusion == 'success'
continue-on-error: true
env:
TAGS: ${{ steps.meta.outputs.tags }}
COSIGN_YES: true
BUILD_ID: ${{ github.run_id }}-${{ github.run_number }}
COMMIT_SHA: ${{ github.sha }}
run: |
echo "β†’ Signing images with Cosign (keyless OIDC + metadata)"
# Track successful signatures
declare -a signed_tags=()
declare -a failed_tags=()
# Sign all images sequentially to better track failures
for tag in ${TAGS}; do
echo " Signing: ${tag}"
if cosign sign --yes \
-a "build-id=${BUILD_ID}" \
-a "commit=${COMMIT_SHA}" \
-a "variant=${{ matrix.variant }}" \
-a "version=${{ matrix.version }}" \
-a "workflow=${GITHUB_WORKFLOW}" \
"${tag}" 2>&1 | sed 's/^/ /'; then
echo " βœ“ Signed: ${tag}"
signed_tags+=("${tag}")
else
echo " βœ— Failed to sign: ${tag}"
failed_tags+=("${tag}")
fi
done
echo ""
echo "β†’ Signature Summary:"
echo " Successful: ${#signed_tags[@]}/${#TAGS[@]}"
if [ ${#failed_tags[@]} -gt 0 ]; then
echo " Failed: ${failed_tags[*]}"
fi
# Only verify successfully signed images
if [ ${#signed_tags[@]} -gt 0 ]; then
echo ""
echo "β†’ Verifying signatures..."
for tag in "${signed_tags[@]}"; do
echo " Verifying: ${tag}"
if cosign verify \
--certificate-identity-regexp=".*" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
"${tag}" > /dev/null 2>&1; then
echo " βœ“ Verified: ${tag}"
else
echo " ⚠️ Verification failed for ${tag} (signature may not be propagated yet)"
fi
done
fi
echo "βœ“ Signing process completed (${#signed_tags[@]} successful, ${#failed_tags[@]} failed)"
- name: "πŸ“Š Generate Build Summary"
if: steps.variant-filter.outputs.skip != 'true' && always()
run: |
{
echo "## 🐳 Docker Build Summary"
echo ""
echo "**Variant:** ${{ matrix.variant }}"
echo "**Version:** ${{ matrix.version }}"
echo "**Build Status:** ${{ steps.build.conclusion }}"
echo "**Trigger:** ${{ github.event_name }}"
echo "**Ref:** \`${{ github.ref }}\`"
echo "**Workflow Run:** [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
echo ""
if [ "${{ steps.build.conclusion }}" = "success" ]; then
echo "### ⏱️ Build Performance"
echo ""
echo "- **Duration:** ${{ steps.cache-stats.outputs.duration_seconds }}s (${{ steps.cache-stats.outputs.duration_minutes }} min)"
echo "- **Cache Efficiency:** ${{ steps.cache-stats.outputs.cache_efficiency }}"
echo "- **Started:** ${{ steps.start-time.outputs.datetime }}"
echo ""
echo "### πŸ“¦ Published Images"
echo ""
echo '```'
echo "${{ steps.meta.outputs.tags }}"
echo '```'
echo ""
if [ -n "${{ steps.manifest-check.outputs.platform_count }}" ]; then
echo "### πŸ” Manifest Verification"
echo ""
# Set expected count based on variant
case "${{ matrix.variant }}" in
alpine|debian)
expected_count=8
;;
rocky)
expected_count=4
;;
*)
expected_count=8
;;
esac
echo "- **Platforms Verified:** ${{ steps.manifest-check.outputs.platform_count }}/${expected_count}"
echo "- **Status:** ${{ steps.manifest-check.outputs.status }}"
echo ""
fi
echo "### 🏷️ Image Labels"
echo ""
echo "- **Created:** ${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}"
echo "- **Version:** ${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}"
echo "- **Revision:** \`${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}\`"
echo ""
echo "### πŸ—οΈ Build Configuration"
echo ""
echo "- **Variant:** ${{ matrix.variant }}"
echo "- **OS Version:** ${{ matrix.version }}"
# Set platform count based on variant
case "${{ matrix.variant }}" in
alpine|debian)
echo "- **Platforms:** 8 architectures"
;;
rocky)
echo "- **Platforms:** 4 architectures"
;;
*)
echo "- **Platforms:** 8 architectures"
;;
esac
echo "- **Package Manager:** ${{ matrix.package_manager }}"
echo "- **Registries:** DockerHub (${{ steps.login-dockerhub.outcome }}), GHCR (${{ steps.login-ghcr.outcome }}), Quay.io (${{ steps.login-quay.outcome }})"
echo "- **Cache Sources:** Current branch + main + dev"
echo ""
echo "### πŸ” Security & Compliance"
echo ""
echo "- βœ… SBOM Generated (CycloneDX)"
echo "- βœ… Provenance Attestation (SLSA)"
echo "- βœ… Trivy Vulnerability Scan (optimized, ignore-unfixed)"
if [ -n "${{ steps.trivy-summary.outputs.critical }}" ]; then
echo " - πŸ”΄ Critical: ${{ steps.trivy-summary.outputs.critical }}"
echo " - 🟠 High: ${{ steps.trivy-summary.outputs.high }}"
echo " - 🟑 Medium: ${{ steps.trivy-summary.outputs.medium }}"
fi
echo "- βœ… Cosign Signature (keyless OIDC + metadata)"
echo "- βœ… Multi-registry Smoke Tests"
elif [ "${{ steps.build.conclusion }}" = "skipped" ]; then
echo "### ⏭️ Build Skipped"
echo ""
echo "This variant was not built because the filter did not match."
echo ""
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "**User selected:** ${{ inputs.variant }}"
else
case "${{ github.ref }}" in
refs/tags/*)
echo "**Tag:** \`${{ github.ref }}\`"
echo "**Expected pattern:** \`${{ matrix.variant }}-*\`"
;;
esac
fi
else
echo "### ❌ Build Failed"
echo ""
echo "The build process encountered an error. Please check the logs above for details."
echo ""
echo "**Common issues:**"
echo "- Network connectivity to registries"
echo "- Insufficient disk space"
echo "- Platform-specific build failures"
echo "- Registry authentication issues"
fi
} >> "$GITHUB_STEP_SUMMARY"
- name: "⚠️ Annotate Build Failure"
if: steps.variant-filter.outputs.skip != 'true' && failure()
run: |
echo "::error title=Docker Build Failed (${{ matrix.variant }} ${{ matrix.version }})::The multi-platform Docker build failed. Check the workflow logs for details."
# Detailed failure annotations
if [ "${{ steps.build.conclusion }}" = "failure" ]; then
echo "::error title=Build Step Failed::Docker build process failed for ${{ matrix.variant }} ${{ matrix.version }}"
fi
if [ "${{ steps.trivy.conclusion }}" = "failure" ]; then
echo "::error title=Security Scan Failed::Trivy vulnerability scan failed for ${{ matrix.variant }} ${{ matrix.version }}"
fi
if [ "${{ steps.login-dockerhub.outcome }}" != "success" ] && \
[ "${{ steps.login-quay.outcome }}" != "success" ] && \
[ "${{ steps.login-ghcr.outcome }}" != "success" ]; then
echo "::error title=Registry Login Failed::All registry authentications failed"
fi