From 4999f0648638266b6d1b46220bb7e8e571dd27d7 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 23 Oct 2026 13:26:27 +0530 Subject: [PATCH 01/17] feat(newrelic-daemon): pick the daemon binary for the build architecture The New Relic tarball ships newrelic-daemon.x64 and newrelic-daemon.aarch64; the hardcoded x64 copy made an arm64 build ship an amd64 binary. --- newrelic-daemon/Dockerfile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/newrelic-daemon/Dockerfile b/newrelic-daemon/Dockerfile index 209d346..714dab2 100644 --- a/newrelic-daemon/Dockerfile +++ b/newrelic-daemon/Dockerfile @@ -8,11 +8,16 @@ LABEL org.label-schema.name="newrelic-daemon" ENV NR_PORT=/run/newrelic/newrelic.sock RUN set -ex; \ + case "$(apk --print-arch)" in \ + x86_64) nr_arch=x64 ;; \ + aarch64) nr_arch=aarch64 ;; \ + *) echo "unsupported arch: $(apk --print-arch)" >&2; exit 1 ;; \ + esac; \ apk --no-cache add curl; \ export NR_FILE_URL="https://download.newrelic.com$(curl -s https://download.newrelic.com/php_agent/release/ | grep 'linux-musl.tar.gz' | sed 's/.*"\(.*\)".*/\1/')"; \ curl --connect-timeout 10 -o nr.tar.gz -fSL $NR_FILE_URL; \ tar -xf nr.tar.gz; \ - cp newrelic-php5-*/daemon/newrelic-daemon.x64 /usr/local/bin/newrelic-daemon; \ + cp newrelic-php5-*/daemon/newrelic-daemon.${nr_arch} /usr/local/bin/newrelic-daemon; \ mkdir -p /var/log/newrelic /run/newrelic; \ rm -rf newrelic-php5* nr.tar.gz; From 24e2a1938784bb750950ba8626249292f38c78bb Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 23 Oct 2026 14:11:41 +0530 Subject: [PATCH 02/17] feat(mailhog): build MailHog v1.0.1 from source for every architecture mailhog/mailhog:v1.0.1 is an amd64-only image from 2020 on golang:1.14 alpine 3.12, and upstream is unmaintained. Build the same v1.0.1 tag from its vendored sources in a Go builder stage and ship it on alpine 3.24.3 with the same user (mailhog, uid 1000), workdir, entrypoint and ports. The Go toolchain and its GOPATH/GOLANG_VERSION env are no longer in the image, which shrinks it from 572 MB to 29 MB. --- mailhog/Dockerfile | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/mailhog/Dockerfile b/mailhog/Dockerfile index fe4349e..1c7e442 100644 --- a/mailhog/Dockerfile +++ b/mailhog/Dockerfile @@ -1,4 +1,21 @@ -FROM mailhog/mailhog:v1.0.1 +# mailhog/mailhog is amd64-only and unmaintained, so build the same v1.0.1 from source for every arch. +FROM golang:1.27.3-alpine3.24 AS build +ARG MAILHOG_VERSION=v1.0.1 +RUN apk add --no-cache git \ + && git clone --depth 1 --branch "${MAILHOG_VERSION}" https://github.com/mailhog/MailHog.git /go/src/github.com/mailhog/MailHog +WORKDIR /go/src/github.com/mailhog/MailHog +# v1.0.1 predates Go modules; build from its vendor/ tree in GOPATH mode. +RUN CGO_ENABLED=0 GO111MODULE=off go build -trimpath -ldflags '-s -w' -o /MailHog . + +FROM alpine:3.24.3 LABEL org.label-schema.schema-version="1.0.0" LABEL org.label-schema.vendor="EasyEngine" LABEL org.label-schema.name="mailhog" + +# Same user, workdir, entrypoint and ports as mailhog/mailhog:v1.0.1. +RUN adduser -D -u 1000 mailhog +COPY --from=build /MailHog /usr/local/bin/MailHog +USER mailhog +WORKDIR /home/mailhog +ENTRYPOINT ["MailHog"] +EXPOSE 1025 8025 From 8a598f3e125760910e87fec22f1c1b02c72d14fe Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 23 Oct 2026 15:12:04 +0530 Subject: [PATCH 03/17] test: add a smoke test for every image One script for CI and local runs: it checks the image platform and runs each image's main checks (php modules, gd formats, composer, wp-cli, php-fpm -t and New Relic; nginx -t; docker-gen and the proxy template; redis PING; mariadb SELECT VERSION(); postfix status; ofelia validate; a MailHog SMTP round trip; the New Relic daemon). --- .github/scripts/smoke-test.sh | 98 +++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100755 .github/scripts/smoke-test.sh diff --git a/.github/scripts/smoke-test.sh b/.github/scripts/smoke-test.sh new file mode 100755 index 0000000..c67e513 --- /dev/null +++ b/.github/scripts/smoke-test.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# Smoke-test one built image on one arch. +# Usage: smoke-test.sh +# image-name: php8.1..php8.5, nginx, nginx-proxy, postfix, redis, cron, mailhog, mariadb, newrelic-daemon +# arch: amd64 | arm64 +set -euo pipefail + +name=$1 ref=$2 arch=$3 +cid="" +cleanup() { [ -n "$cid" ] && docker rm -f "$cid" >/dev/null 2>&1 || true; } +trap cleanup EXIT + +fail() { echo "FAIL [$name/$arch]: $*" >&2; exit 1; } +run() { docker run --rm --platform "linux/$arch" "$@"; } +start() { cid=$(docker run -d --platform "linux/$arch" "$@"); } +# Retry a command inside the started container; emulated arm64 can be slow to boot. +wait_exec() { + local tries=$1; shift + for _ in $(seq "$tries"); do + if docker exec "$cid" "$@" >/dev/null 2>&1; then return 0; fi + sleep 2 + done + docker logs "$cid" 2>&1 | tail -20 >&2 + return 1 +} + +got=$(docker image inspect -f '{{.Os}}/{{.Architecture}}' "$ref") +[ "$got" = "linux/$arch" ] || fail "image platform is $got, want linux/$arch" + +case $name in +php*) + ver=${name#php} + run "$ref" php -v + mods=$(run "$ref" php -m | tr '[:upper:]' '[:lower:]') + want="apcu bcmath calendar exif gd gmp imagick imap intl mcrypt memcached mysqli pcntl pdo_mysql redis shmop soap sockets sysvsem sysvshm timezonedb zend.opcache zip" + # gmagick and New Relic are not built for 8.5 (see php/8.5/Dockerfile). + [ "$ver" != "8.5" ] && want="$want gmagick newrelic" + for m in $want; do + grep -qx "${m/./ }" <<<"$mods" || fail "php module $m missing" + done + run "$ref" php -r '$g = gd_info(); exit($g["WebP Support"] && $g["FreeType Support"] && $g["JPEG Support"] && $g["PNG Support"] ? 0 : 1);' \ + || fail "gd lacks WebP/FreeType/JPEG/PNG" + run "$ref" php -r 'exit(count(Imagick::queryFormats()) > 50 ? 0 : 1);' || fail "imagick has too few formats" + run "$ref" composer --version + run "$ref" wp --version + run "$ref" php-fpm -t + if [ "$ver" != "8.5" ]; then + run "$ref" sh -c '/usr/bin/newrelic-daemon --version | head -1' || fail "newrelic-daemon missing" + fi + echo "modules: $(tr '\n' ' ' <<<"$mods")" + ;; +nginx) + run --entrypoint sh "$ref" -c 'echo "server { listen 80; }" > /usr/local/openresty/nginx/conf/conf.d/main.conf && nginx -V 2>&1 | head -1 && nginx -t' + ;; +nginx-proxy) + run --entrypoint sh "$ref" -c 'nginx -v && docker-gen -version && command -v forego htpasswd && grep -q "define \"acl\"" /app/nginx.tmpl && nginx -t' + ;; +redis) + run "$ref" redis-server --version + extra=() + # Redis's arm64 fork/COW self-test fails under QEMU user emulation only; native arm64 must pass without this. + host=$(uname -m) + [ "$arch" = arm64 ] && [ "$host" != aarch64 ] && extra=(--ignore-warnings ARM64-COW-BUG) + start "$ref" redis-server /usr/local/etc/redis/redis.conf "${extra[@]}" + wait_exec 30 sh -c '[ "$(redis-cli ping)" = PONG ]' || fail "redis did not answer PING" + docker exec "$cid" test -f /data/purge_all_cache.lua || fail "purge_all_cache.lua missing" + ;; +mariadb) + start -e MARIADB_ROOT_PASSWORD=smoke "$ref" + wait_exec 90 mariadb -uroot -psmoke -e 'SELECT 1' || fail "mariadb did not start" + docker exec "$cid" mariadb -uroot -psmoke -N -e 'SELECT VERSION()' + ;; +postfix) + run "$ref" postconf -h mail_version + start "$ref" + wait_exec 30 postfix status || fail "postfix did not start" + ;; +cron) + run --entrypoint sh "$ref" -c 'printf "[job-local \"smoke\"]\nschedule = @every 1h\ncommand = true\n" > /tmp/c.ini && ofelia validate --config=/tmp/c.ini' + ;; +mailhog) + start "$ref" -invite-jim=false + wait_exec 30 sh -c 'printf "GET /api/v2/messages HTTP/1.0\r\n\r\n" | nc 127.0.0.1 8025 | grep -q "\"total\""' || fail "mailhog API not up" + docker exec "$cid" sh -c 'printf "HELO smoke\r\nMAIL FROM:\r\nRCPT TO:\r\nDATA\r\nSubject: smoke\r\n\r\nhi\r\n.\r\nQUIT\r\n" | nc 127.0.0.1 1025 >/dev/null' + wait_exec 10 sh -c 'printf "GET /api/v2/messages HTTP/1.0\r\n\r\n" | nc 127.0.0.1 8025 | grep -q "\"total\":1"' || fail "mailhog did not receive the test mail" + ;; +newrelic-daemon) + run "$ref" /usr/local/bin/newrelic-daemon --version + start "$ref" + sleep 5 + [ "$(docker inspect -f '{{.State.Running}}' "$cid")" = true ] || { docker logs "$cid" >&2; fail "newrelic-daemon exited"; } + ;; +*) + fail "unknown image" + ;; +esac + +echo "OK [$name/$arch] $ref" From 183e049d3503038f47dbeb68e4dba30c45966559 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 23 Oct 2026 16:10:45 +0530 Subject: [PATCH 04/17] ci: build and publish images for linux/amd64 and linux/arm64 Each image is built and smoke-tested on a native runner per arch (ubuntu-24.04 and ubuntu-24.04-arm). On v* tags in EasyEngine/dockerfiles the per-arch images are pushed by digest and a merge job creates the multi-arch easyengine/:, then checks that it lists both platforms. The merge runs per image, so one failed image doesn't block the others, and an image that is missing an arch is not published. Pull requests build and test both arches without pushing. Other runs (workflow_dispatch, tags on forks) are dry runs: the per-arch OCI layouts go through artifacts into a local registry, and the same manifest step runs against it. No layer cache, so tag builds still fetch current packages. --- .github/workflows/test_and_publish.yml | 188 +++++++++++++++++-------- 1 file changed, 133 insertions(+), 55 deletions(-) diff --git a/.github/workflows/test_and_publish.yml b/.github/workflows/test_and_publish.yml index a802918..80a8201 100644 --- a/.github/workflows/test_and_publish.yml +++ b/.github/workflows/test_and_publish.yml @@ -1,7 +1,9 @@ # # -# # This action will publish EasyEngine docker image. -# # This is set to publish images when tags are created, -# # and test them by just building them on PR. +# # This action will publish EasyEngine docker images for linux/amd64 and linux/arm64. +# # Each arch is built and smoke-tested on a native runner; on tags the per-arch +# # images are pushed by digest and merged into one multi-arch tag. +# # PRs only build and test. Any other run (workflow_dispatch, tags on forks) is a +# # dry run that publishes into a throwaway local registry instead of Docker Hub. # # name: Test & Publish EE docker images @@ -10,99 +12,175 @@ on: push: tags: - v* + workflow_dispatch: + +env: + # Only v* tag pushes to the upstream repo reach Docker Hub. + PUBLISH: ${{ github.event_name == 'push' && github.repository == 'EasyEngine/dockerfiles' && github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }} + DRY_RUN: ${{ github.event_name != 'pull_request' && !(github.event_name == 'push' && github.repository == 'EasyEngine/dockerfiles' && github.ref_type == 'tag' && startsWith(github.ref_name, 'v')) }} jobs: - php: #---------------------------------------------------------------------- + build: #---------------------------------------------------------------------- - runs-on: ubuntu-latest - name: Build and push PHP ${{ matrix.php }} + runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} + name: Build ${{ matrix.image }} (${{ matrix.arch }}) strategy: fail-fast: false matrix: - php: ['8.1', '8.2', '8.3', '8.4', '8.5'] + image: ['php8.1', 'php8.2', 'php8.3', 'php8.4', 'php8.5', 'nginx', 'nginx-proxy', 'postfix', 'redis', 'cron', 'mailhog', 'mariadb', 'newrelic-daemon'] + arch: ['amd64', 'arm64'] steps: - name: Checkout uses: actions/checkout@v7 - - name: Set up QEMU - uses: docker/setup-qemu-action@v4 + - name: Build context + id: ctx + run: | + IMG=${{ matrix.image }} + case $IMG in php*) echo "dir=php/${IMG#php}" ;; *) echo "dir=$IMG" ;; esac >> "$GITHUB_OUTPUT" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - - name: Get tag - id: tag - run: echo "tag=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT + - name: Build ${{ matrix.image }} image + uses: docker/build-push-action@v7 + with: + context: ${{ steps.ctx.outputs.dir }} + platforms: linux/${{ matrix.arch }} + load: true + tags: easyengine/${{ matrix.image }}:test - - name: Img tag - id: img-tag - run: | - PHP=${{ matrix.php }} - echo "tag=${PHP/stable/}" >> $GITHUB_OUTPUT + - name: Smoke test + run: .github/scripts/smoke-test.sh ${{ matrix.image }} easyengine/${{ matrix.image }}:test ${{ matrix.arch }} - name: Login to DockerHub uses: docker/login-action@v4 - if: github.event_name != 'pull_request' + if: env.PUBLISH == 'true' with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build PHP - ${{ matrix.php }} image + # Same builder as above, so this reuses the tested layers instead of rebuilding. + - name: Push ${{ matrix.image }} by digest + id: push uses: docker/build-push-action@v7 - if: github.event_name == 'pull_request' + if: env.PUBLISH == 'true' || env.DRY_RUN == 'true' with: - context: php/${{ matrix.php }} - tags: easyengine/php${{ steps.img-tag.outputs.tag }}:test + context: ${{ steps.ctx.outputs.dir }} + platforms: linux/${{ matrix.arch }} + outputs: ${{ env.PUBLISH == 'true' && format('type=image,name=easyengine/{0},push-by-digest=true,name-canonical=true,push=true', matrix.image) || format('type=oci,tar=false,dest={0}/oci', runner.temp) }} - - name: Build and push PHP - ${{ matrix.php }} image - uses: docker/build-push-action@v7 - if: github.event_name != 'pull_request' + - name: Export digest + if: steps.push.outcome == 'success' + run: | + mkdir -p "$RUNNER_TEMP/digests" + touch "$RUNNER_TEMP/digests/${DIGEST#sha256:}" + env: + DIGEST: ${{ steps.push.outputs.digest }} + + - name: Upload digest + uses: actions/upload-artifact@v7 + if: steps.push.outcome == 'success' with: - context: php/${{ matrix.php }} - push: true - tags: easyengine/php${{ steps.img-tag.outputs.tag }}:${{ steps.tag.outputs.tag }} - - other-images: #---------------------------------------------------------------------- - - runs-on: ubuntu-latest - name: Build and push ${{ matrix.others }} + name: digests-${{ matrix.image }}-${{ matrix.arch }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + - name: Upload OCI layout (dry run) + uses: actions/upload-artifact@v7 + if: steps.push.outcome == 'success' && env.DRY_RUN == 'true' + with: + name: oci-${{ matrix.image }}-${{ matrix.arch }} + path: ${{ runner.temp }}/oci + if-no-files-found: error + retention-days: 1 + + merge: #---------------------------------------------------------------------- + + # Runs per image even if other images failed, like the old independent jobs. + if: ${{ !cancelled() && github.event_name != 'pull_request' }} + needs: build + runs-on: ubuntu-24.04 + name: Publish ${{ matrix.image }} manifest strategy: fail-fast: false matrix: - others: ['nginx', 'nginx-proxy', 'postfix', 'redis', 'cron', 'mailhog', 'mariadb', 'newrelic-daemon'] + image: ['php8.1', 'php8.2', 'php8.3', 'php8.4', 'php8.5', 'nginx', 'nginx-proxy', 'postfix', 'redis', 'cron', 'mailhog', 'mariadb', 'newrelic-daemon'] steps: - - name: Checkout - uses: actions/checkout@v7 + - name: Download digests + uses: actions/download-artifact@v8 + with: + pattern: digests-${{ matrix.image }}-* + path: ${{ runner.temp }}/digests + merge-multiple: true - - name: Set up QEMU - uses: docker/setup-qemu-action@v4 + - name: Check both arches were built + working-directory: ${{ runner.temp }}/digests + run: | + find . -type f -printf '%f\n' + [ "$(find . -type f | wc -l)" -eq 2 ] || { echo "::error::expected one digest per arch; not publishing a partial image"; exit 1; } - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - - name: Get tag - id: tag - run: echo "tag=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT - - name: Login to DockerHub uses: docker/login-action@v4 - if: github.event_name != 'pull_request' + if: env.PUBLISH == 'true' with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build ${{ matrix.others }} image - uses: docker/build-push-action@v7 - if: github.event_name == 'pull_request' + - name: Image name and tag + id: meta + run: | + if [ "$PUBLISH" = true ]; then + echo "image=easyengine/${{ matrix.image }}" >> "$GITHUB_OUTPUT" + echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" + else + echo "image=localhost:5000/easyengine/${{ matrix.image }}" >> "$GITHUB_OUTPUT" + echo "tag=dry-run" >> "$GITHUB_OUTPUT" + fi + + - name: Download OCI layouts (dry run) + uses: actions/download-artifact@v8 + if: env.DRY_RUN == 'true' with: - context: ${{ matrix.others }} - tags: easyengine/${{ matrix.others }}:test + pattern: oci-${{ matrix.image }}-* + path: ${{ runner.temp }}/oci - - name: Build and push ${{ matrix.others }} image - uses: docker/build-push-action@v7 - if: github.event_name != 'pull_request' - with: - context: ${{ matrix.others }} - push: true - tags: easyengine/${{ matrix.others }}:${{ steps.tag.outputs.tag }} + # Stand-in for the per-arch push-by-digest: copy each arch's index into a local registry, keeping its digest. + - name: Push per-arch images to a local registry (dry run) + if: env.DRY_RUN == 'true' + run: | + docker run -d --name registry -p 5000:5000 registry:3 + for _ in $(seq 30); do curl -fs http://localhost:5000/v2/ >/dev/null && break; sleep 1; done + for d in "$RUNNER_TEMP"/oci/oci-*; do + docker buildx imagetools create --prefer-index=false -t "$IMAGE:${d##*-}" "oci-layout://$d" + done + for f in "$RUNNER_TEMP"/digests/*; do + docker buildx imagetools inspect "$IMAGE@sha256:${f##*/}" >/dev/null + done + env: + IMAGE: ${{ steps.meta.outputs.image }} + + - name: Create manifest list and push + working-directory: ${{ runner.temp }}/digests + run: | + refs=() + for f in *; do refs+=("$IMAGE@sha256:$f"); done + docker buildx imagetools create -t "$IMAGE:$TAG" "${refs[@]}" + env: + IMAGE: ${{ steps.meta.outputs.image }} + TAG: ${{ steps.meta.outputs.tag }} + + - name: Verify platforms + run: | + docker buildx imagetools inspect "$IMAGE:$TAG" + platforms=$(docker buildx imagetools inspect --raw "$IMAGE:$TAG" | jq -r '[.manifests[].platform | select(.architecture != "unknown") | .os + "/" + .architecture] | sort | join(",")') + echo "platforms: $platforms" + [ "$platforms" = "linux/amd64,linux/arm64" ] || { echo "::error::unexpected platforms: $platforms"; exit 1; } + env: + IMAGE: ${{ steps.meta.outputs.image }} + TAG: ${{ steps.meta.outputs.tag }} From 0eeb726a4482064f5f81cc61936d53126115a352 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 23 Oct 2026 16:49:11 +0530 Subject: [PATCH 05/17] fix(ci): don't let the nginx merge pick up nginx-proxy's digests The artifact pattern digests-nginx-* also matched digests-nginx-proxy-*, so the nginx merge saw four digests and refused to publish. Match the two arches explicitly. --- .github/workflows/test_and_publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_and_publish.yml b/.github/workflows/test_and_publish.yml index 80a8201..402f700 100644 --- a/.github/workflows/test_and_publish.yml +++ b/.github/workflows/test_and_publish.yml @@ -112,7 +112,7 @@ jobs: - name: Download digests uses: actions/download-artifact@v8 with: - pattern: digests-${{ matrix.image }}-* + pattern: digests-${{ matrix.image }}-{amd64,arm64} path: ${{ runner.temp }}/digests merge-multiple: true @@ -147,7 +147,7 @@ jobs: uses: actions/download-artifact@v8 if: env.DRY_RUN == 'true' with: - pattern: oci-${{ matrix.image }}-* + pattern: oci-${{ matrix.image }}-{amd64,arm64} path: ${{ runner.temp }}/oci # Stand-in for the per-arch push-by-digest: copy each arch's index into a local registry, keeping its digest. From fa296e71327adf79f93909645050018bd89af98b Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 23 Oct 2026 17:41:37 +0530 Subject: [PATCH 06/17] test: check the ELF architecture of each image's key binaries A binary for the wrong arch can still run through QEMU/binfmt, so version checks alone don't prove an arm64 image ships arm64 binaries. --- .github/scripts/smoke-test.sh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/scripts/smoke-test.sh b/.github/scripts/smoke-test.sh index c67e513..c808546 100755 --- a/.github/scripts/smoke-test.sh +++ b/.github/scripts/smoke-test.sh @@ -24,6 +24,20 @@ wait_exec() { return 1 } +# A wrong-arch binary can still run (QEMU/binfmt), so check the ELF e_machine of the key binaries too. +elf_check() { + local want=3e + [ "$arch" = arm64 ] && want=b7 + run --entrypoint sh "$ref" -c ' + for f; do + case $f in /*) p=$f ;; *) p=$(command -v "$f") ;; esac + echo "$f $(od -An -t x1 -j 18 -N 1 "$p" 2>/dev/null | tr -d " ")" + done' sh "$@" | while read -r f m; do + [ "$m" = "$want" ] || fail "$f is not a linux/$arch ELF (e_machine=${m:-none})" + echo "elf ok: $f" + done +} + got=$(docker image inspect -f '{{.Os}}/{{.Architecture}}' "$ref") [ "$got" = "linux/$arch" ] || fail "image platform is $got, want linux/$arch" @@ -31,6 +45,11 @@ case $name in php*) ver=${name#php} run "$ref" php -v + ext=$(run "$ref" php -r 'echo ini_get("extension_dir");') + bins="php php-fpm $ext/imagick.so $ext/redis.so $ext/memcached.so" + [ "$ver" != "8.5" ] && bins="$bins $ext/newrelic.so /usr/bin/newrelic-daemon" + # shellcheck disable=SC2086 + elf_check $bins mods=$(run "$ref" php -m | tr '[:upper:]' '[:lower:]') want="apcu bcmath calendar exif gd gmp imagick imap intl mcrypt memcached mysqli pcntl pdo_mysql redis shmop soap sockets sysvsem sysvshm timezonedb zend.opcache zip" # gmagick and New Relic are not built for 8.5 (see php/8.5/Dockerfile). @@ -50,12 +69,15 @@ php*) echo "modules: $(tr '\n' ' ' <<<"$mods")" ;; nginx) + elf_check openresty run --entrypoint sh "$ref" -c 'echo "server { listen 80; }" > /usr/local/openresty/nginx/conf/conf.d/main.conf && nginx -V 2>&1 | head -1 && nginx -t' ;; nginx-proxy) + elf_check nginx docker-gen forego run --entrypoint sh "$ref" -c 'nginx -v && docker-gen -version && command -v forego htpasswd && grep -q "define \"acl\"" /app/nginx.tmpl && nginx -t' ;; redis) + elf_check redis-server run "$ref" redis-server --version extra=() # Redis's arm64 fork/COW self-test fails under QEMU user emulation only; native arm64 must pass without this. @@ -66,25 +88,30 @@ redis) docker exec "$cid" test -f /data/purge_all_cache.lua || fail "purge_all_cache.lua missing" ;; mariadb) + elf_check mariadbd start -e MARIADB_ROOT_PASSWORD=smoke "$ref" wait_exec 90 mariadb -uroot -psmoke -e 'SELECT 1' || fail "mariadb did not start" docker exec "$cid" mariadb -uroot -psmoke -N -e 'SELECT VERSION()' ;; postfix) + elf_check /usr/sbin/postfix /usr/lib/postfix/smtpd run "$ref" postconf -h mail_version start "$ref" wait_exec 30 postfix status || fail "postfix did not start" ;; cron) + elf_check ofelia run --entrypoint sh "$ref" -c 'printf "[job-local \"smoke\"]\nschedule = @every 1h\ncommand = true\n" > /tmp/c.ini && ofelia validate --config=/tmp/c.ini' ;; mailhog) + elf_check MailHog start "$ref" -invite-jim=false wait_exec 30 sh -c 'printf "GET /api/v2/messages HTTP/1.0\r\n\r\n" | nc 127.0.0.1 8025 | grep -q "\"total\""' || fail "mailhog API not up" docker exec "$cid" sh -c 'printf "HELO smoke\r\nMAIL FROM:\r\nRCPT TO:\r\nDATA\r\nSubject: smoke\r\n\r\nhi\r\n.\r\nQUIT\r\n" | nc 127.0.0.1 1025 >/dev/null' wait_exec 10 sh -c 'printf "GET /api/v2/messages HTTP/1.0\r\n\r\n" | nc 127.0.0.1 8025 | grep -q "\"total\":1"' || fail "mailhog did not receive the test mail" ;; newrelic-daemon) + elf_check /usr/local/bin/newrelic-daemon run "$ref" /usr/local/bin/newrelic-daemon --version start "$ref" sleep 5 From 9bea0e183d75945393cc90651b326d109781046d Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 23 Oct 2026 18:25:06 +0530 Subject: [PATCH 07/17] ci: log in to Docker Hub before the first build on tag runs Keeps base-image pulls authenticated on publish runs, as in the old workflow. --- .github/workflows/test_and_publish.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test_and_publish.yml b/.github/workflows/test_and_publish.yml index 402f700..8fe19e7 100644 --- a/.github/workflows/test_and_publish.yml +++ b/.github/workflows/test_and_publish.yml @@ -43,6 +43,13 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 + - name: Login to DockerHub + uses: docker/login-action@v4 + if: env.PUBLISH == 'true' + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build ${{ matrix.image }} image uses: docker/build-push-action@v7 with: @@ -54,13 +61,6 @@ jobs: - name: Smoke test run: .github/scripts/smoke-test.sh ${{ matrix.image }} easyengine/${{ matrix.image }}:test ${{ matrix.arch }} - - name: Login to DockerHub - uses: docker/login-action@v4 - if: env.PUBLISH == 'true' - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - # Same builder as above, so this reuses the tested layers instead of rebuilding. - name: Push ${{ matrix.image }} by digest id: push From 7d66d5fe512d5bcc3d84be0cbe4141a0689ba3f3 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 23 Oct 2026 19:00:34 +0530 Subject: [PATCH 08/17] ci: pass GITHUB_TOKEN to the image builds as the github_token secret A Dockerfile can mount it (RUN --mount=type=secret,id=github_token) to authenticate GitHub downloads instead of hitting the anonymous API rate limit. BuildKit secrets never land in a layer or the cache key, so the publish build still reuses the tested layers, and builds that don't mount it are unaffected. --- .github/workflows/test_and_publish.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test_and_publish.yml b/.github/workflows/test_and_publish.yml index 8fe19e7..ada9106 100644 --- a/.github/workflows/test_and_publish.yml +++ b/.github/workflows/test_and_publish.yml @@ -57,6 +57,8 @@ jobs: platforms: linux/${{ matrix.arch }} load: true tags: easyengine/${{ matrix.image }}:test + secrets: | + github_token=${{ secrets.GITHUB_TOKEN }} - name: Smoke test run: .github/scripts/smoke-test.sh ${{ matrix.image }} easyengine/${{ matrix.image }}:test ${{ matrix.arch }} @@ -70,6 +72,8 @@ jobs: context: ${{ steps.ctx.outputs.dir }} platforms: linux/${{ matrix.arch }} outputs: ${{ env.PUBLISH == 'true' && format('type=image,name=easyengine/{0},push-by-digest=true,name-canonical=true,push=true', matrix.image) || format('type=oci,tar=false,dest={0}/oci', runner.temp) }} + secrets: | + github_token=${{ secrets.GITHUB_TOKEN }} - name: Export digest if: steps.push.outcome == 'success' From 7c9384e25a2d3c7626131663e8c221142e11d8c8 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 23 Oct 2026 22:18:37 +0530 Subject: [PATCH 09/17] ci: add a one-time workflow for arm64 builds of legacy images php5.6, php7.0, php7.2, php7.3, php7.4, php8.0 and mariadb (10.11) are no longer in the build matrix, so they stay amd64-only. legacy-arm64.yml (workflow_dispatch only) rebuilds each published amd64 image for arm64 from a recipe and publishes a new tag whose index holds the untouched amd64 image, by digest, plus the new arm64 one. Each recipe names the git ref that matches the published image's history and layers (for php5.6-7.3 that is not the v4.7.4 tree), plus a small patch: a Debian snapshot of the amd64 build date for the frozen bases, PECL versions pinned to the ones in the amd64 image, and for mariadb the 10.11.14 base index that v4.9.1 was really built from. Before anything is pushed, the arm64 image must match the published amd64 one: image config, PHP version, php -m, extension versions, ini values, php-fpm -t, every file under /usr/local/etc (MariaDB: SELECT VERSION(), server variables, /etc/mysql), and its key binaries must be arm64 ELF files. The new tag is refused if it already exists. Without publish it is a dry run into a runner-local registry. New Relic ships no aarch64 agent for PHP 7.x, so php7.0-7.4 refuse to build until their recipe says "newrelic": "skip". --- .github/legacy-arm64/mariadb.patch | 9 ++ .github/legacy-arm64/php5.6.patch | 20 +++ .github/legacy-arm64/php7.0.patch | 36 +++++ .github/legacy-arm64/php7.2.patch | 22 +++ .github/legacy-arm64/php7.3.patch | 30 ++++ .github/legacy-arm64/php7.4.patch | 22 +++ .github/legacy-arm64/php8.0.patch | 31 ++++ .github/legacy-arm64/recipes.json | 65 +++++++++ .github/scripts/legacy-arm64.sh | 126 +++++++++++++++++ .github/workflows/legacy-arm64.yml | 219 +++++++++++++++++++++++++++++ 10 files changed, 580 insertions(+) create mode 100644 .github/legacy-arm64/mariadb.patch create mode 100644 .github/legacy-arm64/php5.6.patch create mode 100644 .github/legacy-arm64/php7.0.patch create mode 100644 .github/legacy-arm64/php7.2.patch create mode 100644 .github/legacy-arm64/php7.3.patch create mode 100644 .github/legacy-arm64/php7.4.patch create mode 100644 .github/legacy-arm64/php8.0.patch create mode 100644 .github/legacy-arm64/recipes.json create mode 100755 .github/scripts/legacy-arm64.sh create mode 100644 .github/workflows/legacy-arm64.yml diff --git a/.github/legacy-arm64/mariadb.patch b/.github/legacy-arm64/mariadb.patch new file mode 100644 index 0000000..a391494 --- /dev/null +++ b/.github/legacy-arm64/mariadb.patch @@ -0,0 +1,9 @@ +--- a/Dockerfile ++++ b/Dockerfile +@@ -1,4 +1,5 @@ +-FROM mariadb:11.5 ++# v4.9.1 was built from this MariaDB 10.11.14 index (docker-library/repo-info, 2025-09-02), not from the 11.5 named here. ++FROM mariadb:10.11.14@sha256:e4ad7c5ad8874c2b049cef7566ccf534856b9fcde1c3c89c3251e8fa6fd6915c + LABEL org.label-schema.schema-version="1.0.0" + LABEL org.label-schema.vendor="EasyEngine" + LABEL org.label-schema.name="db" diff --git a/.github/legacy-arm64/php5.6.patch b/.github/legacy-arm64/php5.6.patch new file mode 100644 index 0000000..57619af --- /dev/null +++ b/.github/legacy-arm64/php5.6.patch @@ -0,0 +1,20 @@ +--- a/Dockerfile ++++ b/Dockerfile +@@ -1,5 +1,8 @@ + FROM php:5.6-fpm + ++# Frozen base: its Debian suites are only on snapshot.debian.org now; use the snapshot from the amd64 build date. ++RUN sed -i -E 's#^deb http://(deb|security)\.debian\.org/(debian-security|debian) #deb [check-valid-until=no] http://snapshot.debian.org/archive/\2/20230220T000000Z #' /etc/apt/sources.list ++ + LABEL maintainer="Riddhesh Sanghvi , Devarshi Sathiya " + LABEL org.label-schema.schema-version="1.0.0-rc1" + LABEL org.label-schema.vendor="EasyEngine" +@@ -27,7 +30,7 @@ + vim \ + zip + +-RUN pecl install imagick memcached-2.2.0; \ ++RUN pecl install imagick-3.7.0 memcached-2.2.0; \ + printf "\n" | pecl install mcrypt-1.0.1; \ + printf "\n" | pecl install redis; \ + docker-php-ext-configure gd --with-png-dir=/usr --with-jpeg-dir=/usr; \ diff --git a/.github/legacy-arm64/php7.0.patch b/.github/legacy-arm64/php7.0.patch new file mode 100644 index 0000000..dd1e1a3 --- /dev/null +++ b/.github/legacy-arm64/php7.0.patch @@ -0,0 +1,36 @@ +--- a/Dockerfile ++++ b/Dockerfile +@@ -1,14 +1,18 @@ + FROM php:7.0-fpm@sha256:8e8644c0fd61424939fff072ca617469550bbb659158301a2dcadf334746a1c2 + ++# Frozen base: its Debian suites are only on snapshot.debian.org now; use the snapshot from the amd64 build date. ++RUN sed -i -E 's#^deb http://(deb|security)\.debian\.org/(debian-security|debian) #deb [check-valid-until=no] http://snapshot.debian.org/archive/\2/20230421T000000Z #' /etc/apt/sources.list ++ + LABEL maintainer="Devarshi Sathiya , Riddhesh Sanghvi " + LABEL org.label-schema.schema-version="1.0.0-rc1" + LABEL org.label-schema.vendor="EasyEngine" + LABEL org.label-schema.name="php" + + RUN set -ex; \ +- apt-get update; \ ++ apt-get update && apt-get upgrade -y; \ + apt-get install -y --no-install-recommends \ + imagemagick \ ++ openssl \ + less \ + mariadb-client msmtp \ + libc-client-dev \ +@@ -29,10 +33,10 @@ + vim \ + zip + +-RUN pecl install imagick; \ +- pecl install memcached; \ ++RUN pecl install imagick-3.7.0; \ ++ pecl install memcached-3.2.0; \ + pecl install mcrypt-1.0.3; \ +- pecl install redis; \ ++ pecl install redis-5.3.7; \ + docker-php-ext-configure gd --enable-gd-native-ttf --with-freetype-dir=/usr/include/freetype2 --with-png-dir=/usr/include --with-jpeg-dir=/usr/include --with-webp-dir=/usr/include; \ + docker-php-ext-configure zip; \ + docker-php-ext-install gd; \ diff --git a/.github/legacy-arm64/php7.2.patch b/.github/legacy-arm64/php7.2.patch new file mode 100644 index 0000000..fb5922e --- /dev/null +++ b/.github/legacy-arm64/php7.2.patch @@ -0,0 +1,22 @@ +--- a/Dockerfile ++++ b/Dockerfile +@@ -1,5 +1,8 @@ + FROM php:7.2-fpm@sha256:a8d1ebf6390aa47574fd8aec875d907a4fa1169de8c88bd256890921e6454436 + ++# Frozen base: its Debian suites are only on snapshot.debian.org now; use the snapshot from the amd64 build date. ++RUN sed -i -E 's#^deb http://(deb|security)\.debian\.org/(debian-security|debian) #deb [check-valid-until=no] http://snapshot.debian.org/archive/\2/20241009T000000Z #' /etc/apt/sources.list ++ + LABEL maintainer="Devarshi Sathiya , Riddhesh Sanghvi " + LABEL org.label-schema.schema-version="1.0.0-rc1" + LABEL org.label-schema.vendor="EasyEngine" +@@ -28,8 +31,8 @@ + vim \ + zip + +-RUN pecl install imagick; \ +- pecl install memcached; \ ++RUN pecl install imagick-3.7.0; \ ++ pecl install memcached-3.2.0; \ + pecl install mcrypt-1.0.3; \ + pecl install redis; \ + docker-php-ext-configure gd --with-freetype-dir=/usr/include/freetype2 --with-png-dir=/usr/include --with-jpeg-dir=/usr/include; \ diff --git a/.github/legacy-arm64/php7.3.patch b/.github/legacy-arm64/php7.3.patch new file mode 100644 index 0000000..eec7287 --- /dev/null +++ b/.github/legacy-arm64/php7.3.patch @@ -0,0 +1,30 @@ +--- a/Dockerfile ++++ b/Dockerfile +@@ -1,5 +1,8 @@ + FROM php:7.3-fpm@sha256:a810876c96407cbbddef949379d2057f4f85d5c1d892dcf2c8d9503239549990 + ++# Frozen base: its Debian suites are only on snapshot.debian.org now; use the snapshot from the amd64 build date. ++RUN sed -i -E 's#^deb http://(deb|security)\.debian\.org/(debian-security|debian) #deb [check-valid-until=no] http://snapshot.debian.org/archive/\2/20241009T000000Z #' /etc/apt/sources.list ++ + LABEL maintainer="Devarshi Sathiya , Riddhesh Sanghvi " + LABEL org.label-schema.schema-version="1.0.0" + LABEL org.label-schema.vendor="EasyEngine" +@@ -30,13 +33,13 @@ + vim \ + zip + +-RUN pecl install imagick; \ +- pecl install memcached; \ +- pecl install mcrypt; \ ++RUN pecl install imagick-3.7.0; \ ++ pecl install memcached-3.2.0; \ ++ pecl install mcrypt-1.0.7; \ + pecl install redis; \ +- pecl install apcu; \ ++ pecl install apcu-5.1.24; \ + pecl install gmagick-2.0.6RC1; \ +- pecl install timezonedb; \ ++ pecl install timezonedb-2024.2; \ + docker-php-ext-configure gd --with-freetype-dir=/usr/include/freetype2 --with-png-dir=/usr/include --with-jpeg-dir=/usr/include; \ + docker-php-ext-configure zip; \ + docker-php-ext-install gd; \ diff --git a/.github/legacy-arm64/php7.4.patch b/.github/legacy-arm64/php7.4.patch new file mode 100644 index 0000000..b4b46aa --- /dev/null +++ b/.github/legacy-arm64/php7.4.patch @@ -0,0 +1,22 @@ +--- a/Dockerfile ++++ b/Dockerfile +@@ -33,13 +33,13 @@ + vim \ + zip + +-RUN pecl install imagick; \ +- pecl install memcached; \ +- pecl install mcrypt; \ +- pecl install redis; \ +- pecl install apcu; \ ++RUN pecl install imagick-3.8.1; \ ++ pecl install memcached-3.4.0; \ ++ pecl install mcrypt-1.0.9; \ ++ pecl install redis-6.3.0; \ ++ pecl install apcu-5.1.28; \ + pecl install gmagick-2.0.6RC1; \ +- pecl install timezonedb; \ ++ pecl install timezonedb-2026.4; \ + docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp; \ + docker-php-ext-configure zip; \ + docker-php-ext-install gd; \ diff --git a/.github/legacy-arm64/php8.0.patch b/.github/legacy-arm64/php8.0.patch new file mode 100644 index 0000000..d6f5a5e --- /dev/null +++ b/.github/legacy-arm64/php8.0.patch @@ -0,0 +1,31 @@ +--- a/Dockerfile ++++ b/Dockerfile +@@ -33,13 +33,13 @@ + vim \ + zip + +-RUN pecl install imagick; \ +- pecl install memcached; \ +- pecl install redis; \ +- pecl install apcu; \ +- pecl install mcrypt; \ ++RUN pecl install imagick-3.8.1; \ ++ pecl install memcached-3.4.0; \ ++ pecl install redis-6.3.0; \ ++ pecl install apcu-5.1.28; \ ++ pecl install mcrypt-1.0.9; \ + pecl install gmagick-2.0.6RC1; \ +- pecl install timezonedb; \ ++ pecl install timezonedb-2026.4; \ + docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp; \ + docker-php-ext-configure zip; \ + docker-php-ext-install gd; \ +@@ -98,7 +98,7 @@ + chown www-data:www-data /usr/local/etc/misc/msmtprc && \ + chown www-data:www-data /etc/msmtprc + +-RUN latest_build=$(curl -s https://download.newrelic.com/php_agent/release/ | grep 'linux.tar.gz' | sed 's/.*"\(.*\)".*/\1/') && \ ++RUN latest_build=/php_agent/archive/12.11.0.40/newrelic-php5-12.11.0.40-linux.tar.gz && \ + curl -L "https://download.newrelic.com$latest_build" | tar -C /tmp -zx && \ + export NR_INSTALL_USE_CP_NOT_LN=1 && \ + export NR_INSTALL_SILENT=1 && \ diff --git a/.github/legacy-arm64/recipes.json b/.github/legacy-arm64/recipes.json new file mode 100644 index 0000000..90b2f83 --- /dev/null +++ b/.github/legacy-arm64/recipes.json @@ -0,0 +1,65 @@ +{ + "php5.6": { + "amd64_tag": "v4.7.4", + "amd64_digest": "sha256:0d02a5f8d255a49f6f3207cf481795d0666995817784f8dfd6e29b92fc21508c", + "amd64_manifest": "sha256:0d02a5f8d255a49f6f3207cf481795d0666995817784f8dfd6e29b92fc21508c", + "ref": "681d01ac6c44ae7d9133e4f6e6606b34b44e2931", + "context": "php/5.6", + "newrelic": "none", + "note": "The published v4.7.4 image matches 681d01a, not the v4.7.4 tree (no tmux, no usermod). Patch: Debian snapshot of the amd64 build date (2023-02-20), imagick pinned to the amd64 version. mcrypt-1.0.1 and redis fail on PHP 5.6 and imagick is never enabled, exactly as on amd64." + }, + "php7.0": { + "amd64_tag": "v4.7.4", + "amd64_digest": "sha256:dc42a302e1b29627c8dee3fb604fde824bda813336b4ced46b1f8064ab446dad", + "amd64_manifest": "sha256:dc42a302e1b29627c8dee3fb604fde824bda813336b4ced46b1f8064ab446dad", + "ref": "a528d2945d0ea4c2cc2dab241a7e34af3fc56950", + "context": "php/7.0", + "newrelic": "unavailable", + "note": "The published v4.7.4 image (= v4.6.6, built 2023-04-21) matches a528d29 plus 'apt-get upgrade -y' and the openssl package, which its history shows and no commit has. Patch: those two, the Debian snapshot of the build date, PECL versions pinned to the amd64 ones. mcrypt-1.0.3 fails on PHP 7.0, so imagick and redis are never enabled, exactly as on amd64." + }, + "php7.2": { + "amd64_tag": "v4.7.4", + "amd64_digest": "sha256:bd388833c25634a000207ea450bc4b4044c57ae722eb1fa24d3a550ada4f5d8a", + "amd64_manifest": "sha256:bd388833c25634a000207ea450bc4b4044c57ae722eb1fa24d3a550ada4f5d8a", + "ref": "7b74e928e21b6825c1c1e290918b324bdffb3a5a", + "context": "php/7.2", + "newrelic": "unavailable", + "note": "The published v4.7.4 image (built 2024-10-09) matches 7b74e92 (no WebP, no tmux). Patch: Debian snapshot of the build date, PECL versions pinned to the amd64 ones. redis fails on PHP 7.2, so imagick and mcrypt are never enabled, exactly as on amd64." + }, + "php7.3": { + "amd64_tag": "v4.7.4", + "amd64_digest": "sha256:e85e80a6392cec7d9dd9e9c68985d9e461ba70211d532bd7b009990f59081d4b", + "amd64_manifest": "sha256:e85e80a6392cec7d9dd9e9c68985d9e461ba70211d532bd7b009990f59081d4b", + "ref": "2807ccb3a6d371835488c950ec9164388a4d2c92", + "context": "php/7.3", + "newrelic": "unavailable", + "note": "The published v4.7.4 image (built 2024-10-09) matches 2807ccb (no WebP, no tmux). Patch: Debian snapshot of the build date, PECL versions pinned to the amd64 ones. redis fails on PHP 7.3, so imagick, mcrypt, timezonedb and apcu are never enabled, exactly as on amd64." + }, + "php7.4": { + "amd64_tag": "v4.13.0", + "amd64_digest": "sha256:f1a007ff60dd2a831449d8ac7795cc6381577ae887b8f316f3d2c37fabb5261f", + "amd64_manifest": "sha256:84463e410c19a1b5523dc7ee547042311f53d884ba30353228c47b8fef4b7905", + "ref": "v4.13.0", + "context": "php/7.4", + "newrelic": "unavailable", + "note": "Built from the v4.13.0 tree. Patch: PECL versions pinned to the amd64 ones." + }, + "php8.0": { + "amd64_tag": "v4.13.0", + "amd64_digest": "sha256:8a4848e0e574558dbc3eb7e0f7505d6e93a7d7a341d82120062a8547e1514f5b", + "amd64_manifest": "sha256:b7da1852dc8820982f61d249cdb4fd6ae124cec9c0f0f4f4435f49670b5c7f90", + "ref": "v4.13.0", + "context": "php/8.0", + "newrelic": "agent", + "note": "Built from the v4.13.0 tree. Patch: PECL versions and the New Relic agent (12.11.0.40) pinned to the amd64 ones." + }, + "mariadb": { + "amd64_tag": "v4.9.1", + "amd64_digest": "sha256:607745183a0ecdf3b59ae0ff39d4a64703e85374eae6aab204183c3b7283b53f", + "amd64_manifest": "sha256:607745183a0ecdf3b59ae0ff39d4a64703e85374eae6aab204183c3b7283b53f", + "ref": "v4.9.1", + "context": "mariadb", + "newrelic": "none", + "note": "The published v4.9.1 image is MariaDB 10.11.14 on Ubuntu 22.04: its eight base layers are the amd64 image of mariadb:10.11.14@sha256:e4ad7c5a... (docker-library/repo-info, 2025-09-02), whose arm64 image is from the same official build. Patch: FROM that index instead of the mariadb:11.5 the tag names; ee.cnf and my.cnf are byte-identical to the published layers." + } +} diff --git a/.github/scripts/legacy-arm64.sh b/.github/scripts/legacy-arm64.sh new file mode 100755 index 0000000..fcc18d0 --- /dev/null +++ b/.github/scripts/legacy-arm64.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# One-time arm64 builds of images outside the build matrix; recipes in .github/legacy-arm64/recipes.json. +# Usage: +# legacy-arm64.sh prepare build context = recipe ref + .patch +# legacy-arm64.sh facts collect what must match across arches +# legacy-arm64.sh elf key binaries must be linux/arm64 ELF +# legacy-arm64.sh compare +# legacy-arm64.sh tag-free fails if the Docker Hub tag exists +set -euo pipefail + +cmd=$1 img=$2 +shift 2 +dir=$(cd "$(dirname "$0")/../legacy-arm64" && pwd) +recipe() { jq -er --arg i "$img" '.[$i].'"$1" "$dir/recipes.json"; } +fail() { echo "::error::$img: $*" >&2; exit 1; } + +# Host- or arch-specific server variables, excluded from the MariaDB comparison. +VAR_EXCLUDE='hostname|pid_file|general_log_file|slow_query_log_file|log_bin_basename|log_bin_index|relay_log_basename|relay_log_index|server_uid|version_compile_machine|thread_pool_size|large_page_size' + +prepare() { + local out=$1 ref ctx nr + ref=$(recipe ref) ctx=$(recipe context) nr=$(recipe newrelic) + [ "$nr" = unavailable ] && fail "New Relic publishes no aarch64 agent for this PHP version, but the amd64 image has it. Set \"newrelic\": \"skip\" in recipes.json to build arm64 without it, or leave $img amd64-only." + rm -rf "$out" && mkdir -p "$out" + git archive "$ref" "$ctx" | tar -x -C "$out" --strip-components="$(awk -F/ '{print NF}' <<<"$ctx")" + patch -d "$out" -p1 --forward --batch --no-backup-if-mismatch <"$dir/$img.patch" + if [ "$nr" = skip ]; then + # Drop the agent but keep newrelic.ini without its extension line, so the entrypoint's sed still finds it. + sed -i -e '/^RUN latest_build=/,/rm -rf \/tmp\/newrelic-php5-/d' "$out/Dockerfile" + sed -i -e '/^extension *= *"newrelic.so"/d' "$out/newrelic.ini" + ! grep -q newrelic-install "$out/Dockerfile" || fail "could not drop the New Relic agent" + fi + cat "$out/Dockerfile" +} + +facts() { + local ref=$1 out=$2 + mkdir -p "$out" + docker image inspect -f '{{json .Config}}' "$ref" | + jq -S '{User, Entrypoint, Cmd, WorkingDir, Env, ExposedPorts, Volumes, Labels, StopSignal, Healthcheck}' >"$out/config.json" + case $img in + php*) + local run=(docker run --rm "$ref") + "${run[@]}" php -r 'echo PHP_VERSION, "\n";' >"$out/version" + "${run[@]}" php -m >"$out/modules" 2>&1 + "${run[@]}" php -r '$e = get_loaded_extensions(); sort($e); foreach ($e as $x) echo $x, " ", phpversion($x), "\n";' >"$out/extensions" 2>&1 + "${run[@]}" php -r '$a = ini_get_all(null, false); ksort($a); foreach ($a as $k => $v) echo $k, "=", $v, "\n";' >"$out/ini" 2>&1 + "${run[@]}" php-fpm -t >"$out/fpm.log" 2>&1 && echo ok >"$out/fpm" || echo failed >"$out/fpm" + docker run --rm --entrypoint sh "$ref" -c 'find /usr/local/etc -type f | sort | xargs sha256sum' >"$out/etc" + { + "${run[@]}" sh -c 'composer --version 2>/dev/null | head -1' || echo "composer failed" + "${run[@]}" wp --version || echo "wp failed" + } >"$out/tools" 2>&1 + docker run --rm --entrypoint dpkg-query "$ref" -W -f '${Package} ${Version}\n' >"$out/dpkg" + ;; + mariadb) + cid=$(docker run -d -e MARIADB_ROOT_PASSWORD=smoke "$ref") + trap 'docker rm -f "$cid" >/dev/null 2>&1 || true' EXIT + for _ in $(seq 90); do docker exec "$cid" mariadb -uroot -psmoke -e 'SELECT 1' >/dev/null 2>&1 && break; sleep 2; done + docker exec "$cid" mariadb -uroot -psmoke -N -e 'SELECT VERSION()' >"$out/version" || { docker logs "$cid" | tail -30; fail "mariadb did not start"; } + docker exec "$cid" mariadb -uroot -psmoke -N -e 'SHOW GLOBAL VARIABLES' | grep -vE "^($VAR_EXCLUDE)\s" >"$out/variables" + docker exec "$cid" sh -c 'find /etc/mysql -printf "%M %u:%g %p\n" | sort; find /etc/mysql -type f | sort | xargs sha256sum' >"$out/etc" + docker exec "$cid" dpkg-query -W -f '${Package} ${Version}\n' >"$out/dpkg" + ;; + *) fail "no facts for this image" ;; + esac + head -n 50 "$out"/version "$out"/modules "$out"/tools 2>/dev/null || true +} + +elf() { + local ref=$1 got + got=$(docker image inspect -f '{{.Os}}/{{.Architecture}}' "$ref") + [ "$got" = linux/arm64 ] || fail "image platform is $got" + local list + case $img in + php*) list='ext=$(php -r "echo ini_get(\"extension_dir\");"); set -- "$(command -v php)" "$(command -v php-fpm)" "$ext"/*.so; [ -e /usr/bin/newrelic-daemon ] && set -- "$@" /usr/bin/newrelic-daemon' ;; + mariadb) list='set -- "$(command -v mariadbd)" "$(command -v mariadb)" "$(command -v gosu)" "$(command -v my_print_defaults)"' ;; + esac + docker run --rm --entrypoint sh "$ref" -c "$list"'; for f; do echo "$f $(od -An -t x1 -j 18 -N 1 "$f" | tr -d " ")"; done' | + while read -r f m; do + [ "$m" = b7 ] || fail "$f is not an arm64 ELF (e_machine=${m:-none})" + echo "elf ok: $f" + done +} + +compare() { + local a=$1 b=$2 nr bad=0 f + nr=$(recipe newrelic) + for f in config.json version modules extensions ini fpm etc variables; do + [ -e "$a/$f" ] || continue + if [ "$nr" = skip ] && [ "$f" != config.json ]; then + grep -q newrelic.so "$b/$f" && { echo "arm64 $f mentions newrelic.so"; bad=1; } + if diff -u <(grep -v newrelic "$a/$f") <(grep -v newrelic "$b/$f"); then echo "same: $f (New Relic left out)"; else bad=1; fi + elif diff -u "$a/$f" "$b/$f"; then + echo "same: $f" + else + bad=1 + fi + done + echo "--- informational: tools" + diff "$a/tools" "$b/tools" || true + if [ -e "$a/dpkg" ]; then + echo "--- informational: packages (amd64 $(wc -l <"$a/dpkg"), arm64 $(wc -l <"$b/dpkg"))" + diff "$a/dpkg" "$b/dpkg" || true + fi + [ "$bad" = 0 ] || fail "arm64 differs from the published amd64 image (see the diff above)" +} + +tag_free() { + local tag=$1 tok code + tok=$(curl -fsS "https://auth.docker.io/token?service=registry.docker.io&scope=repository:$img:pull" | jq -r .token) + code=$(curl -sS -o /dev/null -w '%{http_code}' -I -H "Authorization: Bearer $tok" \ + -H 'Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json' \ + "https://registry-1.docker.io/v2/$img/manifests/$tag") + case $code in + 404) echo "$img:$tag does not exist yet" ;; + 200) fail "$img:$tag already exists; tags are never overwritten" ;; + *) fail "could not check $img:$tag (HTTP $code)" ;; + esac +} + +case $cmd in +prepare | facts | elf | compare) "$cmd" "$@" ;; +tag-free) tag_free "$@" ;; +*) echo "unknown command $cmd" >&2; exit 2 ;; +esac diff --git a/.github/workflows/legacy-arm64.yml b/.github/workflows/legacy-arm64.yml new file mode 100644 index 0000000..0a98c04 --- /dev/null +++ b/.github/workflows/legacy-arm64.yml @@ -0,0 +1,219 @@ +# # +# # One-time arm64 builds of images that are no longer in the build matrix (old PHP versions, MariaDB 10.11). +# # Each recipe in .github/legacy-arm64/recipes.json rebuilds the published amd64 image for arm64 and publishes +# # a NEW tag whose index holds the untouched amd64 image (same digest) plus the new arm64 one. +# # Without "publish" (or outside EasyEngine/dockerfiles) it is a dry run into a runner-local registry. +# # + +name: One-time arm64 builds of legacy images +on: + workflow_dispatch: + inputs: + images: + description: 'Images to build (space-separated keys of .github/legacy-arm64/recipes.json)' + required: true + default: 'php5.6 php7.0 php7.2 php7.3 php7.4 php8.0 mariadb' + tag: + description: 'New tag for the amd64 + arm64 image; refused if it already exists on Docker Hub' + required: true + publish: + description: 'Push to Docker Hub (otherwise a dry run into a runner-local registry)' + type: boolean + default: false + +env: + IMAGES: ${{ inputs.images }} + TAG: ${{ inputs.tag }} + PUBLISH: ${{ inputs.publish && github.repository == 'EasyEngine/dockerfiles' }} + +jobs: + + setup: #---------------------------------------------------------------------- + + runs-on: ubuntu-24.04 + outputs: + images: ${{ steps.check.outputs.images }} + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Check inputs + id: check + env: + PUBLISH_REQUESTED: ${{ inputs.publish }} + run: | + if [ "$PUBLISH_REQUESTED" = true ] && [ "$PUBLISH" != true ]; then + echo "::error::publishing is only possible in EasyEngine/dockerfiles"; exit 1 + fi + [[ "$TAG" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]] || { echo "::error::invalid tag: $TAG"; exit 1; } + images=$(jq -cn --arg s "$IMAGES" '$s | split(" ") | map(select(. != "")) | unique') + unknown=$(jq -r --argjson want "$images" '$want - keys | join(" ")' .github/legacy-arm64/recipes.json) + [ -z "$unknown" ] || { echo "::error::no recipe for: $unknown"; exit 1; } + [ "$images" != "[]" ] || { echo "::error::no images given"; exit 1; } + echo "images=$images" >> "$GITHUB_OUTPUT" + echo "Images: $images, tag: $TAG, publish: $PUBLISH" + + reference: #------------------------------------------------------------------ + + needs: setup + runs-on: ubuntu-24.04 + name: Reference ${{ matrix.image }} (published amd64) + strategy: + fail-fast: false + matrix: + image: ${{ fromJSON(needs.setup.outputs.images) }} + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Pull the published amd64 image + id: ref + run: | + r() { jq -er --arg i "${{ matrix.image }}" ".[\$i].$1" .github/legacy-arm64/recipes.json; } + src="easyengine/${{ matrix.image }}@$(r amd64_digest)" + docker pull "$src" + now=sha256:$(printf '%s' "$(docker buildx imagetools inspect --raw "easyengine/${{ matrix.image }}:$(r amd64_tag)")" | sha256sum | cut -d' ' -f1) + [ "$now" = "$(r amd64_digest)" ] || echo "::warning::easyengine/${{ matrix.image }}:$(r amd64_tag) now points to $now; the recipe digest is used" + echo "src=$src" >> "$GITHUB_OUTPUT" + + - name: Collect facts + run: .github/scripts/legacy-arm64.sh facts ${{ matrix.image }} "${{ steps.ref.outputs.src }}" "$RUNNER_TEMP/facts" + + - name: Upload facts + uses: actions/upload-artifact@v7 + with: + name: facts-${{ matrix.image }}-amd64 + path: ${{ runner.temp }}/facts + retention-days: 7 + + build: #---------------------------------------------------------------------- + + # A failed reference job only fails its own image (its facts are missing). + if: ${{ !cancelled() && needs.setup.result == 'success' }} + needs: [setup, reference] + runs-on: ubuntu-24.04-arm + name: Build ${{ matrix.image }} (arm64) + strategy: + fail-fast: false + matrix: + image: ${{ fromJSON(needs.setup.outputs.images) }} + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Build context from the recipe + run: .github/scripts/legacy-arm64.sh prepare ${{ matrix.image }} "$RUNNER_TEMP/ctx" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to DockerHub + uses: docker/login-action@v4 + if: env.PUBLISH == 'true' + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build ${{ matrix.image }} for arm64 + uses: docker/build-push-action@v7 + with: + context: ${{ runner.temp }}/ctx + platforms: linux/arm64 + load: true + tags: local/${{ matrix.image }}:arm64 + + - name: Check the ELF architecture of the key binaries + run: .github/scripts/legacy-arm64.sh elf ${{ matrix.image }} local/${{ matrix.image }}:arm64 + + - name: Collect facts + run: .github/scripts/legacy-arm64.sh facts ${{ matrix.image }} local/${{ matrix.image }}:arm64 "$RUNNER_TEMP/facts-arm64" + + - name: Download the amd64 facts + uses: actions/download-artifact@v8 + with: + name: facts-${{ matrix.image }}-amd64 + path: ${{ runner.temp }}/facts-amd64 + + - name: Compare with the published amd64 image + run: .github/scripts/legacy-arm64.sh compare ${{ matrix.image }} "$RUNNER_TEMP/facts-amd64" "$RUNNER_TEMP/facts-arm64" + + - name: Upload arm64 facts + uses: actions/upload-artifact@v7 + if: always() + with: + name: facts-${{ matrix.image }}-arm64 + path: ${{ runner.temp }}/facts-arm64 + retention-days: 7 + + - name: Refuse an existing tag + run: .github/scripts/legacy-arm64.sh tag-free easyengine/${{ matrix.image }} "$TAG" + + # Same builder as above, so this reuses the tested layers instead of rebuilding. + - name: Push the arm64 image by digest + id: push + uses: docker/build-push-action@v7 + with: + context: ${{ runner.temp }}/ctx + platforms: linux/arm64 + outputs: ${{ env.PUBLISH == 'true' && format('type=image,name=easyengine/{0},push-by-digest=true,name-canonical=true,push=true', matrix.image) || format('type=oci,tar=false,dest={0}/oci', runner.temp) }} + + - name: Target + id: target + run: | + if [ "$PUBLISH" = true ]; then repo=easyengine/${{ matrix.image }}; else repo=localhost:5000/easyengine/${{ matrix.image }}; fi + echo "repo=$repo" >> "$GITHUB_OUTPUT" + echo "ref=$repo:$TAG" >> "$GITHUB_OUTPUT" + + # Stand-in for the push by digest: a carbon copy of the arm64 index into a local registry. + - name: Push the arm64 image to a local registry (dry run) + if: env.PUBLISH != 'true' + run: | + docker run -d --name registry -p 5000:5000 registry:3 + for _ in $(seq 30); do curl -fs http://localhost:5000/v2/ >/dev/null && break; sleep 1; done + docker buildx imagetools create --prefer-index=false -t "$REPO:arm64-build" "oci-layout://$RUNNER_TEMP/oci" + docker buildx imagetools inspect "$REPO@$DIGEST" >/dev/null + env: + REPO: ${{ steps.target.outputs.repo }} + DIGEST: ${{ steps.push.outputs.digest }} + + - name: Create the amd64 + arm64 image + run: | + .github/scripts/legacy-arm64.sh tag-free easyengine/${{ matrix.image }} "$TAG" + amd64=$(jq -er --arg i "${{ matrix.image }}" '.[$i].amd64_digest' .github/legacy-arm64/recipes.json) + docker buildx imagetools create -t "$TARGET" "docker.io/easyengine/${{ matrix.image }}@$amd64" "$REPO@$DIGEST" + env: + REPO: ${{ steps.target.outputs.repo }} + TARGET: ${{ steps.target.outputs.ref }} + DIGEST: ${{ steps.push.outputs.digest }} + + - name: Verify the new image + run: | + r() { jq -er --arg i "${{ matrix.image }}" ".[\$i].$1" .github/legacy-arm64/recipes.json; } + raw=$(docker buildx imagetools inspect --raw "$TARGET") + echo "$raw" | jq . + platforms=$(jq -r '[.manifests[].platform | select(.architecture != "unknown") | .os + "/" + .architecture] | sort | join(",")' <<<"$raw") + [ "$platforms" = "linux/amd64,linux/arm64" ] || { echo "::error::unexpected platforms: $platforms"; exit 1; } + amd64=$(jq -r '[.manifests[] | select(.platform.architecture == "amd64")][0].digest' <<<"$raw") + [ "$amd64" = "$(r amd64_manifest)" ] || { echo "::error::amd64 digest $amd64 is not the published $(r amd64_manifest)"; exit 1; } + arm=$(docker buildx imagetools inspect --raw "$REPO@$DIGEST") + want=$(jq -r 'if .manifests then [.manifests[] | select(.platform.architecture == "arm64")][0].digest else empty end' <<<"$arm") + [ -n "$want" ] || want=$DIGEST + got=$(jq -r '[.manifests[] | select(.platform.architecture == "arm64")][0].digest' <<<"$raw") + [ "$got" = "$want" ] || { echo "::error::arm64 digest $got is not the tested $want"; exit 1; } + # The pull on this arm64 runner must give the tested image, and it must behave the same. + docker pull "$TARGET" + [ "$(docker image inspect -f '{{json .RootFS.Layers}}' "$TARGET")" = "$(docker image inspect -f '{{json .RootFS.Layers}}' local/${{ matrix.image }}:arm64)" ] || { echo "::error::pulled image differs from the tested one"; exit 1; } + .github/scripts/legacy-arm64.sh facts ${{ matrix.image }} "$TARGET" "$RUNNER_TEMP/facts-pulled" + diff -r --exclude=fpm.log "$RUNNER_TEMP/facts-arm64" "$RUNNER_TEMP/facts-pulled" + { + echo "### ${{ matrix.image }} → \`$TARGET\`" + echo "- amd64: \`$amd64\` (published easyengine/${{ matrix.image }}:$(r amd64_tag), unchanged)" + echo "- arm64: \`$got\`" + echo "- publish: $PUBLISH" + } >> "$GITHUB_STEP_SUMMARY" + env: + REPO: ${{ steps.target.outputs.repo }} + TARGET: ${{ steps.target.outputs.ref }} + DIGEST: ${{ steps.push.outputs.digest }} From 48a1e5c75c41078ef223ccd294e3691f762cb550 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 23 Oct 2026 22:22:53 +0530 Subject: [PATCH 10/17] fix(ci): build legacy arm64 recipes from commit SHAs and verify stretch release files by hash Forks don't carry the upstream v4.13.0 and v4.9.1 tags, so recipes name the commits and the script fetches a missing one from EasyEngine/dockerfiles. Stretch's archive keys have expired, so apt rejects the snapshot; php5.6 and php7.0 now pin the snapshot's release files to the copies verified against the stretch keys as of the build date, and apt checks every index and package against them. --- .github/legacy-arm64/php5.6.patch | 19 +++++++++++++++---- .github/legacy-arm64/php7.0.patch | 19 +++++++++++++++---- .github/legacy-arm64/recipes.json | 12 ++++++------ .github/scripts/legacy-arm64.sh | 2 ++ .github/workflows/legacy-arm64.yml | 2 +- 5 files changed, 39 insertions(+), 15 deletions(-) diff --git a/.github/legacy-arm64/php5.6.patch b/.github/legacy-arm64/php5.6.patch index 57619af..5acea16 100644 --- a/.github/legacy-arm64/php5.6.patch +++ b/.github/legacy-arm64/php5.6.patch @@ -1,15 +1,26 @@ --- a/Dockerfile +++ b/Dockerfile -@@ -1,5 +1,8 @@ +@@ -1,5 +1,19 @@ FROM php:5.6-fpm -+# Frozen base: its Debian suites are only on snapshot.debian.org now; use the snapshot from the amd64 build date. -+RUN sed -i -E 's#^deb http://(deb|security)\.debian\.org/(debian-security|debian) #deb [check-valid-until=no] http://snapshot.debian.org/archive/\2/20230220T000000Z #' /etc/apt/sources.list ++# Frozen base: its Debian suites are only on snapshot.debian.org now and stretch's archive keys have expired. ++# The release files are pinned to the copies verified against those keys as of the amd64 build date; apt checks ++# every index and package against them. ++RUN set -e; cd /tmp; s=http://snapshot.debian.org/archive; \ ++ curl -fsSLo r1 $s/debian/20230220T000000Z/dists/stretch/Release; \ ++ curl -fsSLo r2 $s/debian/20230220T000000Z/dists/stretch-updates/InRelease; \ ++ curl -fsSLo r3 $s/debian-security/20230220T000000Z/dists/stretch/updates/InRelease; \ ++ printf '%s %s\n' \ ++ fe55c42941dd43102b9490af1678e7962ea1e257ccb8f942b43770475c336eb3 r1 \ ++ 35b3294728144de605c0192e533ca97de6b77112b52d565566ab5ce7ef432a2c r2 \ ++ cb3eb154bf0311d53c98eab79692e55aae49a4bcf13c23bd12943a4fb5a0b05f r3 | sha256sum -c -; \ ++ rm r1 r2 r3; \ ++ sed -i -E 's#^deb http://(deb|security)\.debian\.org/(debian-security|debian) #deb [trusted=yes] http://snapshot.debian.org/archive/\2/20230220T000000Z #' /etc/apt/sources.list + LABEL maintainer="Riddhesh Sanghvi , Devarshi Sathiya " LABEL org.label-schema.schema-version="1.0.0-rc1" LABEL org.label-schema.vendor="EasyEngine" -@@ -27,7 +30,7 @@ +@@ -27,7 +41,7 @@ vim \ zip diff --git a/.github/legacy-arm64/php7.0.patch b/.github/legacy-arm64/php7.0.patch index dd1e1a3..8e466bd 100644 --- a/.github/legacy-arm64/php7.0.patch +++ b/.github/legacy-arm64/php7.0.patch @@ -1,10 +1,21 @@ --- a/Dockerfile +++ b/Dockerfile -@@ -1,14 +1,18 @@ +@@ -1,14 +1,29 @@ FROM php:7.0-fpm@sha256:8e8644c0fd61424939fff072ca617469550bbb659158301a2dcadf334746a1c2 -+# Frozen base: its Debian suites are only on snapshot.debian.org now; use the snapshot from the amd64 build date. -+RUN sed -i -E 's#^deb http://(deb|security)\.debian\.org/(debian-security|debian) #deb [check-valid-until=no] http://snapshot.debian.org/archive/\2/20230421T000000Z #' /etc/apt/sources.list ++# Frozen base: its Debian suites are only on snapshot.debian.org now and stretch's archive keys have expired. ++# The release files are pinned to the copies verified against those keys as of the amd64 build date; apt checks ++# every index and package against them. ++RUN set -e; cd /tmp; s=http://snapshot.debian.org/archive; \ ++ curl -fsSLo r1 $s/debian/20230421T000000Z/dists/stretch/Release; \ ++ curl -fsSLo r2 $s/debian/20230421T000000Z/dists/stretch-updates/InRelease; \ ++ curl -fsSLo r3 $s/debian-security/20230421T000000Z/dists/stretch/updates/InRelease; \ ++ printf '%s %s\n' \ ++ fe55c42941dd43102b9490af1678e7962ea1e257ccb8f942b43770475c336eb3 r1 \ ++ 35b3294728144de605c0192e533ca97de6b77112b52d565566ab5ce7ef432a2c r2 \ ++ cb3eb154bf0311d53c98eab79692e55aae49a4bcf13c23bd12943a4fb5a0b05f r3 | sha256sum -c -; \ ++ rm r1 r2 r3; \ ++ sed -i -E 's#^deb http://(deb|security)\.debian\.org/(debian-security|debian) #deb [trusted=yes] http://snapshot.debian.org/archive/\2/20230421T000000Z #' /etc/apt/sources.list + LABEL maintainer="Devarshi Sathiya , Riddhesh Sanghvi " LABEL org.label-schema.schema-version="1.0.0-rc1" @@ -20,7 +31,7 @@ less \ mariadb-client msmtp \ libc-client-dev \ -@@ -29,10 +33,10 @@ +@@ -29,10 +44,10 @@ vim \ zip diff --git a/.github/legacy-arm64/recipes.json b/.github/legacy-arm64/recipes.json index 90b2f83..5178116 100644 --- a/.github/legacy-arm64/recipes.json +++ b/.github/legacy-arm64/recipes.json @@ -39,27 +39,27 @@ "amd64_tag": "v4.13.0", "amd64_digest": "sha256:f1a007ff60dd2a831449d8ac7795cc6381577ae887b8f316f3d2c37fabb5261f", "amd64_manifest": "sha256:84463e410c19a1b5523dc7ee547042311f53d884ba30353228c47b8fef4b7905", - "ref": "v4.13.0", + "ref": "7fa7f03ff62d9cd2ed55946223cbc1082a7d5cc5", "context": "php/7.4", "newrelic": "unavailable", - "note": "Built from the v4.13.0 tree. Patch: PECL versions pinned to the amd64 ones." + "note": "Built from the v4.13.0 tag (7fa7f03). Patch: PECL versions pinned to the amd64 ones." }, "php8.0": { "amd64_tag": "v4.13.0", "amd64_digest": "sha256:8a4848e0e574558dbc3eb7e0f7505d6e93a7d7a341d82120062a8547e1514f5b", "amd64_manifest": "sha256:b7da1852dc8820982f61d249cdb4fd6ae124cec9c0f0f4f4435f49670b5c7f90", - "ref": "v4.13.0", + "ref": "7fa7f03ff62d9cd2ed55946223cbc1082a7d5cc5", "context": "php/8.0", "newrelic": "agent", - "note": "Built from the v4.13.0 tree. Patch: PECL versions and the New Relic agent (12.11.0.40) pinned to the amd64 ones." + "note": "Built from the v4.13.0 tag (7fa7f03). Patch: PECL versions and the New Relic agent (12.11.0.40) pinned to the amd64 ones." }, "mariadb": { "amd64_tag": "v4.9.1", "amd64_digest": "sha256:607745183a0ecdf3b59ae0ff39d4a64703e85374eae6aab204183c3b7283b53f", "amd64_manifest": "sha256:607745183a0ecdf3b59ae0ff39d4a64703e85374eae6aab204183c3b7283b53f", - "ref": "v4.9.1", + "ref": "f6ce783aa21e6d8d4141083f969d5c0499719566", "context": "mariadb", "newrelic": "none", - "note": "The published v4.9.1 image is MariaDB 10.11.14 on Ubuntu 22.04: its eight base layers are the amd64 image of mariadb:10.11.14@sha256:e4ad7c5a... (docker-library/repo-info, 2025-09-02), whose arm64 image is from the same official build. Patch: FROM that index instead of the mariadb:11.5 the tag names; ee.cnf and my.cnf are byte-identical to the published layers." + "note": "Ref = the v4.9.1 tag. The published v4.9.1 image is MariaDB 10.11.14 on Ubuntu 22.04: its eight base layers are the amd64 image of mariadb:10.11.14@sha256:e4ad7c5a... (docker-library/repo-info, 2025-09-02), whose arm64 image is from the same official build. Patch: FROM that index instead of the mariadb:11.5 the tag names; ee.cnf and my.cnf are byte-identical to the published layers." } } diff --git a/.github/scripts/legacy-arm64.sh b/.github/scripts/legacy-arm64.sh index fcc18d0..906f55c 100755 --- a/.github/scripts/legacy-arm64.sh +++ b/.github/scripts/legacy-arm64.sh @@ -22,6 +22,8 @@ prepare() { ref=$(recipe ref) ctx=$(recipe context) nr=$(recipe newrelic) [ "$nr" = unavailable ] && fail "New Relic publishes no aarch64 agent for this PHP version, but the amd64 image has it. Set \"newrelic\": \"skip\" in recipes.json to build arm64 without it, or leave $img amd64-only." rm -rf "$out" && mkdir -p "$out" + # Forks may lack the upstream tags these commits sit on. + git cat-file -e "$ref^{commit}" 2>/dev/null || git fetch -q --depth=1 https://github.com/EasyEngine/dockerfiles.git "$ref" git archive "$ref" "$ctx" | tar -x -C "$out" --strip-components="$(awk -F/ '{print NF}' <<<"$ctx")" patch -d "$out" -p1 --forward --batch --no-backup-if-mismatch <"$dir/$img.patch" if [ "$nr" = skip ]; then diff --git a/.github/workflows/legacy-arm64.yml b/.github/workflows/legacy-arm64.yml index 0a98c04..242415b 100644 --- a/.github/workflows/legacy-arm64.yml +++ b/.github/workflows/legacy-arm64.yml @@ -24,7 +24,7 @@ on: env: IMAGES: ${{ inputs.images }} TAG: ${{ inputs.tag }} - PUBLISH: ${{ inputs.publish && github.repository == 'EasyEngine/dockerfiles' }} + PUBLISH: ${{ inputs.publish == true && github.repository == 'EasyEngine/dockerfiles' }} jobs: From dd22593a99b514e7da5b7e5a062b4a10cae19ab5 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 23 Oct 2026 22:26:11 +0530 Subject: [PATCH 11/17] fix(ci): ignore the container hostname in the MariaDB variable comparison wsrep_node_name defaults to the hostname, which differs per container. --- .github/scripts/legacy-arm64.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/scripts/legacy-arm64.sh b/.github/scripts/legacy-arm64.sh index 906f55c..e5cfae9 100755 --- a/.github/scripts/legacy-arm64.sh +++ b/.github/scripts/legacy-arm64.sh @@ -15,7 +15,7 @@ recipe() { jq -er --arg i "$img" '.[$i].'"$1" "$dir/recipes.json"; } fail() { echo "::error::$img: $*" >&2; exit 1; } # Host- or arch-specific server variables, excluded from the MariaDB comparison. -VAR_EXCLUDE='hostname|pid_file|general_log_file|slow_query_log_file|log_bin_basename|log_bin_index|relay_log_basename|relay_log_index|server_uid|version_compile_machine|thread_pool_size|large_page_size' +VAR_EXCLUDE='hostname|pid_file|general_log_file|slow_query_log_file|log_bin_basename|log_bin_index|relay_log_basename|relay_log_index|server_uid|version_compile_machine|thread_pool_size|large_page_size|wsrep_node_name' prepare() { local out=$1 ref ctx nr @@ -99,8 +99,10 @@ compare() { bad=1 fi done - echo "--- informational: tools" - diff "$a/tools" "$b/tools" || true + if [ -e "$a/tools" ]; then + echo "--- informational: tools" + diff "$a/tools" "$b/tools" || true + fi if [ -e "$a/dpkg" ]; then echo "--- informational: packages (amd64 $(wc -l <"$a/dpkg"), arm64 $(wc -l <"$b/dpkg"))" diff "$a/dpkg" "$b/dpkg" || true From 9f071e780477ece79e7ffcd266364b48e31ecd03 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 23 Oct 2026 22:30:01 +0530 Subject: [PATCH 12/17] fix(ci): allow the opcache.jit settings to be absent on arm64 php8.0 PHP 8.0 has no JIT on AArch64 (it came in 8.1), so its opcache.jit* settings don't exist there. The recipe names them; the check drops them from the amd64 side and fails if arm64 has any. JIT is off on amd64 too (opcache.jit_buffer_size=0). --- .github/legacy-arm64/recipes.json | 3 ++- .github/scripts/legacy-arm64.sh | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/legacy-arm64/recipes.json b/.github/legacy-arm64/recipes.json index 5178116..7126996 100644 --- a/.github/legacy-arm64/recipes.json +++ b/.github/legacy-arm64/recipes.json @@ -51,7 +51,8 @@ "ref": "7fa7f03ff62d9cd2ed55946223cbc1082a7d5cc5", "context": "php/8.0", "newrelic": "agent", - "note": "Built from the v4.13.0 tag (7fa7f03). Patch: PECL versions and the New Relic agent (12.11.0.40) pinned to the amd64 ones." + "arm64_ini_absent": "^opcache\\.jit", + "note": "Built from the v4.13.0 tag (7fa7f03). Patch: PECL versions and the New Relic agent (12.11.0.40) pinned to the amd64 ones. PHP 8.0 has no JIT on AArch64 (it came in 8.1), so the opcache.jit* settings don't exist there; JIT is off on amd64 too (opcache.jit_buffer_size=0)." }, "mariadb": { "amd64_tag": "v4.9.1", diff --git a/.github/scripts/legacy-arm64.sh b/.github/scripts/legacy-arm64.sh index e5cfae9..1f5fa89 100755 --- a/.github/scripts/legacy-arm64.sh +++ b/.github/scripts/legacy-arm64.sh @@ -86,8 +86,15 @@ elf() { } compare() { - local a=$1 b=$2 nr bad=0 f + local a=$1 b=$2 nr absent bad=0 f nr=$(recipe newrelic) + # ini settings that don't exist on arm64 for this PHP version (documented in the recipe). + absent=$(recipe arm64_ini_absent 2>/dev/null || true) + if [ -n "$absent" ]; then + grep -E "$absent" "$b/ini" && { echo "arm64 has settings expected to be absent"; bad=1; } + grep -E "$absent" "$a/ini" | sed 's/^/absent on arm64: /' + grep -vE "$absent" "$a/ini" >"$a/ini.tmp" && mv "$a/ini.tmp" "$a/ini" + fi for f in config.json version modules extensions ini fpm etc variables; do [ -e "$a/$f" ] || continue if [ "$nr" = skip ] && [ "$f" != config.json ]; then From 0212bb386986140f17068e5025129471dac6b98c Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 23 Oct 2026 22:37:25 +0530 Subject: [PATCH 13/17] fix(ci): read optional recipe fields as empty when absent jq -e prints null for a missing field, which turned the php8.0 ini allowance on for every image. --- .github/scripts/legacy-arm64.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/scripts/legacy-arm64.sh b/.github/scripts/legacy-arm64.sh index 1f5fa89..20fa609 100755 --- a/.github/scripts/legacy-arm64.sh +++ b/.github/scripts/legacy-arm64.sh @@ -12,6 +12,7 @@ cmd=$1 img=$2 shift 2 dir=$(cd "$(dirname "$0")/../legacy-arm64" && pwd) recipe() { jq -er --arg i "$img" '.[$i].'"$1" "$dir/recipes.json"; } +optional() { jq -r --arg i "$img" '.[$i].'"$1"' // empty' "$dir/recipes.json"; } fail() { echo "::error::$img: $*" >&2; exit 1; } # Host- or arch-specific server variables, excluded from the MariaDB comparison. @@ -89,7 +90,7 @@ compare() { local a=$1 b=$2 nr absent bad=0 f nr=$(recipe newrelic) # ini settings that don't exist on arm64 for this PHP version (documented in the recipe). - absent=$(recipe arm64_ini_absent 2>/dev/null || true) + absent=$(optional arm64_ini_absent) if [ -n "$absent" ]; then grep -E "$absent" "$b/ini" && { echo "arm64 has settings expected to be absent"; bad=1; } grep -E "$absent" "$a/ini" | sed 's/^/absent on arm64: /' From 7c3327d7fdda768c2787b03bdd7d29c473d78009 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 23 Oct 2026 23:01:05 +0530 Subject: [PATCH 14/17] fix(ci): bind apt to the verified stretch release files and serialize runs per image and tag The stretch recipes checked separately downloaded copies of the release files, but apt fetched its own. They now pin the hashes of the files apt stores and check them right after apt-get update, before anything is upgraded or installed; apt then checks every index and package against those files. Build jobs for the same image and tag now share a non-cancelling concurrency group, so a second run waits and then refuses the tag the first one created. --- .github/legacy-arm64/php5.6.patch | 29 +++++++++++++++++------------ .github/legacy-arm64/php7.0.patch | 28 +++++++++++++--------------- .github/workflows/legacy-arm64.yml | 14 +++++++++----- 3 files changed, 39 insertions(+), 32 deletions(-) diff --git a/.github/legacy-arm64/php5.6.patch b/.github/legacy-arm64/php5.6.patch index 5acea16..1c1c1a4 100644 --- a/.github/legacy-arm64/php5.6.patch +++ b/.github/legacy-arm64/php5.6.patch @@ -1,26 +1,31 @@ --- a/Dockerfile +++ b/Dockerfile -@@ -1,5 +1,19 @@ +@@ -1,5 +1,16 @@ FROM php:5.6-fpm +# Frozen base: its Debian suites are only on snapshot.debian.org now and stretch's archive keys have expired. -+# The release files are pinned to the copies verified against those keys as of the amd64 build date; apt checks -+# every index and package against them. -+RUN set -e; cd /tmp; s=http://snapshot.debian.org/archive; \ -+ curl -fsSLo r1 $s/debian/20230220T000000Z/dists/stretch/Release; \ -+ curl -fsSLo r2 $s/debian/20230220T000000Z/dists/stretch-updates/InRelease; \ -+ curl -fsSLo r3 $s/debian-security/20230220T000000Z/dists/stretch/updates/InRelease; \ ++# /etc/apt/snapshot.sha256 pins the release files to the copies verified against those keys as of the amd64 build ++# date; the next RUN checks what apt fetched against it, and apt checks every index and package against those files. ++RUN set -e; l=snapshot.debian.org_archive; \ + printf '%s %s\n' \ -+ fe55c42941dd43102b9490af1678e7962ea1e257ccb8f942b43770475c336eb3 r1 \ -+ 35b3294728144de605c0192e533ca97de6b77112b52d565566ab5ce7ef432a2c r2 \ -+ cb3eb154bf0311d53c98eab79692e55aae49a4bcf13c23bd12943a4fb5a0b05f r3 | sha256sum -c -; \ -+ rm r1 r2 r3; \ ++ fe55c42941dd43102b9490af1678e7962ea1e257ccb8f942b43770475c336eb3 ${l}_debian_20230220T000000Z_dists_stretch_Release \ ++ 35b3294728144de605c0192e533ca97de6b77112b52d565566ab5ce7ef432a2c ${l}_debian_20230220T000000Z_dists_stretch-updates_Release \ ++ cb3eb154bf0311d53c98eab79692e55aae49a4bcf13c23bd12943a4fb5a0b05f ${l}_debian-security_20230220T000000Z_dists_stretch_updates_Release \ ++ > /etc/apt/snapshot.sha256; \ + sed -i -E 's#^deb http://(deb|security)\.debian\.org/(debian-security|debian) #deb [trusted=yes] http://snapshot.debian.org/archive/\2/20230220T000000Z #' /etc/apt/sources.list + LABEL maintainer="Riddhesh Sanghvi , Devarshi Sathiya " LABEL org.label-schema.schema-version="1.0.0-rc1" LABEL org.label-schema.vendor="EasyEngine" -@@ -27,7 +41,7 @@ +@@ -7,6 +18,7 @@ + + RUN set -ex; \ + apt-get update; \ ++ (cd /var/lib/apt/lists && sha256sum -c /etc/apt/snapshot.sha256); \ + apt-get install -y --no-install-recommends \ + imagemagick \ + less \ +@@ -27,7 +39,7 @@ vim \ zip diff --git a/.github/legacy-arm64/php7.0.patch b/.github/legacy-arm64/php7.0.patch index 8e466bd..e05ad63 100644 --- a/.github/legacy-arm64/php7.0.patch +++ b/.github/legacy-arm64/php7.0.patch @@ -1,37 +1,35 @@ --- a/Dockerfile +++ b/Dockerfile -@@ -1,14 +1,29 @@ +@@ -1,5 +1,16 @@ FROM php:7.0-fpm@sha256:8e8644c0fd61424939fff072ca617469550bbb659158301a2dcadf334746a1c2 +# Frozen base: its Debian suites are only on snapshot.debian.org now and stretch's archive keys have expired. -+# The release files are pinned to the copies verified against those keys as of the amd64 build date; apt checks -+# every index and package against them. -+RUN set -e; cd /tmp; s=http://snapshot.debian.org/archive; \ -+ curl -fsSLo r1 $s/debian/20230421T000000Z/dists/stretch/Release; \ -+ curl -fsSLo r2 $s/debian/20230421T000000Z/dists/stretch-updates/InRelease; \ -+ curl -fsSLo r3 $s/debian-security/20230421T000000Z/dists/stretch/updates/InRelease; \ ++# /etc/apt/snapshot.sha256 pins the release files to the copies verified against those keys as of the amd64 build ++# date; the next RUN checks what apt fetched against it, and apt checks every index and package against those files. ++RUN set -e; l=snapshot.debian.org_archive; \ + printf '%s %s\n' \ -+ fe55c42941dd43102b9490af1678e7962ea1e257ccb8f942b43770475c336eb3 r1 \ -+ 35b3294728144de605c0192e533ca97de6b77112b52d565566ab5ce7ef432a2c r2 \ -+ cb3eb154bf0311d53c98eab79692e55aae49a4bcf13c23bd12943a4fb5a0b05f r3 | sha256sum -c -; \ -+ rm r1 r2 r3; \ ++ fe55c42941dd43102b9490af1678e7962ea1e257ccb8f942b43770475c336eb3 ${l}_debian_20230421T000000Z_dists_stretch_Release \ ++ 35b3294728144de605c0192e533ca97de6b77112b52d565566ab5ce7ef432a2c ${l}_debian_20230421T000000Z_dists_stretch-updates_Release \ ++ cb3eb154bf0311d53c98eab79692e55aae49a4bcf13c23bd12943a4fb5a0b05f ${l}_debian-security_20230421T000000Z_dists_stretch_updates_Release \ ++ > /etc/apt/snapshot.sha256; \ + sed -i -E 's#^deb http://(deb|security)\.debian\.org/(debian-security|debian) #deb [trusted=yes] http://snapshot.debian.org/archive/\2/20230421T000000Z #' /etc/apt/sources.list + LABEL maintainer="Devarshi Sathiya , Riddhesh Sanghvi " LABEL org.label-schema.schema-version="1.0.0-rc1" LABEL org.label-schema.vendor="EasyEngine" - LABEL org.label-schema.name="php" +@@ -7,8 +18,11 @@ RUN set -ex; \ -- apt-get update; \ -+ apt-get update && apt-get upgrade -y; \ + apt-get update; \ ++ (cd /var/lib/apt/lists && sha256sum -c /etc/apt/snapshot.sha256); \ ++ apt-get upgrade -y; \ apt-get install -y --no-install-recommends \ imagemagick \ + openssl \ less \ mariadb-client msmtp \ libc-client-dev \ -@@ -29,10 +44,10 @@ +@@ -29,10 +43,10 @@ vim \ zip diff --git a/.github/workflows/legacy-arm64.yml b/.github/workflows/legacy-arm64.yml index 242415b..f13bad1 100644 --- a/.github/workflows/legacy-arm64.yml +++ b/.github/workflows/legacy-arm64.yml @@ -46,12 +46,12 @@ jobs: echo "::error::publishing is only possible in EasyEngine/dockerfiles"; exit 1 fi [[ "$TAG" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]] || { echo "::error::invalid tag: $TAG"; exit 1; } - images=$(jq -cn --arg s "$IMAGES" '$s | split(" ") | map(select(. != "")) | unique') - unknown=$(jq -r --argjson want "$images" '$want - keys | join(" ")' .github/legacy-arm64/recipes.json) + list=$(jq -cn --arg s "$IMAGES" '$s | split(" ") | map(select(. != "")) | unique') + unknown=$(jq -r --argjson want "$list" '$want - keys | join(" ")' .github/legacy-arm64/recipes.json) [ -z "$unknown" ] || { echo "::error::no recipe for: $unknown"; exit 1; } - [ "$images" != "[]" ] || { echo "::error::no images given"; exit 1; } - echo "images=$images" >> "$GITHUB_OUTPUT" - echo "Images: $images, tag: $TAG, publish: $PUBLISH" + [ "$list" != "[]" ] || { echo "::error::no images given"; exit 1; } + echo "images=$list" >> "$GITHUB_OUTPUT" + echo "Images: $list, tag: $TAG, publish: $PUBLISH" reference: #------------------------------------------------------------------ @@ -93,6 +93,10 @@ jobs: needs: [setup, reference] runs-on: ubuntu-24.04-arm name: Build ${{ matrix.image }} (arm64) + # Runs for the same image and tag wait for each other, so the later one sees the earlier tag and refuses. + concurrency: + group: legacy-arm64-${{ matrix.image }}-${{ inputs.tag }} + cancel-in-progress: false strategy: fail-fast: false matrix: From 4e5efee5111401d8192c92103ff361e180c01382 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 23 Oct 2026 23:06:47 +0530 Subject: [PATCH 15/17] ci: build php7.0-7.4 for arm64 without New Relic New Relic publishes no aarch64 agent for PHP 7.x, so the arm64 variants of php7.0, php7.2, php7.3 and php7.4 ship without it. newrelic.ini stays, without its extension line, so the entrypoint still finds it; the parity check allows exactly the New Relic lines to differ. --- .github/legacy-arm64/recipes.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/legacy-arm64/recipes.json b/.github/legacy-arm64/recipes.json index 7126996..7e4c20f 100644 --- a/.github/legacy-arm64/recipes.json +++ b/.github/legacy-arm64/recipes.json @@ -14,8 +14,8 @@ "amd64_manifest": "sha256:dc42a302e1b29627c8dee3fb604fde824bda813336b4ced46b1f8064ab446dad", "ref": "a528d2945d0ea4c2cc2dab241a7e34af3fc56950", "context": "php/7.0", - "newrelic": "unavailable", - "note": "The published v4.7.4 image (= v4.6.6, built 2023-04-21) matches a528d29 plus 'apt-get upgrade -y' and the openssl package, which its history shows and no commit has. Patch: those two, the Debian snapshot of the build date, PECL versions pinned to the amd64 ones. mcrypt-1.0.3 fails on PHP 7.0, so imagick and redis are never enabled, exactly as on amd64." + "newrelic": "skip", + "note": "The published v4.7.4 image (= v4.6.6, built 2023-04-21) matches a528d29 plus 'apt-get upgrade -y' and the openssl package, which its history shows and no commit has. Patch: those two, the Debian snapshot of the build date, PECL versions pinned to the amd64 ones. mcrypt-1.0.3 fails on PHP 7.0, so imagick and redis are never enabled, exactly as on amd64. New Relic publishes no aarch64 agent for PHP 7.x, so arm64 ships without it (maintainer decision); newrelic.ini stays, without its extension line." }, "php7.2": { "amd64_tag": "v4.7.4", @@ -23,8 +23,8 @@ "amd64_manifest": "sha256:bd388833c25634a000207ea450bc4b4044c57ae722eb1fa24d3a550ada4f5d8a", "ref": "7b74e928e21b6825c1c1e290918b324bdffb3a5a", "context": "php/7.2", - "newrelic": "unavailable", - "note": "The published v4.7.4 image (built 2024-10-09) matches 7b74e92 (no WebP, no tmux). Patch: Debian snapshot of the build date, PECL versions pinned to the amd64 ones. redis fails on PHP 7.2, so imagick and mcrypt are never enabled, exactly as on amd64." + "newrelic": "skip", + "note": "The published v4.7.4 image (built 2024-10-09) matches 7b74e92 (no WebP, no tmux). Patch: Debian snapshot of the build date, PECL versions pinned to the amd64 ones. redis fails on PHP 7.2, so imagick and mcrypt are never enabled, exactly as on amd64. New Relic publishes no aarch64 agent for PHP 7.x, so arm64 ships without it (maintainer decision); newrelic.ini stays, without its extension line." }, "php7.3": { "amd64_tag": "v4.7.4", @@ -32,8 +32,8 @@ "amd64_manifest": "sha256:e85e80a6392cec7d9dd9e9c68985d9e461ba70211d532bd7b009990f59081d4b", "ref": "2807ccb3a6d371835488c950ec9164388a4d2c92", "context": "php/7.3", - "newrelic": "unavailable", - "note": "The published v4.7.4 image (built 2024-10-09) matches 2807ccb (no WebP, no tmux). Patch: Debian snapshot of the build date, PECL versions pinned to the amd64 ones. redis fails on PHP 7.3, so imagick, mcrypt, timezonedb and apcu are never enabled, exactly as on amd64." + "newrelic": "skip", + "note": "The published v4.7.4 image (built 2024-10-09) matches 2807ccb (no WebP, no tmux). Patch: Debian snapshot of the build date, PECL versions pinned to the amd64 ones. redis fails on PHP 7.3, so imagick, mcrypt, timezonedb and apcu are never enabled, exactly as on amd64. New Relic publishes no aarch64 agent for PHP 7.x, so arm64 ships without it (maintainer decision); newrelic.ini stays, without its extension line." }, "php7.4": { "amd64_tag": "v4.13.0", @@ -41,8 +41,8 @@ "amd64_manifest": "sha256:84463e410c19a1b5523dc7ee547042311f53d884ba30353228c47b8fef4b7905", "ref": "7fa7f03ff62d9cd2ed55946223cbc1082a7d5cc5", "context": "php/7.4", - "newrelic": "unavailable", - "note": "Built from the v4.13.0 tag (7fa7f03). Patch: PECL versions pinned to the amd64 ones." + "newrelic": "skip", + "note": "Built from the v4.13.0 tag (7fa7f03). Patch: PECL versions pinned to the amd64 ones. New Relic publishes no aarch64 agent for PHP 7.x, so arm64 ships without it (maintainer decision); newrelic.ini stays, without its extension line." }, "php8.0": { "amd64_tag": "v4.13.0", From 1ac9d6c618556edec4db77b46f3779fbebc5711a Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 23 Oct 2026 23:06:47 +0530 Subject: [PATCH 16/17] ci: stop publishing mariadb from the tag-triggered matrix Core stays on MariaDB 10.11, so the tag run no longer builds mariadb/Dockerfile (12.3). The one-time legacy-arm64 workflow publishes the multi-arch 10.11.14 image instead. --- .github/workflows/test_and_publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_and_publish.yml b/.github/workflows/test_and_publish.yml index ada9106..1fa77b1 100644 --- a/.github/workflows/test_and_publish.yml +++ b/.github/workflows/test_and_publish.yml @@ -28,7 +28,7 @@ jobs: strategy: fail-fast: false matrix: - image: ['php8.1', 'php8.2', 'php8.3', 'php8.4', 'php8.5', 'nginx', 'nginx-proxy', 'postfix', 'redis', 'cron', 'mailhog', 'mariadb', 'newrelic-daemon'] + image: ['php8.1', 'php8.2', 'php8.3', 'php8.4', 'php8.5', 'nginx', 'nginx-proxy', 'postfix', 'redis', 'cron', 'mailhog', 'newrelic-daemon'] arch: ['amd64', 'arm64'] steps: - name: Checkout @@ -111,7 +111,7 @@ jobs: strategy: fail-fast: false matrix: - image: ['php8.1', 'php8.2', 'php8.3', 'php8.4', 'php8.5', 'nginx', 'nginx-proxy', 'postfix', 'redis', 'cron', 'mailhog', 'mariadb', 'newrelic-daemon'] + image: ['php8.1', 'php8.2', 'php8.3', 'php8.4', 'php8.5', 'nginx', 'nginx-proxy', 'postfix', 'redis', 'cron', 'mailhog', 'newrelic-daemon'] steps: - name: Download digests uses: actions/download-artifact@v8 From 38476cbb6e58e63a8584fc09a82f8c672dd46610 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 23 Oct 2026 23:06:55 +0530 Subject: [PATCH 17/17] ci: default the one-time legacy tag to v4.14.0 The legacy PHP images and MariaDB 10.11.14 all get their amd64 + arm64 index under v4.14.0. --- .github/workflows/legacy-arm64.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/legacy-arm64.yml b/.github/workflows/legacy-arm64.yml index f13bad1..452f66a 100644 --- a/.github/workflows/legacy-arm64.yml +++ b/.github/workflows/legacy-arm64.yml @@ -16,6 +16,7 @@ on: tag: description: 'New tag for the amd64 + arm64 image; refused if it already exists on Docker Hub' required: true + default: 'v4.14.0' publish: description: 'Push to Docker Hub (otherwise a dry run into a runner-local registry)' type: boolean