Aller au contenu
Intégrer en continu

Intégrer en continu

100 Comprendre ⏱ 45 min gitci-cdpython

À la fin, vous saurez

  • Définir l'intégration continue par les trois conditions qui permettent de dire qu'une équipe la pratique
  • Expliquer pourquoi la taille des lots, et non la vitesse, détermine le coût d'une intégration
  • Reproduire un conflit sémantique : une fusion sans conflit textuel qui casse l'application
  • Comparer les branches longues, le développement sur le tronc et les branches courtes
  • Situer l'intégration continue par rapport à la livraison continue et au déploiement continu

Prérequis

Testé avec docker 29.8.1 git 2.43.0 pytest 9.1.1 python 3.14.7 ruff 0.16.9 , vérifié le 1 octobre 2026

Pourquoi

Imaginez deux développeurs, Camille et Dominique, qui travaillent sur l'application Signalements. Chacun part de la même version, crée sa branche, avance de son côté pendant deux semaines. Sur sa machine, tout fonctionne : chaque branche compile, chaque branche passe ses tests. Le vendredi de la mise en production, on fusionne. Et rien ne marche.

Ce scénario a un nom dans les équipes qui l'ont vécu : l'enfer de l'intégration (integration hell). Il ne vient pas d'une faute de quelqu'un. Il vient d'une propriété simple du travail en parallèle : deux changements corrects séparément peuvent être incorrects ensemble, et plus on attend pour les réunir, plus il y a de combinaisons à démêler, et moins on se souvient de ce que l'on a fait. Le problème ne grandit pas en proportion du temps écoulé : il grandit plus vite, parce que chaque jour de divergence ajoute des changements qui interagissent avec tous ceux des jours précédents.

L'intégration continue est la réponse que l'industrie a trouvée dans les années 1990, et elle tient dans un renversement : puisque l'intégration est douloureuse, on la fait plus souvent, jusqu'à ce qu'elle devienne banale. Le programme de recherche DORA, qui étudie depuis 2014 les pratiques des équipes de livraison logicielle, le formule ainsi : quand une chose prend beaucoup de temps et d'énergie, il faut la faire plus souvent, pour se forcer à la rendre moins pénible.

Sans intégration continue, une équipe paie de trois façons :

  • Les défauts sont découverts tard, quand leur auteur est passé à autre chose. Les auteurs du chapitre consacré à la CI dans Software Engineering at Google rappellent que le coût d'un défaut croît presque exponentiellement avec le temps qui sépare son introduction de sa découverte.
  • La date de livraison devient imprévisible, parce que personne ne sait combien de temps prendra la fusion finale.
  • L'équipe cesse de remanier le code (renommer, réorganiser), parce que chaque remaniement crée des conflits avec les branches des autres. Le code se dégrade.

Ce chapitre Livrer s'ouvre sur cette leçon parce que tout le reste, livraison continue, déploiement continu, GitOps, repose sur elle : on ne peut pas livrer souvent un logiciel que l'on n'intègre que rarement.

Les concepts

Une pratique, pas un outil

Le contresens le plus répandu consiste à croire qu'une équipe « fait de la CI » parce qu'elle a installé Jenkins ou écrit un fichier GitHub Actions. L'outil est nécessaire, il ne suffit pas. Martin Fowler, dont l'article de référence a été entièrement réécrit en 2023, définit l'intégration continue comme une pratique où chaque membre de l'équipe fusionne ses changements dans la base de code commune, avec ceux de ses collègues, au moins une fois par jour, chaque intégration étant vérifiée par une construction automatique qui inclut les tests.

Jez Humble, coauteur du livre Continuous Delivery, en a tiré un test de trois questions qu'il pose à ses auditoires (rapporté par Fowler sous le nom de Continuous Integration Certification) :

  1. Chaque membre de l'équipe pousse-t-il son travail sur la branche principale commune au moins une fois par jour ?
  2. Chaque commit sur cette branche déclenche-t-il une construction et l'exécution automatique de tous les tests ?
  3. Quand la construction échoue, est-elle réparée en moins de dix minutes ?

Trois « oui » : l'équipe pratique l'intégration continue. Le troisième point surprend souvent. Dix minutes ne suffisent pas toujours à corriger un défaut, et ce n'est pas ce qui est demandé : si la correction n'est pas immédiate, on annule le commit fautif (git revert) pour revenir à la dernière version verte, et l'on corrige ensuite tranquillement. La branche principale doit rester saine, parce que tout le monde travaille à partir d'elle.

