DevOps, CI/CD et conteneurs
Intégration continue : pratiques, pipeline de build et mise en place concrète
Débutant10 min de lectureMis à jour
Cette page a été traduite de l'anglais avec l'aide de l'IA, puis vérifiée par rapport à l'original. Lire la version anglaise
L'intégration continue, c'est chaque développeur qui merge au moins une fois par jour dans la branche principale partagée, et un build automatisé qui prouve que chaque merge compile et passe ses tests. Ce guide couvre les pratiques qui fondent la CI, l'anatomie d'un pipeline de build, un workflow de CI complet pour un projet Java, la façon de garder des builds rapides et fiables, et les habitudes qui distinguent une équipe qui pratique la CI d'une équipe qui possède simplement un serveur de CI.
Ce que vous allez apprendre
- Énoncer les pratiques qui définissent l'intégration continue et la raison d'être de chacune
- Concevoir les étapes d'un build de CI : compilation, tests unitaires, analyse statique, packaging
- Écrire un workflow de CI qui lance le build à chaque push et à chaque pull request
- Garder un build rapide et fiable grâce au cache, au parallélisme et à une bonne hygiène face aux tests flaky
- Choisir une politique de merge — trunk-based ou branches de courte durée — compatible avec une intégration quotidienne
L'intégration était autrefois une phase. Les équipes travaillaient des semaines sur des branches séparées, puis passaient des jours — parfois des semaines — à merger, à découvrir que deux personnes avaient modifié la même interface de façon incompatible et à traquer des bugs qui n'apparaissaient qu'une fois tout assemblé. L'intégration continue (CI) fait disparaître cette phase en rendant l'intégration si fréquente qu'elle n'est plus jamais douloureuse : chacun merge chaque jour de petits changements dans une branche principale partagée, et un build automatisé prouve après chaque merge que le logiciel compile toujours et que ses tests passent encore. La pratique est simple à décrire et étonnamment exigeante à tenir. Ce guide explique ce qu'elle requiert, comment se construit un pipeline de build et comment garder l'ensemble assez rapide pour que l'équipe lui fasse confiance.
Ce qu'exige l'intégration continue
La CI est une pratique, et le serveur de CI n'en est qu'un ingrédient. Voici la liste complète :
Un dépôt de sources unique qui contient tout ce qu'il faut pour builder le système : code, tests, scripts de build, migrations de base de données, modèles de configuration. Une personne avec une machine neuve et un checkout doit pouvoir builder sans demander à un collègue ce qu'il reste à installer.
Un build automatisé qui se lance en une seule commande. mvn verify, ./gradlew build, npm ci && npm test — l'outil exact compte moins que la garantie qu'aucune étape manuelle n'intervient. Le build doit pouvoir tourner sur n'importe quelle machine, pas seulement sur celle où il a été configuré la première fois.
Un build auto-testé. Le build exécute les tests automatisés et échoue si l'un d'eux échoue. Un build qui se contente de compiler ne prouve pas grand-chose ; ce sont les tests qui donnent du sens à un résultat vert.
Tout le monde commite sur la branche principale chaque jour. C'est la règle que les équipes sautent le plus souvent. Une branche qui vit deux semaines, ce sont deux semaines de travail non intégré, quelle que soit la couleur de son propre build. Des merges petits et quotidiens maintiennent l'écart entre développeurs assez faible pour être résorbé en quelques minutes.
Chaque commit déclenche un build sur une machine d'intégration. Pas de « ça marche sur mon laptop » : un environnement propre, un checkout neuf, les mêmes étapes à chaque fois. C'est le rôle du serveur de CI.
Un build cassé se répare immédiatement. La branche principale est le bien commun de l'équipe ; quand elle est rouge, personne ne peut merger sans risque, donc la réparer (ou faire un revert) passe avant tout le reste.
Le build est rapide. Un feedback qui arrive une heure plus tard n'est plus du feedback, c'est une interruption. Dix minutes, c'est le plafond couramment cité pour le build principal.
Tout le monde voit le résultat. Un dashboard, un message Slack, un badge de statut sur la pull request. Cacher le statut du build dans un serveur que personne ne consulte, c'est le meilleur moyen de laisser passer des builds rouges.
Anatomie d'un build de CI
Le build d'un service typique enchaîne des étapes de coût croissant et de fréquence décroissante. Chaque étape joue le rôle de barrière : si elle échoue, les suivantes ne tournent pas, ce qui garde un feedback rapide pour les échecs les plus courants.
| Étape | Rôle | Durée typique | Échoue sur |
|---|---|---|---|
| Checkout | Clone neuf du commit testé | quelques secondes | Dépôt inaccessible |
| Compilation | Compile les sources principales et de test | de quelques secondes à une minute | Erreurs de syntaxe et de typage |
| Tests unitaires | Tests rapides et isolés, sans I/O | une à cinq minutes | Régressions de logique |
| Analyse statique | Linters, contrôles de style, scan de vulnérabilités des dépendances | une à trois minutes | Code smells, CVE connues |
| Packaging | Construit le jar ou l'image de conteneur | une minute | Packaging mal configuré |
| Tests d'intégration | Tests contre une vraie base de données ou un vrai broker (souvent dans un conteneur) | cinq à quinze minutes | Bugs de câblage, de SQL, de sérialisation |
| Publication | Pousse l'artefact vers un dépôt, tagué avec la version | quelques secondes | Permissions sur le dépôt |
Maven encode l'essentiel de cette séquence dans son cycle de vie : compile, test (tests unitaires via Surefire), package, verify (tests d'intégration via Failsafe, plus des contrôles comme les seuils de couverture), install et deploy. Lancer mvn verify depuis la CI exécute donc compilation, tests unitaires, packaging et tests d'intégration dans le bon ordre, et s'arrête au premier échec.
<!-- pom.xml: run *IT classes as integration tests during "verify" -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.5.2</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
Séparer les tests unitaires (Surefire, *Test) des tests d'intégration (Failsafe, *IT) est déterminant pour la vitesse : les développeurs lancent mvn test en local en quelques secondes, tandis que le serveur de CI exécute le verify complet.
Un workflow de CI complet
Voici un workflow GitHub Actions pour un projet Maven en Java 21. La même structure se transpose directement en pipeline déclaratif Jenkins (voir le guide du Jenkinsfile) ou en GitLab CI.
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
# Cancel an older run of the same branch when a new push arrives
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 20
services:
postgres: # real database for the integration tests
image: postgres:16
env:
POSTGRES_PASSWORD: ci
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s --health-timeout 5s --health-retries 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
cache: maven # caches ~/.m2 keyed on pom.xml hashes
- name: Build and test
run: mvn -B --no-transfer-progress verify
env:
DB_URL: jdbc:postgresql://localhost:5432/postgres
DB_PASSWORD: ci
- name: Publish test report
if: always() # upload even when tests failed
uses: actions/upload-artifact@v4
with:
name: surefire-reports
path: |
target/surefire-reports
target/failsafe-reports
Quelques points à remarquer. -B (mode batch) empêche Maven d'afficher des barres de progression et des codes couleur qui encombrent les logs. concurrency avec cancel-in-progress évite de mettre en file cinq builds pour cinq pushs rapprochés sur la même pull request. timeout-minutes empêche un test bloqué d'occuper un runner pendant six heures. Le if: always() sur l'upload des rapports garantit qu'un build en échec fournit quand même les rapports nécessaires pour le diagnostiquer. Mettre en cache le dépôt Maven avec une clé calculée sur le contenu de pom.xml transforme cinq minutes de téléchargement de dépendances en quelques secondes quand elles n'ont pas changé.
Garder un build rapide
Un build lent est la cause la plus fréquente de la dégradation d'une CI. Voici les stratégies, à peu près par ordre de rentabilité :
Mettez les dépendances en cache. Le ~/.m2 de Maven, le cache de Gradle, le node_modules de npm restauré depuis un cache indexé sur le lock file. C'est le gain le plus important sur les runners hébergés, qui démarrent d'une machine vierge.
Séparez le rapide du lent. Les tests unitaires dans le job principal ; les tests d'intégration et end-to-end dans un second job qui tourne après, ou dans un pipeline nocturne s'ils prennent une heure. Les développeurs obtiennent d'abord le verdict rapide.
Parallélisez les tests. Surefire et JUnit 5 savent exécuter les classes de test en parallèle sur plusieurs cœurs (<parallel> / junit.jupiter.execution.parallel.enabled), et une grosse suite peut être répartie sur plusieurs runners.
Ne buildez que ce qui a changé. Dans un monorepo, des outils comme le build cache de Gradle, Nx ou Bazel sautent les modules dont les entrées n'ont pas changé.
Ne testez pas via l'interface ce qu'un test unitaire peut couvrir. Une pyramide de tests avec beaucoup de tests unitaires rapides, moins de tests d'intégration et une poignée de tests end-to-end est plus rapide et moins flaky que l'inverse. Le guide sur la stratégie d'automatisation des tests explique comment lui donner cette forme.
Surveillez la tendance. Enregistrez la durée des builds et tracez-la. Les builds ralentissent un test après l'autre ; personne ne s'en aperçoit jusqu'au jour où le build de dix minutes en prend quarante.
Tests flaky et confiance
Un test flaky passe et échoue sur le même code. Son coût dépasse de loin les minutes perdues : dès que les développeurs apprennent qu'un build rouge n'est peut-être « que le test flaky », ils cessent d'enquêter sur les builds rouges, et les vrais échecs passent entre les mailles du filet avec les faux.
Les causes typiques : des tests dépendants de l'ordre d'exécution qui partagent un état statique, des tests qui dépendent de l'heure système ou du fuseau horaire, des race conditions dans du code asynchrone testé avec Thread.sleep, des collisions de ports quand les tests tournent en parallèle, et la dépendance à des services externes. La solution n'est jamais d'ajouter une boucle de retry autour du build entier ; il faut mettre le test en quarantaine (le déplacer dans un job séparé et non bloquant), ouvrir un ticket, puis le corriger ou le supprimer sous quelques jours. Une suite de tests n'est utile que si un résultat rouge veut dire quelque chose.
Stratégie de merge : trunk ou branches
L'exigence d'intégration quotidienne de la CI contraint la façon dont vous utilisez les branches.
Le trunk-based development — tout le monde commite sur main, directement ou via des pull requests qui vivent quelques heures — est l'approche la plus alignée avec la CI. Les fonctionnalités inachevées sont masquées derrière des feature flags (un interrupteur de configuration qui garde le chemin de code désactivé en production) au lieu de rester sur une branche : le code est intégré et compilé chaque jour, tandis que la fonctionnalité reste invisible pour les utilisateurs.
Les branches de fonctionnalité de courte durée avec pull requests fonctionnent bien quand chaque branche dure un jour ou deux et que son build doit être vert avant le merge. Exigez que la branche soit à jour avec main avant de merger (ou utilisez une merge queue) pour que le résultat du build reflète réellement la combinaison.
Les branches de longue durée — une branche develop en retard de plusieurs semaines sur main, des branches de release qui reçoivent des correctifs en parallèle, des branches par développeur mergées une fois par mois — c'est là que la douleur de l'intégration réapparaît. Elles sont parfois nécessaires (maintenir une ancienne version) mais devraient rester l'exception. Les compromis entre ces modèles sont détaillés dans Branches et workflows Git.
Contrôles sur les pull requests
La CI moderne tourne sur les pull requests, pas seulement sur les merges, et les plateformes permettent de conditionner le bouton de merge à des contrôles. Pour un service, des contrôles obligatoires raisonnables :
- Le build et les tests unitaires passent.
- L'analyse statique ne signale aucun nouveau problème critique (quality gate SonarQube, ESLint, Checkstyle).
- Le scan des dépendances ne signale aucune nouvelle vulnérabilité de gravité élevée (OWASP Dependency-Check, Dependabot, Trivy).
- La couverture de code sur les lignes modifiées n'est pas passée sous le seuil convenu — une règle sur le code nouveau, car imposer un chiffre global à une base de code legacy pousse à tricher avec la métrique.
- Au moins un relecteur a approuvé.
Gardez la liste assez courte pour qu'un changement ordinaire passe en quelques minutes ; chaque contrôle ajouté est un contrôle que l'équipe cherchera à contourner s'il est lent ou bruyant.
Erreurs fréquentes
- Commiter sur main une fois par semaine depuis une longue branche. Le serveur de CI tourne, mais l'intégration n'a rien de continu. Mergez chaque jour ; utilisez des feature flags pour le travail inachevé.
- Ne builder que sur le serveur de CI. Si
mvn verifyne tourne pas en local, les développeurs pushent pour savoir si leur changement compile, et le pipeline devient un compilateur lent. Le build doit fonctionner sur un laptop. - Laisser le build rouge. Au bout d'une journée, chacun suppose que quelqu'un d'autre s'en occupe. Attribuez-le explicitement, ou faites un revert.
- Des tests qui tapent sur des environnements partagés. Une suite de tests qui a besoin de la base de staging ne peut s'exécuter que sur un build à la fois et casse dès que quelqu'un d'autre utilise le staging. Démarrez plutôt une base de données dans un conteneur à chaque build.
- Des secrets dans le fichier de pipeline. Un mot de passe de base de données dans
ci.ymlreste à jamais dans l'historique Git. Utilisez le coffre à secrets du système de CI et injectez-le comme variable d'environnement à l'exécution. - Ignorer les warnings. Un build qui affiche trois cents warnings de dépréciation cache celui qui compte. Faites échouer le build sur les nouveaux warnings, ou au minimum suivez leur nombre.
- Construire un artefact différent pour chaque environnement. Le build de CI doit produire un seul artefact que les étapes suivantes promeuvent ; rebuilder par environnement signifie que ce que vous avez testé n'est pas ce que vous avez déployé. C'est la passerelle vers la livraison continue.
Points clés à retenir
- L'intégration continue est une pratique — des merges quotidiens dans une branche principale partagée, vérifiés par un build automatisé et auto-testé — qu'un serveur de CI ne suffit pas à fournir.
- Structurez le build en étapes de coût croissant pour que les échecs courants (erreurs de compilation, régressions des tests unitaires) soient signalés en quelques minutes.
- Gardez le build principal sous les dix minutes grâce au cache des dépendances, aux tests parallèles et à une pyramide de tests raisonnable ; déplacez les suites lentes vers une étape ultérieure.
- Les tests flaky détruisent la confiance plus vite que les builds lents ; mettez-les en quarantaine et corrigez-les au lieu de relancer.
- Le trunk-based development ou des branches courtes avec contrôles obligatoires permettent l'intégration quotidienne ; les branches de longue durée recréent la phase d'intégration que la CI a précisément pour but de supprimer.
- Une branche principale rouge est la priorité absolue de l'équipe : corrigez en quelques minutes ou faites un revert.
Sources et lectures complémentaires
Questions fréquentes
- Nous avons un serveur Jenkins qui lance un build chaque nuit. Est-ce de l'intégration continue ?
- Pas vraiment. Un build nocturne vous apprend le lendemain matin qu'un merge de la veille a cassé le code, une fois tout le monde rentré chez soi et, souvent, après que d'autres changements se sont empilés par-dessus. La CI, c'est un build qui tourne à chaque push, se termine en quelques minutes, et qui prévient la personne responsable de la casse pendant que le changement est encore frais dans sa tête. Le serveur est nécessaire, mais c'est la fréquence et la réaction qui font la CI.
- Combien de temps doit durer un build de CI ?
- Visez moins de dix minutes entre le push et le résultat pour la boucle de feedback principale. Au-delà, les développeurs n'attendent plus le résultat, passent à la tâche suivante et vivent les échecs comme des interruptions plutôt que comme du feedback. Les suites plus longues — intégration, end-to-end, performance — ont leur place dans une étape ultérieure du pipeline, lancée une fois l'étape rapide passée.
- Faut-il lancer la CI sur les branches de fonctionnalité ou seulement sur la branche principale ?
- Les deux. Lancer le build sur chaque branche et chaque pull request détecte les problèmes avant le merge et garde main au vert. Mais les builds de branche ne remplacent pas des merges fréquents : une branche verte depuis trois semaines peut quand même entrer lourdement en conflit avec main. Gardez des branches de courte durée et faites un rebase ou un merge depuis main chaque jour.
- Que faire quand le build est rouge ?
- Le réparer devient la priorité absolue de la personne qui l'a cassé, et personne d'autre ne merge par-dessus un build rouge. Si la correction ne saute pas aux yeux en dix minutes, faites un revert du commit fautif et enquêtez sur une branche. Une équipe qui tolère une branche principale rouge pendant des jours a complètement perdu le bénéfice de la CI.