Build Docker images #89
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: Build Docker images | |
| concurrency: | |
| cancel-in-progress: true | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| on: | |
| push: | |
| branches: | |
| - main | |
| schedule: | |
| # Runs every Monday at 03:00 UTC | |
| - cron: '0 3 * * 1' | |
| env: | |
| LATEST_VERSION: ${{ vars.LATEST_PHP_VERSION || '8.5' }} | |
| # PHP versions to build - keep in sync with build matrix | |
| PHP_VERSIONS: '8.4 8.5' | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write | |
| attestations: write | |
| jobs: | |
| check-updates: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| changed: ${{ steps.check.outputs.changed }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Cache digests | |
| id: cache-digests | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: latest-digests.txt | |
| key: digest-cache-${{ runner.os }}-${{ hashFiles('*Dockerfile') }} | |
| restore-keys: | | |
| digest-cache-${{ runner.os }}- | |
| - name: Extract Base Images from Dockerfiles | |
| id: extract | |
| # language=bash | |
| run: | | |
| shopt -s globstar nullglob | |
| base_images=() | |
| for file in ./*Dockerfile; do | |
| # Resolve FROM-line placeholders from the ARG defaults declared in | |
| # the same Dockerfile (PIE_VERSION, COMPOSER_VERSION, ...), keeping | |
| # the Dockerfiles the single source of truth for those versions. | |
| # PHP_VERSION is expanded separately below, since multiple PHP | |
| # versions are built from the same Dockerfile. | |
| declare -A arg_defaults=() | |
| while IFS='=' read -r arg_name arg_value; do | |
| arg_defaults["${arg_name}"]="${arg_value}" | |
| done < <(sed -nE 's/^ARG ([A-Za-z_][A-Za-z0-9_]*)="([^"]*)".*$/\1=\2/p' "${file}") | |
| while IFS= read -r image; do | |
| # only include real image names, ignoring internal stages | |
| if [[ "${image}" == *"/"* || "${image}" == *":"* ]]; then | |
| for arg_name in "${!arg_defaults[@]}"; do | |
| if [[ "${arg_name}" != "PHP_VERSION" ]]; then | |
| image="${image//\$\{${arg_name}\}/${arg_defaults[${arg_name}]}}" | |
| image="${image//\$${arg_name}/${arg_defaults[${arg_name}]}}" | |
| fi | |
| done | |
| # Expand PHP_VERSION variable for each version in the matrix | |
| if [[ "${image}" == *'${PHP_VERSION}'* || "${image}" == *'$PHP_VERSION'* ]]; then | |
| for php_version in ${PHP_VERSIONS}; do | |
| expanded="${image//\$\{PHP_VERSION\}/${php_version}}" | |
| expanded="${expanded//\$PHP_VERSION/${php_version}}" | |
| base_images+=("${expanded}") | |
| done | |
| else | |
| base_images+=("${image}") | |
| fi | |
| fi | |
| done < <( \ | |
| grep -e '^FROM ' "${file}" | \ | |
| awk '{print $2}' | \ | |
| sort -u \ | |
| ) | |
| done | |
| # Deduplicate and format for github actions | |
| IFS=" " read -r -a unique_images <<< "$( \ | |
| echo "${base_images[@]}" | \ | |
| tr ' ' '\n' | \ | |
| sort -u | \ | |
| tr '\n' ' ' \ | |
| )" | |
| echo "base images detected: ${unique_images[*]}" | |
| # Save images as space-separated list for later use | |
| echo "base_images=${unique_images[*]}" >> "${GITHUB_ENV}" | |
| - name: Check upstream image digests | |
| id: check | |
| # language=bash | |
| run: | | |
| # Check if the base images have changed | |
| # If not a scheduled run, always consider the images changed | |
| if [[ "${GITHUB_EVENT_NAME}" != "schedule" ]]; then | |
| echo changed=true >> "${GITHUB_OUTPUT}" | |
| exit 0 | |
| fi | |
| digests=() | |
| for image in $(echo "${base_images}" | tr ' ' '\n'); do | |
| # Fetch image manifest without pulling the full image, so we can extract the digests | |
| manifest=$(docker manifest inspect "${image}") | |
| if [[ -z "${manifest}" ]]; then | |
| echo "Failed to fetch manifest for ${image}" | |
| exit 1 | |
| fi | |
| # Extract digests for linux/amd64 and linux/arm64 | |
| amd64_digest=$( \ | |
| echo "${manifest}" | \ | |
| jq -r '.manifests[] | select(.platform.architecture=="amd64" and .platform.os=="linux") | .digest' | \ | |
| head -n1 \ | |
| ) | |
| arm64_digest=$( \ | |
| echo "${manifest}" | \ | |
| jq -r '.manifests[] | select(.platform.architecture=="arm64" and .platform.os=="linux") | .digest' | \ | |
| head -n1 \ | |
| ) | |
| if [[ -z "${amd64_digest}" || -z "${arm64_digest}" ]]; then | |
| echo "Warning: Missing digest for one of the architectures in ${image}" | |
| exit 1 | |
| fi | |
| digests+=("${image} (linux/amd64): ${amd64_digest}") | |
| digests+=("${image} (linux/arm64): ${arm64_digest}") | |
| done | |
| echo "Latest digests:" | |
| echo "${digests[*]}" | |
| echo "${digests}" > latest-digests.txt | |
| if [[ -f cached-digests.txt && "$(diff -q latest-digests.txt cached-digests.txt)" == "" ]]; then | |
| echo "No changes in base images." | |
| echo changed=false >> "${GITHUB_OUTPUT}" | |
| else | |
| echo "Base images changed." | |
| echo changed=true >> "${GITHUB_OUTPUT}" | |
| # Save for next run | |
| cp latest-digests.txt cached-digests.txt | |
| fi | |
| - name: Save digests cache | |
| if: ${{ fromJSON(steps.check.outputs.changed) }} | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: latest-digests.txt | |
| key: digest-cache-${{ runner.os }}-${{ hashFiles('*Dockerfile') }} | |
| restore-keys: | | |
| digest-cache-${{ runner.os }}- | |
| build: | |
| name: Build Docker images | |
| runs-on: ubuntu-latest | |
| needs: check-updates | |
| if: ${{ fromJSON(needs.check-updates.outputs.changed) }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| stage: | |
| - dev | |
| - prod | |
| dockerfile: | |
| - Dockerfile | |
| - alpine.Dockerfile | |
| - frankenphp.Dockerfile | |
| # Keep in sync with the PHP_VERSIONS env variable at the top | |
| php-version: | |
| - '8.4' | |
| - '8.5' | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Set up QEMU (for ARM builds) | |
| uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 | |
| - name: Login to Container Registry | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ github.token }} | |
| - name: Determine Image Name | |
| id: config | |
| # language=bash | |
| run: | | |
| flavor=$( \ | |
| echo "${{ matrix.dockerfile }}" | \ | |
| sed -E 's/.?Dockerfile//' | \ | |
| tr '/' '-' \ | |
| ) | |
| latest=${{ (matrix.stage == 'prod' && matrix.php-version == env.LATEST_VERSION && matrix.dockerfile == 'Dockerfile' ) && 'true' || 'false' }} | |
| suffix="${{ matrix.stage == 'dev' && '-dev' || '' }}" | |
| echo "latest=${latest}" >> $GITHUB_OUTPUT | |
| echo "version=${{matrix.php-version}}${flavor:+-${flavor}}${suffix}" >> $GITHUB_OUTPUT | |
| echo "image-name=${{ github.repository_owner }}/${{ vars.image-name || 'php' }}" >> $GITHUB_OUTPUT | |
| - name: Set up Docker Metadata | |
| id: meta | |
| uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 | |
| env: | |
| DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index | |
| with: | |
| images: | | |
| ghcr.io/${{ steps.config.outputs.image-name }} | |
| labels: | | |
| org.opencontainers.image.title=${{ steps.config.outputs.image-name }} | |
| org.opencontainers.image.version=${{ steps.config.outputs.version }} | |
| org.opencontainers.image.description="The Docker base image for Matchory applications running on the PHP runtime." | |
| org.opencontainers.vendor="Matchory GmbH" | |
| flavor: | | |
| latest=${{ steps.config.outputs.latest }} | |
| tags: | | |
| type=sha,prefix=${{ steps.config.outputs.version }}- | |
| type=raw,value=${{ steps.config.outputs.version }} | |
| - name: Build and Push | |
| id: build | |
| uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 | |
| with: | |
| context: . | |
| file: ./${{ matrix.dockerfile }} | |
| target: ${{ matrix.stage }} | |
| build-args: | | |
| PHP_VERSION=${{ matrix.php-version }} | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| annotations: ${{ steps.meta.outputs.annotations }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| provenance: mode=max | |
| sbom: true | |
| - name: Generate artifact attestation | |
| uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 | |
| with: | |
| subject-name: ghcr.io/${{ steps.config.outputs.image-name }} | |
| subject-digest: ${{ steps.build.outputs.digest }} | |
| push-to-registry: true |