On voit que deux des trois conditions portent sur le comportement des personnes, pas sur l'outillage.

Les termes de base

Quelques mots reviennent dans tout ce chapitre :

  • La branche principale (mainline, trunk, souvent nommée main) est la branche commune, celle à partir de laquelle on livre. On l'appelle aussi le tronc.
  • Intégrer, c'est réunir son travail avec celui des autres dans la branche principale. Récupérer les changements des autres dans sa propre branche n'est qu'une demi-intégration : les autres ne voient toujours pas votre travail, donc ne peuvent pas détecter les conflits avec le leur. Fowler insiste sur ce point.
  • La construction (build) transforme les sources en quelque chose d'exécutable et le vérifie : compilation, tests, analyse, image de conteneur. Une construction est verte si tout a réussi, rouge sinon.
  • Une construction auto-vérifiante (self-testing build) contient assez de tests automatiques pour qu'une construction verte donne une confiance raisonnable dans le logiciel.

Pourquoi la taille des lots compte plus que la vitesse

Le cœur de l'intégration continue n'est pas d'aller vite, c'est de travailler en petits lots. Comparez :

Intégration tous les quinze joursIntégration plusieurs fois par jour
Taille d'une intégrationDes centaines de lignes, des dizaines de fichiersQuelques dizaines de lignes
ConflitsNombreux, entremêlés, découverts en blocRares, petits, isolés
Quand un test casseQuel changement, parmi cinquante, est en cause ?Le dernier commit, que son auteur a encore en tête
Remaniement du codeÉvité, il crée des conflits partoutPossible, les autres le récupèrent le jour même
Date de livraisonInconnue tant que la fusion n'est pas faiteLe logiciel est intégré, donc livrable, en permanence

Un petit lot est plus facile à relire, à tester, à comprendre quand il casse, et à annuler. C'est la même idée qui, appliquée à la mise en production, donnera le déploiement continu (leçon 4).

Conflit textuel et conflit sémantique

Git sait détecter un conflit textuel : deux branches ont modifié les mêmes lignes, et Git ne peut pas choisir. Ce conflit est désagréable mais honnête, puisqu'il se signale. Le danger est ailleurs : le conflit sémantique, où deux changements touchent des lignes différentes, se fusionnent sans aucun message, et produisent ensemble un programme faux. Git compare du texte ; il ne comprend pas le programme. Seule l'exécution du programme fusionné, par des tests, révèle ce type de conflit. C'est la raison d'être de la deuxième condition du test de Humble, et la démonstration qui suit.

Branches longues, tronc et branches courtes

Trois façons d'organiser les branches coexistent :

    flowchart TB
  subgraph L["Branches longues"]
    direction LR
    l1["main"] --> l2["...2 semaines..."] --> l3["fusion<br/>douloureuse"]
  end
  subgraph T["Développement sur le tronc"]
    direction LR
    t1["commit"] --> t2["commit"] --> t3["commit"] --> t4["commit"]
  end
  subgraph C["Branches courtes"]
    direction LR
    c1["branche<br/>quelques heures"] --> c2["revue +<br/>tests"] --> c3["fusion<br/>dans main"]
  end
  
  • Les branches longues (feature branches de plusieurs jours ou semaines) isolent le travail, ce qui rassure, mais repoussent l'intégration. Elles sont incompatibles avec l'intégration continue au sens strict.
  • Le développement sur le tronc (trunk-based development) : chacun pousse directement sur main, plusieurs fois par jour, après avoir vérifié localement. C'est la forme d'origine, encore courante dans les petites équipes très soudées.
  • Les branches courtes : une branche par changement, qui ne vit que quelques heures, un jour ou deux au plus, le temps d'une revue de code et d'une vérification automatique, puis fusionnée et supprimée. C'est la forme la plus répandue aujourd'hui, avec les demandes de fusion (pull requests chez GitHub, merge requests chez GitLab). Le site Trunk Based Development de Paul Hammant la décrit comme la variante du tronc adaptée aux équipes plus nombreuses.

Les études de DORA de 2016 et 2017 associent de meilleures performances de livraison à trois habitudes : trois branches actives au plus dans le dépôt, une fusion dans le tronc au moins une fois par jour, et ni gel du code ni phase d'intégration. Dans ce modèle, une branche vit typiquement quelques heures. La durée de vie des branches est donc l'indicateur à surveiller : une branche courte, fusionnée le jour même, est de l'intégration continue ; une « branche courte » de trois semaines n'en est pas.

Comment livrer une fonctionnalité qui demande trois semaines de travail sans branche de trois semaines ? En masquant le travail en cours : on fusionne chaque jour du code inactif, désactivé par un drapeau de fonctionnalité (feature flag), ou caché derrière une abstraction que l'on remplace progressivement (branch by abstraction). Le code est intégré, testé, livré, mais pas encore visible des utilisateurs. Le cours Feature flags du chapitre traite ces techniques.

Une brève histoire

DateÉtape
1991Grady Booch emploie l'expression continuous integration au détour d'une phrase, sans en faire une pratique
Fin des années 1990Kent Beck en fait l'une des pratiques de l'Extreme Programming (livre paru en 1999) : intégrer et tester plusieurs fois par jour
2000Premier article de Martin Fowler sur le sujet, avec Matthew Foemmel
2001CruiseControl, premier serveur de CI libre répandu, chez ThoughtWorks
2005-2011Hudson, qui devient Jenkins en 2011 après un différend avec Oracle
2010Jez Humble et David Farley publient Continuous Delivery
2011-2019CI hébergée et décrite dans le dépôt : Travis CI (2011), GitLab CI intégré à GitLab (2015), GitHub Actions disponible pour tous (novembre 2019)

Ce qui a changé en trente ans, c'est l'outillage : le serveur dans un placard est devenu un service hébergé, la configuration est passée d'une interface web à un fichier versionné avec le code. Ce qui n'a pas changé, ce sont les trois conditions du test.

En pratique

Nous allons rejouer l'histoire de Camille et Dominique, en accéléré, avec un vrai dépôt Git et les vrais tests de Signalements. Les sorties de cette leçon ont été produites avec LC_ALL=C.UTF-8 pour obtenir les messages de Git en anglais, tels qu'ils apparaissent dans les journaux d'un agent de CI ; avec une machine configurée en français, Git affiche les mêmes informations traduites.

Préparer un dépôt partagé

Le dépôt « central » est un dépôt nu (bare), sans copie de travail, qui joue le rôle de GitHub ou GitLab. Chaque développeur en a un clone :

$ mkdir integration && cd integration
$ git init --bare -b main depot.git
$ git clone depot.git camille
$ cd camille
$ git config user.name Camille && git config user.email camille@exemple.fr

Copiez dans camille/ les fichiers de Signalements : app.py, test_app.py (les deux tests du cours précédent), requirements.txt, et un fichier requirements-dev.txt pour les outils de développement :

-r requirements.txt
pytest==9.1.1
ruff==0.16.9

Puis publiez la version initiale et créez le clone de Dominique :

$ git add . && git commit -m "Signalements : version initiale" && git push origin main
$ cd .. && git clone depot.git dominique
$ cd dominique
$ git config user.name Dominique && git config user.email dominique@exemple.fr
$ git log --oneline
09a1236 Signalements : version initiale

Les empreintes de commit (09a1236) seront différentes chez vous : elles dépendent du nom, de l'adresse et de la date.

Deux branches, chacune correcte

Camille remanie le code : la liste des signalements en mémoire s'appelle _memoire, un nom peu parlant. Camille la renomme en _signalements :

$ cd ../camille
$ git switch -c renommage
Switched to a new branch 'renommage'
$ sed -i 's/_memoire/_signalements/g' app.py
$ git diff --stat
 app.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)
$ git commit -am "Renommer _memoire en _signalements"

Pendant ce temps, Dominique ajoute la consultation d'un signalement par son identifiant, GET /signalements/<id>. En mode mémoire, la fonction parcourt la liste... qui s'appelle encore _memoire dans sa branche :

@app.get("/signalements/<int:ident>")
def detail(ident):
    if not DATABASE_URL:
        for signalement in _memoire:
            if signalement["id"] == ident:
                return jsonify(signalement)
        return jsonify(erreur="introuvable"), 404
    with connexion() as cnx:
        ligne = cnx.execute(
            "SELECT id, lieu, description FROM signalements WHERE id = %s", (ident,)
        ).fetchone()
    if ligne is None:
        return jsonify(erreur="introuvable"), 404
    return jsonify(id=ligne[0], lieu=ligne[1], description=ligne[2])

Dominique, consciencieux, ajoute un test dans test_app.py :

def test_detail():
    client = app.test_client()
    cree = client.post("/signalements", json={"lieu": "Place du Marché", "description": "Banc cassé"})
    ident = cree.get_json()["id"]
    assert client.get(f"/signalements/{ident}").get_json()["lieu"] == "Place du Marché"
    assert client.get("/signalements/9999").status_code == 404
$ cd ../dominique
$ git switch -c detail
$ # ... ajout de la route dans app.py et du test dans test_app.py ...
$ git diff --stat
 app.py      | 16 ++++++++++++++++
 test_app.py |  8 ++++++++
 2 files changed, 24 insertions(+)
$ git commit -am "Ajouter la consultation d'un signalement"

Chacun lance les tests de sa branche. Pour qu'ils s'exécutent dans un environnement propre, sans dépendre de ce qui est installé sur le poste, on les lance dans un conteneur Python neuf, avec les sources montées en lecture seule :

$ cd ../camille
$ docker run --rm -e PIP_ROOT_USER_ACTION=ignore -e PIP_DISABLE_PIP_VERSION_CHECK=1 \
    -v "$PWD":/src:ro -w /src python:3.14-slim \
    sh -c 'pip install --quiet -r requirements-dev.txt && pytest -q -p no:cacheprovider'
..                                                                       [100%]
2 passed in 0.05s
$ cd ../dominique
$ docker run --rm ... (même commande)
...                                                                      [100%]
3 passed in 0.11s

Les options méritent une explication :

  • --rm supprime le conteneur à la fin : rien ne subsiste d'une exécution à l'autre.
  • -v "$PWD":/src:ro monte le répertoire courant en lecture seule : les tests ne peuvent pas modifier les sources.
  • PIP_ROOT_USER_ACTION=ignore et PIP_DISABLE_PIP_VERSION_CHECK=1 font taire deux avertissements de pip sans intérêt ici.
  • -p no:cacheprovider empêche pytest d'essayer d'écrire son cache .pytest_cache dans le répertoire monté en lecture seule.

Les deux branches sont vertes. Chacun est convaincu que son travail est terminé.

La fusion qui ne se plaint pas

Camille fusionne sa branche dans main et la pousse. Dominique récupère main, puis y fusionne sa branche :

$ cd ../camille
$ git switch main && git merge renommage && git push origin main
$ cd ../dominique
$ git switch main && git pull --ff-only
Updating 09a1236..b02a56e
Fast-forward
 app.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)
$ git merge detail -m "Fusionner detail"
Auto-merging app.py
Merge made by the 'ort' strategy.
 app.py      | 16 ++++++++++++++++
 test_app.py |  8 ++++++++
 2 files changed, 24 insertions(+)

Aucun conflit. Git a fusionné app.py automatiquement (Auto-merging), avec sa stratégie par défaut ort, parce que les deux branches n'ont pas touché les mêmes lignes. L'historique montre les deux lignes de travail réunies :

$ git log --oneline --graph
*   d7d986d Fusionner detail
|\
| * d88189d Ajouter la consultation d'un signalement
* | b02a56e Renommer _memoire en _signalements
|/
* 09a1236 Signalements : version initiale

Lançons maintenant les tests sur le résultat de la fusion :

$ docker run --rm ... (même commande)
..F                                                                      [100%]
=================================== FAILURES ===================================
_________________________________ test_detail __________________________________
...
>       assert client.get(f"/signalements/{ident}").get_json()["lieu"] == "Place du Marché"
E       TypeError: 'NoneType' object is not subscriptable

test_app.py:22: TypeError
------------------------------ Captured log call -------------------------------
ERROR    app:app.py:875 Exception on /signalements/2 [GET]
Traceback (most recent call last):
...
  File "/src/app.py", line 60, in detail
    for signalement in _memoire:
                       ^^^^^^^^
NameError: name '_memoire' is not defined
=========================== short test summary info ============================
FAILED test_app.py::test_detail - TypeError: 'NoneType' object is not subscri...
1 failed, 2 passed in 0.13s
$ echo $?
1

