π³ Docker Multi-Platform Build #17
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| # 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 |