GitHub Actions pour vos projets PHP
GitHub Actions offre une solution de CI/CD intégrée directement dans votre dépôt. Voici comment configurer un pipeline complet.
L'intérêt principal est l'absence d'infrastructure à maintenir : les workflows sont des fichiers YAML placés dans .github/workflows/, versionnés avec le code, et exécutés sur des machines virtuelles fournies par GitHub (les runners). Chaque push ou pull request déclenche les vérifications, et leur résultat s'affiche directement dans l'interface de revue. Pour un projet PHP/Symfony, on obtient en quelques dizaines de lignes l'analyse statique, les tests avec une vraie base de données et le déploiement automatique.
Les notions de base
- Workflow : un fichier YAML qui décrit quand s'exécuter (
on) et quoi faire (jobs). - Job : un ensemble d'étapes exécutées sur un même runner. Les jobs d'un workflow tournent en parallèle par défaut, sauf si
needsimpose un ordre. - Step : une commande shell (
run) ou une action réutilisable (uses), publiée sur le Marketplace ou dans votre propre dépôt. - Service : un conteneur annexe (MySQL, Redis…) démarré à côté du job, le temps de son exécution.
Workflow de test
Ce premier workflow s'exécute à chaque push sur main et develop, ainsi que sur chaque pull request visant main. Il démarre un MySQL 8, installe PHP 8.3 avec les extensions nécessaires, puis lance PHPStan et PHPUnit.
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
tests:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: test
ports:
- 3306:3306
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, pdo_mysql, intl
coverage: xdebug
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Run PHPStan
run: vendor/bin/phpstan analyse src
- name: Run tests
run: php bin/phpunit --coverage-clover coverage.xml
env:
DATABASE_URL: mysql://root:[email protected]:3306/test
Quelques détails qui comptent :
shivammathur/setup-phpest l'action de référence pour PHP : elle installe la version demandée, les extensions, Composer et le pilote de couverture de code.- Le port
3306:3306expose MySQL sur le runner, d'où l'adresse127.0.0.1dansDATABASE_URL. Si le job tournait lui-même dans un conteneur (container:), il faudrait utiliser le nom du service,mysql, comme hôte. - Le mot de passe
rootest acceptable ici : la base est jetable et n'existe que le temps du job. Les vrais secrets, eux, ne doivent jamais apparaître dans le YAML.
Déploiement automatique
Le job de déploiement s'ajoute sous jobs:. Il attend le succès des tests (needs) et ne s'exécute que sur la branche main.
deploy:
needs: tests
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to production
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: deploy
key: ${{ secrets.SSH_KEY }}
script: |
cd /var/www/app
git pull origin main
composer install --no-dev
php bin/console cache:clear
php bin/console doctrine:migrations:migrate -n
Ce script est volontairement simple. En production, je recommande d'ajouter set -e en première ligne pour interrompre le déploiement à la première erreur, et --optimize-autoloader à composer install. Pour éviter les quelques secondes pendant lesquelles le code et les dépendances ne sont pas synchronisés, la stratégie des répertoires de release (un dossier par version et un lien symbolique current) reste la plus sûre.
Un workflow prêt pour la production
La version suivante reprend les mêmes étapes en appliquant les bonnes pratiques : lint et tests dans des jobs séparés qui tournent en parallèle, matrice de versions PHP, cache Composer, attente de MySQL et permissions minimales.
name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
coverage: none
- run: composer install --prefer-dist --no-progress
- run: vendor/bin/php-cs-fixer fix --dry-run --diff
- run: vendor/bin/phpstan analyse src --error-format=github
tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3', '8.4']
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: mbstring, pdo_mysql, intl
coverage: pcov
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- run: composer install --prefer-dist --no-progress
- run: php bin/phpunit --coverage-clover coverage.xml
env:
DATABASE_URL: mysql://root:[email protected]:3306/test
Ce qui change par rapport au premier workflow :
permissions: contents: readréduit les droits duGITHUB_TOKENau strict nécessaire. Un job qui doit commenter une pull request ou publier une image demandera explicitement des droits supplémentaires.concurrencyannule l'exécution précédente quand un nouveau commit arrive sur la même branche : inutile de tester un code déjà remplacé.- La matrice lance un job par version de PHP. Avec
fail-fast: false, un échec sur PHP 8.4 n'interrompt pas les autres versions, ce qui donne une vision complète de la compatibilité. - Le health check MySQL : sans les
options, le job peut démarrer alors que MySQL initialise encore sa base, et les premiers tests échouent de façon aléatoire. GitHub attend désormais que le conteneur soit déclaré sain. - Le cache Composer est indexé sur le hash de
composer.lock: tant que les dépendances ne changent pas, les paquets sont restaurés au lieu d'être téléchargés. pcovcollecte la couverture de code beaucoup plus vite que Xdebug, qui n'est utile que si vous avez besoin de ses autres fonctionnalités.--error-format=githubtransforme les erreurs PHPStan en annotations affichées directement sur les lignes concernées de la pull request.
Protéger le déploiement avec un environnement
GitHub propose les environnements : des secrets propres à chaque cible et, selon votre offre GitHub, des règles de protection comme une validation manuelle par des relecteurs désignés. Le job de déploiement déclare simplement l'environnement qu'il utilise :
deploy:
needs: [lint, tests]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
concurrency:
group: production
cancel-in-progress: false
steps:
- name: Deploy to production
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: deploy
key: ${{ secrets.SSH_KEY }}
script: |
set -e
cd /var/www/app
git pull origin main
composer install --no-dev --optimize-autoloader
php bin/console cache:clear
php bin/console doctrine:migrations:migrate -n
Ici, le groupe de concurrence production garantit qu'un seul déploiement s'exécute à la fois, sans jamais en annuler un en cours : interrompre un déploiement au milieu des migrations serait bien pire que d'attendre.
Pièges fréquents
- Secrets et forks : les workflows déclenchés par une pull request provenant d'un fork n'ont pas accès aux secrets. Les tests doivent donc fonctionner sans eux.
- Actions tierces : une action référencée par un tag (
@v1) peut changer sans que vous le sachiez. Pour les actions sensibles, épinglez-les sur un SHA de commit complet et laissez Dependabot proposer les mises à jour. - Noms des status checks : avec une matrice, les checks s'appellent par exemple
tests (8.3). Renommer un job ou modifier la matrice oblige à mettre à jour les règles de protection de branche. - Déploiement depuis une pull request : la condition
github.event_name == 'push'évite qu'un événement inattendu surmaindéclenche une mise en production.
Bonnes pratiques
- Utiliser le cache des dépendances Composer
- Paralléliser les jobs de test et de lint
- Protéger les branches avec des status checks requis
- Stocker les secrets dans GitHub Secrets
- Limiter les
permissionsduGITHUB_TOKEN - Utiliser un environnement protégé pour la production
Avec ces quelques règles, chaque pull request est vérifiée automatiquement, les erreurs apparaissent directement dans la revue de code, et la mise en production devient une opération banale plutôt qu'un événement redouté.