Chapitre 1419 minIntermédiaire
Exercices corrigés CI/CD : workflows et journaux d'erreurs
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- écrire un workflow pour un projet SLAM ou SISR ;
- trouver la cause d'un pipeline en échec à partir de son journal ;
- concevoir et justifier une chaîne CI/CD complète devant un jury.
10 exercices corrigés
Ce chapitre rassemble des exercices corrigés d'intégration et de déploiement continus, du niveau attendu au BTS SIO : écrire des workflows GitHub Actions pour des projets SLAM et SISR, lire le journal d'un pipeline en échec pour trouver la cause, et un cas de synthèse qui reprend tout le cours, de la pull request au retour arrière, comme on peut te le demander à l'oral de l'E6 ou de l'E5. Chaque correction explique le raisonnement.
La méthode pour lire un pipeline en échec
Devant une croix rouge, suis toujours les mêmes étapes :
- Repère le job et l'étape en échec : l'onglet Actions les marque en rouge. Les étapes suivantes, grisées, n'ont pas tourné : elles ne sont pas en cause.
- Lis la fin de la sortie de l'étape : le message utile est presque toujours dans les dernières lignes, juste avant
Error: Process completed with exit code 1. Remonte ensuite pour trouver la première erreur, pas la dernière conséquence. - Classe l'erreur : syntaxe du workflow (le pipeline ne démarre même pas), installation (dépendances, versions), qualité (lint, tests), environnement (service, variable, secret, permission), ou infrastructure (réseau, quota, panne de la forge).
- Reproduis en local avec la même commande. Si ça passe chez toi, cherche ce qui diffère : un fichier non commité, une variable de ton
.env, la casse d'un nom de fichier, une version d'outil. - Corrige, pousse, vérifie que le pipeline repasse au vert. Ne relance pas en boucle un job qui échoue toujours au même endroit : Re-run ne sert que pour un incident passager.
Série 1 : écrire des workflows
Exercice 1 sur 10 CI d'une application Symfony Réussi À revoir
L'application de gestion des stages de l'équipe est écrite en PHP 8.5 avec Symfony. Écris un workflow ci.yml qui tourne sur chaque pull request et sur chaque push sur main, et qui :
- installe PHP 8.5 avec l'action
shivammathur/setup-php@v2; - installe les dépendances avec Composer, avec un cache ;
- lance l'analyse statique
vendor/bin/phpstan analyse; - lance les tests
vendor/bin/phpunitcontre une base MySQL 8.4 nomméestages_test, l'application lisant l'adresse de la base dans la variableDATABASE_URL; - n'a que le droit de lire le code.
Voir la réponseMasquer la réponse de l'exercice 1
name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
tests:
runs-on: ubuntu-latest
timeout-minutes: 15
services:
mysql:
image: mysql:8.4
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: stages_test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1 -proot"
--health-interval=5s
--health-timeout=3s
--health-retries=10
env:
DATABASE_URL: mysql://root:root@127.0.0.1:3306/stages_test?serverVersion=8.4
steps:
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@v2
with:
php-version: '8.5'
- name: Trouver le dossier de cache de Composer
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v6
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: composer-${{ runner.os }}-${{ hashFiles('composer.lock') }}
restore-keys: |
composer-${{ runner.os }}-
- run: composer install --no-interaction --no-progress
- run: vendor/bin/phpstan analyse
- run: vendor/bin/phpunit
Points de vigilance : '8.5' entre guillemets (une habitude qui évite le piège de 8.10) ; le contrôle de santé de MySQL avant les tests ; DATABASE_URL au niveau du job, avec un mot de passe qui n'est pas un secret puisque la base est jetable. Selon le projet, une étape php bin/console doctrine:migrations:migrate --no-interaction --env=test crée les tables avant les tests. shivammathur/setup-php est une action tierce très utilisée : dans un vrai projet, épingle-la sur une empreinte de commit.
Tu avais trouvé ?
Exercice 2 sur 10 Vérifier des scripts d'exploitation (SISR) Réussi À revoir
L'équipe d'exploitation versionne ses scripts Bash dans scripts/ et le fichier compose.yaml du serveur de supervision à la racine. Écris un workflow qui, à chaque pull request :
- vérifie les scripts avec ShellCheck, préinstallé sur les runners Ubuntu ;
- vérifie que le fichier Compose est valide avec
docker compose config --quiet; - ne tourne que si un fichier de
scripts/oucompose.yamla changé.
Voir la réponseMasquer la réponse de l'exercice 2
name: Scripts d'exploitation
on:
pull_request:
paths:
- 'scripts/**'
- 'compose.yaml'
permissions:
contents: read
jobs:
verifier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: ShellCheck
run: shellcheck scripts/*.sh
- name: Valider le fichier Compose
run: docker compose -f compose.yaml config --quiet
ShellCheck renvoie un code d'erreur dès qu'il trouve un problème (variable non protégée par des guillemets, commande mal utilisée) : l'étape devient rouge. docker compose config --quiet lit le fichier, vérifie sa syntaxe et sa structure, et n'affiche rien si tout va bien. Si compose.yaml utilise des variables d'un .env absent du dépôt, il faut fournir des valeurs factices dans le job (env:), sans jamais y mettre de vrais secrets.
Tu avais trouvé ?
Exercice 3 sur 10 Un audit chaque nuit Réussi À revoir
Écris un workflow qui, chaque nuit à 2 h 00 heure de Paris en hiver et sur demande manuelle, lance npm audit --audit-level=high sur sorbier-frais. En cas d'échec, une étape doit écrire « Des vulnérabilités hautes ou critiques ont été trouvées » dans le résumé de l'exécution.
Voir la réponseMasquer la réponse de l'exercice 3
name: Audit nocturne
on:
schedule:
- cron: '0 1 * * *'
workflow_dispatch:
permissions:
contents: read
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- run: npm ci
- run: npm audit --audit-level=high
- name: Signaler l'échec
if: failure()
run: echo "Des vulnérabilités hautes ou critiques ont été trouvées" >> "$GITHUB_STEP_SUMMARY"
2 h 00 à Paris en hiver (UTC+1) = 1 h 00 UTC, d'où 0 1 * * *. Sans timezone, cron compte en UTC : en été, l'audit partira à 3 h 00 heure de Paris. Mieux, depuis mars 2026 : - cron: '0 2 * * *' suivi de timezone: Europe/Paris, qui tourne à 2 h 00 à Paris toute l'année. if: failure() ne fait tourner la dernière étape que si une étape précédente a échoué. Quand une exécution planifiée échoue, GitHub prévient la personne qui a modifié en dernier la ligne cron du workflow. Rappel : le workflow doit être sur la branche par défaut pour que schedule et workflow_dispatch fonctionnent.
Tu avais trouvé ?
Série 2 : lire des journaux d'erreurs
Exercice 4 sur 10 Journal n° 1 : l'installation Réussi À revoir
Run npm ci
npm error code EUSAGE
npm error
npm error `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing.
npm error
npm error Missing: dayjs@1.11.13 from lock file
Error: Process completed with exit code 1.
Quelle est la cause, et comment la corriger ?
Voir la réponseMasquer la réponse de l'exercice 4
Quelqu'un a ajouté dayjs dans package.json (à la main, ou avec un npm install dont il n'a pas commité le package-lock.json). Le fichier de verrouillage ne correspond plus : npm ci refuse d'installer, c'est voulu. Correction : lancer npm install en local, vérifier que package-lock.json est modifié, le commiter et pousser. Ne remplace pas npm ci par npm install dans le workflow : tu perdrais la garantie d'installer partout les mêmes versions.
Tu avais trouvé ?
Exercice 5 sur 10 Journal n° 2 : le pipeline ne démarre pas Réussi À revoir
Après un push, l'onglet Actions affiche, sans qu'aucun job n'ait tourné :
Invalid workflow file: .github/workflows/ci.yml#L14
You have an error in your yaml syntax on line 14
Les lignes 12 à 15 du fichier sont :
12 steps:
13 - uses: actions/checkout@v7
14 - run: npm ci
15 - run: npm test
Trouve l'erreur.
Voir la réponseMasquer la réponse de l'exercice 5
La ligne 14 commence par une tabulation (visible ici par le décalage différent), alors que YAML n'accepte que des espaces pour l'indentation. Remplace-la par des espaces, alignés sur les autres - de la liste. Configure ton éditeur pour insérer des espaces quand tu appuies sur Tab dans les fichiers YAML. Une erreur de syntaxe empêche le workflow de démarrer : aucune étape n'apparaît, seulement ce message.
Tu avais trouvé ?
Exercice 6 sur 10 Journal n° 3 : la base de données Réussi À revoir
Run npm run test:integration
FAIL tests/integration/notes.test.js
● Test suite failed to run
connect ECONNREFUSED 127.0.0.1:3306
Test Suites: 1 failed, 1 total
Error: Process completed with exit code 1.
Les tests passent sur le poste de Hugo. Donne deux causes possibles et leur correction.
Voir la réponseMasquer la réponse de l'exercice 6
ECONNREFUSED 127.0.0.1:3306 : rien n'écoute sur le port de MySQL du runner.
- Aucun service MySQL n'est déclaré dans le job : sur le poste de Hugo, MySQL tourne en permanence ; le runner, lui, démarre vide. Correction : ajouter le bloc
services: mysqlavecports: - 3306:3306(tester avec une vraie base). - Le service existe mais n'est pas prêt, ou son port n'est pas publié : sans contrôle de santé (
--health-cmd), les tests démarrent pendant que MySQL s'initialise ; sansports, il n'est joignable que depuis le réseau des conteneurs. Correction : ajouter les options--health-*et la publication du port.
Une troisième cause possible : l'application lit une variable DB_HOST que le job ne définit pas, et prend une valeur par défaut incorrecte.
Tu avais trouvé ?
Exercice 7 sur 10 Journal n° 4 : « chez moi, ça marche » Réussi À revoir
Run npm test -- --ci
FAIL tests/format.test.js
● Test suite failed to run
Cannot find module '../src/Utils/format' from 'tests/format.test.js'
Le dépôt contient le fichier src/utils/format.js. Lina, sous Windows, n'a jamais eu cette erreur. Explique.
Voir la réponseMasquer la réponse de l'exercice 7
Le test importe ../src/Utils/format avec un U majuscule, alors que le dossier s'appelle utils. Windows et macOS ne distinguent pas, par défaut, les majuscules des minuscules dans les noms de fichiers : l'import fonctionne chez Lina. Le runner tourne sous Linux, qui les distingue : le module est introuvable. Correction : écrire ../src/utils/format dans l'import. C'est un bon exemple de ce que la CI apporte : elle révèle les différences d'environnement avant la production, qui tourne elle aussi sous Linux.
Tu avais trouvé ?
Exercice 8 sur 10 Journal n° 5 : un job qui n'en finit pas Réussi À revoir
Run npm run test:integration
Test Suites: 4 passed, 4 total
Tests: 23 passed, 23 total
Jest did not exit one second after the test run has completed.
This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with `--detectOpenHandles` to troubleshoot this issue.
Error: The operation was canceled.
Le résumé du job indique : « The job has exceeded the maximum execution time of 15m0s ». Tous les tests sont pourtant passés. Explique et corrige.
Voir la réponseMasquer la réponse de l'exercice 8
Les 23 tests réussissent, mais Jest ne s'arrête pas : une opération asynchrone reste ouverte, très probablement le pool de connexions MySQL, jamais fermé. Le processus attend indéfiniment, jusqu'à ce que timeout-minutes: 15 annule le job. Corrections :
- fermer les ressources à la fin des tests :
afterAll(async () => { await pool.end(); });; - trouver la ressource coupable en local avec
npx jest --detectOpenHandles; - garder un
timeout-minutesraisonnable : sans lui, le job aurait tourné jusqu'à la limite par défaut de 6 heures.
L'option --forceExit de Jest ferait disparaître le symptôme sans corriger la fuite : à éviter.
Tu avais trouvé ?
Exercice 9 sur 10 Journal n° 6 : le workflow ne peut pas pousser Réussi À revoir
Un workflow doit pousser un tag après la construction. L'étape échoue :
Run git push origin v2.5.0
remote: Permission to sorbier/sorbier-frais.git denied to github-actions[bot].
fatal: unable to access 'https://github.com/sorbier/sorbier-frais/': The requested URL returned error: 403
Error: Process completed with exit code 128.
Le workflow commence par permissions: contents: read. Explique, corrige, et signale l'effet de bord à connaître.
Voir la réponseMasquer la réponse de l'exercice 9
Le jeton GITHUB_TOKEN du job n'a que le droit de lire le contenu (contents: read) : pousser un tag est une écriture, refusée (erreur 403). On donne contents: write au seul job qui pousse le tag :
permissions:
contents: write
L'effet de bord : un tag poussé avec GITHUB_TOKEN ne déclenche pas les workflows qui écoutent push: tags (protection contre les boucles). Si un workflow de publication doit partir sur ce tag, on enchaîne la publication dans le même workflow, ou on utilise un jeton d'application GitHub (publier une version).
Tu avais trouvé ?
Série 3 : un cas de synthèse
Exercice 10 sur 10 Mettre en place la CI/CD de l'application de prêt de matériel Réussi À revoir
Le service informatique d'une communauté de communes développe une application Web de prêt de matériel (ordinateurs portables, vidéoprojecteurs) en Node.js et MySQL. Deux développeurs, un dépôt public sur GitHub, un serveur de recette et un serveur de production sous Debian avec Docker. Aujourd'hui, les tests ne tournent que sur les postes, et le déploiement se fait par copie de fichiers en SFTP.
- Propose les étapes du pipeline, dans l'ordre, en séparant l'intégration continue et la livraison.
- Quelles règles de protection poses-tu sur la branche
main? - Où ranges-tu la clé SSH de déploiement en production, et qui peut s'en servir ?
- La responsable du service veut valider chaque mise en production. Comment le traduis-tu dans GitHub ? Est-ce de la livraison ou du déploiement continu ?
- Après une mise en production, les réservations échouent. Décris ta réaction, dans l'ordre.
- Rédige en trois ou quatre phrases ce que tu diras au jury pour présenter ce travail.
Voir la réponseMasquer la réponse de l'exercice 10
- Intégration continue, à chaque pull request et sur
main: récupération du code ; installation avecnpm ci; lint et formatage ; tests unitaires ; tests d'intégration contre un service MySQL 8.4 ; revue des dépendances et analyse CodeQL ; construction de l'image Docker et test de fumée de l'image. Livraison, après fusion surmain: publication de l'imagesha-…dansghcr.io; déploiement automatique en recette avec migration et test de fumée ; déploiement de la même image en production après approbation, puis test de fumée. - Un ruleset sur
main: pull request obligatoire, une approbation de l'autre développeur, approbation annulée par un nouveau commit, contrôles exigés (lint, tests unitaires, tests d'intégration, construction de l'image), branche à jour, suppressions etpush --forceinterdits, liste de contournement vide. Un fichierCODEOWNERSpour les workflows. - Comme secret de l'environnement
production, pas du dépôt : seul un job qui déclareenvironment: productionpeut la lire, et seulement après l'approbation. Sur le serveur, la clé correspond à un comptedeploysans droits d'administration, limité au script de déploiement. L'empreinte du serveur est enregistrée comme variable (known_hosts). - Un environnement GitHub
productionavec la responsable comme relecteur obligatoire (Required reviewers) ; le job de déploiement en production attend son clic Approve and deploy. Le dépôt étant public, la fonction est disponible avec l'offre gratuite. C'est de la livraison continue : tout est automatique jusqu'à la production, mais une personne décide du moment. - Prévenir les utilisateurs et l'équipe ; lancer le retour arrière vers l'image précédente (workflow de retour arrière, avec
MIGRER=non) ; vérifier que les réservations refonctionnent ; analyser les journaux de la version fautive ; corriger dans une pull request avec un test qui reproduit le problème ; écrire un post-mortem et ajouter au pipeline le contrôle qui aurait détecté le problème. - Par exemple : « J'ai remplacé un déploiement manuel par SFTP par un pipeline GitHub Actions : à chaque pull request, le lint, 64 tests unitaires et d'intégration et une analyse de sécurité tournent automatiquement, et la fusion est bloquée si l'un échoue. Après fusion, la même image Docker est déployée en recette en dix minutes, puis en production après validation de la responsable. Un retour arrière vers la version précédente prend moins de deux minutes. Cela répond aux compétences “Intégrer en continu les versions” et “Mettre à disposition des utilisateurs un service informatique”. »
Tu avais trouvé ?