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..1c1c1a4 --- /dev/null +++ b/.github/legacy-arm64/php5.6.patch @@ -0,0 +1,36 @@ +--- a/Dockerfile ++++ b/Dockerfile +@@ -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. ++# /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 ${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" +@@ -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 + +-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..e05ad63 --- /dev/null +++ b/.github/legacy-arm64/php7.0.patch @@ -0,0 +1,45 @@ +--- a/Dockerfile ++++ b/Dockerfile +@@ -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. ++# /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 ${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" +@@ -7,8 +18,11 @@ + + RUN set -ex; \ + 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 +43,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..7e4c20f --- /dev/null +++ b/.github/legacy-arm64/recipes.json @@ -0,0 +1,66 @@ +{ + "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": "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", + "amd64_digest": "sha256:bd388833c25634a000207ea450bc4b4044c57ae722eb1fa24d3a550ada4f5d8a", + "amd64_manifest": "sha256:bd388833c25634a000207ea450bc4b4044c57ae722eb1fa24d3a550ada4f5d8a", + "ref": "7b74e928e21b6825c1c1e290918b324bdffb3a5a", + "context": "php/7.2", + "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", + "amd64_digest": "sha256:e85e80a6392cec7d9dd9e9c68985d9e461ba70211d532bd7b009990f59081d4b", + "amd64_manifest": "sha256:e85e80a6392cec7d9dd9e9c68985d9e461ba70211d532bd7b009990f59081d4b", + "ref": "2807ccb3a6d371835488c950ec9164388a4d2c92", + "context": "php/7.3", + "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", + "amd64_digest": "sha256:f1a007ff60dd2a831449d8ac7795cc6381577ae887b8f316f3d2c37fabb5261f", + "amd64_manifest": "sha256:84463e410c19a1b5523dc7ee547042311f53d884ba30353228c47b8fef4b7905", + "ref": "7fa7f03ff62d9cd2ed55946223cbc1082a7d5cc5", + "context": "php/7.4", + "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", + "amd64_digest": "sha256:8a4848e0e574558dbc3eb7e0f7505d6e93a7d7a341d82120062a8547e1514f5b", + "amd64_manifest": "sha256:b7da1852dc8820982f61d249cdb4fd6ae124cec9c0f0f4f4435f49670b5c7f90", + "ref": "7fa7f03ff62d9cd2ed55946223cbc1082a7d5cc5", + "context": "php/8.0", + "newrelic": "agent", + "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", + "amd64_digest": "sha256:607745183a0ecdf3b59ae0ff39d4a64703e85374eae6aab204183c3b7283b53f", + "amd64_manifest": "sha256:607745183a0ecdf3b59ae0ff39d4a64703e85374eae6aab204183c3b7283b53f", + "ref": "f6ce783aa21e6d8d4141083f969d5c0499719566", + "context": "mariadb", + "newrelic": "none", + "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 new file mode 100755 index 0000000..20fa609 --- /dev/null +++ b/.github/scripts/legacy-arm64.sh @@ -0,0 +1,138 @@ +#!/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"; } +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. +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 + 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 + # 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 absent bad=0 f + nr=$(recipe newrelic) + # ini settings that don't exist on arm64 for this PHP version (documented in the recipe). + 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: /' + 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 + 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 + 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 + 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/scripts/smoke-test.sh b/.github/scripts/smoke-test.sh new file mode 100755 index 0000000..c808546 --- /dev/null +++ b/.github/scripts/smoke-test.sh @@ -0,0 +1,125 @@ +#!/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 +} + +# 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" + +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). + [ "$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) + 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. + 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) + 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 + [ "$(docker inspect -f '{{.State.Running}}' "$cid")" = true ] || { docker logs "$cid" >&2; fail "newrelic-daemon exited"; } + ;; +*) + fail "unknown image" + ;; +esac + +echo "OK [$name/$arch] $ref" diff --git a/.github/workflows/legacy-arm64.yml b/.github/workflows/legacy-arm64.yml new file mode 100644 index 0000000..452f66a --- /dev/null +++ b/.github/workflows/legacy-arm64.yml @@ -0,0 +1,224 @@ +# # +# # 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 + default: 'v4.14.0' + 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 == true && 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; } + 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; } + [ "$list" != "[]" ] || { echo "::error::no images given"; exit 1; } + echo "images=$list" >> "$GITHUB_OUTPUT" + echo "Images: $list, 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) + # 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: + 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 }} diff --git a/.github/workflows/test_and_publish.yml b/.github/workflows/test_and_publish.yml index a802918..1fa77b1 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,179 @@ 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', '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: Img tag - id: img-tag - run: | - PHP=${{ matrix.php }} - echo "tag=${PHP/stable/}" >> $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 PHP - ${{ matrix.php }} image + - name: Build ${{ matrix.image }} image uses: docker/build-push-action@v7 - if: github.event_name == 'pull_request' with: - context: php/${{ matrix.php }} - tags: easyengine/php${{ steps.img-tag.outputs.tag }}:test - - - name: Build and push PHP - ${{ matrix.php }} image + context: ${{ steps.ctx.outputs.dir }} + 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 }} + + # 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 }} - 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 }} + 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' + 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: + 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', 'newrelic-daemon'] steps: - - name: Checkout - uses: actions/checkout@v7 + - name: Download digests + uses: actions/download-artifact@v8 + with: + pattern: digests-${{ matrix.image }}-{amd64,arm64} + 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 }}-{amd64,arm64} + 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 }} 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 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;