diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml index 82a3ee9..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: @@ -273,11 +273,12 @@ 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 - runs-on: arc-s2-runner + runs-on: ubuntu-latest + # runs-on: arc-s2-runner strategy: fail-fast: false matrix: @@ -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,7 @@ 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 }} steps: - name: Checkout @@ -386,77 +385,762 @@ 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 }} + + 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=$((5 * 2 ** (ATTEMPT - 1))) + 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 }} + + 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=$((5 * 2 ** (ATTEMPT - 1))) + 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=$((5 * 2 ** (ATTEMPT - 1))) + 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=$((5 * 2 ** (ATTEMPT - 1))) + 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=$((5 * 2 ** (ATTEMPT - 1))) + 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 $((5 * 2 ** (ATTEMPT - 1))) + 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::" diff --git a/Dockerfile.v1 b/Dockerfile.v1 index 682fbca..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 @@ -24,12 +24,47 @@ 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 \ + 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; \ + 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 \ + 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 \ + 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 diff --git a/Dockerfile.v2 b/Dockerfile.v2 index b1e40ff..dcf65ba 100644 --- a/Dockerfile.v2 +++ b/Dockerfile.v2 @@ -157,8 +157,26 @@ 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 \ + 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; \ + 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 && \