mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-07-11 12:28:23 +02:00
33a0e8a584
* Avoid dev port clashes and isolate docker compose per git worktree Default dev ports (80/443/5432) clash with other projects' compose stacks. Remap them to 8080/8443/5433+ by default, and add `just init` (auto-run by `just up`) to generate a per-worktree `.env.local` with a unique COMPOSE_PROJECT_NAME, image tag, and free host ports, so multiple worktrees can run `just up` concurrently without sharing containers, volumes, images, or ports. * Pin CI to port 80 for the HTTP reachability check; use 5430 as default Postgres dev port CI runs in an isolated single-purpose runner with no port-clash concern, so pin the HTTP reachability check back to port 80 explicitly rather than changing the dev default. Also move the default dev Postgres port range from 5433 to 5430, since 5433 clashes with other commonly used local projects. * Fix Justfile init: propagate free_port failures and use portable hash free_port exhaustion was swallowed inside a command substitution used as an echo argument, silently writing empty ports to .env.local. shasum is also not guaranteed on stripped-down Linux hosts.
328 lines
13 KiB
YAML
328 lines
13 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
|
|
env:
|
|
HTTP_PORT: "80"
|
|
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
|
|
code-quality: write
|
|
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
|
|
# Reports are written outside var/ since var/ is a Docker volume (see the Dockerfile's
|
|
# VOLUME /app/var/) and isn't bind-mounted to the runner, unlike the rest of the project.
|
|
run: docker compose exec -T -e MAILER_DSN=null://null php vendor/bin/phpunit --log-junit reports/junit.xml --coverage-cobertura reports/coverage/cobertura.xml
|
|
- name: Publish PHPUnit test results
|
|
if: always()
|
|
uses: mikepenz/action-junit-report@d9f48fc87bc235f7e214acf696ca5abc0a986f16 # v6
|
|
with:
|
|
report_paths: reports/junit.xml
|
|
check_name: PHPUnit
|
|
- name: Upload code coverage
|
|
# Requires "Code Quality" to be enabled for this repository under
|
|
# Settings > Code security > Code quality; fail-on-error is false so CI
|
|
# doesn't go red before that one-time, manual repository setting is turned on.
|
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
|
uses: actions/upload-code-coverage@82c7aee3fb2ad768e00b00a0a8d749c5815085b6 # v1
|
|
with:
|
|
file: reports/coverage/cobertura.xml
|
|
language: PHP
|
|
label: phpunit
|
|
fail-on-error: false
|
|
- 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:]')
|
|
echo "build_time=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
|
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
|
|
*.args.BUILD_TIME=${{ steps.meta.outputs.build_time }}
|
|
*.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
|