From 806cff8c0fb166cd4f720672b68a789da821c3d3 Mon Sep 17 00:00:00 2001 From: Marijn Doeve Date: Fri, 3 Jul 2026 12:18:29 +0200 Subject: [PATCH] =?UTF-8?q?ci:=20optimise=20build=20pipeline=20=E2=80=94?= =?UTF-8?q?=20shared=20dev=20image=20build=20and=20skip=20tests=20on=20tag?= =?UTF-8?q?=20push=20(#167)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ci: split dev image build into a shared job Extract the Docker build step into a dedicated `build` job so `quality` and `tests` no longer each build the image independently. Both jobs now load from the shared `devbuild` GHA cache scope and declare `needs: build`. * ci: skip quality+tests on tag push, verify prior CI run instead When tagging a commit that already passed CI on main, there is no need to run quality and tests again. Both jobs now skip for tag refs. A new `verify-prior-run` job runs instead: it queries the GitHub API for a prior successful CI run on the same SHA (excluding the current run) and fails fast if none is found, preventing deployment of untested tags. `build-deploy` now uses `always() && !cancelled() && !failure()` so it handles the mix of skipped (quality/tests) and successful (verify-prior-run) needed jobs correctly. * ci: bump GitHub Actions to Node.js 24 compatible versions * ci: add Dependabot config for GitHub Actions version updates * ci: pin all GitHub Actions to commit SHAs * ci: disable credential persistence on all checkout steps --- .github/dependabot.yml | 4 ++ .github/workflows/ci.yml | 106 +++++++++++++++++++++++++++++---------- 2 files changed, 83 insertions(+), 27 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b256bd7..715a40c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -28,3 +28,7 @@ updates: directory: "/" schedule: interval: "daily" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a00085..56d4763 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,31 +17,57 @@ permissions: contents: read jobs: - quality: - name: Code Quality + build: + name: Build Dev Image runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 15 permissions: contents: read steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false - name: Lint Dockerfile - uses: hadolint/hadolint-action@v3.1.0 + uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 - name: Build Docker images - uses: docker/bake-action@v5 + uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7 with: pull: true + files: | + compose.yaml + compose.override.yaml + set: | + *.cache-from=type=gha,scope=${{github.ref}}-devbuild + *.cache-from=type=gha,scope=refs/heads/main-devbuild + *.cache-to=type=gha,scope=${{github.ref}}-devbuild,mode=${{ github.event_name == 'pull_request' && 'min' || 'max' }} + + quality: + name: Code Quality + runs-on: ubuntu-latest + timeout-minutes: 20 + needs: build + if: "!startsWith(github.ref, 'refs/tags/')" + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 + - name: Load Docker images + uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7 + with: load: true files: | compose.yaml compose.override.yaml set: | - *.cache-from=type=gha,scope=${{github.ref}}-quality - *.cache-from=type=gha,scope=refs/heads/main - *.cache-to=type=gha,scope=${{github.ref}}-quality,mode=${{ github.event_name == 'pull_request' && 'min' || 'max' }} + *.cache-from=type=gha,scope=${{github.ref}}-devbuild - name: Start services run: docker compose up php database --wait --no-build - name: Warm up dev cache @@ -78,27 +104,28 @@ jobs: name: Tests runs-on: ubuntu-latest timeout-minutes: 20 + needs: build + if: "!startsWith(github.ref, 'refs/tags/')" permissions: checks: write pull-requests: write contents: read steps: - name: Checkout - uses: actions/checkout@v4 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Build Docker images - uses: docker/bake-action@v5 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 + - name: Load Docker images + uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7 with: - pull: true load: true files: | compose.yaml compose.override.yaml set: | - *.cache-from=type=gha,scope=${{github.ref}}-tests - *.cache-from=type=gha,scope=refs/heads/main - *.cache-to=type=gha,scope=${{github.ref}}-tests,mode=${{ github.event_name == 'pull_request' && 'min' || 'max' }} + *.cache-from=type=gha,scope=${{github.ref}}-devbuild - name: Start services run: docker compose up php database --wait --no-build - name: Create test database @@ -111,13 +138,34 @@ jobs: run: docker compose exec -T php vendor/bin/phpunit --log-junit var/phpunit/junit.xml - name: Publish PHPUnit test results if: always() - uses: mikepenz/action-junit-report@v5 + uses: mikepenz/action-junit-report@d9f48fc87bc235f7e214acf696ca5abc0a986f16 # v6 with: report_paths: var/phpunit/junit.xml check_name: PHPUnit - name: Doctrine Schema Validator run: docker compose exec -T php bin/console -e test doctrine:schema:validate + verify-prior-run: + name: Verify Prior CI Run + runs-on: ubuntu-latest + needs: build + if: startsWith(github.ref, 'refs/tags/') + permissions: + actions: read + steps: + - name: Check for successful CI run on this commit + env: + GH_TOKEN: ${{ github.token }} + run: | + count=$(gh api \ + "repos/${{ github.repository }}/actions/workflows/ci.yml/runs?head_sha=${{ github.sha }}&status=success&per_page=5" \ + --jq "[.workflow_runs[] | select(.id != ${{ github.run_id }})] | length") + if [[ "$count" -eq 0 ]]; then + echo "::error::No prior successful CI run found for ${{ github.sha }}. Only tag commits that have passed CI on main." + exit 1 + fi + echo "Found $count prior successful CI run(s) for this commit." + build-deploy: name: Build and Deploy permissions: @@ -126,17 +174,21 @@ jobs: environment: name: ${{ startsWith(github.ref, 'refs/tags/') && 'production' || 'acceptance' }} url: ${{ vars.URL }} - needs: [quality, tests] + needs: [quality, tests, verify-prior-run] runs-on: ubuntu-latest timeout-minutes: 15 - if: (github.ref == 'refs/heads/main' && false) || startsWith(github.ref, 'refs/tags/') + if: >- + always() && !cancelled() && !failure() && + ((github.ref == 'refs/heads/main' && false) || startsWith(github.ref, 'refs/tags/')) steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -164,7 +216,7 @@ jobs: fi - name: Build and Push Docker images - uses: docker/bake-action@v5 + uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7 with: pull: true push: true @@ -178,7 +230,7 @@ jobs: *.tags=${{ steps.meta.outputs.full_name }} - name: Create Sentry release - uses: getsentry/action-release@v3 + uses: getsentry/action-release@ff07929a6537bac57790c3451cf4d364aca38528 # v3 env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }}