Aller au contenu principal
Retour au blog

CI/CD Symfony : le pipeline GitHub Actions qui passe en prod

Flavien Métivier5 mai 20266 min

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é critiquecomposer audit et 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/coverage

Un 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.

Cet article vous a plu ? Partagez-le !

Besoin d'un expert Symfony ?

20 ans d'expérience sur l'écosystème PHP/Symfony.