Voilà le conflit sémantique. Deux changements corrects, une fusion sans conflit, un programme faux. Lisez la chaîne de bas en haut : le renommage de Camille a fait disparaître _memoire, donc la route de Dominique lève une NameError (section Captured log call), que Flask transforme en réponse d'erreur au corps vide ; le test reçoit ce corps vide (None) et échoue à son tour avec une TypeError en essayant d'y lire une clé. Un seul défaut, deux visages. Dans la vraie vie, sans tests exécutés sur le résultat de la fusion, cette erreur partirait en production et apparaîtrait au premier utilisateur qui consulte un signalement. Notez aussi le code de sortie 1 : c'est par lui, et non par le texte affiché, qu'un système d'intégration continue sait qu'une étape a échoué (leçon 2).

Tester l'intégration avant de la publier

Annulons la fusion, et faisons ce que fait une équipe qui pratique l'intégration continue : avant de fusionner, Dominique rejoue sa branche par-dessus le main le plus récent et teste ce résultat-là.

$ git reset --hard origin/main
$ git switch detail
$ git rebase origin/main
Successfully rebased and updated refs/heads/detail.
$ git log --oneline
6f95fb2 Ajouter la consultation d'un signalement
b02a56e Renommer _memoire en _signalements
09a1236 Signalements : version initiale
$ docker run --rm ... (même commande)
FAILED test_app.py::test_detail - TypeError: 'NoneType' object is not subscri...
1 failed, 2 passed in 0.07s

L'échec est le même, mais il se produit sur la branche de Dominique, avant la fusion : main reste vert pour toute l'équipe. Dominique corrige dans la minute, puisque le changement de Camille date du jour même et tient en quatre lignes :

$ sed -i 's/_memoire/_signalements/' app.py
$ git commit -am "Consultation : suivre le renommage de _memoire"
$ docker run --rm ... (même commande)
3 passed in 0.11s

Les plateformes de CI automatisent exactement ce geste : pour une demande de fusion, elles construisent et testent non pas la branche seule, mais le résultat de sa fusion avec la branche cible. GitHub, par exemple, prépare pour chaque pull request une référence refs/pull/<numéro>/merge qui contient ce commit de fusion hypothétique.

Sous le capot

Comment Git fusionne. Une fusion part de trois versions : l'ancêtre commun des deux branches (ici 09a1236), et les deux extrémités. Pour chaque fichier, Git compare chaque extrémité à l'ancêtre. Si un seul côté a modifié une zone, il prend ce côté ; si les deux ont modifié la même zone différemment, il signale un conflit. C'est la fusion à trois voies. Le renommage de Camille touchait les lignes qui utilisaient _memoire ; la route de Dominique était un bloc nouveau, inséré ailleurs. Aucune zone commune, donc aucun conflit : Git n'a aucun moyen de savoir que le nouveau bloc fait référence à un nom qui vient de disparaître.

On peut demander à Git le résultat d'une fusion sans la faire, ni toucher à la copie de travail, avec git merge-tree (option --write-tree, disponible depuis Git 2.38) :

$ git merge-tree --write-tree origin/main detail
1e63578e2af4c38f6b62fd4cbcf09866bfd48f7d
$ echo $?
0

La commande affiche l'identifiant de l'arbre fusionné et sort avec le code 0 : fusion propre. En cas de conflit textuel, elle sort avec le code 1 et liste les fichiers en conflit. Les forges logicielles s'en servent pour afficher « cette branche peut être fusionnée » sur une demande de fusion. Ce message parle de texte, jamais de comportement.

Pourquoi un environnement propre. Les tests ont tourné dans un conteneur créé pour l'occasion, depuis une image publique, avec des dépendances aux versions fixées dans requirements-dev.txt. Sur le poste d'un développeur, un paquet installé à la main il y a six mois, une variable d'environnement oubliée ou un fichier non versionné peuvent faire passer des tests qui échoueraient ailleurs. L'environnement propre ne contient que ce qui est dans le dépôt : c'est la première pratique de la liste de Fowler, « tout mettre dans le dépôt ». La leçon 2 généralise ce principe aux agents de CI jetables.

Pièges courants

