From 82c84313f5f52586dde730e4262228b2bcca1d6a Mon Sep 17 00:00:00 2001 From: Rick Lam Date: Thu, 28 May 2026 17:27:23 +0100 Subject: [PATCH 1/3] CI: native arm64 runners, registry buildx cache, drop arm/v7 on 8.3+ - arm64 jobs run on ubuntu-24.04-arm natively; arm/v7 keeps QEMU but only the older versions (7.3-8.2) still target it. - 8.3-8.5 drop arm/v7 entirely (32-bit ARM has no common modern PHP deployment target and is the slowest QEMU build). - Build phase is now per-(version, platform); a manifest job per version assembles the multi-arch tags from per-arch digests via `docker buildx imagetools create`. - Per-(version, platform) registry cache (`buildcache-VER-SLUG`) speeds up reruns of the compile-heavy 8.3+ images. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/build.yml | 144 ++++++++++++++++++++++++++++-------- versions.json | 6 +- 2 files changed, 118 insertions(+), 32 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 203e32e..4e13314 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,26 +11,41 @@ jobs: name: Load matrix runs-on: ubuntu-latest outputs: - include: ${{ steps.load.outputs.include }} + test_matrix: ${{ steps.load.outputs.test_matrix }} + build_matrix: ${{ steps.load.outputs.build_matrix }} + versions: ${{ steps.load.outputs.versions }} steps: - uses: actions/checkout@v6 - id: load - run: echo "include=$(jq -c . versions.json)" >> "$GITHUB_OUTPUT" + run: | + { + echo "test_matrix=$(jq -c . versions.json)" + echo "build_matrix=$(jq -c '[ + .[] | . as $v | + ($v.platforms | split(",")) as $ps | + $ps[] | . as $p | { + version: $v.version, + platform: $p, + platform_slug: ($p | gsub("/"; "-")), + runner: (if $p == "linux/arm64" then "ubuntu-24.04-arm" else "ubuntu-latest" end) + } + ]' versions.json)" + echo "versions=$(jq -c '[.[].version]' versions.json)" + } >> "$GITHUB_OUTPUT" - build: - name: PHP ${{ matrix.version }} + test: + name: PHP ${{ matrix.version }} test needs: matrix runs-on: ubuntu-latest strategy: fail-fast: false matrix: - include: ${{ fromJson(needs.matrix.outputs.include) }} + include: ${{ fromJson(needs.matrix.outputs.test_matrix) }} steps: - uses: actions/checkout@v6 - # Test phase uses the default docker driver so the debug image's - # `FROM graze/php-alpine:VER` can resolve the just-built base from - # the local daemon. The docker driver is single-platform only. + # Tests run single-arch on amd64; the docker driver lets the debug + # image's `FROM graze/php-alpine:VER` resolve from the local daemon. - name: Build (single-arch for tests) run: make build-${{ matrix.version }} platform=--platform=linux/amd64 @@ -40,28 +55,105 @@ jobs: - name: Test run: make test-${{ matrix.version }} + build: + name: PHP ${{ matrix.version }} (${{ matrix.platform }}) + needs: [matrix, test] + if: github.ref == 'refs/heads/master' && github.event_name == 'push' + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: ${{ fromJson(needs.matrix.outputs.build_matrix) }} + steps: + - uses: actions/checkout@v6 + + # arm/v7 has no native GitHub-hosted runner, so it still needs QEMU. + # amd64 and arm64 build natively on their respective runners. - name: Set up QEMU - if: github.ref == 'refs/heads/master' && github.event_name == 'push' + if: matrix.platform == 'linux/arm/v7' uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - if: github.ref == 'refs/heads/master' && github.event_name == 'push' uses: docker/setup-buildx-action@v4 - name: Login to Docker Hub - if: github.ref == 'refs/heads/master' && github.event_name == 'push' uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push (multi-arch) - if: github.ref == 'refs/heads/master' && github.event_name == 'push' + - name: Build and push by digest + env: + V: ${{ matrix.version }} + PLATFORM: ${{ matrix.platform }} + SLUG: ${{ matrix.platform_slug }} run: | - V="${{ matrix.version }}" BUILD_DATE="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" VCS_REF="$(git rev-parse --short HEAD)" + mkdir -p /tmp/digests/main /tmp/digests/test + + docker buildx build \ + --platform="$PLATFORM" \ + --build-arg BUILD_DATE="$BUILD_DATE" \ + --build-arg VCS_REF="$VCS_REF" \ + --cache-from="type=registry,ref=graze/php-alpine:buildcache-${V}-${SLUG}" \ + --cache-to="type=registry,ref=graze/php-alpine:buildcache-${V}-${SLUG},mode=max" \ + --output="type=image,name=graze/php-alpine,push-by-digest=true,name-canonical=true,push=true" \ + --metadata-file=/tmp/main-metadata.json \ + "./$V" + + docker buildx build \ + --platform="$PLATFORM" \ + --build-arg BUILD_DATE="$BUILD_DATE" \ + --build-arg VCS_REF="$VCS_REF" \ + -f "./$V/debug.Dockerfile" \ + --cache-from="type=registry,ref=graze/php-alpine:buildcache-${V}-test-${SLUG}" \ + --cache-to="type=registry,ref=graze/php-alpine:buildcache-${V}-test-${SLUG},mode=max" \ + --output="type=image,name=graze/php-alpine,push-by-digest=true,name-canonical=true,push=true" \ + --metadata-file=/tmp/test-metadata.json \ + "./$V" + + main_digest=$(jq -r '."containerimage.digest"' /tmp/main-metadata.json) + test_digest=$(jq -r '."containerimage.digest"' /tmp/test-metadata.json) + touch "/tmp/digests/main/${main_digest#sha256:}" + touch "/tmp/digests/test/${test_digest#sha256:}" + + - name: Upload digests + uses: actions/upload-artifact@v4 + with: + name: digests-${{ matrix.version }}-${{ matrix.platform_slug }} + path: /tmp/digests + retention-days: 1 + + manifest: + name: PHP ${{ matrix.version }} manifest + needs: [matrix, build] + if: github.ref == 'refs/heads/master' && github.event_name == 'push' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + version: ${{ fromJson(needs.matrix.outputs.versions) }} + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: /tmp/digests + pattern: digests-${{ matrix.version }}-* + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to Docker Hub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Create multi-arch manifests + env: + V: ${{ matrix.version }} + run: | main_tags="-t graze/php-alpine:${V}" test_tags="-t graze/php-alpine:${V}-test" case "$V" in @@ -79,19 +171,13 @@ jobs: ;; esac - docker buildx build \ - --push \ - --platform=${{ matrix.platforms }} \ - --build-arg BUILD_DATE="$BUILD_DATE" \ - --build-arg VCS_REF="$VCS_REF" \ - $main_tags \ - "./$V" + main_sources=$(find /tmp/digests -path '*/main/*' -type f -printf 'graze/php-alpine@sha256:%f ') + test_sources=$(find /tmp/digests -path '*/test/*' -type f -printf 'graze/php-alpine@sha256:%f ') - docker buildx build \ - --push \ - --platform=${{ matrix.platforms }} \ - --build-arg BUILD_DATE="$BUILD_DATE" \ - --build-arg VCS_REF="$VCS_REF" \ - -f "./$V/debug.Dockerfile" \ - $test_tags \ - "./$V" + if [ -z "$main_sources" ] || [ -z "$test_sources" ]; then + echo "Missing digests for ${V}" >&2 + exit 1 + fi + + docker buildx imagetools create $main_tags $main_sources + docker buildx imagetools create $test_tags $test_sources diff --git a/versions.json b/versions.json index 17642f8..b423d83 100644 --- a/versions.json +++ b/versions.json @@ -8,7 +8,7 @@ {"version": "8.0", "platforms": "linux/amd64,linux/arm/v7,linux/arm64"}, {"version": "8.1", "platforms": "linux/amd64,linux/arm/v7,linux/arm64"}, {"version": "8.2", "platforms": "linux/amd64,linux/arm/v7,linux/arm64"}, - {"version": "8.3", "platforms": "linux/amd64,linux/arm/v7,linux/arm64"}, - {"version": "8.4", "platforms": "linux/amd64,linux/arm/v7,linux/arm64"}, - {"version": "8.5", "platforms": "linux/amd64,linux/arm/v7,linux/arm64"} + {"version": "8.3", "platforms": "linux/amd64,linux/arm64"}, + {"version": "8.4", "platforms": "linux/amd64,linux/arm64"}, + {"version": "8.5", "platforms": "linux/amd64,linux/arm64"} ] From b494b91b399950fd1574e7cc3470c3765552fa69 Mon Sep 17 00:00:00 2001 From: Rick Lam Date: Thu, 28 May 2026 17:37:29 +0100 Subject: [PATCH 2/3] Drop linux/arm/v7 from all versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No remaining versions target arm/v7, so QEMU is no longer needed in the build job — every platform now has a native GitHub-hosted runner (amd64 → ubuntu-latest, arm64 → ubuntu-24.04-arm). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/build.yml | 6 ------ versions.json | 10 +++++----- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4e13314..797d35f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,12 +67,6 @@ jobs: steps: - uses: actions/checkout@v6 - # arm/v7 has no native GitHub-hosted runner, so it still needs QEMU. - # amd64 and arm64 build natively on their respective runners. - - name: Set up QEMU - if: matrix.platform == 'linux/arm/v7' - uses: docker/setup-qemu-action@v4 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 diff --git a/versions.json b/versions.json index b423d83..bb3a844 100644 --- a/versions.json +++ b/versions.json @@ -3,11 +3,11 @@ {"version": "7.0", "platforms": "linux/amd64"}, {"version": "7.1", "platforms": "linux/amd64"}, {"version": "7.2", "platforms": "linux/amd64"}, - {"version": "7.3", "platforms": "linux/amd64,linux/arm/v7,linux/arm64"}, - {"version": "7.4", "platforms": "linux/amd64,linux/arm/v7,linux/arm64"}, - {"version": "8.0", "platforms": "linux/amd64,linux/arm/v7,linux/arm64"}, - {"version": "8.1", "platforms": "linux/amd64,linux/arm/v7,linux/arm64"}, - {"version": "8.2", "platforms": "linux/amd64,linux/arm/v7,linux/arm64"}, + {"version": "7.3", "platforms": "linux/amd64,linux/arm64"}, + {"version": "7.4", "platforms": "linux/amd64,linux/arm64"}, + {"version": "8.0", "platforms": "linux/amd64,linux/arm64"}, + {"version": "8.1", "platforms": "linux/amd64,linux/arm64"}, + {"version": "8.2", "platforms": "linux/amd64,linux/arm64"}, {"version": "8.3", "platforms": "linux/amd64,linux/arm64"}, {"version": "8.4", "platforms": "linux/amd64,linux/arm64"}, {"version": "8.5", "platforms": "linux/amd64,linux/arm64"} From 2947e98bbe4b296556b1a392e610c43fa8d347ed Mon Sep 17 00:00:00 2001 From: Rick Lam Date: Thu, 28 May 2026 17:43:01 +0100 Subject: [PATCH 3/3] Fix debug image base: pin to just-built main digest via --build-context Copilot review caught that the debug build's `FROM graze/php-alpine:${V}` would resolve to whatever `:${V}` was currently published (the previous master build, or nothing for a first-time version deploy) because the main image is now pushed by digest only. Override the FROM lookup with the digest we just built so the debug image is always derived from the matching commit's main image for the same arch. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/build.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 797d35f..ff48981 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -96,10 +96,18 @@ jobs: --metadata-file=/tmp/main-metadata.json \ "./$V" + main_digest=$(jq -r '."containerimage.digest"' /tmp/main-metadata.json) + + # The debug Dockerfile does `FROM graze/php-alpine:${V}`. We push the + # main image by digest only (no tag), so override the FROM lookup with + # the digest we just built — otherwise the debug image would derive + # from whatever `:${V}` is currently published (the previous build, or + # nothing on a first-time deploy). docker buildx build \ --platform="$PLATFORM" \ --build-arg BUILD_DATE="$BUILD_DATE" \ --build-arg VCS_REF="$VCS_REF" \ + --build-context "graze/php-alpine:${V}=docker-image://graze/php-alpine@${main_digest}" \ -f "./$V/debug.Dockerfile" \ --cache-from="type=registry,ref=graze/php-alpine:buildcache-${V}-test-${SLUG}" \ --cache-to="type=registry,ref=graze/php-alpine:buildcache-${V}-test-${SLUG},mode=max" \ @@ -107,7 +115,6 @@ jobs: --metadata-file=/tmp/test-metadata.json \ "./$V" - main_digest=$(jq -r '."containerimage.digest"' /tmp/main-metadata.json) test_digest=$(jq -r '."containerimage.digest"' /tmp/test-metadata.json) touch "/tmp/digests/main/${main_digest#sha256:}" touch "/tmp/digests/test/${test_digest#sha256:}"