Gestion de version distribuée et build incassable

Dans un précédent article, nous avons introduit les concepts qui accompagnent la gestion de versions distribuée afin de comprendre son fonctionnement de base. À l’aide de ces quelques concepts, nous allons voir comment il est possible de mettre en place un build d’Intégration Continue « incassable » sans effort (ie. sans développement d’une infrastructure dédiée : avec un gestionnaire de versions non distribué celà reste possible avec un peu de développement ou avec encore avec la solution TeamCity de JetBrains) grâce à la flexibilité de ce type d’outils. Git continuera à nous servir d’exemple mais cette fois-ci, les détails d’implémentation (en comparaison avec Mercurial ou Bazaar) auront leur importance dans la mise en place de la solution.

Mais avant de présenter la solution, revenons sur le principe du build incassable. Le build incassable fait référence au build automatique qui s’exécute sur un serveur d’Intégration Continue. Ce build consiste à compiler, déployer et tester une application en cours de développement et est exécuté régulièrement en surveillant les modifications dans le gestionnaire de versions.

Aujourd’hui lorsqu’on utilise un gestionnaire de versions centralisé, le développeur partage ses modifications de l’application (commit avec Subversion) en les envoyant sur le dépôt central utilisé par tous les développeurs et le système d’Intégration Continue.
Si le build échoue, l’Intégration Continue notifie l’ensemble des développeurs qui ont partagé leurs modifications que l’état des sources dans le gestionnaire de versions n’est pas stable. Ceci permet de savoir rapidement qu’il y a quelque chose à corriger dans l’application avant de démarrer une autre fonctionnalité. Plus le problème est découvert tôt, moins il prendra de temps à corriger (Principe exposé par Martin Fowler dans son article l’intégration continue). Cette utilisation de l’Intégration Continue apporte beaucoup à l’amélioration de la qualité des applications mais il reste encore quelques faiblesses à cette procédure.

En effet, entre le moment où un développeur partage ses modifications et le moment où le build échoue, il peux se passer un certain temps (temps de détection des modifications + temps du build qui peut être plus ou moins long) pendant lequel les autres développeurs peuvent mettre à jour leur copie de travail et ainsi la rendre instable (problème de compilation, tests en échec, …).
La deuxième faiblesse est qu’une fois que l’Intégration Continue a notifié les développeurs que la version de l’application dans le gestionnaire de sources est invalide, ils ne peuvent plus envoyer leurs modifications (il est en général recommandé de mettre à jour sa copie de travail en résolvant les conflits de merge avant d’envoyer ses modifications) sous peine de récupérer la version instable et ceci tant que le build n’est pas corrigé. Cela les conduit soit à créer une version plus volumineuse qui contiendra plusieurs fonctionnalités, soit à mettre à jour leur copie de travail malgré les erreurs de build.

Malgré les apports indéniables de l’Intégration Continue, celle-ci reste bridée par le fait que les versions non testés des développeurs et la version testé et considérée stable par l’Intégration Continue sont mélangées dans une seule et même branche du gestionnaire de versions. Le build incassable n’est donc pas un build qui n’échoue jamais mais un build qui assure que toutes les versions de l’application disponibles dans le gestionnaire de sources sont saines.

Principe de la solution

Le coeur de la solution de build incassable que nous allons voir permet de séparer les versions partagées par les développeurs de la version stable depuis laquelle se font les mises à jour des copies de travail des développeurs.

Comme expliqué dans le premier article, avec un gestionnaire de versions décentralisé, il est possible d’imaginer toute sorte de topologie des dépôts. Nous utiliserons donc un dépôt centralisé en plus du dépôt par développeur. Ce dépôt centralisé aura de particulier de posséder une branche par développeur. Ces derniers enverront leurs modifications (push avec Git) sur leur branche personnelle sur le dépôt central (cf étape 1 du schéma). Si un développeur casse le build avec ses modifications, il ne cassera que sa branche dans le dépôt central.