Confondre « on a un outil de CI » et « on fait de la CI ». Un serveur Jenkins qui construit des branches de trois semaines vérifie chaque branche isolément ; il ne fait pas d'intégration. Posez les trois questions de Humble.

Une construction rouge que personne ne répare. Quand main est rouge pendant des heures, chacun continue de pousser par-dessus, les échecs s'empilent, et plus personne ne sait lequel a causé quoi. L'équipe finit par ignorer le statut, et la CI n'a plus de valeur. Kent Beck : personne n'a de tâche plus prioritaire que de réparer la construction. Si la correction n'est pas immédiate, git revert.

La demi-intégration. Faire régulièrement git pull origin main dans sa branche longue donne l'impression d'intégrer. Mais l'autre moitié manque : votre travail reste invisible pour les autres, qui découvriront les conflits avec lui le jour de votre fusion.

Les branches « courtes » qui durent. Une demande de fusion ouverte depuis dix jours, en attente de relecture, est une branche longue. La relecture rapide fait partie de l'intégration continue : une équipe qui laisse attendre les revues pousse mécaniquement vers des lots plus gros.

Croire que « pas de conflit » veut dire « ça marche ». La démonstration de cette leçon : seule l'exécution du programme fusionné le prouve.

Sécurité

L'intégration continue a des effets directs sur la sécurité, positifs à condition de les organiser :

  • Les petits changements se relisent vraiment. Une revue de code de 30 lignes repère une injection SQL ou un secret ajouté par erreur ; une revue de 3 000 lignes se termine par « ça a l'air bien ». La petite taille des lots est une mesure de sécurité.
  • Le correctif d'une vulnérabilité doit pouvoir partir le jour même. Quand une faille critique est publiée dans une dépendance, une équipe qui intègre et livre en continu met à jour, teste et déploie dans la journée. Une équipe qui fusionne toutes les deux semaines doit d'abord résoudre deux semaines de conflits.
  • La branche principale devient un actif critique. Puisque tout le monde construit à partir de main, et que la suite du chapitre en tire les images déployées, un changement malveillant qui y entre se propage partout. Protégez-la : pas de poussée directe sans vérification, tests obligatoires avant fusion, revue par une seconde personne. La leçon 5 détaille les risques propres aux pipelines eux-mêmes.
  • Ne versionnez jamais de secret. « Tout mettre dans le dépôt » s'arrête aux mots de passe et aux clés : un secret poussé une fois reste dans l'historique de tous les clones. Les secrets sont fournis au pipeline par d'autres moyens, vus à la leçon 5.

En production

  • À plusieurs dizaines de développeurs, la file des fusions devient un goulet : deux demandes testées séparément contre main peuvent, comme Camille et Dominique, se casser mutuellement une fois fusionnées l'une après l'autre. Les files de fusion (merge queue chez GitHub, merge trains chez GitLab) testent chaque demande par-dessus celles qui la précèdent dans la file, et ne fusionnent que si ce résultat est vert.
  • Quand la suite de tests devient longue, on ne peut plus tout exécuter avant chaque fusion. Google, qui reçoit des dizaines de milliers de changements par jour dans un dépôt unique, n'exécute avant la soumission que les tests rapides et fiables, et réserve les suites longues à une vérification après la soumission, avec des personnes chargées de réparer rapidement la branche commune. La leçon 3 traite cet arbitrage.
  • Mesurez la durée de vie des branches et le temps d'attente des revues : ce sont les indicateurs avancés de la taille des lots. La leçon 6 présente les indicateurs DORA, qui mesurent le résultat.
  • Chez Lyneko, le site que vous lisez suit ce modèle : chaque changement est poussé sur main ou passe par une branche courte, et chaque commit sur main déclenche la construction, la publication de l'image et le déploiement (le pipeline est détaillé à la leçon 4).

Exercices

1. Le test de Humble (niveau 100). Une équipe de six personnes travaille sur des branches d'une semaine, fusionnées le vendredi après une revue. Chaque fusion déclenche une construction complète avec tests sur un serveur Jenkins. Quand la construction échoue, un ticket est créé et traité dans le sprint suivant. Répondez aux trois questions du test. L'équipe pratique-t-elle l'intégration continue ? Que changeriez-vous en premier ?

Solution

