From 47d6959ca6b8b36322467c331fd7cfebb78ae948 Mon Sep 17 00:00:00 2001 From: KingPin <{ID}+{username}@users.noreply.github.com> Date: Sun, 2 Nov 2025 21:38:50 -0500 Subject: [PATCH 1/9] ci(docker-ci): split publish into per-arch jobs and add robust publish/manifest flow - Replace single multi-arch `publish` job with per-arch jobs: publish-amd64, publish-arm64, publish-armv7 - Add publish-manifest job that assembles multi-arch manifests from per-arch staging images - Push per-arch "staging" tags and capture metadata (metadata-*.json) to extract image digests as job outputs - Add retry/backoff loops for per-arch builds and manifest creation to improve reliability - Use per-arch QEMU/platform settings and explicit buildx args (provenance mode, metadata-file, cache-from/to, labels) - Create bookworm compatibility tags for v2 trixie images by aliasing the multi-arch manifest (with retries) - Update Trivy SARIF filenames to include architecture and adjust references to staging images --- .github/workflows/docker-ci.yml | 806 +++++++++++++++++++++++++++++--- 1 file changed, 748 insertions(+), 58 deletions(-) diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml index 82a3ee9..9dc11e2 100644 --- a/.github/workflows/docker-ci.yml +++ b/.github/workflows/docker-ci.yml @@ -273,7 +273,8 @@ jobs: run: | echo "::notice::✅ Build and tests passed for ${{ matrix.variant }} - ${{ steps.vars.outputs.TAG }}" - publish: + # Per-architecture build and push jobs + publish-amd64: needs: build-and-test if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'schedule') # runs-on: ubuntu-latest @@ -288,11 +289,9 @@ jobs: exclude: - php-type: apache php-base: alpine - # v2 uses trixie as the Debian base; bookworm retained for v1 - variant: v2 php-base: bookworm include: - # v2 builds on trixie for Debian images - variant: v2 php-version: '8.4' php-type: fpm @@ -330,7 +329,9 @@ jobs: php-type: apache php-base: trixie - name: publish-${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }} + name: amd64-${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }} + outputs: + digest: ${{ steps.build.outputs.digest }} steps: - name: Checkout @@ -386,77 +387,766 @@ jobs: BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + # Staging tags for per-arch builds (will be assembled into multi-arch manifest later) + STAGING_TAG="${VERSION}${TAG_SUFFIX}-amd64-staging" + echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT echo "TAG_SUFFIX=${TAG_SUFFIX}" >> $GITHUB_OUTPUT echo "DOCKERFILE=${DOCKERFILE}" >> $GITHUB_OUTPUT echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_OUTPUT - echo "DOCKERHUB_TAG=docker.io/${{ secrets.DOCKERHUB_USERNAME }}/php-docker:${VERSION}${TAG_SUFFIX}" >> $GITHUB_OUTPUT - echo "GHCR_TAG=ghcr.io/kingpin/php-docker:${VERSION}${TAG_SUFFIX}" >> $GITHUB_OUTPUT - echo "QUAY_TAG=quay.io/kingpinx1/php-docker:${VERSION}${TAG_SUFFIX}" >> $GITHUB_OUTPUT - echo "CACHE_SCOPE=${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }}" >> $GITHUB_OUTPUT + echo "STAGING_TAG=${STAGING_TAG}" >> $GITHUB_OUTPUT + echo "DOCKERHUB_STAGING=docker.io/${{ secrets.DOCKERHUB_USERNAME }}/php-docker:${STAGING_TAG}" >> $GITHUB_OUTPUT + echo "GHCR_STAGING=ghcr.io/kingpin/php-docker:${STAGING_TAG}" >> $GITHUB_OUTPUT + echo "QUAY_STAGING=quay.io/kingpinx1/php-docker:${STAGING_TAG}" >> $GITHUB_OUTPUT + echo "CACHE_SCOPE=${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }}-amd64" >> $GITHUB_OUTPUT - - name: Build and push multi-arch image - uses: docker/build-push-action@v6 - with: - context: . - file: ${{ steps.vars.outputs.DOCKERFILE }} - platforms: linux/amd64,linux/arm64,linux/arm/v7 - push: true - provenance: mode=max - cache-from: type=gha,scope=${{ steps.vars.outputs.CACHE_SCOPE }} - cache-to: type=gha,mode=max,scope=${{ steps.vars.outputs.CACHE_SCOPE }} - build-args: | - VERSION=${{ steps.vars.outputs.VERSION }} - PHPVERSION=${{ matrix.php-version }} - BASEOS=${{ matrix.php-base }} - S6_OVERLAY_VERSION=${{ steps.s6-version.outputs.version }} - BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }} - VCS_REF=${{ github.sha }} - tags: | - ${{ steps.vars.outputs.DOCKERHUB_TAG }} - ${{ steps.vars.outputs.GHCR_TAG }} - ${{ steps.vars.outputs.QUAY_TAG }} - labels: | - com.sumguy.php-docker.php.variant=${{ matrix.php-type }} - com.sumguy.php-docker.image.variant=${{ matrix.variant }} - com.sumguy.php-docker.build_id=${{ github.run_id }} - com.sumguy.php-docker.build_url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - com.sumguy.php-docker.built_by=github-actions/docker-ci - - - name: Create bookworm compatibility tag for v2 trixie images - if: matrix.variant == 'v2' && matrix.php-base == 'trixie' + - name: Build and push amd64 image with retry + id: build run: | - echo "::group::Creating bookworm compatibility tags for trixie-built v2 image" - - # Replace 'trixie' with 'bookworm' in tag names to maintain backward compatibility - BOOKWORM_VERSION="${{ matrix.php-version }}-${{ matrix.php-type }}-bookworm" - - # Create manifest aliases pointing trixie-built images to bookworm tags - docker buildx imagetools create -t \ - docker.io/${{ secrets.DOCKERHUB_USERNAME }}/php-docker:${BOOKWORM_VERSION}-v2 \ - ${{ steps.vars.outputs.DOCKERHUB_TAG }} + MAX_ATTEMPTS=3 + ATTEMPT=1 + SUCCESS=false - docker buildx imagetools create -t \ - ghcr.io/kingpin/php-docker:${BOOKWORM_VERSION}-v2 \ - ${{ steps.vars.outputs.GHCR_TAG }} + while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do + echo "::group::Build attempt $ATTEMPT of $MAX_ATTEMPTS" + + if docker buildx build \ + --platform linux/amd64 \ + --file ${{ steps.vars.outputs.DOCKERFILE }} \ + --push \ + --provenance mode=max \ + --cache-from type=gha,scope=${{ steps.vars.outputs.CACHE_SCOPE }} \ + --cache-to type=gha,mode=max,scope=${{ steps.vars.outputs.CACHE_SCOPE }} \ + --build-arg VERSION=${{ steps.vars.outputs.VERSION }} \ + --build-arg PHPVERSION=${{ matrix.php-version }} \ + --build-arg BASEOS=${{ matrix.php-base }} \ + --build-arg S6_OVERLAY_VERSION=${{ steps.s6-version.outputs.version }} \ + --build-arg BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }} \ + --build-arg VCS_REF=${{ github.sha }} \ + --tag ${{ steps.vars.outputs.DOCKERHUB_STAGING }} \ + --tag ${{ steps.vars.outputs.GHCR_STAGING }} \ + --tag ${{ steps.vars.outputs.QUAY_STAGING }} \ + --label com.sumguy.php-docker.php.variant=${{ matrix.php-type }} \ + --label com.sumguy.php-docker.image.variant=${{ matrix.variant }} \ + --label com.sumguy.php-docker.build_id=${{ github.run_id }} \ + --label com.sumguy.php-docker.build_url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} \ + --label com.sumguy.php-docker.built_by=github-actions/docker-ci \ + --metadata-file /tmp/metadata-amd64.json \ + .; then + + SUCCESS=true + echo "✅ Build and push succeeded on attempt $ATTEMPT" + echo "::endgroup::" + break + else + echo "::warning::Build attempt $ATTEMPT failed" + echo "::endgroup::" + + if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then + SLEEP_TIME=$((2 ** ATTEMPT)) + echo "⏳ Waiting ${SLEEP_TIME}s before retry..." + sleep $SLEEP_TIME + fi + + ATTEMPT=$((ATTEMPT + 1)) + fi + done - docker buildx imagetools create -t \ - quay.io/kingpinx1/php-docker:${BOOKWORM_VERSION}-v2 \ - ${{ steps.vars.outputs.QUAY_TAG }} + if [ "$SUCCESS" = "false" ]; then + echo "::error::All $MAX_ATTEMPTS build attempts failed" + exit 1 + fi - echo "✅ Created bookworm compatibility tags pointing to trixie image" - echo "::endgroup::" + # Extract digest from metadata + DIGEST=$(jq -r '."containerimage.digest"' /tmp/metadata-amd64.json) + echo "digest=${DIGEST}" >> $GITHUB_OUTPUT + echo "📋 Image digest: ${DIGEST}" - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: scan-type: image - image-ref: ${{ steps.vars.outputs.DOCKERHUB_TAG }} + image-ref: ${{ steps.vars.outputs.DOCKERHUB_STAGING }} format: 'sarif' severity: 'CRITICAL,HIGH' - output: 'trivy-results-${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }}.sarif' + output: 'trivy-results-amd64-${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }}.sarif' - name: Upload Trivy results uses: github/codeql-action/upload-sarif@v3 with: - sarif_file: 'trivy-results-${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }}.sarif' + sarif_file: 'trivy-results-amd64-${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }}.sarif' + + publish-arm64: + needs: build-and-test + if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'schedule') + # runs-on: ubuntu-latest + runs-on: arc-s2-runner + strategy: + fail-fast: false + matrix: + variant: [v1, v2] + php-version: ['8.4', '8.3', '8.2'] + php-type: [fpm, cli, apache] + php-base: [alpine, bookworm] + exclude: + - php-type: apache + php-base: alpine + - variant: v2 + php-base: bookworm + include: + - variant: v2 + php-version: '8.4' + php-type: fpm + php-base: trixie + - variant: v2 + php-version: '8.4' + php-type: cli + php-base: trixie + - variant: v2 + php-version: '8.4' + php-type: apache + php-base: trixie + - variant: v2 + php-version: '8.3' + php-type: fpm + php-base: trixie + - variant: v2 + php-version: '8.3' + php-type: cli + php-base: trixie + - variant: v2 + php-version: '8.3' + php-type: apache + php-base: trixie + - variant: v2 + php-version: '8.2' + php-type: fpm + php-base: trixie + - variant: v2 + php-version: '8.2' + php-type: cli + php-base: trixie + - variant: v2 + php-version: '8.2' + php-type: apache + php-base: trixie + + name: arm64-${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }} + outputs: + digest: ${{ steps.build.outputs.digest }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get latest s6-overlay version + id: s6-version + run: | + S6_OVERLAY_VERSION="$(curl -s https://api.github.com/repos/just-containers/s6-overlay/releases/latest | jq -r .tag_name)" + echo "version=${S6_OVERLAY_VERSION}" >> $GITHUB_OUTPUT + echo "✅ Latest s6-overlay version: ${S6_OVERLAY_VERSION}" + + - name: Setup QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to Quay.io + uses: docker/login-action@v3 + with: + registry: quay.io + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_ROBOT_TOKEN }} + + - name: Set publish variables + id: vars + run: | + VERSION="${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }}" + + if [ "${{ matrix.variant }}" = "v2" ]; then + TAG_SUFFIX="-v2" + DOCKERFILE="Dockerfile.v2" + else + TAG_SUFFIX="" + DOCKERFILE="Dockerfile.v1" + fi + + BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + + # Staging tags for per-arch builds + STAGING_TAG="${VERSION}${TAG_SUFFIX}-arm64-staging" + + echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT + echo "TAG_SUFFIX=${TAG_SUFFIX}" >> $GITHUB_OUTPUT + echo "DOCKERFILE=${DOCKERFILE}" >> $GITHUB_OUTPUT + echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_OUTPUT + echo "STAGING_TAG=${STAGING_TAG}" >> $GITHUB_OUTPUT + echo "DOCKERHUB_STAGING=docker.io/${{ secrets.DOCKERHUB_USERNAME }}/php-docker:${STAGING_TAG}" >> $GITHUB_OUTPUT + echo "GHCR_STAGING=ghcr.io/kingpin/php-docker:${STAGING_TAG}" >> $GITHUB_OUTPUT + echo "QUAY_STAGING=quay.io/kingpinx1/php-docker:${STAGING_TAG}" >> $GITHUB_OUTPUT + echo "CACHE_SCOPE=${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }}-arm64" >> $GITHUB_OUTPUT + + - name: Build and push arm64 image with retry + id: build + run: | + MAX_ATTEMPTS=3 + ATTEMPT=1 + SUCCESS=false + + while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do + echo "::group::Build attempt $ATTEMPT of $MAX_ATTEMPTS" + + if docker buildx build \ + --platform linux/arm64 \ + --file ${{ steps.vars.outputs.DOCKERFILE }} \ + --push \ + --provenance mode=max \ + --cache-from type=gha,scope=${{ steps.vars.outputs.CACHE_SCOPE }} \ + --cache-to type=gha,mode=max,scope=${{ steps.vars.outputs.CACHE_SCOPE }} \ + --build-arg VERSION=${{ steps.vars.outputs.VERSION }} \ + --build-arg PHPVERSION=${{ matrix.php-version }} \ + --build-arg BASEOS=${{ matrix.php-base }} \ + --build-arg S6_OVERLAY_VERSION=${{ steps.s6-version.outputs.version }} \ + --build-arg BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }} \ + --build-arg VCS_REF=${{ github.sha }} \ + --tag ${{ steps.vars.outputs.DOCKERHUB_STAGING }} \ + --tag ${{ steps.vars.outputs.GHCR_STAGING }} \ + --tag ${{ steps.vars.outputs.QUAY_STAGING }} \ + --label com.sumguy.php-docker.php.variant=${{ matrix.php-type }} \ + --label com.sumguy.php-docker.image.variant=${{ matrix.variant }} \ + --label com.sumguy.php-docker.build_id=${{ github.run_id }} \ + --label com.sumguy.php-docker.build_url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} \ + --label com.sumguy.php-docker.built_by=github-actions/docker-ci \ + --metadata-file /tmp/metadata-arm64.json \ + .; then + + SUCCESS=true + echo "✅ Build and push succeeded on attempt $ATTEMPT" + echo "::endgroup::" + break + else + echo "::warning::Build attempt $ATTEMPT failed" + echo "::endgroup::" + + if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then + SLEEP_TIME=$((2 ** ATTEMPT)) + echo "⏳ Waiting ${SLEEP_TIME}s before retry..." + sleep $SLEEP_TIME + fi + + ATTEMPT=$((ATTEMPT + 1)) + fi + done + + if [ "$SUCCESS" = "false" ]; then + echo "::error::All $MAX_ATTEMPTS build attempts failed" + exit 1 + fi + + # Extract digest from metadata + DIGEST=$(jq -r '."containerimage.digest"' /tmp/metadata-arm64.json) + echo "digest=${DIGEST}" >> $GITHUB_OUTPUT + echo "📋 Image digest: ${DIGEST}" + + publish-armv7: + needs: build-and-test + if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'schedule') + # runs-on: ubuntu-latest + runs-on: arc-s2-runner + strategy: + fail-fast: false + matrix: + variant: [v1, v2] + php-version: ['8.4', '8.3', '8.2'] + php-type: [fpm, cli, apache] + php-base: [alpine, bookworm] + exclude: + - php-type: apache + php-base: alpine + - variant: v2 + php-base: bookworm + include: + - variant: v2 + php-version: '8.4' + php-type: fpm + php-base: trixie + - variant: v2 + php-version: '8.4' + php-type: cli + php-base: trixie + - variant: v2 + php-version: '8.4' + php-type: apache + php-base: trixie + - variant: v2 + php-version: '8.3' + php-type: fpm + php-base: trixie + - variant: v2 + php-version: '8.3' + php-type: cli + php-base: trixie + - variant: v2 + php-version: '8.3' + php-type: apache + php-base: trixie + - variant: v2 + php-version: '8.2' + php-type: fpm + php-base: trixie + - variant: v2 + php-version: '8.2' + php-type: cli + php-base: trixie + - variant: v2 + php-version: '8.2' + php-type: apache + php-base: trixie + + name: armv7-${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }} + outputs: + digest: ${{ steps.build.outputs.digest }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get latest s6-overlay version + id: s6-version + run: | + S6_OVERLAY_VERSION="$(curl -s https://api.github.com/repos/just-containers/s6-overlay/releases/latest | jq -r .tag_name)" + echo "version=${S6_OVERLAY_VERSION}" >> $GITHUB_OUTPUT + echo "✅ Latest s6-overlay version: ${S6_OVERLAY_VERSION}" + + - name: Setup QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: arm + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to Quay.io + uses: docker/login-action@v3 + with: + registry: quay.io + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_ROBOT_TOKEN }} + + - name: Set publish variables + id: vars + run: | + VERSION="${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }}" + + if [ "${{ matrix.variant }}" = "v2" ]; then + TAG_SUFFIX="-v2" + DOCKERFILE="Dockerfile.v2" + else + TAG_SUFFIX="" + DOCKERFILE="Dockerfile.v1" + fi + + BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + + # Staging tags for per-arch builds + STAGING_TAG="${VERSION}${TAG_SUFFIX}-armv7-staging" + + echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT + echo "TAG_SUFFIX=${TAG_SUFFIX}" >> $GITHUB_OUTPUT + echo "DOCKERFILE=${DOCKERFILE}" >> $GITHUB_OUTPUT + echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_OUTPUT + echo "STAGING_TAG=${STAGING_TAG}" >> $GITHUB_OUTPUT + echo "DOCKERHUB_STAGING=docker.io/${{ secrets.DOCKERHUB_USERNAME }}/php-docker:${STAGING_TAG}" >> $GITHUB_OUTPUT + echo "GHCR_STAGING=ghcr.io/kingpin/php-docker:${STAGING_TAG}" >> $GITHUB_OUTPUT + echo "QUAY_STAGING=quay.io/kingpinx1/php-docker:${STAGING_TAG}" >> $GITHUB_OUTPUT + echo "CACHE_SCOPE=${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }}-armv7" >> $GITHUB_OUTPUT + + - name: Build and push armv7 image with retry + id: build + run: | + MAX_ATTEMPTS=3 + ATTEMPT=1 + SUCCESS=false + + while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do + echo "::group::Build attempt $ATTEMPT of $MAX_ATTEMPTS" + + if docker buildx build \ + --platform linux/arm/v7 \ + --file ${{ steps.vars.outputs.DOCKERFILE }} \ + --push \ + --provenance mode=max \ + --cache-from type=gha,scope=${{ steps.vars.outputs.CACHE_SCOPE }} \ + --cache-to type=gha,mode=max,scope=${{ steps.vars.outputs.CACHE_SCOPE }} \ + --build-arg VERSION=${{ steps.vars.outputs.VERSION }} \ + --build-arg PHPVERSION=${{ matrix.php-version }} \ + --build-arg BASEOS=${{ matrix.php-base }} \ + --build-arg S6_OVERLAY_VERSION=${{ steps.s6-version.outputs.version }} \ + --build-arg BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }} \ + --build-arg VCS_REF=${{ github.sha }} \ + --tag ${{ steps.vars.outputs.DOCKERHUB_STAGING }} \ + --tag ${{ steps.vars.outputs.GHCR_STAGING }} \ + --tag ${{ steps.vars.outputs.QUAY_STAGING }} \ + --label com.sumguy.php-docker.php.variant=${{ matrix.php-type }} \ + --label com.sumguy.php-docker.image.variant=${{ matrix.variant }} \ + --label com.sumguy.php-docker.build_id=${{ github.run_id }} \ + --label com.sumguy.php-docker.build_url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} \ + --label com.sumguy.php-docker.built_by=github-actions/docker-ci \ + --metadata-file /tmp/metadata-armv7.json \ + .; then + + SUCCESS=true + echo "✅ Build and push succeeded on attempt $ATTEMPT" + echo "::endgroup::" + break + else + echo "::warning::Build attempt $ATTEMPT failed" + echo "::endgroup::" + + if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then + SLEEP_TIME=$((2 ** ATTEMPT)) + echo "⏳ Waiting ${SLEEP_TIME}s before retry..." + sleep $SLEEP_TIME + fi + + ATTEMPT=$((ATTEMPT + 1)) + fi + done + + if [ "$SUCCESS" = "false" ]; then + echo "::error::All $MAX_ATTEMPTS build attempts failed" + exit 1 + fi + + # Extract digest from metadata + DIGEST=$(jq -r '."containerimage.digest"' /tmp/metadata-armv7.json) + echo "digest=${DIGEST}" >> $GITHUB_OUTPUT + echo "📋 Image digest: ${DIGEST}" + + # Manifest assembly job - creates multi-arch manifests from per-arch images + publish-manifest: + needs: [publish-amd64, publish-arm64, publish-armv7] + if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'schedule') + # runs-on: ubuntu-latest + runs-on: arc-s2-runner + strategy: + fail-fast: false + matrix: + variant: [v1, v2] + php-version: ['8.4', '8.3', '8.2'] + php-type: [fpm, cli, apache] + php-base: [alpine, bookworm] + exclude: + - php-type: apache + php-base: alpine + - variant: v2 + php-base: bookworm + include: + - variant: v2 + php-version: '8.4' + php-type: fpm + php-base: trixie + - variant: v2 + php-version: '8.4' + php-type: cli + php-base: trixie + - variant: v2 + php-version: '8.4' + php-type: apache + php-base: trixie + - variant: v2 + php-version: '8.3' + php-type: fpm + php-base: trixie + - variant: v2 + php-version: '8.3' + php-type: cli + php-base: trixie + - variant: v2 + php-version: '8.3' + php-type: apache + php-base: trixie + - variant: v2 + php-version: '8.2' + php-type: fpm + php-base: trixie + - variant: v2 + php-version: '8.2' + php-type: cli + php-base: trixie + - variant: v2 + php-version: '8.2' + php-type: apache + php-base: trixie + + name: manifest-${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to Quay.io + uses: docker/login-action@v3 + with: + registry: quay.io + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_ROBOT_TOKEN }} + + - name: Set manifest variables + id: vars + run: | + VERSION="${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }}" + + if [ "${{ matrix.variant }}" = "v2" ]; then + TAG_SUFFIX="-v2" + else + TAG_SUFFIX="" + fi + + # Staging tags for source images + STAGING_AMD64="${VERSION}${TAG_SUFFIX}-amd64-staging" + STAGING_ARM64="${VERSION}${TAG_SUFFIX}-arm64-staging" + STAGING_ARMV7="${VERSION}${TAG_SUFFIX}-armv7-staging" + + echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT + echo "TAG_SUFFIX=${TAG_SUFFIX}" >> $GITHUB_OUTPUT + + # Final canonical tags + echo "DOCKERHUB_TAG=docker.io/${{ secrets.DOCKERHUB_USERNAME }}/php-docker:${VERSION}${TAG_SUFFIX}" >> $GITHUB_OUTPUT + echo "GHCR_TAG=ghcr.io/kingpin/php-docker:${VERSION}${TAG_SUFFIX}" >> $GITHUB_OUTPUT + echo "QUAY_TAG=quay.io/kingpinx1/php-docker:${VERSION}${TAG_SUFFIX}" >> $GITHUB_OUTPUT + + # Source staging tags + echo "DOCKERHUB_AMD64=docker.io/${{ secrets.DOCKERHUB_USERNAME }}/php-docker:${STAGING_AMD64}" >> $GITHUB_OUTPUT + echo "DOCKERHUB_ARM64=docker.io/${{ secrets.DOCKERHUB_USERNAME }}/php-docker:${STAGING_ARM64}" >> $GITHUB_OUTPUT + echo "DOCKERHUB_ARMV7=docker.io/${{ secrets.DOCKERHUB_USERNAME }}/php-docker:${STAGING_ARMV7}" >> $GITHUB_OUTPUT + + echo "GHCR_AMD64=ghcr.io/kingpin/php-docker:${STAGING_AMD64}" >> $GITHUB_OUTPUT + echo "GHCR_ARM64=ghcr.io/kingpin/php-docker:${STAGING_ARM64}" >> $GITHUB_OUTPUT + echo "GHCR_ARMV7=ghcr.io/kingpin/php-docker:${STAGING_ARMV7}" >> $GITHUB_OUTPUT + + echo "QUAY_AMD64=quay.io/kingpinx1/php-docker:${STAGING_AMD64}" >> $GITHUB_OUTPUT + echo "QUAY_ARM64=quay.io/kingpinx1/php-docker:${STAGING_ARM64}" >> $GITHUB_OUTPUT + echo "QUAY_ARMV7=quay.io/kingpinx1/php-docker:${STAGING_ARMV7}" >> $GITHUB_OUTPUT + + - name: Create and push DockerHub multi-arch manifest with retry + run: | + MAX_ATTEMPTS=3 + ATTEMPT=1 + SUCCESS=false + + while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do + echo "::group::DockerHub manifest attempt $ATTEMPT of $MAX_ATTEMPTS" + + if docker buildx imagetools create -t ${{ steps.vars.outputs.DOCKERHUB_TAG }} \ + ${{ steps.vars.outputs.DOCKERHUB_AMD64 }} \ + ${{ steps.vars.outputs.DOCKERHUB_ARM64 }} \ + ${{ steps.vars.outputs.DOCKERHUB_ARMV7 }}; then + + SUCCESS=true + echo "✅ DockerHub manifest created successfully on attempt $ATTEMPT" + echo "::endgroup::" + break + else + echo "::warning::DockerHub manifest creation attempt $ATTEMPT failed" + echo "::endgroup::" + + if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then + SLEEP_TIME=$((2 ** ATTEMPT)) + echo "⏳ Waiting ${SLEEP_TIME}s before retry..." + sleep $SLEEP_TIME + fi + + ATTEMPT=$((ATTEMPT + 1)) + fi + done + + if [ "$SUCCESS" = "false" ]; then + echo "::error::All $MAX_ATTEMPTS DockerHub manifest attempts failed" + exit 1 + fi + + - name: Create and push GHCR multi-arch manifest with retry + run: | + MAX_ATTEMPTS=3 + ATTEMPT=1 + SUCCESS=false + + while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do + echo "::group::GHCR manifest attempt $ATTEMPT of $MAX_ATTEMPTS" + + if docker buildx imagetools create -t ${{ steps.vars.outputs.GHCR_TAG }} \ + ${{ steps.vars.outputs.GHCR_AMD64 }} \ + ${{ steps.vars.outputs.GHCR_ARM64 }} \ + ${{ steps.vars.outputs.GHCR_ARMV7 }}; then + + SUCCESS=true + echo "✅ GHCR manifest created successfully on attempt $ATTEMPT" + echo "::endgroup::" + break + else + echo "::warning::GHCR manifest creation attempt $ATTEMPT failed" + echo "::endgroup::" + + if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then + SLEEP_TIME=$((2 ** ATTEMPT)) + echo "⏳ Waiting ${SLEEP_TIME}s before retry..." + sleep $SLEEP_TIME + fi + + ATTEMPT=$((ATTEMPT + 1)) + fi + done + + if [ "$SUCCESS" = "false" ]; then + echo "::error::All $MAX_ATTEMPTS GHCR manifest attempts failed" + exit 1 + fi + + - name: Create and push Quay multi-arch manifest with retry + run: | + MAX_ATTEMPTS=3 + ATTEMPT=1 + SUCCESS=false + + while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do + echo "::group::Quay manifest attempt $ATTEMPT of $MAX_ATTEMPTS" + + if docker buildx imagetools create -t ${{ steps.vars.outputs.QUAY_TAG }} \ + ${{ steps.vars.outputs.QUAY_AMD64 }} \ + ${{ steps.vars.outputs.QUAY_ARM64 }} \ + ${{ steps.vars.outputs.QUAY_ARMV7 }}; then + + SUCCESS=true + echo "✅ Quay manifest created successfully on attempt $ATTEMPT" + echo "::endgroup::" + break + else + echo "::warning::Quay manifest creation attempt $ATTEMPT failed" + echo "::endgroup::" + + if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then + SLEEP_TIME=$((2 ** ATTEMPT)) + echo "⏳ Waiting ${SLEEP_TIME}s before retry..." + sleep $SLEEP_TIME + fi + + ATTEMPT=$((ATTEMPT + 1)) + fi + done + + if [ "$SUCCESS" = "false" ]; then + echo "::error::All $MAX_ATTEMPTS Quay manifest attempts failed" + exit 1 + fi + + - name: Create bookworm compatibility tag for v2 trixie images + if: matrix.variant == 'v2' && matrix.php-base == 'trixie' + run: | + echo "::group::Creating bookworm compatibility tags for trixie-built v2 image" + + # Replace 'trixie' with 'bookworm' in tag names to maintain backward compatibility + BOOKWORM_VERSION="${{ matrix.php-version }}-${{ matrix.php-type }}-bookworm-v2" + + # Create manifest aliases pointing to the main trixie multi-arch manifest + MAX_ATTEMPTS=3 + + for registry in dockerhub ghcr quay; do + case $registry in + dockerhub) + TAG="docker.io/${{ secrets.DOCKERHUB_USERNAME }}/php-docker:${BOOKWORM_VERSION}" + SOURCE="${{ steps.vars.outputs.DOCKERHUB_TAG }}" + ;; + ghcr) + TAG="ghcr.io/kingpin/php-docker:${BOOKWORM_VERSION}" + SOURCE="${{ steps.vars.outputs.GHCR_TAG }}" + ;; + quay) + TAG="quay.io/kingpinx1/php-docker:${BOOKWORM_VERSION}" + SOURCE="${{ steps.vars.outputs.QUAY_TAG }}" + ;; + esac + + ATTEMPT=1 + SUCCESS=false + while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do + if docker buildx imagetools create -t "$TAG" "$SOURCE"; then + echo "✅ Created $registry bookworm compat tag on attempt $ATTEMPT" + SUCCESS=true + break + else + echo "::warning::$registry bookworm compat tag attempt $ATTEMPT failed" + if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then + sleep $((2 ** ATTEMPT)) + fi + ATTEMPT=$((ATTEMPT + 1)) + fi + done + + if [ "$SUCCESS" = "false" ]; then + echo "::error::Failed to create $registry bookworm compat tag after $MAX_ATTEMPTS attempts" + exit 1 + fi + done + + echo "✅ Created all bookworm compatibility tags" + echo "::endgroup::" + + - name: Verify multi-arch manifest + run: | + echo "::group::Verifying DockerHub manifest" + docker buildx imagetools inspect ${{ steps.vars.outputs.DOCKERHUB_TAG }} + echo "::endgroup::" + + echo "::group::Verifying GHCR manifest" + docker buildx imagetools inspect ${{ steps.vars.outputs.GHCR_TAG }} + echo "::endgroup::" + + echo "::group::Verifying Quay manifest" + docker buildx imagetools inspect ${{ steps.vars.outputs.QUAY_TAG }} + echo "::endgroup::" From 1014e1ddc7c0d8984d7047b49c7caf038386b41e Mon Sep 17 00:00:00 2001 From: KingPin <{ID}+{username}@users.noreply.github.com> Date: Sun, 2 Nov 2025 21:44:54 -0500 Subject: [PATCH 2/9] Fix: Remove outputs declarations from matrix jobs Matrix jobs with strategy cannot reliably expose outputs due to conflicts between matrix combinations. The digest outputs in publish-amd64, publish-arm64, and publish-armv7 jobs cannot be reliably consumed and should be removed. The manifest assembly job still functions correctly as it assembles manifests from the staging tags (not from job outputs), so this removal does not break the workflow. --- .github/workflows/docker-ci.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml index 9dc11e2..5347ac3 100644 --- a/.github/workflows/docker-ci.yml +++ b/.github/workflows/docker-ci.yml @@ -330,8 +330,6 @@ jobs: php-base: trixie name: amd64-${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }} - outputs: - digest: ${{ steps.build.outputs.digest }} steps: - name: Checkout @@ -532,8 +530,6 @@ jobs: php-base: trixie name: arm64-${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }} - outputs: - digest: ${{ steps.build.outputs.digest }} steps: - name: Checkout @@ -720,8 +716,6 @@ jobs: php-base: trixie name: armv7-${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }} - outputs: - digest: ${{ steps.build.outputs.digest }} steps: - name: Checkout From 1172d4dd95d5130e68e307b73c3b8f2edd25943c Mon Sep 17 00:00:00 2001 From: KingPin <{ID}+{username}@users.noreply.github.com> Date: Sun, 2 Nov 2025 21:46:37 -0500 Subject: [PATCH 3/9] Fix: Improve exponential backoff timing in retry loops Changed backoff calculation from 2 ** ATTEMPT to 2 ** (ATTEMPT - 1) to produce more reasonable retry intervals: - Old: 2s, 4s, 8s (starting too short after first failure) - New: 1s, 2s, 4s (more appropriate for transient registry issues) Applied to all retry loops in: - publish-amd64 build step - publish-arm64 build step - publish-armv7 build step - DockerHub manifest creation - GHCR manifest creation - Quay manifest creation - Bookworm compatibility tag creation --- .github/workflows/docker-ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml index 5347ac3..e3ab6c0 100644 --- a/.github/workflows/docker-ci.yml +++ b/.github/workflows/docker-ci.yml @@ -641,7 +641,7 @@ jobs: echo "::endgroup::" if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then - SLEEP_TIME=$((2 ** ATTEMPT)) + SLEEP_TIME=$((2 ** (ATTEMPT - 1))) echo "⏳ Waiting ${SLEEP_TIME}s before retry..." sleep $SLEEP_TIME fi @@ -827,7 +827,7 @@ jobs: echo "::endgroup::" if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then - SLEEP_TIME=$((2 ** ATTEMPT)) + SLEEP_TIME=$((2 ** (ATTEMPT - 1))) echo "⏳ Waiting ${SLEEP_TIME}s before retry..." sleep $SLEEP_TIME fi @@ -991,7 +991,7 @@ jobs: echo "::endgroup::" if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then - SLEEP_TIME=$((2 ** ATTEMPT)) + SLEEP_TIME=$((2 ** (ATTEMPT - 1))) echo "⏳ Waiting ${SLEEP_TIME}s before retry..." sleep $SLEEP_TIME fi @@ -1028,7 +1028,7 @@ jobs: echo "::endgroup::" if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then - SLEEP_TIME=$((2 ** ATTEMPT)) + SLEEP_TIME=$((2 ** (ATTEMPT - 1))) echo "⏳ Waiting ${SLEEP_TIME}s before retry..." sleep $SLEEP_TIME fi @@ -1065,7 +1065,7 @@ jobs: echo "::endgroup::" if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then - SLEEP_TIME=$((2 ** ATTEMPT)) + SLEEP_TIME=$((2 ** (ATTEMPT - 1))) echo "⏳ Waiting ${SLEEP_TIME}s before retry..." sleep $SLEEP_TIME fi @@ -1116,7 +1116,7 @@ jobs: else echo "::warning::$registry bookworm compat tag attempt $ATTEMPT failed" if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then - sleep $((2 ** ATTEMPT)) + sleep $((2 ** (ATTEMPT - 1))) fi ATTEMPT=$((ATTEMPT + 1)) fi From ece416f563546a2f45852f2832235e512f5c6d2b Mon Sep 17 00:00:00 2001 From: KingPin <{ID}+{username}@users.noreply.github.com> Date: Sun, 2 Nov 2025 21:48:20 -0500 Subject: [PATCH 4/9] Improve: Upgrade exponential backoff to 5 * 2 ** (ATTEMPT - 1) Changed backoff calculation from 2 ** (ATTEMPT - 1) to 5 * 2 ** (ATTEMPT - 1) to produce more appropriate retry intervals for transient registry issues: - Old: 1s, 2s, 4s (too aggressive for registry recovery) - New: 5s, 10s, 20s (gives transient issues time to resolve) This is more effective for handling temporary API rate limits, service disruptions, and network issues that typically need 5+ seconds to recover. Applied to all retry loops: - Per-arch build steps (amd64, arm64, armv7) - Manifest creation (DockerHub, GHCR, Quay) - Bookworm compatibility tag creation --- .github/workflows/docker-ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml index e3ab6c0..6ff5722 100644 --- a/.github/workflows/docker-ci.yml +++ b/.github/workflows/docker-ci.yml @@ -641,7 +641,7 @@ jobs: echo "::endgroup::" if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then - SLEEP_TIME=$((2 ** (ATTEMPT - 1))) + SLEEP_TIME=$((5 * 2 ** (ATTEMPT - 1))) echo "⏳ Waiting ${SLEEP_TIME}s before retry..." sleep $SLEEP_TIME fi @@ -827,7 +827,7 @@ jobs: echo "::endgroup::" if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then - SLEEP_TIME=$((2 ** (ATTEMPT - 1))) + SLEEP_TIME=$((5 * 2 ** (ATTEMPT - 1))) echo "⏳ Waiting ${SLEEP_TIME}s before retry..." sleep $SLEEP_TIME fi @@ -991,7 +991,7 @@ jobs: echo "::endgroup::" if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then - SLEEP_TIME=$((2 ** (ATTEMPT - 1))) + SLEEP_TIME=$((5 * 2 ** (ATTEMPT - 1))) echo "⏳ Waiting ${SLEEP_TIME}s before retry..." sleep $SLEEP_TIME fi @@ -1028,7 +1028,7 @@ jobs: echo "::endgroup::" if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then - SLEEP_TIME=$((2 ** (ATTEMPT - 1))) + SLEEP_TIME=$((5 * 2 ** (ATTEMPT - 1))) echo "⏳ Waiting ${SLEEP_TIME}s before retry..." sleep $SLEEP_TIME fi @@ -1065,7 +1065,7 @@ jobs: echo "::endgroup::" if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then - SLEEP_TIME=$((2 ** (ATTEMPT - 1))) + SLEEP_TIME=$((5 * 2 ** (ATTEMPT - 1))) echo "⏳ Waiting ${SLEEP_TIME}s before retry..." sleep $SLEEP_TIME fi @@ -1116,7 +1116,7 @@ jobs: else echo "::warning::$registry bookworm compat tag attempt $ATTEMPT failed" if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then - sleep $((2 ** (ATTEMPT - 1))) + sleep $((5 * 2 ** (ATTEMPT - 1))) fi ATTEMPT=$((ATTEMPT + 1)) fi From e5b454db6fc312b542066b40e9b8618a9d7b6797 Mon Sep 17 00:00:00 2001 From: KingPin <{ID}+{username}@users.noreply.github.com> Date: Sun, 2 Nov 2025 22:17:20 -0500 Subject: [PATCH 5/9] fix(dockerfile): add retry logic for s6-overlay download with exponential backoff Adds 3-attempt retry loop with 5s/10s/20s exponential backoff for s6-overlay wget downloads. This addresses transient network timeouts (HTTP 28 timeout) that occur during Alpine builds, which is a common issue with external repository downloads during Docker builds. The retry logic: - Attempts download up to 3 times - Uses 5 * 2^(ATTEMPT-1) backoff (5s, 10s, 20s) - Cleans up partial downloads before retry - Provides clear logging of retry attempts - Fails fast with descriptive error if all attempts fail --- Dockerfile.v2 | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Dockerfile.v2 b/Dockerfile.v2 index b1e40ff..c743763 100644 --- a/Dockerfile.v2 +++ b/Dockerfile.v2 @@ -157,8 +157,22 @@ RUN if [ "$BASEOS" = "trixie" ] || [ "$BASEOS" = "bookworm" ]; then \ *) S6_ARCH="x86_64" ;; \ esac && \ echo "Downloading s6-overlay ${S6_OVERLAY_VERSION} for ${S6_ARCH}" && \ - wget -O /tmp/s6-overlay-noarch.tar.xz https://github.com/just-containers/s6-overlay/releases/download/${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz && \ - wget -O /tmp/s6-overlay-${S6_ARCH}.tar.xz https://github.com/just-containers/s6-overlay/releases/download/${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz && \ + for ATTEMPT in 1 2 3; do \ + if wget -O /tmp/s6-overlay-noarch.tar.xz https://github.com/just-containers/s6-overlay/releases/download/${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz && \ + wget -O /tmp/s6-overlay-${S6_ARCH}.tar.xz https://github.com/just-containers/s6-overlay/releases/download/${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz; then \ + break; \ + else \ + if [ $ATTEMPT -lt 3 ]; then \ + SLEEP_TIME=$((5 * 2 ** (ATTEMPT - 1))); \ + echo "Download attempt $ATTEMPT failed, retrying in ${SLEEP_TIME}s..."; \ + sleep $SLEEP_TIME; \ + rm -f /tmp/s6-overlay-*.tar.xz; \ + else \ + echo "Failed to download s6-overlay after 3 attempts"; \ + exit 1; \ + fi; \ + fi; \ + done && \ tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz && \ tar -C / -Jxpf /tmp/s6-overlay-${S6_ARCH}.tar.xz && \ rm /tmp/s6-overlay-noarch.tar.xz /tmp/s6-overlay-${S6_ARCH}.tar.xz && \ From 1ea2ea562497f0de364a513c36fd9191b746e085 Mon Sep 17 00:00:00 2001 From: KingPin <{ID}+{username}@users.noreply.github.com> Date: Mon, 3 Nov 2025 14:23:42 -0500 Subject: [PATCH 6/9] fix(dockerfile): add retry logic for install-php-extensions download in v1 Adds 3-attempt retry loop with 5s/10s/20s exponential backoff for: 1. install-php-extensions script download from GitHub 2. PHP extension compilation and installation This addresses transient network timeouts that occur on both Alpine and Debian builds. The retry logic handles: - Network timeouts during curl download - Compilation/installation failures due to transient issues - Clear logging of retry attempts and failures Each layer has independent retry loops for robustness. --- Dockerfile.v1 | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/Dockerfile.v1 b/Dockerfile.v1 index 682fbca..e10c6bb 100644 --- a/Dockerfile.v1 +++ b/Dockerfile.v1 @@ -24,12 +24,39 @@ RUN if [ "$BASEOS" = "bullseye" ]; then \ apk add --no-cache curl git zip unzip ghostscript imagemagick optipng gifsicle pngcrush jpegoptim libjpeg-turbo libjpeg-turbo-utils pngquant libwebp-tools; \ fi -# Add all needed PHP extensions -RUN curl -sSLf -o /usr/local/bin/install-php-extensions \ - https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions && \ - chmod +x /usr/local/bin/install-php-extensions && \ - install-php-extensions amqp bcmath bz2 calendar ctype exif intl imagick imap json mbstring ldap mcrypt memcached mongodb \ - mysqli opcache pdo_mysql pdo_pgsql pgsql redis snmp soap sockets tidy timezonedb uuid vips xsl yaml zip zstd @composer +# Add all needed PHP extensions with retry logic for transient network failures +RUN for ATTEMPT in 1 2 3; do \ + if curl -sSLf -o /usr/local/bin/install-php-extensions \ + https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions; then \ + break; \ + else \ + if [ $ATTEMPT -lt 3 ]; then \ + SLEEP_TIME=$((5 * 2 ** (ATTEMPT - 1))); \ + echo "Download attempt $ATTEMPT failed, retrying in ${SLEEP_TIME}s..."; \ + sleep $SLEEP_TIME; \ + rm -f /usr/local/bin/install-php-extensions; \ + else \ + echo "Failed to download install-php-extensions after 3 attempts"; \ + exit 1; \ + fi; \ + fi; \ + done && \ + chmod +x /usr/local/bin/install-php-extensions && \ + for ATTEMPT in 1 2 3; do \ + if install-php-extensions amqp bcmath bz2 calendar ctype exif intl imagick imap json mbstring ldap mcrypt memcached mongodb \ + mysqli opcache pdo_mysql pdo_pgsql pgsql redis snmp soap sockets tidy timezonedb uuid vips xsl yaml zip zstd @composer; then \ + break; \ + else \ + if [ $ATTEMPT -lt 3 ]; then \ + SLEEP_TIME=$((5 * 2 ** (ATTEMPT - 1))); \ + echo "Extension installation attempt $ATTEMPT failed, retrying in ${SLEEP_TIME}s..."; \ + sleep $SLEEP_TIME; \ + else \ + echo "Failed to install PHP extensions after 3 attempts"; \ + exit 1; \ + fi; \ + fi; \ + done # Enable Apache rewrite mod, if applicable RUN if command -v a2enmod; then a2enmod rewrite; fi From b2d11b09148c522669cd2e3394aa1e22056ae12a Mon Sep 17 00:00:00 2001 From: KingPin <{ID}+{username}@users.noreply.github.com> Date: Mon, 3 Nov 2025 14:36:45 -0500 Subject: [PATCH 7/9] fix(dockerfile): use POSIX-compliant arithmetic for retry backoff Replace bash-specific exponentiation (5 * 2 ** (ATTEMPT - 1)) with POSIX sh-compatible case statement for hardcoded backoff values: - ATTEMPT 1: 5 seconds - ATTEMPT 2: 10 seconds - ATTEMPT 3: 20 seconds This fixes 'arithmetic expression: expecting primary' errors in Alpine and Debian builds which use /bin/sh instead of bash. Applies to both: - Dockerfile.v1: install-php-extensions download and installation - Dockerfile.v2: s6-overlay download --- Dockerfile.v1 | 12 ++++++++++-- Dockerfile.v2 | 6 +++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Dockerfile.v1 b/Dockerfile.v1 index e10c6bb..e6ba17e 100644 --- a/Dockerfile.v1 +++ b/Dockerfile.v1 @@ -31,7 +31,11 @@ RUN for ATTEMPT in 1 2 3; do \ break; \ else \ if [ $ATTEMPT -lt 3 ]; then \ - SLEEP_TIME=$((5 * 2 ** (ATTEMPT - 1))); \ + case $ATTEMPT in \ + 1) SLEEP_TIME=5 ;; \ + 2) SLEEP_TIME=10 ;; \ + 3) SLEEP_TIME=20 ;; \ + esac; \ echo "Download attempt $ATTEMPT failed, retrying in ${SLEEP_TIME}s..."; \ sleep $SLEEP_TIME; \ rm -f /usr/local/bin/install-php-extensions; \ @@ -48,7 +52,11 @@ RUN for ATTEMPT in 1 2 3; do \ break; \ else \ if [ $ATTEMPT -lt 3 ]; then \ - SLEEP_TIME=$((5 * 2 ** (ATTEMPT - 1))); \ + case $ATTEMPT in \ + 1) SLEEP_TIME=5 ;; \ + 2) SLEEP_TIME=10 ;; \ + 3) SLEEP_TIME=20 ;; \ + esac; \ echo "Extension installation attempt $ATTEMPT failed, retrying in ${SLEEP_TIME}s..."; \ sleep $SLEEP_TIME; \ else \ diff --git a/Dockerfile.v2 b/Dockerfile.v2 index c743763..dcf65ba 100644 --- a/Dockerfile.v2 +++ b/Dockerfile.v2 @@ -163,7 +163,11 @@ RUN if [ "$BASEOS" = "trixie" ] || [ "$BASEOS" = "bookworm" ]; then \ break; \ else \ if [ $ATTEMPT -lt 3 ]; then \ - SLEEP_TIME=$((5 * 2 ** (ATTEMPT - 1))); \ + case $ATTEMPT in \ + 1) SLEEP_TIME=5 ;; \ + 2) SLEEP_TIME=10 ;; \ + 3) SLEEP_TIME=20 ;; \ + esac; \ echo "Download attempt $ATTEMPT failed, retrying in ${SLEEP_TIME}s..."; \ sleep $SLEEP_TIME; \ rm -f /tmp/s6-overlay-*.tar.xz; \ From eb5335e56f7845a96b1adc9a713f822d60fa3a3b Mon Sep 17 00:00:00 2001 From: KingPin <{ID}+{username}@users.noreply.github.com> Date: Mon, 3 Nov 2025 14:40:27 -0500 Subject: [PATCH 8/9] fix(dockerfile): switch v1 to ECR to avoid Docker Hub rate limits Change base image from docker.io/library/php to public.ecr.aws/docker/library/php to avoid Docker Hub's unauthenticated pull rate limits (100 pulls per 6 hours). AWS ECR Public Gallery has no rate limits for public images and is a reliable mirror for Docker Hub's official images. This matches the approach already used in Dockerfile.v2. Fixes: 'toomanyrequests: You have reached your unauthenticated pull rate limit' --- Dockerfile.v1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.v1 b/Dockerfile.v1 index e6ba17e..20551f2 100644 --- a/Dockerfile.v1 +++ b/Dockerfile.v1 @@ -1,5 +1,5 @@ ARG VERSION=8.3-cli-alpine -FROM php:${VERSION} +FROM public.ecr.aws/docker/library/php:${VERSION} ARG PHPVERSION ARG BASEOS From 00bce3de57ddad84a285b43d181482142a70352f Mon Sep 17 00:00:00 2001 From: KingPin <{ID}+{username}@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:13:57 -0500 Subject: [PATCH 9/9] switch back to github runners for now --- .github/workflows/docker-ci.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml index 6ff5722..fc43a09 100644 --- a/.github/workflows/docker-ci.yml +++ b/.github/workflows/docker-ci.yml @@ -31,8 +31,8 @@ concurrency: jobs: build-and-test: - # runs-on: ubuntu-latest - runs-on: arc-s2-runner + runs-on: ubuntu-latest + # runs-on: arc-s2-runner strategy: fail-fast: false matrix: @@ -277,8 +277,8 @@ jobs: publish-amd64: needs: build-and-test if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'schedule') - # runs-on: ubuntu-latest - runs-on: arc-s2-runner + runs-on: ubuntu-latest + # runs-on: arc-s2-runner strategy: fail-fast: false matrix: @@ -477,8 +477,8 @@ jobs: publish-arm64: needs: build-and-test if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'schedule') - # runs-on: ubuntu-latest - runs-on: arc-s2-runner + runs-on: ubuntu-latest + # runs-on: arc-s2-runner strategy: fail-fast: false matrix: @@ -663,8 +663,8 @@ jobs: publish-armv7: needs: build-and-test if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'schedule') - # runs-on: ubuntu-latest - runs-on: arc-s2-runner + runs-on: ubuntu-latest + # runs-on: arc-s2-runner strategy: fail-fast: false matrix: @@ -850,8 +850,8 @@ jobs: publish-manifest: needs: [publish-amd64, publish-arm64, publish-armv7] if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'schedule') - # runs-on: ubuntu-latest - runs-on: arc-s2-runner + runs-on: ubuntu-latest + # runs-on: arc-s2-runner strategy: fail-fast: false matrix: