Files
TijdVoorDeTest/.github/workflows/ci.yml
T
Marijn ba1e8d8eb6 ci: auto-trigger main CI when tagging a Dependabot commit (#188)
* ci: auto-trigger main CI run when tagging a Dependabot commit

Dependabot auto-merges use GITHUB_TOKEN which GitHub intentionally does
not re-trigger other workflows on. This means tagging those commits
immediately fails the verify-prior-run gate.

Instead of hard-failing, trigger ci.yml on main and wait for it to
succeed before proceeding with the deploy. Only triggers once; detects
and surfaces failures from the triggered run.

Bumps actions permission from read to write to allow workflow dispatch.

* ci: address CodeRabbit feedback on verify-prior-run job

Reduce max_attempts from 40 to 30 so worst-case runtime (15m) fits within
the 20-minute job timeout with margin. Trigger the fallback workflow run on
the tag ref instead of main so the dispatched run's head_sha matches the
tagged commit SHA that the polling loop is checking for.
2026-07-08 08:00:37 +00:00

310 lines
12 KiB
YAML

name: CI
on:
push:
branches:
- main
tags:
- '*'
pull_request: ~
workflow_dispatch: ~
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
build:
name: Build Dev Image
runs-on: ubuntu-latest
timeout-minutes: 15
if: "!startsWith(github.ref, 'refs/tags/')"
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Lint Dockerfile
uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
- name: Build Docker images
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}}-devbuild
- 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: Assert all checks passed
if: always()
run: |
failed=0
check() {
local name="$1" outcome="$2"
if [[ "$outcome" == "failure" ]]; then
echo "::error::$name failed"
failed=1
fi
}
check "Twig Lint" "${{ steps.twig_lint.outcome }}"
check "Coding Style" "${{ steps.cs.outcome }}"
check "Twig Coding Style" "${{ steps.twig_cs.outcome }}"
check "PHPStan" "${{ steps.phpstan.outcome }}"
check "Rector" "${{ steps.rector.outcome }}"
exit $failed
tests:
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@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}}-devbuild
- name: Start services
run: docker compose up php database --wait --no-build
- name: Build SCSS
run: docker compose exec -T php bin/console sass:build
- name: Create test database
run: docker compose exec -T php bin/console -e test doctrine:database:create
- name: Run migrations
run: docker compose exec -T php bin/console -e test doctrine:migrations:migrate --no-interaction
- name: Load fixtures
run: docker compose exec -T php bin/console -e test doctrine:fixtures:load --no-interaction --group=test
- name: Run PHPUnit
run: docker compose exec -T -e MAILER_DSN=null://null php vendor/bin/phpunit --log-junit var/phpunit/junit.xml
- name: Publish PHPUnit test results
if: always()
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
timeout-minutes: 20
if: startsWith(github.ref, 'refs/tags/')
permissions:
actions: write
steps:
- name: Wait for and verify successful CI run on this commit
env:
GH_TOKEN: ${{ github.token }}
run: |
max_attempts=30
attempt=0
triggered=false
while [[ $attempt -lt $max_attempts ]]; do
attempt=$((attempt + 1))
success_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 [[ "$success_count" -gt 0 ]]; then
echo "Found $success_count prior successful CI run(s) for ${{ github.sha }}."
exit 0
fi
in_progress_count=$(gh api \
"repos/${{ github.repository }}/actions/workflows/ci.yml/runs?head_sha=${{ github.sha }}&per_page=10" \
--jq "[.workflow_runs[] | select(.id != ${{ github.run_id }}) | select(.status == \"in_progress\" or .status == \"queued\" or .status == \"waiting\" or .status == \"requested\" or .status == \"pending\")] | length")
if [[ "$in_progress_count" -gt 0 ]]; then
echo "CI in progress (attempt $attempt/$max_attempts), waiting 30s..."
sleep 30
continue
fi
if [[ "$triggered" == "false" ]]; then
echo "No prior CI run found for ${{ github.sha }}. Triggering CI on ${{ github.ref }}..."
gh workflow run ci.yml --repo "${{ github.repository }}" --ref "${{ github.ref }}"
triggered=true
echo "Triggered. Waiting 20s for run to register..."
sleep 20
continue
fi
failed_conclusion=$(gh api \
"repos/${{ github.repository }}/actions/workflows/ci.yml/runs?head_sha=${{ github.sha }}&per_page=10" \
--jq "[.workflow_runs[] | select(.id != ${{ github.run_id }}) | select(.status == \"completed\") | select(.conclusion != \"success\")] | first | .conclusion // empty" \
--raw-output)
if [[ -n "$failed_conclusion" ]]; then
echo "::error::Triggered CI run on main failed with conclusion: $failed_conclusion. Fix the issue before re-tagging."
exit 1
fi
echo "Waiting for triggered run to register (attempt $attempt/$max_attempts)..."
sleep 20
done
echo "::error::Timed out waiting for CI run to complete for ${{ github.sha }}."
exit 1
build-deploy:
name: Build and Deploy
permissions:
contents: read
packages: write
environment:
name: ${{ startsWith(github.ref, 'refs/tags/') && 'production' || 'acceptance' }}
url: ${{ vars.URL }}
needs: [quality, tests, verify-prior-run]
runs-on: ubuntu-latest
timeout-minutes: 15
if: >-
always() && !cancelled() && !failure() &&
((github.ref == 'refs/heads/main' && false) || startsWith(github.ref, 'refs/tags/'))
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: Log in to GitHub Container Registry
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
run: |
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
- name: Build and Push Docker images
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7
with:
pull: true
push: true
files: |
compose.yaml
compose.build.yaml
set: |
*.cache-from=type=gha,scope=${{github.ref}}-devbuild
*.cache-from=type=gha,scope=refs/heads/main-devbuild
*.cache-from=type=gha,scope=${{github.ref}}
*.cache-from=type=gha,scope=refs/heads/main
*.cache-to=type=gha,scope=${{github.ref}},mode=max
*.tags=${{ steps.meta.outputs.full_name }}
- name: Create Sentry release
uses: getsentry/action-release@ff07929a6537bac57790c3451cf4d364aca38528 # v3
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
with:
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}}
IMAGE_TAG: ${{steps.meta.outputs.tag}}
SENTRY_RELEASE: ${{steps.meta.outputs.sentry_version}}
run: |
curl -v -X POST "${PORTAINER_WEBHOOK}?IMAGE_TAG=${IMAGE_TAG}&SENTRY_RELEASE=${SENTRY_RELEASE}" --fail-with-body