Docker CI (v1 + v2) #63
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Docker CI (v1 + v2) | |
| on: | |
| push: | |
| # Only run full CI on pushes to main (publishes/metrics). Running on | |
| # every branch push plus pull_request creates duplicate runs when a PR | |
| # receives new commits because GitHub emits both push and pull_request | |
| # events for the same push. Restrict push to main to avoid that. | |
| branches: | |
| - 'main' | |
| paths-ignore: | |
| - '**.md' | |
| - 'docs/**' | |
| - 'LICENSE' | |
| - '.gitignore' | |
| pull_request: | |
| branches: | |
| - '**' | |
| paths-ignore: | |
| - '**.md' | |
| - 'docs/**' | |
| - 'LICENSE' | |
| - '.gitignore' | |
| schedule: | |
| - cron: '0 3 * * 2' # Weekly on Tuesday at 3:00 AM UTC | |
| workflow_dispatch: | |
| concurrency: | |
| group: docker-ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| build-and-test: | |
| # runs-on: ubuntu-latest | |
| runs-on: arc-s2-runner | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| variant: [v1, v2] | |
| php-version: ['8.4', '8.3', '8.2'] | |
| php-type: [fpm, cli] | |
| php-base: [alpine, bookworm] | |
| exclude: | |
| - php-type: apache | |
| php-base: alpine | |
| # v2 uses trixie as the Debian base; bookworm retained for v1 | |
| - variant: v2 | |
| php-base: bookworm | |
| include: | |
| # v2 builds on trixie for Debian images | |
| - 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.3' | |
| php-type: fpm | |
| php-base: trixie | |
| - variant: v2 | |
| php-version: '8.3' | |
| php-type: cli | |
| 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 | |
| name: ${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }} | |
| steps: | |
| - 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 | |
| - name: Set build variables | |
| id: vars | |
| run: | | |
| VERSION="${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }}" | |
| TAG_BASE="php-docker:${VERSION}" | |
| if [ "${{ matrix.variant }}" = "v2" ]; then | |
| TAG="${TAG_BASE}-v2" | |
| DOCKERFILE="Dockerfile.v2" | |
| else | |
| TAG="${TAG_BASE}" | |
| DOCKERFILE="Dockerfile.v1" | |
| fi | |
| BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") | |
| echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT | |
| echo "TAG=${TAG}" >> $GITHUB_OUTPUT | |
| echo "DOCKERFILE=${DOCKERFILE}" >> $GITHUB_OUTPUT | |
| echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_OUTPUT | |
| echo "CACHE_SCOPE=${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }}" >> $GITHUB_OUTPUT | |
| - name: Build test image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: ${{ steps.vars.outputs.DOCKERFILE }} | |
| load: true | |
| platforms: linux/amd64 | |
| cache-from: type=gha,scope=${{ steps.vars.outputs.CACHE_SCOPE }} | |
| cache-to: type=gha,mode=max,scope=${{ steps.vars.outputs.CACHE_SCOPE }} | |
| build-args: | | |
| VERSION=${{ steps.vars.outputs.VERSION }} | |
| PHPVERSION=${{ matrix.php-version }} | |
| BASEOS=${{ matrix.php-base }} | |
| S6_OVERLAY_VERSION=${{ steps.s6-version.outputs.version }} | |
| BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }} | |
| VCS_REF=${{ github.sha }} | |
| tags: test-${{ steps.vars.outputs.TAG }} | |
| - name: Smoke tests - PHP version | |
| run: | | |
| echo "::group::Testing PHP version" | |
| if ! docker run --rm test-${{ steps.vars.outputs.TAG }} php -v | tee php-version.txt; then | |
| echo "::error::Failed to run php -v" | |
| docker logs test-${{ steps.vars.outputs.TAG }} 2>&1 || true | |
| exit 1 | |
| fi | |
| if ! grep -q "${{ matrix.php-version }}" php-version.txt; then | |
| echo "::error::PHP version mismatch - expected ${{ matrix.php-version }}" | |
| cat php-version.txt | |
| exit 1 | |
| fi | |
| echo "✅ PHP version correct" | |
| echo "::endgroup::" | |
| - name: Smoke tests - Basic PHP CLI run | |
| run: | | |
| echo "::group::Testing basic PHP CLI execution" | |
| SAPI=$(docker run --rm test-${{ steps.vars.outputs.TAG }} php -r "echo PHP_SAPI;" 2>&1) | |
| if [ $? -ne 0 ]; then | |
| echo "::error::Failed to execute PHP CLI test" | |
| echo "$SAPI" | |
| exit 1 | |
| fi | |
| echo "✅ PHP CLI runs successfully (SAPI: $SAPI)" | |
| echo "::endgroup::" | |
| - name: Smoke tests - Extensions | |
| run: | | |
| echo "::group::Testing PHP extensions" | |
| if ! docker run --rm test-${{ steps.vars.outputs.TAG }} php -m | tee extensions.txt; then | |
| echo "::error::Failed to list PHP extensions" | |
| docker logs test-${{ steps.vars.outputs.TAG }} 2>&1 || true | |
| exit 1 | |
| fi | |
| # Core extensions that should be present | |
| REQUIRED_EXTS="gd json mysqli zip" | |
| MISSING_EXTS="" | |
| for ext in $REQUIRED_EXTS; do | |
| if ! grep -qi "$ext" extensions.txt; then | |
| MISSING_EXTS="$MISSING_EXTS $ext" | |
| echo "::error::Missing extension: $ext" | |
| else | |
| echo "✅ Extension $ext found" | |
| fi | |
| done | |
| if [ -n "$MISSING_EXTS" ]; then | |
| echo "::error::Missing required extensions:$MISSING_EXTS" | |
| echo "Available extensions:" | |
| cat extensions.txt | |
| exit 1 | |
| fi | |
| echo "::endgroup::" | |
| - name: Smoke tests - Entrypoint quick-run | |
| run: | | |
| echo "::group::Testing entrypoint/init quick-run" | |
| OUTPUT=$(docker run --rm test-${{ steps.vars.outputs.TAG }} php -r "echo 'entrypoint-ok';" 2>&1) | |
| EXIT_CODE=$? | |
| if [ $EXIT_CODE -ne 0 ]; then | |
| echo "::error::Entrypoint test failed with exit code $EXIT_CODE" | |
| echo "$OUTPUT" | |
| exit 1 | |
| fi | |
| if ! echo "$OUTPUT" | grep -q "entrypoint-ok"; then | |
| echo "::error::Entrypoint did not produce expected output" | |
| echo "Output: $OUTPUT" | |
| exit 1 | |
| fi | |
| echo "✅ Entrypoint executes successfully" | |
| echo "::endgroup::" | |
| - name: Smoke tests - Directory permissions | |
| run: | | |
| echo "::group::Testing directory permissions" | |
| DIRS_TO_CHECK="/tmp /var/www" | |
| for dir in $DIRS_TO_CHECK; do | |
| if ! docker run --rm test-${{ steps.vars.outputs.TAG }} sh -c "test -d $dir && [ -w $dir ]" 2>&1; then | |
| echo "::warning::Directory $dir either doesn't exist or is not writable" | |
| else | |
| echo "✅ Directory $dir exists and is writable" | |
| fi | |
| done | |
| echo "::endgroup::" | |
| - name: Smoke tests - v2 specific (s6-overlay) | |
| if: matrix.variant == 'v2' | |
| run: | | |
| echo "::group::Testing s6-overlay presence and PID1 behavior" | |
| # Check s6-overlay directory | |
| if ! docker run --rm test-${{ steps.vars.outputs.TAG }} sh -c "test -d /etc/s6-overlay" 2>&1; then | |
| echo "::error::s6-overlay directory not found at /etc/s6-overlay" | |
| docker run --rm test-${{ steps.vars.outputs.TAG }} ls -la /etc/ 2>&1 || true | |
| exit 1 | |
| fi | |
| echo "✅ s6-overlay directory exists" | |
| # Check init binary | |
| if ! docker run --rm test-${{ steps.vars.outputs.TAG }} sh -c "test -f /init" 2>&1; then | |
| echo "::error::s6 init binary not found at /init" | |
| docker run --rm test-${{ steps.vars.outputs.TAG }} ls -la / 2>&1 || true | |
| exit 1 | |
| fi | |
| echo "✅ s6 init binary exists" | |
| # Check for s6-overlay services directory | |
| if ! docker run --rm test-${{ steps.vars.outputs.TAG }} sh -c "test -d /etc/s6-overlay/s6-rc.d || test -d /etc/services.d" 2>&1; then | |
| echo "::warning::s6 services directory not found (expected /etc/s6-overlay/s6-rc.d or /etc/services.d)" | |
| else | |
| echo "✅ s6 services directory found" | |
| fi | |
| echo "::endgroup::" | |
| - name: Smoke tests - FPM specific | |
| if: matrix.php-type == 'fpm' | |
| run: | | |
| echo "::group::Testing PHP-FPM" | |
| if ! docker run --rm test-${{ steps.vars.outputs.TAG }} php-fpm --version 2>&1 | tee fpm-version.txt; then | |
| echo "::error::Failed to run php-fpm --version" | |
| cat fpm-version.txt || true | |
| exit 1 | |
| fi | |
| echo "✅ PHP-FPM version check passed" | |
| echo "::endgroup::" | |
| - name: Summary | |
| run: | | |
| echo "::notice::✅ Build and tests passed for ${{ matrix.variant }} - ${{ steps.vars.outputs.TAG }}" | |
| publish: | |
| needs: build-and-test | |
| if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'schedule') | |
| # runs-on: ubuntu-latest | |
| runs-on: arc-s2-runner | |
| 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 | |
| # v2 uses trixie as the Debian base; bookworm retained for v1 | |
| - variant: v2 | |
| php-base: bookworm | |
| include: | |
| # v2 builds on trixie for Debian images | |
| - 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 | |
| name: publish-${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }} | |
| steps: | |
| - 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 | |
| - name: Login to DockerHub | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Login to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.repository_owner }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Login to Quay.io | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: quay.io | |
| username: ${{ secrets.QUAY_USERNAME }} | |
| password: ${{ secrets.QUAY_ROBOT_TOKEN }} | |
| - name: Set publish variables | |
| id: vars | |
| run: | | |
| VERSION="${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }}" | |
| if [ "${{ matrix.variant }}" = "v2" ]; then | |
| TAG_SUFFIX="-v2" | |
| DOCKERFILE="Dockerfile.v2" | |
| else | |
| TAG_SUFFIX="" | |
| DOCKERFILE="Dockerfile.v1" | |
| fi | |
| BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") | |
| echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT | |
| echo "TAG_SUFFIX=${TAG_SUFFIX}" >> $GITHUB_OUTPUT | |
| echo "DOCKERFILE=${DOCKERFILE}" >> $GITHUB_OUTPUT | |
| echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_OUTPUT | |
| echo "DOCKERHUB_TAG=docker.io/${{ secrets.DOCKERHUB_USERNAME }}/php-docker:${VERSION}${TAG_SUFFIX}" >> $GITHUB_OUTPUT | |
| echo "GHCR_TAG=ghcr.io/kingpin/php-docker:${VERSION}${TAG_SUFFIX}" >> $GITHUB_OUTPUT | |
| echo "QUAY_TAG=quay.io/kingpinx1/php-docker:${VERSION}${TAG_SUFFIX}" >> $GITHUB_OUTPUT | |
| echo "CACHE_SCOPE=${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }}" >> $GITHUB_OUTPUT | |
| - name: Build and push multi-arch image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: ${{ steps.vars.outputs.DOCKERFILE }} | |
| platforms: linux/amd64,linux/arm64,linux/arm/v7 | |
| push: true | |
| provenance: mode=max | |
| cache-from: type=gha,scope=${{ steps.vars.outputs.CACHE_SCOPE }} | |
| cache-to: type=gha,mode=max,scope=${{ steps.vars.outputs.CACHE_SCOPE }} | |
| build-args: | | |
| VERSION=${{ steps.vars.outputs.VERSION }} | |
| PHPVERSION=${{ matrix.php-version }} | |
| BASEOS=${{ matrix.php-base }} | |
| S6_OVERLAY_VERSION=${{ steps.s6-version.outputs.version }} | |
| BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }} | |
| VCS_REF=${{ github.sha }} | |
| tags: | | |
| ${{ steps.vars.outputs.DOCKERHUB_TAG }} | |
| ${{ steps.vars.outputs.GHCR_TAG }} | |
| ${{ steps.vars.outputs.QUAY_TAG }} | |
| labels: | | |
| com.sumguy.php-docker.php.variant=${{ matrix.php-type }} | |
| com.sumguy.php-docker.image.variant=${{ matrix.variant }} | |
| com.sumguy.php-docker.build_id=${{ github.run_id }} | |
| com.sumguy.php-docker.build_url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| com.sumguy.php-docker.built_by=github-actions/docker-ci | |
| - name: Create bookworm compatibility tag for v2 trixie images | |
| if: matrix.variant == 'v2' && matrix.php-base == 'trixie' | |
| run: | | |
| echo "::group::Creating bookworm compatibility tags for trixie-built v2 image" | |
| # Replace 'trixie' with 'bookworm' in tag names to maintain backward compatibility | |
| BOOKWORM_VERSION="${{ matrix.php-version }}-${{ matrix.php-type }}-bookworm" | |
| # Create manifest aliases pointing trixie-built images to bookworm tags | |
| docker buildx imagetools create -t \ | |
| docker.io/${{ secrets.DOCKERHUB_USERNAME }}/php-docker:${BOOKWORM_VERSION}-v2 \ | |
| ${{ steps.vars.outputs.DOCKERHUB_TAG }} | |
| docker buildx imagetools create -t \ | |
| ghcr.io/kingpin/php-docker:${BOOKWORM_VERSION}-v2 \ | |
| ${{ steps.vars.outputs.GHCR_TAG }} | |
| docker buildx imagetools create -t \ | |
| quay.io/kingpinx1/php-docker:${BOOKWORM_VERSION}-v2 \ | |
| ${{ steps.vars.outputs.QUAY_TAG }} | |
| echo "✅ Created bookworm compatibility tags pointing to trixie image" | |
| echo "::endgroup::" | |
| - name: Run Trivy vulnerability scanner | |
| uses: aquasecurity/trivy-action@master | |
| with: | |
| scan-type: image | |
| image-ref: ${{ steps.vars.outputs.DOCKERHUB_TAG }} | |
| format: 'sarif' | |
| severity: 'CRITICAL,HIGH' | |
| output: 'trivy-results-${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }}.sarif' | |
| - name: Upload Trivy results | |
| uses: github/codeql-action/upload-sarif@v3 | |
| with: | |
| sarif_file: 'trivy-results-${{ matrix.variant }}-${{ matrix.php-version }}-${{ matrix.php-type }}-${{ matrix.php-base }}.sarif' |