From 764f59e6a712a1189b30646f2503c4ea4ab154bf Mon Sep 17 00:00:00 2001 From: Marijn Doeve Date: Thu, 2 Jul 2026 23:06:23 +0200 Subject: [PATCH] Improve GitHub Actions CI: parallelise jobs, continue-on-error, timeouts, cache optimisation (#165) * Strip v-prefix from version tag before passing to Sentry GitHub tags follow the v1.2.3 convention, but Sentry requires bare semver (1.2.3) to recognise releases as valid semver. Extract a sentry_version output in the meta step that strips the leading v. * Parallelize CI: split quality and tests jobs, add continue-on-error - Split the single tests job into parallel quality and tests jobs, saving ~4 min wall-clock time per run - Quality checks (lint, CS, PHPStan, Rector) now all run with continue-on-error so every failure is visible in one pass; a final Assert step fails the job if any check failed - Add cache:warmup before PHPStan so the Symfony dev container XML exists and the Symfony extension has full type information - Use per-job GHA cache scopes to avoid parallel cache write races - Use cache mode=min on PRs, mode=max on main/tags - Add timeout-minutes (20/20/15) to all jobs - Remove dead if:false Mercure reachability step - Fix Portainer webhook URL quoting - build-deploy now needs: [quality, tests] * Simplify build-deploy job name and environment expressions * Use static name for build-deploy job (expressions not evaluated when skipped) * build-deploy only needs tests, not quality (quality is informational) * Revert: build-deploy needs both quality and tests --- .github/workflows/ci.yml | 77 +++++++++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa8754e..1a00085 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,12 +17,11 @@ permissions: contents: read jobs: - tests: - name: Tests + quality: + name: Code Quality runs-on: ubuntu-latest + timeout-minutes: 20 permissions: - checks: write - pull-requests: write contents: read steps: - name: Checkout @@ -40,26 +39,68 @@ jobs: compose.yaml compose.override.yaml set: | - *.cache-from=type=gha,scope=${{github.ref}} + *.cache-from=type=gha,scope=${{github.ref}}-quality *.cache-from=type=gha,scope=refs/heads/main - *.cache-to=type=gha,scope=${{github.ref}},mode=max + *.cache-to=type=gha,scope=${{github.ref}}-quality,mode=${{ github.event_name == 'pull_request' && 'min' || 'max' }} - name: Start services run: docker compose up php database --wait --no-build + - name: Warm up dev cache + run: docker compose exec -T php bin/console cache:warmup --env=dev - name: Lint Twig templates + id: twig_lint + continue-on-error: true run: docker compose exec -T php bin/console lint:twig --format=github templates - name: Coding Style + id: cs + continue-on-error: true run: docker compose exec -T php vendor/bin/php-cs-fixer check --diff --show-progress=none - name: Twig Coding Style + id: twig_cs + continue-on-error: true run: docker compose exec -T php vendor/bin/twig-cs-fixer check - name: Static Analysis (PHPStan) + id: phpstan + continue-on-error: true run: docker compose exec -T php vendor/bin/phpstan analyse --no-progress --no-ansi --error-format=github - name: Rector + id: rector + continue-on-error: true run: docker compose exec -T php vendor/bin/rector process --dry-run --no-progress-bar --output-format=github - name: Check HTTP reachability run: curl -v --fail-with-body http://localhost - - name: Check Mercure reachability - if: false - run: curl -vkI --fail-with-body https://localhost/.well-known/mercure?topic=test + - name: Assert all checks passed + if: always() + run: | + outcomes="${{ steps.twig_lint.outcome }} ${{ steps.cs.outcome }} ${{ steps.twig_cs.outcome }} ${{ steps.phpstan.outcome }} ${{ steps.rector.outcome }}" + if echo "$outcomes" | grep -q "failure"; then exit 1; fi + + tests: + name: Tests + runs-on: ubuntu-latest + timeout-minutes: 20 + 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 + 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' }} + - name: Start services + run: docker compose up php database --wait --no-build - name: Create test database run: docker compose exec -T php bin/console -e test doctrine:database:create - name: Run migrations @@ -76,17 +117,18 @@ jobs: check_name: PHPUnit - name: Doctrine Schema Validator run: docker compose exec -T php bin/console -e test doctrine:schema:validate - + build-deploy: - name: Build and deploy to ${{ startsWith(github.ref, 'refs/tags/') && 'production' || (github.ref == 'refs/heads/main' && 'acceptance' || '') }} + name: Build and Deploy permissions: contents: read packages: write environment: - name: ${{ startsWith(github.ref, 'refs/tags/') && 'production' || (github.ref == 'refs/heads/main' && 'acceptance' || '') }} + name: ${{ startsWith(github.ref, 'refs/tags/') && 'production' || 'acceptance' }} url: ${{ vars.URL }} - needs: tests + needs: [quality, tests] runs-on: ubuntu-latest + timeout-minutes: 15 if: (github.ref == 'refs/heads/main' && false) || startsWith(github.ref, 'refs/tags/') steps: - name: Checkout @@ -106,14 +148,17 @@ jobs: REPO_LOWER=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') if [[ "${{ github.ref }}" == refs/tags/* ]]; then TAG="${GITHUB_REF#refs/tags/}" + SENTRY_VERSION="${TAG#v}" { echo "tag=$TAG" + echo "sentry_version=$SENTRY_VERSION" echo "full_name=ghcr.io/${REPO_LOWER}:$TAG" } >> "$GITHUB_OUTPUT" else SHORT_SHA=$(git rev-parse --short HEAD) { echo "tag=$SHORT_SHA" + echo "sentry_version=$SHORT_SHA" echo "full_name=ghcr.io/${REPO_LOWER}:$SHORT_SHA" } >> "$GITHUB_OUTPUT" fi @@ -139,12 +184,12 @@ jobs: SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} with: - release: ${{steps.meta.outputs.tag}} - environment: ${{ startsWith(github.ref, 'refs/tags/') && 'production' || (github.ref == 'refs/heads/main' && 'acceptance' || '') }} + release: ${{steps.meta.outputs.sentry_version}} + environment: ${{ startsWith(github.ref, 'refs/tags/') && 'production' || 'acceptance' }} - name: Trigger Portainer Deployment shell: bash env: PORTAINER_WEBHOOK: ${{secrets.PORTAINER_WEBHOOK}} run: | - curl -v -X POST "$PORTAINER_WEBHOOK"?IMAGE_TAG=${{steps.meta.outputs.tag}} --fail-with-body + curl -v -X POST "${PORTAINER_WEBHOOK}?IMAGE_TAG=${{steps.meta.outputs.tag}}" --fail-with-body