Il reste encore à positionner l’Intégration Continue dans ce système, et puis c’est bien beau d’avoir sa branche personnelle, mais où sont consolidées les modifications des développeurs ? ie. où est la version de l’application qui contient l’ensemble des modifications des développeurs ?
À notre dépôt central, nous ajouterons donc une branche supplémentaire qui contiendra la consolidation de toutes les branches des développeurs. C’est l’Intégration Continue qui se chargera de merger les branches de chaque développeur vers cette branche de référence.
Gestion de version distribuée et intégration continue
Les modifications de la branche de référence ne seront renvoyées par l’Intégration Continue (étape 5) que si le merge de la branche en cours de build sur la branche de référence (étape 3) et le build du résultat de ce merge (étape 4) sont validés par l’Intégration Continue.
C’est cette branche de référence sur le dépôt central qui sera la source de mise à jour de tous les développeurs (étape 7).
C’est la possibilité de partager ses modifications sur une branche et de mettre à jour sa copie de travail depuis une autre qui est particulier aux gestionnaires de version distribués.

Voyons comment se comporte ce système dans les deux situations décrites comme des limites précédemment :

  • Un développeur partage des modifications qui vont faire échouer le build. Comme il les partage sur sa branche personnelle sur le dépôt central, aucun des autres développeurs n’est impacté. Ils vont en effet mettre à jour leur dépôt (et copie de travail) depuis la branche de référence qui n’est pas modifiée tant que le build du développeur n’est pas stable.
  • Les autres développeurs peuvent partager leurs modifications car un build qui échoue sur une branche d’un développeur n’empêche pas d’effectuer un build des autres branches et de continuer à faire évoluer la branche de référence.

Une source supplémentaire d’échec du build apparait cependant. En effet, si un développeur partage ses modifications avant d’avoir mis à jour sa copie de travail (ou si un autre build est en cours), le build de sa branche peut échouer à cause de conflits qui apparaissent lors du merge dans la branche de référence (étape 3). Dans ce cas, il faudra au développeur mettre à jour sa copie de travail depuis la branche de référence, résoudre les conflits et renvoyer ses modifications. Le reste de l’équipe n’est pas impacté par les erreurs de merge, de la même manière qu’il n’est pas impacté par les erreurs de build.

Un dernier avantage, qui n’est pas si négligeable qu’il n’y parait, est que lorsqu’un build échoue, l’Intégration Continue « connait » de façon certaine le développeur incriminé : celui à qui appartient la branche à partir de laquelle le build a été initié. Elle peut alors notifier uniquement ce développeur et non pas tous les développeurs ayant potentiellement cassé le build comme c’est le cas aujourd’hui (ie. tous les développeurs qui ont partagé des modifications depuis le dernier build en succès). C’est un avantage non négligeable car une des raisons pour lesquelles l’Intégration Continue n’est pas très suivie est qu’elle peut générer une quantité importante de notifications pas toujours bien ciblées qui finissent par être ignorées.

Mise en oeuvre

Pour mettre en place cette solution nous allons utiliser le serveur d’Intégration Continue Hudson et son plugin Git.

Commençons par initialiser un projet Git, qui nous servira de dépôt central :

$ mkdir unbreakable_project
$ cd unbreakable_project
$ git init
$ cat build.sh
#!/bin/sh
echo "Build successful"

$ chmod +x build.sh
$ git add build.sh
$ git commit -a -m "ajout du script de build du projet"

Nous utiliserons un simple script shell que nous feront échouer pour simuler un build cassé.

Nous allons maintenant créer un nouveau job Hudson pour qu’il construise automatiquement notre merveilleux projet. Un job « free-style » suffira à faire exécuter notre script shell de build.

