diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml index 5124ebf..54285ea 100644 --- a/.github/workflows/docker-ci.yml +++ b/.github/workflows/docker-ci.yml @@ -30,57 +30,68 @@ concurrency: cancel-in-progress: true jobs: + setup: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} + s6_version: ${{ steps.s6.outputs.version }} + steps: + - name: Compute build matrix + id: matrix + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + PHP_VERSIONS='["8.4","8.2"]' + echo "::notice::PR detected — testing PHP 8.4 + 8.2 only (skipping 8.3)" + else + 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: | + 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}" + 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-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 }} @@ -88,18 +99,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 +137,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 +294,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 +355,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 +421,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..c1fa958 100644 --- a/Dockerfile.v1 +++ b/Dockerfile.v1 @@ -6,57 +6,29 @@ 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 \ + 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..2d7fb79 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,9 @@ 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 \ + if [ "$BASEOS" = "trixie" ] || [ "$BASEOS" = "bookworm" ]; then \ apt-get update && \ apt-get -y upgrade && \ # Install build tools, dev packages, and runtime libraries together @@ -129,25 +134,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 +161,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..d6405f1 --- /dev/null +++ b/extras/retry.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# Retry a command with linear 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}" \