Un projet Symfony sans CI/CD, c'est comme une voiture sans freins : ça avance vite jusqu'au premier mur. Voici le pipeline GitHub Actions que nous utilisons en production, avec 5 jobs qui couvrent le lint, les tests, la sécurité, le build et le déploiement.
Architecture du pipeline
Le pipeline est organisé en 5 jobs séquentiels avec des dépendances claires. Un échec à n'importe quelle étape bloque le déploiement.
- Job 1 — Lint : PHP-CS-Fixer (check) + PHPStan level 9. Si le code ne respecte pas les standards, on s'arrête là.
- Job 2 — Test : PHPUnit avec PostgreSQL en service container, rapport de couverture. Le seuil de couverture est un quality gate.
- Job 3 — Security :
composer audit+ Symfony security check. Zéro vulnérabilité critique autorisée. - Job 4 — Build : Construction de l'image Docker de production, push vers le registry.
- Job 5 — Deploy : Déploiement avec approval gate. Un humain doit valider avant la mise en production.
Le workflow complet
name: CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
PHP_VERSION: "8.3"
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
lint:
name: Lint & Static Analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_VERSION }}
tools: composer:v2
- name: Cache Composer
uses: actions/cache@v4
with:
path: vendor
key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- run: composer install --no-interaction --prefer-dist
- run: vendor/bin/php-cs-fixer fix --dry-run --diff
- run: vendor/bin/phpstan analyse --level=9 --no-progress
test:
name: Tests
needs: lint
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: test_db
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports: ["5432:5432"]
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_VERSION }}
extensions: pdo_pgsql, intl
coverage: xdebug
- name: Cache Composer
uses: actions/cache@v4
with:
path: vendor
key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }}
- run: composer install --no-interaction
- name: Run tests with coverage
run: XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-clover var/coverage/clover.xml
env:
DATABASE_URL: postgresql://test:test@127.0.0.1:5432/test_db
- name: Check coverage threshold
run: |
COVERAGE=$(php -r "\$xml=simplexml_load_file('var/coverage/clover.xml');\$m=\$xml->project->metrics;echo round(100*\$m['coveredstatements']/\$m['statements'],1);")
echo "Coverage: $COVERAGE%"
if (( $(echo "\$COVERAGE < 80" | bc -l) )); then
echo "Coverage below 80% threshold"
exit 1
fi
security:
name: Security Check
needs: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_VERSION }}
- run: composer install --no-interaction
- run: composer audit
- uses: symfonycorp/security-checker-action@v5
build:
name: Build Docker Image
needs: [test, security]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
deploy:
name: Deploy to Production
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: production
steps:
- name: Deploy
run: |
echo "Deploying to production..."
# ssh deploy@server "docker pull image && docker compose up -d"Quality gates : les seuils qui protègent
- Couverture de tests — Seuil à 80%. En dessous, le pipeline échoue. Pas de négociation.
- PHPStan level 9 — Le niveau maximum. Zéro erreur acceptée.
- Zéro vulnérabilité critique —
composer auditet Symfony security check doivent passer au vert. - Approval gate — Le job de déploiement utilise un environnement GitHub avec reviewers obligatoires.
Stratégies de cache pour des builds rapides
Besoin d'un expert Symfony ?
Réserver un appel →Le cache Composer est le quick win le plus impactant. Sans cache, composer install prend 30-60 secondes. Avec le cache par composer.lock hash, c'est 2-3 secondes. On utilise aussi le cache des layers Docker pour accélérer le build de l'image.
Les jobs lint et security tournent en parallèle car ils sont indépendants. Le job test dépend de lint (inutile de lancer les tests si le code n'est pas propre). Le build ne démarre que si test et security sont verts.
Bonus : le Makefile compagnon
En local, les développeurs doivent pouvoir exécuter les mêmes vérifications que le CI. Un Makefile simplifie tout :
.PHONY: qa
qa: cs-check phpstan test ## Lance toutes les vérifications
.PHONY: cs-check
cs-check:
@vendor/bin/php-cs-fixer fix --dry-run --diff
.PHONY: phpstan
phpstan:
@vendor/bin/phpstan analyse --level=9
.PHONY: test
test:
@vendor/bin/phpunit
.PHONY: test-coverage
test-coverage:
@XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html=var/coverageUn make qa avant chaque push évite 90% des échecs de CI. C'est aussi ce que les hooks Claude Code (PostToolUse) peuvent lancer automatiquement après chaque modification de fichier PHP.
En résumé
Ce pipeline est le socle minimal pour un projet Symfony professionnel. Il attrape les erreurs de code, les failles de sécurité et les régressions avant qu'elles n'atteignent la production. Adapte les seuils et les outils à ton contexte, mais ne déploie jamais sans filet. Si tu veux un pipeline sur mesure avec ton SaaS Symfony déployé et monitoré, Bear MVP inclut la mise en place complète du CI/CD.
Besoin d'un expert Symfony ?
20 ans d'expérience sur l'écosystème PHP/Symfony.