Build Docker images #37
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: | |
| REGISTRY: ghcr.io | |
| 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@v4 | |
| - name: Cache digests | |
| id: cache-digests | |
| uses: actions/cache@v4 | |
| 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 | |
| while IFS= read -r image; do | |
| # only include real image names, ignoring internal stages | |
| if [[ "${image}" == *"/"* || "${image}" == *":"* ]]; then | |
| base_images+=("${image}") | |
| 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@v4 | |
| 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 | |
| - frankenphp-alpine.Dockerfile | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set up QEMU (for ARM builds) | |
| uses: docker/setup-qemu-action@v3 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Login to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ vars.DOCKER_USERNAME }} | |
| password: ${{ secrets.DOCKER_PASSWORD }} | |
| - name: Login to Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Determine Image Name | |
| id: image_name | |
| # language=bash | |
| run: | | |
| echo "IMAGE_NAME=${{ github.repository_owner }}/${{ vars.IMAGE_NAME || 'php' }}" >> $GITHUB_ENV | |
| base_version=$( \ | |
| echo "${{ matrix.dockerfile }}" | \ | |
| sed -E 's/.?Dockerfile//' | \ | |
| tr '/' '-' \ | |
| ) | |
| suffix="${{ matrix.stage == 'dev' && 'dev' || '' }}" | |
| echo "VERSION=${base_version:-latest}${suffix:+-}${suffix}" >> $GITHUB_ENV | |
| - name: Set up Docker Metadata | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: | | |
| ${{ env.IMAGE_NAME }} | |
| ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| labels: | | |
| org.opencontainers.image.title=${{ env.IMAGE_NAME }} | |
| org.opencontainers.image.version=${{ env.VERSION }} | |
| org.opencontainers.vendor="Matchory GmbH" | |
| tags: | | |
| type=sha | |
| type=sha,prefix=${{ env.VERSION }}- | |
| type=raw,value=${{ env.VERSION }} | |
| - name: Build and Push | |
| id: build | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: ./${{ matrix.dockerfile }} | |
| target: ${{ matrix.stage }} | |
| 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 for Docker Hub | |
| uses: actions/attest-build-provenance@v2 | |
| with: | |
| subject-name: index.docker.io/${{ env.IMAGE_NAME }} | |
| subject-digest: ${{ steps.build.outputs.digest }} | |
| push-to-registry: true | |
| - name: Generate artifact attestation | |
| uses: actions/attest-build-provenance@v2 | |
| with: | |
| subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| subject-digest: ${{ steps.build.outputs.digest }} | |
| push-to-registry: true |