From 9a2b1607010d7ea63a4eea4be21a95b80706edfd Mon Sep 17 00:00:00 2001 From: KingPin <{ID}+{username}@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:36:19 -0400 Subject: [PATCH 1/2] perf: optimize CI workflow and Dockerfiles for faster builds - Remove QEMU setup from build-and-test (only builds amd64) - Add setup job: fetches s6-overlay version once and computes PR-aware matrix (skip PHP 8.3 on PRs, test 8.4 + 8.2 only) - Add BuildKit cache mounts for apt/apk in both Dockerfiles - Extract retry/backoff logic to shared extras/retry.sh script, replacing ~50 lines of duplicated inline retry loops - Enable BuildKit for v1 in test-build.sh (required for cache mounts) --- .github/workflows/docker-ci.yml | 51 ++++++++++++++++-------------- Dockerfile.v1 | 55 +++++++++------------------------ Dockerfile.v2 | 52 ++++++++----------------------- extras/retry.sh | 21 +++++++++++++ extras/test-build.sh | 2 +- 5 files changed, 77 insertions(+), 104 deletions(-) create mode 100755 extras/retry.sh diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml index 5124ebf..d01bde7 100644 --- a/.github/workflows/docker-ci.yml +++ b/.github/workflows/docker-ci.yml @@ -30,13 +30,37 @@ concurrency: cancel-in-progress: true jobs: + setup: + runs-on: ubuntu-latest + outputs: + php-versions: ${{ steps.matrix.outputs.php-versions }} + s6-version: ${{ steps.s6.outputs.version }} + steps: + - name: Determine PHP versions to test + id: matrix + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + echo 'php-versions=["8.4","8.2"]' >> $GITHUB_OUTPUT + echo "::notice::PR detected — testing PHP 8.4 + 8.2 only (skipping 8.3)" + else + echo 'php-versions=["8.4","8.3","8.2"]' >> $GITHUB_OUTPUT + fi + + - name: Get latest s6-overlay version + id: s6 + run: | + S6_OVERLAY_VERSION="$(curl -s https://api.github.com/repos/just-containers/s6-overlay/releases/latest | jq -r .tag_name)" + echo "version=${S6_OVERLAY_VERSION}" >> $GITHUB_OUTPUT + echo "✅ Latest s6-overlay version: ${S6_OVERLAY_VERSION}" + build-and-test: + needs: setup runs-on: ubuntu-latest strategy: fail-fast: false matrix: variant: [v1, v2] - php-version: ['8.4', '8.3', '8.2'] + php-version: ${{ fromJson(needs.setup.outputs.php-versions) }} php-type: [fpm, cli, apache] php-base: [alpine, bookworm] exclude: @@ -88,18 +112,6 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Get latest s6-overlay version - id: s6-version - run: | - S6_OVERLAY_VERSION="$(curl -s https://api.github.com/repos/just-containers/s6-overlay/releases/latest | jq -r .tag_name)" - echo "version=${S6_OVERLAY_VERSION}" >> $GITHUB_OUTPUT - echo "✅ Latest s6-overlay version: ${S6_OVERLAY_VERSION}" - - - name: Setup QEMU - uses: docker/setup-qemu-action@v3 - with: - platforms: amd64,arm64,arm - - name: Setup Docker Buildx uses: docker/setup-buildx-action@v3 @@ -138,7 +150,7 @@ jobs: VERSION=${{ steps.vars.outputs.VERSION }} PHPVERSION=${{ matrix.php-version }} BASEOS=${{ matrix.php-base }} - S6_OVERLAY_VERSION=${{ steps.s6-version.outputs.version }} + S6_OVERLAY_VERSION=${{ needs.setup.outputs.s6-version }} BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }} VCS_REF=${{ github.sha }} tags: test-${{ steps.vars.outputs.TAG }} @@ -295,7 +307,7 @@ jobs: echo "::notice::✅ Build and tests passed for ${{ matrix.variant }} - ${{ steps.vars.outputs.TAG }}" publish: - needs: build-and-test + needs: [setup, build-and-test] if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'schedule') runs-on: ubuntu-latest strategy: @@ -356,13 +368,6 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Get latest s6-overlay version - id: s6-version - run: | - S6_OVERLAY_VERSION="$(curl -s https://api.github.com/repos/just-containers/s6-overlay/releases/latest | jq -r .tag_name)" - echo "version=${S6_OVERLAY_VERSION}" >> $GITHUB_OUTPUT - echo "✅ Latest s6-overlay version: ${S6_OVERLAY_VERSION}" - - name: Setup QEMU uses: docker/setup-qemu-action@v3 with: @@ -429,7 +434,7 @@ jobs: VERSION=${{ steps.vars.outputs.VERSION }} PHPVERSION=${{ matrix.php-version }} BASEOS=${{ matrix.php-base }} - S6_OVERLAY_VERSION=${{ steps.s6-version.outputs.version }} + S6_OVERLAY_VERSION=${{ needs.setup.outputs.s6-version }} BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }} VCS_REF=${{ github.sha }} tags: | diff --git a/Dockerfile.v1 b/Dockerfile.v1 index d980359..c6acd2f 100644 --- a/Dockerfile.v1 +++ b/Dockerfile.v1 @@ -6,57 +6,30 @@ ARG BASEOS # Set environment variables ENV DEBIAN_FRONTEND=noninteractive +COPY extras/retry.sh /usr/local/bin/retry +RUN chmod +x /usr/local/bin/retry + # Install dependencies based on the base OS -RUN if [ "$BASEOS" = "bookworm" ]; then \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=apt-$BASEOS \ + --mount=type=cache,target=/var/lib/apt/lists,sharing=locked,id=aptlists-$BASEOS \ + --mount=type=cache,target=/var/cache/apk,sharing=locked,id=apk-$BASEOS \ + if [ "$BASEOS" = "bookworm" ]; then \ echo 'deb http://deb.debian.org/debian bookworm main' > /etc/apt/sources.list && \ apt-get update && \ apt-get -y upgrade && \ - apt-get install -y --no-install-recommends curl git zip unzip ghostscript imagemagick optipng gifsicle pngcrush jpegoptim libjpeg-turbo-progs pngquant webp && \ - rm -rf /var/lib/apt/lists/*; \ + apt-get install -y --no-install-recommends curl git zip unzip ghostscript imagemagick optipng gifsicle pngcrush jpegoptim libjpeg-turbo-progs pngquant webp; \ elif [ "$BASEOS" = "alpine" ]; then \ apk update && \ apk add --no-cache curl git zip unzip ghostscript imagemagick optipng gifsicle pngcrush jpegoptim libjpeg-turbo libjpeg-turbo-utils pngquant libwebp-tools; \ fi -# Add all needed PHP extensions with retry logic for transient network failures -RUN for ATTEMPT in 1 2 3; do \ - if curl -sSLf -o /usr/local/bin/install-php-extensions \ - https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions; then \ - break; \ - else \ - if [ $ATTEMPT -lt 3 ]; then \ - case $ATTEMPT in \ - 1) SLEEP_TIME=5 ;; \ - 2) SLEEP_TIME=10 ;; \ - esac; \ - echo "Download attempt $ATTEMPT failed, retrying in ${SLEEP_TIME}s..."; \ - sleep $SLEEP_TIME; \ - rm -f /usr/local/bin/install-php-extensions; \ - else \ - echo "Failed to download install-php-extensions after 3 attempts"; \ - exit 1; \ - fi; \ - fi; \ - done && \ +# Download and install PHP extensions with retry for transient failures +RUN retry 3 curl -sSLf -o /usr/local/bin/install-php-extensions \ + https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions && \ chmod +x /usr/local/bin/install-php-extensions && \ - for ATTEMPT in 1 2 3; do \ - if install-php-extensions amqp bcmath bz2 calendar ctype exif intl imagick imap json mbstring ldap mcrypt memcached mongodb \ - mysqli opcache pdo_mysql pdo_pgsql pgsql redis snmp soap sockets tidy timezonedb uuid vips xsl yaml zip zstd @composer; then \ - break; \ - else \ - if [ $ATTEMPT -lt 3 ]; then \ - case $ATTEMPT in \ - 1) SLEEP_TIME=5 ;; \ - 2) SLEEP_TIME=10 ;; \ - esac; \ - echo "Extension installation attempt $ATTEMPT failed, retrying in ${SLEEP_TIME}s..."; \ - sleep $SLEEP_TIME; \ - else \ - echo "Failed to install PHP extensions after 3 attempts"; \ - exit 1; \ - fi; \ - fi; \ - done + retry 3 install-php-extensions \ + amqp bcmath bz2 calendar ctype exif intl imagick imap json mbstring ldap mcrypt memcached mongodb \ + mysqli opcache pdo_mysql pdo_pgsql pgsql redis snmp soap sockets tidy timezonedb uuid vips xsl yaml zip zstd @composer # Enable Apache rewrite mod, if applicable RUN if command -v a2enmod; then a2enmod rewrite; fi diff --git a/Dockerfile.v2 b/Dockerfile.v2 index 6c03b23..8edea30 100644 --- a/Dockerfile.v2 +++ b/Dockerfile.v2 @@ -12,6 +12,9 @@ ARG VERSION # Set environment variables ENV DEBIAN_FRONTEND=noninteractive +COPY extras/retry.sh /usr/local/bin/retry +RUN chmod +x /usr/local/bin/retry + # OCI standard labels LABEL org.opencontainers.image.title="php-docker" \ org.opencontainers.image.description="PHP runtime with s6-overlay and curated extensions" \ @@ -26,7 +29,10 @@ LABEL org.opencontainers.image.title="php-docker" \ # Install build dependencies, PHP extensions, runtime libraries, and s6-overlay # Then clean up build-only packages in a single layer to minimize image size -RUN if [ "$BASEOS" = "trixie" ] || [ "$BASEOS" = "bookworm" ]; then \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=apt-$BASEOS \ + --mount=type=cache,target=/var/lib/apt/lists,sharing=locked,id=aptlists-$BASEOS \ + --mount=type=cache,target=/var/cache/apk,sharing=locked,id=apk-$BASEOS \ + if [ "$BASEOS" = "trixie" ] || [ "$BASEOS" = "bookworm" ]; then \ apt-get update && \ apt-get -y upgrade && \ # Install build tools, dev packages, and runtime libraries together @@ -129,25 +135,8 @@ RUN if [ "$BASEOS" = "trixie" ] || [ "$BASEOS" = "bookworm" ]; then \ libxpm libxpm-dev; \ fi && \ # Download PHP extension installer with retry - for ATTEMPT in 1 2 3; do \ - if curl -sSLf -o /usr/local/bin/install-php-extensions \ - https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions; then \ - break; \ - else \ - if [ $ATTEMPT -lt 3 ]; then \ - case $ATTEMPT in \ - 1) SLEEP_TIME=5 ;; \ - 2) SLEEP_TIME=10 ;; \ - esac; \ - echo "Download attempt $ATTEMPT failed, retrying in ${SLEEP_TIME}s..."; \ - sleep $SLEEP_TIME; \ - rm -f /usr/local/bin/install-php-extensions; \ - else \ - echo "Failed to download install-php-extensions after 3 attempts"; \ - exit 1; \ - fi; \ - fi; \ - done && \ + retry 3 curl -sSLf -o /usr/local/bin/install-php-extensions \ + https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions && \ chmod +x /usr/local/bin/install-php-extensions && \ # Install PHP extensions install-php-extensions \ @@ -173,25 +162,10 @@ RUN if [ "$BASEOS" = "trixie" ] || [ "$BASEOS" = "bookworm" ]; then \ *) S6_ARCH="x86_64" ;; \ esac && \ echo "Downloading s6-overlay ${S6_OVERLAY_VERSION} for ${S6_ARCH}" && \ - for ATTEMPT in 1 2 3; do \ - if wget -O /tmp/s6-overlay-noarch.tar.xz https://github.com/just-containers/s6-overlay/releases/download/${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz && \ - wget -O /tmp/s6-overlay-${S6_ARCH}.tar.xz https://github.com/just-containers/s6-overlay/releases/download/${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz; then \ - break; \ - else \ - if [ $ATTEMPT -lt 3 ]; then \ - case $ATTEMPT in \ - 1) SLEEP_TIME=5 ;; \ - 2) SLEEP_TIME=10 ;; \ - esac; \ - echo "Download attempt $ATTEMPT failed, retrying in ${SLEEP_TIME}s..."; \ - sleep $SLEEP_TIME; \ - rm -f /tmp/s6-overlay-*.tar.xz; \ - else \ - echo "Failed to download s6-overlay after 3 attempts"; \ - exit 1; \ - fi; \ - fi; \ - done && \ + retry 3 wget -O /tmp/s6-overlay-noarch.tar.xz \ + https://github.com/just-containers/s6-overlay/releases/download/${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz && \ + retry 3 wget -O /tmp/s6-overlay-${S6_ARCH}.tar.xz \ + https://github.com/just-containers/s6-overlay/releases/download/${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz && \ tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz && \ tar -C / -Jxpf /tmp/s6-overlay-${S6_ARCH}.tar.xz && \ rm /tmp/s6-overlay-noarch.tar.xz /tmp/s6-overlay-${S6_ARCH}.tar.xz && \ diff --git a/extras/retry.sh b/extras/retry.sh new file mode 100755 index 0000000..75affcb --- /dev/null +++ b/extras/retry.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# Retry a command with exponential backoff +# Usage: retry +# Example: retry 3 curl -sSLf -o /tmp/file https://example.com/file + +MAX=$1 +shift + +for ATTEMPT in $(seq 1 "$MAX"); do + if "$@"; then + exit 0 + fi + if [ "$ATTEMPT" -lt "$MAX" ]; then + SLEEP_TIME=$((5 * ATTEMPT)) + echo "Attempt $ATTEMPT/$MAX failed, retrying in ${SLEEP_TIME}s..." + sleep "$SLEEP_TIME" + fi +done + +echo "Failed after $MAX attempts: $*" +exit 1 diff --git a/extras/test-build.sh b/extras/test-build.sh index 386be95..effab73 100755 --- a/extras/test-build.sh +++ b/extras/test-build.sh @@ -51,7 +51,7 @@ build_v1() { echo "Building v1: ${IMAGE_NAME}:${tag}" echo " VERSION=${version}, PHPVERSION=${phpversion}, BASEOS=${baseos}" - docker build -f Dockerfile.v1 \ + DOCKER_BUILDKIT=1 docker build -f Dockerfile.v1 \ --build-arg VERSION="${version}" \ --build-arg PHPVERSION="${phpversion}" \ --build-arg BASEOS="${baseos}" \ From c11ad2375be3b0aae99a375a6acff0f8cbda881d Mon Sep 17 00:00:00 2001 From: KingPin <{ID}+{username}@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:35:29 -0400 Subject: [PATCH 2/2] fix: address PR review feedback - Rename hyphenated output keys to underscores (php_versions, s6_version) - Generate full matrix (including trixie includes) in setup job so PR matrix reduction correctly skips PHP 8.3 everywhere - Harden s6-overlay API call with -f flag, auth token, and null check - Remove useless apk cache mount (apk --no-cache bypasses cache dir) - Fix retry.sh comment: backoff is linear, not exponential --- .github/workflows/docker-ci.yml | 97 ++++++++++++++------------------- Dockerfile.v1 | 1 - Dockerfile.v2 | 1 - extras/retry.sh | 2 +- 4 files changed, 43 insertions(+), 58 deletions(-) diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml index d01bde7..54285ea 100644 --- a/.github/workflows/docker-ci.yml +++ b/.github/workflows/docker-ci.yml @@ -33,23 +33,56 @@ jobs: setup: runs-on: ubuntu-latest outputs: - php-versions: ${{ steps.matrix.outputs.php-versions }} - s6-version: ${{ steps.s6.outputs.version }} + matrix: ${{ steps.matrix.outputs.matrix }} + s6_version: ${{ steps.s6.outputs.version }} steps: - - name: Determine PHP versions to test + - name: Compute build matrix id: matrix run: | if [ "${{ github.event_name }}" = "pull_request" ]; then - echo 'php-versions=["8.4","8.2"]' >> $GITHUB_OUTPUT + PHP_VERSIONS='["8.4","8.2"]' echo "::notice::PR detected — testing PHP 8.4 + 8.2 only (skipping 8.3)" else - echo 'php-versions=["8.4","8.3","8.2"]' >> $GITHUB_OUTPUT + PHP_VERSIONS='["8.4","8.3","8.2"]' fi + # Build trixie include list for v2 based on selected PHP versions + INCLUDES="[]" + for ver in $(echo "$PHP_VERSIONS" | jq -r '.[]'); do + for type in fpm cli apache; do + INCLUDES=$(echo "$INCLUDES" | jq -c ". + [{\"variant\":\"v2\",\"php-version\":\"$ver\",\"php-type\":\"$type\",\"php-base\":\"trixie\"}]") + done + done + + MATRIX=$(jq -n -c \ + --argjson versions "$PHP_VERSIONS" \ + --argjson includes "$INCLUDES" \ + '{ + "variant": ["v1","v2"], + "php-version": $versions, + "php-type": ["fpm","cli","apache"], + "php-base": ["alpine","bookworm"], + "exclude": [ + {"php-type":"apache","php-base":"alpine"}, + {"variant":"v2","php-base":"bookworm"} + ], + "include": $includes + }') + + echo "matrix=$MATRIX" >> $GITHUB_OUTPUT + - name: Get latest s6-overlay version id: s6 run: | - S6_OVERLAY_VERSION="$(curl -s https://api.github.com/repos/just-containers/s6-overlay/releases/latest | jq -r .tag_name)" + set -euo pipefail + RESPONSE="$(curl -fSLs \ + -H "Authorization: Bearer ${{ github.token }}" \ + https://api.github.com/repos/just-containers/s6-overlay/releases/latest)" + S6_OVERLAY_VERSION="$(echo "$RESPONSE" | jq -r .tag_name)" + if [ -z "$S6_OVERLAY_VERSION" ] || [ "$S6_OVERLAY_VERSION" = "null" ]; then + echo "::error::Failed to determine s6-overlay version" + exit 1 + fi echo "version=${S6_OVERLAY_VERSION}" >> $GITHUB_OUTPUT echo "✅ Latest s6-overlay version: ${S6_OVERLAY_VERSION}" @@ -58,53 +91,7 @@ jobs: runs-on: ubuntu-latest strategy: fail-fast: false - matrix: - variant: [v1, v2] - php-version: ${{ fromJson(needs.setup.outputs.php-versions) }} - php-type: [fpm, cli, apache] - php-base: [alpine, bookworm] - exclude: - - php-type: apache - php-base: alpine - - variant: v2 - php-base: bookworm - include: - - variant: v2 - php-version: '8.4' - php-type: fpm - php-base: trixie - - variant: v2 - php-version: '8.4' - php-type: cli - php-base: trixie - - variant: v2 - php-version: '8.4' - php-type: apache - php-base: trixie - - variant: v2 - php-version: '8.3' - php-type: fpm - php-base: trixie - - variant: v2 - php-version: '8.3' - php-type: cli - php-base: trixie - - variant: v2 - php-version: '8.3' - php-type: apache - php-base: trixie - - variant: v2 - php-version: '8.2' - php-type: fpm - php-base: trixie - - variant: v2 - php-version: '8.2' - php-type: cli - php-base: trixie - - variant: v2 - php-version: '8.2' - php-type: apache - php-base: trixie + matrix: ${{ fromJson(needs.setup.outputs.matrix) }} name: ${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }} @@ -150,7 +137,7 @@ jobs: VERSION=${{ steps.vars.outputs.VERSION }} PHPVERSION=${{ matrix.php-version }} BASEOS=${{ matrix.php-base }} - S6_OVERLAY_VERSION=${{ needs.setup.outputs.s6-version }} + S6_OVERLAY_VERSION=${{ needs.setup.outputs.s6_version }} BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }} VCS_REF=${{ github.sha }} tags: test-${{ steps.vars.outputs.TAG }} @@ -434,7 +421,7 @@ jobs: VERSION=${{ steps.vars.outputs.VERSION }} PHPVERSION=${{ matrix.php-version }} BASEOS=${{ matrix.php-base }} - S6_OVERLAY_VERSION=${{ needs.setup.outputs.s6-version }} + S6_OVERLAY_VERSION=${{ needs.setup.outputs.s6_version }} BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }} VCS_REF=${{ github.sha }} tags: | diff --git a/Dockerfile.v1 b/Dockerfile.v1 index c6acd2f..c1fa958 100644 --- a/Dockerfile.v1 +++ b/Dockerfile.v1 @@ -12,7 +12,6 @@ RUN chmod +x /usr/local/bin/retry # Install dependencies based on the base OS RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=apt-$BASEOS \ --mount=type=cache,target=/var/lib/apt/lists,sharing=locked,id=aptlists-$BASEOS \ - --mount=type=cache,target=/var/cache/apk,sharing=locked,id=apk-$BASEOS \ if [ "$BASEOS" = "bookworm" ]; then \ echo 'deb http://deb.debian.org/debian bookworm main' > /etc/apt/sources.list && \ apt-get update && \ diff --git a/Dockerfile.v2 b/Dockerfile.v2 index 8edea30..2d7fb79 100644 --- a/Dockerfile.v2 +++ b/Dockerfile.v2 @@ -31,7 +31,6 @@ LABEL org.opencontainers.image.title="php-docker" \ # Then clean up build-only packages in a single layer to minimize image size RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=apt-$BASEOS \ --mount=type=cache,target=/var/lib/apt/lists,sharing=locked,id=aptlists-$BASEOS \ - --mount=type=cache,target=/var/cache/apk,sharing=locked,id=apk-$BASEOS \ if [ "$BASEOS" = "trixie" ] || [ "$BASEOS" = "bookworm" ]; then \ apt-get update && \ apt-get -y upgrade && \ diff --git a/extras/retry.sh b/extras/retry.sh index 75affcb..d6405f1 100755 --- a/extras/retry.sh +++ b/extras/retry.sh @@ -1,5 +1,5 @@ #!/bin/sh -# Retry a command with exponential backoff +# Retry a command with linear backoff # Usage: retry # Example: retry 3 curl -sSLf -o /tmp/file https://example.com/file