Question 1 : non, chacun n'intègre qu'une fois par semaine. Question 2 : oui, chaque commit sur la branche principale déclenche construction et tests. Question 3 : non, une construction rouge reste rouge pendant des jours. L'équipe a un outil de CI, mais ne pratique pas l'intégration continue. Le premier changement, le moins coûteux et le plus utile, est la règle de réparation : une construction rouge est réparée ou annulée (git revert) immédiatement. Le second est de raccourcir les branches, ce qui demande souvent d'accélérer les revues et d'apprendre à masquer le travail en cours.

2. Provoquer et détecter un conflit textuel (niveau 100). Dans votre dépôt integration, faites modifier à Camille et à Dominique la même ligne de app.py (par exemple le message "introuvable", chacun avec un texte différent), chacun sur sa branche. Avant de fusionner, utilisez git merge-tree --write-tree pour savoir si la fusion sera propre. Que renvoie la commande, avec quel code de sortie ?

Solution

La commande sort avec le code 1. Sa sortie ressemble à ceci (les empreintes diffèrent chez vous) :

$ git merge-tree --write-tree msg-a msg-b
22c1f154e4ba583bc812f9498e78dd1db33a37ec
100644 013bcc41a5b4a532b03407caecd71371d8d92981 1	app.py
100644 09ffbf0c228231bd233ce0175ba019c294f59730 2	app.py
100644 c3f2f3ba1fe9ed77f5be347d91422325c31e8ff8 3	app.py

Auto-merging app.py
CONFLICT (content): Merge conflict in app.py
$ echo $?
1

La première ligne est l'arbre fusionné, qui contient les marqueurs de conflit. Viennent ensuite les trois versions du fichier en conflit, numérotées comme dans l'index de Git : 1 pour l'ancêtre commun, 2 pour la première branche, 3 pour la seconde. Enfin, les messages de fusion. Contrairement au conflit sémantique de la leçon, celui-ci est signalé par Git avant toute fusion : il est gênant, mais visible.

3. Concevoir la règle d'équipe (niveau 100). Rédigez en cinq lignes au maximum la règle de travail sur les branches que vous proposeriez à une équipe de quatre personnes qui livre une API utilisée par des clients. Justifiez chaque ligne par un des problèmes vus dans la leçon.

Solution

Une proposition parmi d'autres : (1) une branche par changement, fusionnée dans main le jour même ou le lendemain au plus tard, pour garder des lots petits ; (2) avant fusion, les tests s'exécutent sur le résultat de la fusion avec main, pour attraper les conflits sémantiques ; (3) une revue par une autre personne, demandée et rendue dans la demi-journée, pour que la revue ne devienne pas un goulet ; (4) un travail de plusieurs jours est fusionné désactivé derrière un drapeau, pour ne pas créer de branche longue ; (5) main rouge : on répare ou on annule dans les dix minutes, pour que tout le monde puisse continuer à partir d'une base saine.

Récapitulatif

  • L'intégration continue est une pratique : chacun intègre dans la branche principale au moins une fois par jour, chaque commit déclenche construction et tests automatiques, une construction rouge est réparée ou annulée en quelques minutes.
  • Elle repose sur les petits lots : un petit changement se relit, se teste, se comprend et s'annule facilement.
  • Git détecte les conflits textuels, jamais les conflits sémantiques : seule l'exécution des tests sur le résultat de la fusion les révèle.
  • Les branches courtes (quelques heures à un jour) et le développement sur le tronc sont compatibles avec l'intégration continue ; les branches longues ne le sont pas. Le travail long se fusionne masqué (drapeaux, abstraction).
  • Les tests s'exécutent dans un environnement propre, qui ne contient que ce qui est versionné.
  • L'intégration continue est le socle de la livraison continue et du déploiement continu, présentés à la leçon 4.

Pour aller plus loin

  • L'article Continuous Integration de Martin Fowler, dans sa version réécrite : il détaille les onze pratiques et discute longuement les branches de fonctionnalité.
  • Le site Trunk Based Development de Paul Hammant, pour les techniques de masquage du travail en cours et les variantes du modèle.
  • Le chapitre 23 de Software Engineering at Google, pour voir les mêmes principes appliqués à l'échelle d'un dépôt unique de plusieurs milliards de lignes.
  • La leçon suivante, où vous construisez le système qui exécute automatiquement ces tests à chaque poussée.
Voir ma constellation →

Sources