Si le plugin Git a bien été installé, celui-ci devrait être sélectionnable dans la section « Gestion de code source ».

  • Remplir le champs de l’URL du dépôt avec le chemin qui convient (http://…, ssh://…, file:///…),
  • Laisser le champs Branch vide, de cette façon le plugin Git pour Hudson surveillera toutes les branches (ie. chacune des branches de nos développeurs),
  • Dans « Avancé », sélectionner l’option merge before build et indiquer master (branche par défaut de Git et notre branche de référence) dans le champs Branch to merge to,
  • Et enfin, dans la section Post-build Actions, sélectionner l’option Push Git tags back to origin repository.

Et voilà, Hudson et le plugin Git fonctionneront comme décrit précédemment.

La dernière étape consiste à configurer l’environnement des développeurs. En effet, il faut configurer Git pour lui indiquer sur quelle branche aller chercher les mises à jour et sur quelle autre les envoyer.

Mettons-nous dans la peau de Bob :

$ export EMAIL="Bob "
$ git clone ssh://[email protected]/projects/unbreakable_project
$ cd unbreakable_project

Ici nous configurons le dépôt de Bob pour que Git pousse les modifications de la branche master du dépôt local vers la branche bob sur le dépôt distant :

$ git config remote.origin.push master:refs/heads/bob

Bob peut maintenant exécuter git pull qui ira chercher les mises à jour dans la branche master du dépôt central (comportement par défaut lors de l’utilisation de git clone et git push qui enverra ses modifications sur la branche bob du même dépôt.

Si Alice fait échouer le build (ou le merge en provoquant un conflit), Hudson marquera le build en échec mais la branche master du dépôt central restera saine.

$ export EMAIL="Alice "
$ git clone ssh://[email protected]/projects/unbreakable_project
$ cd unbreakable_project
$ git config remote.origin.push master:refs/heads/alice
... Alice modifie le script de build
$ cat build.sh
#!/bin/sh
echo "Build successful"
false

$ git commit -a -m "casse le build"
$ git push

Erreur de build
L’étape 4 de build échoue, Hudson envoie alors une notification de cet échec à Alice et ne renvoie pas le merge de la branche alice sur master vers le dépôt central.

Bob n’est alors pas perturbé par cette modification lorsqu’il essaye de mettre son dépôt à jour :

$ git pull
...
Already up-to-date.

Alice ne pourra pas intégrer ses modifications à la version de référence tant qu’elle n’aura pas corrigé sa version. Elle pourra cependant continuer de la mettre à jour depuis le dépôt central ou même aller chercher des modifications directement dans le dépôt de Bob (s’il a un quelconque moyen d’y accéder).

Nous avons donc vu qu’un gestionnaire de versions distribué tel que Git nous permet de pousser encore plus loin le principe d’Intégration Continue sans faire le moindre développement. Vivement que les plugins des gestionnaires de versions distribués pour nos IDE favoris arrivent à maturité pour pouvoir démocratiser ce genre de solution !

12 commentaires sur “Gestion de version distribuée et build incassable”

  • Excellent article David, merci. Je me demande par contre jusqu'où il faut aller, et si la complexification du process d'IC dans le but d'offrir un zero-break est vraiment pertinent. Est-ce réellement un but sacro-saint ? Je joue un peu l'avocat du diable ici, mais comment comparer la solution décrite ici, avec une solution plus simple (citée dans l'article) offrant déjà un bon compromis : TeamCity w/ personal builds ? Il pourrait y avoir plusieurs avantages, si on considère que le choix du serveur est une option : - Possibilité d'utiliser un VCS non distribué (évoqué dans l'article) - Possibilité de laisser l'équipe décider qui résoudra quoi (take responsability/give up) - Du point de vue monitoring : prise en compte et historisation des échecs - Parfois, on peut vouloir casser la build. C'est très discutable, mais dans la pratique il m'arrive de créer les tests unitaires qui permettent de reproduire un problème que j'ai trouvé. Je peux décider de les commiter tels quels si je ne suis pas celui qui se chargera de la résolution du problème, et à ce moment je veux qu'ils provoquent un break. Cela dit, cette configuration est à essayer. As-tu déjà un retour d'expérience à son sujet ? Est-ce que la souplesse est suffisante pour que tout le monde s'en accommode ?
  • Je ne pense pas que ce soit un but sacro-saint :) Aujourd'hui l'IC nous aide déjà beaucoup et on arrive à faire avec les quelques faiblesses que j'expose dans l'article mais des solutions apparaissent et c'est tant mieux. Pour ce qui est de la comparaison avec TeamCity (que je n'ai pas personnellement expérimenté) : - la possibilité d'utiliser un VCS non distribué est effectivement le point fort. Je ne pense qu'il faille utiliser un DVCS juste pour faire un build incassable. Par contre, si pour un projet, utiliser Git ou un autre DVCS est envisageable, cette solution de build incassable peut aider à faire pencher la balance. - Pour le monitoring, ce n'est pas parce que Hudson ne renvoi pas les build qui échouent dans la branche master du dépôt central qu'il n'historise pas. D'ailleurs la dernière option "Push Git tags back to origin repository" permet de tagger les sources à chaque build. Ces tags seront renvoyer sur le dépôt central quelque soit l'état du build (succès ou échec). - La possibilité de laisser décider qui résoudra et créer un test qui casse le build : c'est dans cette situation que l'aspect distribué de Git prend tout son sens. Pas besoin de commiter sur le dépôt central (donc on casse toujours pas le build) mais on envoi ce test directement dans le dépôt du développeur qui se chargera d'écrire la correction (on fait du P2P avec les sources du projet :) ). Je n'ai malheureusement pas encore d'expérience en situation réelle de cette solution :( mais il pourrait être envisageable de profiter d'un projet en cours de prototypage pour la tester.
  • Excellent article ! Très bien expliqué ! Ce sujet est peu connu (et peu reconnu) et pourtant très intéressant ! L'utilisation de git force la maitrise d'une nouvelle façon de faire qui n'est pas triviale (eg: git config remote.origin.push master:refs/heads/alice). Les plugins intégrés dans les IDE se doivent d'aider un maximum les développeurs pour faciliter l'utilisation de ce nouvel outil (Bob push => Alice). Mais une fois ce problème technique réglé, c'est tout un système qui devient plus souple ! Des outils comme SVN ne risquent t'ils pas de migrer vers ce type d'architecture (en option) ? L'administration de ce type d'outillage n'est pas extrêmement plus lourde qu'un simple SVN ? (un repo / développeur etc...)
  • Effectivement Subversion tente d'une certaine manière de suivre ce que font les systèmes distribués sans pour autant le devenir (distribué). La version 1.5 de Subversion améliore grandement la gestion des branches grâce au merge tracking (même si ça n'est pas encore du niveau de flexibilité des branches d'un gestionnaire de versions distribué, selon moi :) ). Il y a aussi les changelists qui permettraient de travailler sur plusieurs fonctionnalités/bugs en parallèle (mais reste néanmoins très limité par rapport à une branche locale). Pour ce qui est de l'administration, tout dépend de ce que l'on fait avec ces dépôts. Dans le cas d'une utilisation "à la Subversion", le dépôt local peut être complètement transparent. Si l'on souhaite profiter des branches locales, il va falloir suivre ce que l'on fait sur ces branches pour ne pas tout mélanger et ne pas oublier de partager des patchs. Je pense donc que si on veux profiter un maximum de cet outils, il faut aussi être un peu plus rigoureux.
  • Tout d'abord merci pour cet article très intéressant. Nous songions justement depuis peu à mettre en place ce genre de structure sur notre projet. Mais à la suite de la lecture de votre article, quelques interrogations se sont posées au sein de notre équipe : - La gestion 1 développeur / 1 branche est-elle réellement envisageable sur un gros projet (une quarantaine de développeurs) ? - Nos développeurs ont déjà du mal avec l'intégration continue actuelle (beaucoup de "rétention" de commit), comment la transition s'est-elle passée sur vos projets ? - N'avez-vous pas rencontré de résistance par rapport aux équipes de dév ? Ne craignez vous pas que le build incassable soit percu comme du flicage par les développeurs ? En bref, comment accompagnez-vous ce changement d'un point de vue humain sur vos projets ? Merci
  • - La gestion une branche par développeur est tout à fait envisageable avec un outil comme Git. Il est fait pour gérer beaucoup de branches. Il utilise plusieurs technique pour minimiser à la fois l'espace disque utiliser le la vitesse d'exécution des commandes sur son dépôt (il est réputé pour ça). - Le but de l'intégration continue n'est pas désigner un coupable mais de corriger au plus tôt les erreurs dans le code. C'est un outil pour aider le développeur à être plus confiant envers le code qu'il fournit (ex: les développeurs qui ne pensent pas toujours, ou qui ne prennent pas le temps de passer l'ensemble des tests de l'application pourront s appuyer sur l'intégration continue pour effectuer ces test à leur place). Plus spécifiquement, le build incassable permet au développeur d'effectuer des commits plus régulier sans craindre de bloquer son équipe. Selon moi, il faut faire comprendre aux développeurs que l'intégration continue est d'abord un outil fait pour les aider (leur objectif est de découvrir et corriger le plus de bugs possible, pas d'en cacher le plus possible) plutôt que pour aider le management à les surveiller.
  • Je viens de suivre avec beaucoup de plaisir ces explications qui redonnent envie d'utiliser hudson que j'avais jusqu'alors délaissé. Je me pose toutefois une question quant à la bonne marche à suivre pour cette méthodologie. Je comprends bien le rôle d'Alice et Bob ainsi que leurs branches respectives qui sont construites et testées par hudson. Toutefois, dans le cas présenté, Bob a poussé une modification "correcte" et validée par hudson. Serait-il possible ensuite de demander à hudson de pousser ces modification sur le master automatiquement ? Je n'arrive pas, d'une part, à trouver comment faire cela automatiquement à la fin du build. D'autre part, je me demande si cette automatisation est pertinente. Merci pour vos réponses.
  • En fait, d'après la configuration décrite dans l'article, les modifications sont déjà intégrées dans la branche master avec l'option d'Hudson "merge before build". Je pense qu'elle est intéressante pour simplifier la vie d'Alice et Bob qui n'ont qu'a faire un push et pull comme ils faisaient avant sans cette configuration de build incassable.
  • Nous utilisons subversion comme gestionnaire de version. La solution décrite ci-dessus me parait très bonne, mais je ne vois pas très bien comment la paramétrer avec svn. L'option merge before build est-elle spécifique à git? Doit-on passer par l'utilisation de scripts svn pour ce type de solution?
  • La solution décrite dans cet article est effectivement spécifique à Git (et au plugin Git de Jenkins/Hudson) et ne s'applique pas à SVN. C'est la capacité de Git à pousser des modifications dans une branche différente (la branche par utilisateur) de celle d'où ont été tirées les mises à jour (master) qui permet d'appliquer cette solution.
  • Article super interessant. J'aime bien l'idée de sandboxer les devs et de propager les modifs uniquement si le build passe coté CI. De plus avec cette approche, la CI (re)devient utile. Typiquement, le passage des tests en général fastidieux et parfois couteux sur une bécane de développeur peut être passé sur des machines "dédiées", en général plus véloces. Moins de stress car moins de risque de "casser le build", surtout sur des gros projets ou plusieurs dizaines de développeurs collaborent. Cette solution parait très prometteuse. Je serais très intéressé par un retour d'expérience (l'article date un peu). Quels sont les collatéraux, pitfalls, pièges à c..s de cette solution ? Merci, BQ.
  • La solution a été mise en place à plusieurs reprises et elle tient globalement ses promesses. Les points d'attention sont : - il reste possible de pousser des modifications sur la branche master et donc de casser le build, - la solution augmente le temps de diffusion des modifications. S'il s'agit de pousser des modifications pour les partager rapidement avec un binôme, elle peut être un frein. Pour contourner ce problème, il est possible d'utiliser l'aspect distribué de Git et d'aller faire des 'git pull' directement sur le dépôt du binôme. - les modifications listées lors d'un build par le plugin Git ne sont pas les modifications par rapport à la branche master (seulement les modifications d'Alice par exemple) mais par rapport à la branche utilisateur (d'Alice) qui intègre donc les modifications des autres utilisateurs (de Bob donc) récupérées lors du dernier pull (en faisant un 'git pull', Alice récupère les modifications de Bob et les pousse sur sa branche lors du 'git push'). Les notifications ne sont donc pas aussi précises qu'espéré.
    1. Laisser un commentaire

      Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *


      Ce formulaire est protégé par Google Recaptcha