Cours 3 · Intégration et livraison continuesLeçon 1 sur 2
Intégration continue
7 h de lecture9 sections Version PDF
Principe et discipline associée ; pipeline de compilation, tests et analyse statique ; déclencheurs, jobs, artefacts et cache ; échec rapide et boucle de rétroaction courte.
Deux développeurs travaillent trois semaines chacun de leur côté. Le jour de l'intégration arrive, on fusionne, et rien ne compile. Les deux ont modifié la même interface, chacun de façon cohérente avec son propre travail, et il faut maintenant décider laquelle survit — puis corriger tout ce qui en dépendait des deux côtés.
Cette journée porte un nom dans les équipes qui la vivent : merge day. Elle prend un jour, ou trois. Et sa cause n'est pas technique : les deux modifications étaient compatibles il y a trois semaines, et elles ont divergé sans que personne ne s'en aperçoive.
L'intégration continue est la réponse, et elle n'est pas un outil : c'est la discipline d'intégrer son travail dans la branche principale au moins une fois par jour, en s'assurant à chaque fois, automatiquement, que l'ensemble tient encore debout.
Le principe, et la discipline qui va avec
L'outil — GitHub Actions, GitLab CI, Jenkins — ne fait qu'exécuter des commandes après un
push. Ce qui produit le bénéfice, ce sont quatre règles de comportement.
Intégrer au moins une fois par jour. C'est le trunk-based du chapitre 2. Une branche de trois semaines rend le pipeline inutile : il vérifiera fidèlement une divergence déjà constituée.
Chaque intégration déclenche une vérification automatique. Compilation, tests, analyse statique. Automatique, parce qu'une vérification manuelle est une vérification qu'on saute un vendredi soir.
Une construction cassée s'arrête et se répare tout de suite. C'est la règle qui coûte le plus à installer, et celle qui fait toute la différence. Tant que la branche principale est rouge, personne ne peut savoir si son propre travail est correct — la vérification ne dit plus rien. Réparer passe donc avant d'ajouter.
Personne ne construit par-dessus du rouge. Corollaire du précédent, et il est social autant que technique.
Sans ces règles, on obtient ce qu'on voit dans beaucoup de projets : un pipeline qui existe, qui échoue depuis trois semaines, et que tout le monde a appris à ignorer. Il coûte du temps de calcul et ne rend plus aucun service.
Un pipeline
name: intégrationon: push: branches: [main] pull_request: jobs: verifier: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' # dépendances mises en cache - run: npm ci - run: npm run lint # rapide : en premier - run: npm run test:unit - run: npm run test:integration - uses: actions/upload-artifact@v4 with: name: rapport-couverture path: coverage/Cinq notions se lisent dans ce fichier, et elles sont communes à tous les outils.
Le déclencheur dit quand le pipeline part : à chaque push sur la branche principale, à
chaque demande de fusion, sur une étiquette de version — c'est le chapitre 6 — ou selon un
horaire, pour les vérifications lentes qu'on ne veut pas à chaque commit.
Un job est une unité qui s'exécute sur une machine propre. Deux jobs indépendants tournent en parallèle ; c'est le levier principal pour raccourcir le pipeline.
Une étape est une commande dans un job, exécutée dans l'ordre. La première qui échoue arrête le job.
Un artefact est un fichier produit par un job et conservé : rapport de couverture, image construite, binaire. C'est aussi le moyen de passer un résultat d'un job à un autre.
Le cache conserve entre deux exécutions ce qui est coûteux à reconstituer — dépendances, couches Docker du chapitre 3. Sans lui, chaque exécution repart d'une machine vierge.
L'échec rapide, et la durée du cycle
Le pipeline doit répondre vite, pour une raison qui n'a rien de technique : au-delà d'une dizaine de minutes, on ne l'attend plus. On passe à autre chose, on perd le contexte, et le retour arrive quand on pense à autre chose — la boucle de rétroaction du chapitre 1 est cassée.
Deux leviers, et le premier est gratuit.
Ordonner du plus rapide au plus lent. Le formatage et l'analyse statique prennent dix secondes et attrapent les fautes les plus fréquentes ; les tests unitaires prennent une minute ; les tests d'intégration cinq ; les tests de bout en bout quinze. Dans cet ordre, une faute de lint est signalée en dix secondes au lieu de vingt minutes.
Paralléliser ce qui est indépendant. Les tests unitaires et l'analyse statique n'ont aucune raison d'attendre l'un l'autre. Trois jobs parallèles de cinq minutes valent mieux qu'un job séquentiel de quinze.
séquentiel [lint 10s][unitaires 60s][intégration 300s][e2e 900s] = 21 minparallèle [lint 10s] [unitaires 60s] puis [intégration 300s] [analyse sécurité 120s] [e2e 900s] = 22 min… ?Le second schéma illustre une erreur courante : paralléliser ce qui est déjà rapide ne gagne rien. Le pipeline dure aussi longtemps que son chemin le plus long — c'est le chemin critique du chapitre 4 d'architecture. Pour descendre sous dix minutes, il faut s'attaquer aux tests de bout en bout : les découper, les paralléliser entre eux, ou les sortir du pipeline de chaque commit pour les exécuter à la nuit.
Qualité de code et couverture
Trois outils s'ajoutent au pipeline, et le troisième demande une mise en garde.
Le formateur — Prettier, gofmt, black — met le code en forme automatiquement. Le mettre
dans le pipeline supprime définitivement les discussions d'indentation en revue, ce qui n'est
pas un gain esthétique mais un gain d'attention : le relecteur regarde la logique.
L'analyse statique — ESLint, SonarQube — cherche des motifs suspects sans exécuter le code :
variable inutilisée, comparaison toujours vraie, ressource non fermée. C'est le -Wall du cours
de programmation, appliqué au projet.
La couverture de tests mesure la proportion de lignes exécutées par la suite de tests. Elle est utile comme signal — une couverture de 5 % dit quelque chose — et dangereuse comme objectif. Dès qu'on impose un seuil, on l'atteint : il suffit d'écrire des tests qui appellent le code sans rien vérifier. On obtient 90 % de couverture et aucune garantie, ce qui est pire que 40 % assumés, parce que le chiffre inspire une confiance injustifiée.
C'est un cas particulier d'une règle générale, qu'il vaut mieux connaître : quand une mesure devient un objectif, elle cesse d'être une bonne mesure. On surveille donc la tendance de la couverture plutôt que sa valeur, et l'on regarde surtout ce qui n'est pas couvert du tout.
Un pipeline exécute, dans cet ordre : tests de bout en bout (15 min), tests unitaires (1 min), analyse statique (10 s). Que faut-il changer, et pourquoi ?
Ce qui tue une intégration continue
Trois maux, et le premier est de loin le pire.
Les tests instables (flaky), qui échouent une fois sur vingt sans raison. Leur effet est catastrophique et le calcul est instructif : si chaque test échoue au hasard avec une probabilité , la probabilité qu'au moins un des tests échoue vaut . Avec et 200 tests, cela fait 63 % d'échecs sans aucune cause réelle.
La conséquence est prévisible : l'équipe relance le pipeline « pour voir », et ce faisant apprend à ignorer les échecs. Un vrai échec passe alors inaperçu. Un test instable doit être réparé ou supprimé le jour même — le garder est pire que ne pas l'avoir.
Les pipelines lents. Au-delà de dix minutes, on n'attend plus ; au-delà de trente, on regroupe ses commits pour ne pas payer l'attente, et l'on retombe sur les gros lots du chapitre 1.
Les tests dépendants entre eux. Un test qui suppose qu'un autre a rempli la base ne passe que dans un ordre donné, et devient instable dès qu'on parallélise. Chaque test doit créer et détruire ses propres données.
Une équipe impose un seuil de 90 % de couverture de tests. Quel est le risque ?
À vous
L'exercice construit un exécuteur de pipeline et met en chiffres les trois affirmations du chapitre.
D'abord l'ordonnancement : vous donnez des étapes avec leur durée et leur probabilité d'échec, et le programme calcule le délai moyen avant retour selon l'ordre choisi. L'échec rapide cesse d'être un conseil pour devenir un écart mesuré.
Ensuite le cache et le parallélisme : mêmes étapes, avec et sans cache de dépendances, en séquentiel puis réparties en jobs. Vous chercherez le chemin critique et constaterez que paralléliser ce qui est déjà rapide ne gagne rien.
Enfin les tests instables. Vous ferez varier la probabilité d'instabilité et le nombre de tests, et vous retrouverez les 63 % du cours — puis vous simulerez ce que fait une équipe qui relance « pour voir », et combien de vrais échecs elle laisse passer.
Mesurez l'échec rapide, trouvez le chemin critique, puis chiffrez le coût des tests instables.
// ── 1. Ordre des étapes et délai avant retour ───────────────────────────── // duree en secondes, proba = probabilité que CETTE étape révèle un défaut. const ETAPES = [ { nom: "lint", duree: 10, proba: 0.25 }, { nom: "tests unitaires", duree: 60, proba: 0.35 }, { nom: "intégration", duree: 300, proba: 0.20 }, { nom: "bout en bout", duree: 900, proba: 0.10 }, ]; // Le job s'arrête à la PREMIÈRE étape qui échoue : le délai avant retour // dépend donc de l'ordre. function delaiMoyen(etapes) { let cumul = 0, esperance = 0, resteVrai = 1; for (const e of etapes) { cumul += e.duree; // ← à écrire : probabilité d'arriver jusqu'ici ET d'échouer ici, // pondérée par le temps écoulé } // Cas où tout passe : on paie la totalité. esperance += resteVrai * cumul; return esperance; } // ── 2. Cache et parallélisme ────────────────────────────────────────────── const AVEC_CACHE = { installation: 8 }; const SANS_CACHE = { installation: 95 }; // Des jobs, chacun avec ses étapes et ses dépendances. const JOBS = [ { nom: "lint", duree: 10, depend: ["installation"] }, { nom: "unitaires", duree: 60, depend: ["installation"] }, { nom: "sécurité", duree: 120, depend: ["installation"] }, { nom: "intégration", duree: 300, depend: ["unitaires"] }, { nom: "bout en bout", duree: 900, depend: ["intégration"] }, ]; function cheminCritique(jobs, installation) { const fin = { installation }; // ← à écrire : la fin d'un job est le max des fins de ses dépendances, // plus sa propre durée. Le pipeline dure jusqu'à la fin la plus tardive. return 0; } // ── 3. Tests instables ──────────────────────────────────────────────────── function probaEchecPipeline(nbTests, instabilite) { return 0; // ← à écrire : 1 − (1 − p)^n } // ── À VOUS ──────────────────────────────────────────────────────────────── // 1. Écrivez delaiMoyen, puis comparez l'ordre rapide→lent et lent→rapide. // 2. Écrivez cheminCritique. Que gagne le cache ? Et si l'on parallélise // seulement les étapes déjà rapides ? // 3. Retrouvez les 63 % du cours, puis simulez une équipe qui relance // « pour voir » : combien de VRAIS échecs passent inaperçus ? console.log(" délai moyen :", delaiMoyen(ETAPES).toFixed(0), "s");
En travaux pratiques
Construire le pipeline, et le rendre rapide
Écrire un pipeline complet sur le dépôt du fil rouge, puis le mesurer et le raccourcir — ordre des étapes, cache, parallélisme.
- Le dépôt du TP 2 hébergé sur une forge disposant d'intégration continue
- La pile Compose du TP 4, qui démarre
- Une suite de tests, même minimale
- 1. Un premier pipeline qui échoue
Écrivez un pipeline déclenché sur chaque demande de fusion : installation, analyse statique, tests. Poussez une modification qui casse volontairement le lint, et vérifiez que la demande de fusion est bloquée.
- 2. Mesurer le délai avant retour
Placez délibérément les étapes de la plus lente à la plus rapide. Poussez une faute de lint et chronométrez le temps entre le push et le message d'échec. Réordonnez, refaites la mesure.
- 3. Mettre les dépendances en cache
Activez le cache du gestionnaire de paquets et comparez la durée d'installation sur deux exécutions consécutives.
- 4. Ajouter les tests d'intégration
Faites tourner les tests contre une vraie base, démarrée par le pipeline. Ils ne doivent PAS dépendre les uns des autres : chaque test crée et détruit ses propres données.
- 5. Paralléliser, et trouver la limite
Découpez en jobs indépendants et mesurez le gain. Identifiez ensuite le chemin critique : quelle suite d'étapes fixe la durée totale ? Que gagneriez-vous en parallélisant ce qui n'est PAS sur ce chemin ?
- 6. Éprouver l'instabilité
Introduisez un test qui échoue aléatoirement une fois sur vingt. Relancez le pipeline dix fois et comptez les échecs. Discutez : que fera l'équipe au bout d'une semaine ?
- 7. Publier l'image
Ajoutez un job final qui construit l'image du TP 3 et la pousse dans un registre, étiquetée avec l'empreinte du commit. C'est cet artefact que le TP 6 déploiera.
- Une faute de lint est signalée en moins de trente secondes
- Le pipeline complet dure moins de dix minutes
- Deux exécutions consécutives sans changement de dépendances ne réinstallent rien
- L'image publiée porte l'empreinte du commit qui l'a produite
Ce que la suite en fait
Le chapitre 6 prolonge le pipeline jusqu'à la production. Tout ce qui vient d'être posé y reste valable : le même déclencheur, les mêmes artefacts — et l'image construite ici devient exactement celle qui sera déployée, ce qui est la garantie centrale de tout le bloc.
C'est aussi là que la discipline de ce chapitre prend son sens complet : on ne déploie automatiquement que ce en quoi on a confiance, et cette confiance vient précisément d'un pipeline vert que personne n'a appris à ignorer.
À retenir
Vous avez parcouru les 9 sections.
Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.