Tout bon projet nécessite la mise en place d’une stratégie de versioning. Vous vous êtes probablement déjà posé les questions « Quel format utiliser ? », « Où écrire le numéro de version ? », « Quel numéro modifier et quand ? ». Cet article vous apportera une solution, validée par l’expérience que nous avons acquise à Odeven à travers nos différents projets.
A propos du numéro de version
Pourquoi versionner ?
L’intérêt est multiple :
- Définir une roadmap claire
- Savoir quel code est associé à une instance (lorsqu’on rencontre un bug par exemple)
- Vérifier simplement que les dernières fonctionnalités/correctifs ont bien été installés
- Communiquer avec les autres développeurs
Bref, il y a suffisamment de raisons qui justifient la mise en place d’un système de version.
Quel format utiliser ?
Il existe une convention en développement informatique basée sur une gestion sémantique. Celle-ci ressemble à ça :
{Major}.{Minor}.{Patch}
Voici quelques exemples :
- 10.24.10245
- 1.0.0
- 38.0.265
On parle de gestion « sémantique », car chaque numéro à une signification bien précise :
- Major : évolue lorsqu’il y a des changement non rétrocompatibles
- Minor : évolue lorsqu’il y a des nouvelles fonctionnalités rétrocompatibles
- Patch : évolue lorsqu’il y a des correctifs rétrocompatibles
Quel que soit votre type de projet (application personnelle, vendu en SaaS ou non, bibliothèque, etc…) ce format a fait ses preuves et a ses avantages :
- La simple comparaison des numéros permet de connaître l’étendue de la mise-à-jour
- Un changement dans le numéro « Patch » permet de savoir que la mise-à-jour est très peu risquée (en théorie, elle devrait régler plus de soucis qu’elle n’en crée)
- Un changement dans le numéro « Minor » permet de savoir qu’il y a de nouvelles fonctionnalités, donc potentiellement un risque, mais limité par une « garantie » de la part du développeur concernant la rétrocompatibilité
- Un changement dans le numéro « Major » permet de savoir qu’il y a un risque plus ou moins élevé, lié à la rétrocompatibilité.
Dans ce contexte, une rupture de rétrocompatibilité signifie qu’une simple mise-à-jour du projet ne suffit pas à s’assurer de son bon fonctionnement. Il y a généralement d’autres choses à faire avant. Exemples :
- Mettre à jour des dépendances systèmes
- Mettre à jour un autre service (ex: serveur de base de données)
- Modifier le code pour ne plus utiliser de fonctions/APIs devenues obsolètes
- Intervenir manuellement pour renseigner des données manquantes qui deviendront obligatoires
- Etc…
Bref, cela signifie que si les changements de numéros de version sont parfaitement respectés, vous pouvez dans les cas suivants :
- Major : Mettre à jour uniquement après avoir suivi une éventuelle procédure de migration
- Minor : Mettre à jour pour profiter des nouvelles fonctionnalités sans se soucier d’éventuels problèmes
- Patch : Mettre à jour systématiquement pour toujours avoir les derniers correctifs (surtout pour des patches de sécurité)
Pour plus d’information, je vous invite à consulter cette documentation : https://semver.org/lang/fr/
Où stocker ce numéro de version ?
Vous utilisez très probablement un gestionnaire de dépendances pour gérer les différentes bibliothèques que vous utilisez : Composer, Npm, Yarn, Maven, Gradle, etc…
Et bien sachez que ces gestionnaires de dépendances intègrent ce qu’il faut, à savoir un simple attribut « version » dans leur fichier de configuration (ex: « composer.json », « package.json », etc…).
Vous pourrez ainsi y stocker votre numéro de version. Vous trouverez également des bibliothèques/scripts vous permettant de récupérer ce numéro de version pour pouvoir l’afficher en pied de page, dans les headers, etc…
Quand générer un numéro de version ?
Vous devriez déjà être en mesure de répondre à cette question pour la plupart des cas. Mais au démarrage d’un projet, cette question peut paraître plus complexe. La première « vraie » version que vous allez délivrer devrait correspondre au numéro « 1.0.0 ». Il s’agira ici de la version intégrant toutes les fonctionnalités que vous souhaitez intégrer dans votre toute première release/mise en production.
Si vous souhaitez mettre une version intermédiaire à disposition d’éventuels testeurs, vous pouvez simplement utiliser le numéro de version « 0.X.Y » (où X et Y correspondent respectivement à un numéro Minor et Patch). Cela peut être utile également si vous travaillez parallèlement à d’autres équipes, qui auraient un intérêt à utiliser votre application avant que celle-ci ne soit officiellement « lancée ».
Dans ce cas, les personnes qui exploiteront ces versions intermédiaires devront bien être averties que des changements fréquents peuvent intervenir, et qu’il n’y a aucune garantie de rétrocompatibilité.
Roadmap conseillée
L’idéal en versioning, lorsque d’autres projets sont dépendants de votre solution, c’est de toujours laisser une période d’adaptation.
Ne raisonnez pas en vous disant « Là j’ai ajouté des grosses fonctionnalités, ça mérite une version Major », mais plutôt en respectant le principe de rétrocompatibilité. C’est-à-dire que pendant un temps plus ou moins long, vous allez devoir jouer avec des « deprecated ». Vous allez annoter vos méthodes, vos endpoints, la documentation, etc… de manière à laisser le temps aux développeurs d’arrêter d’utiliser du code qui sera prochainement supprimé. Bien sûr, si vous rendez obsolète une fonction, il faudra mettre à disposition au même moment une alternative (à moins de ne plus vouloir prendre en charge la fonctionnalité).
Pour une roadmap claire et simple, vous pouvez proposer la procédure suivante avant de passer à la prochaine version Major :
- Mettre à jour vers la dernière version Minor
- Remplacer tous les appels obsolètes (utilisation de méthodes/endpoint/etc… annotées comme étant « deprecated »)
- Consulter l’éventuel guide de migration s’il y a des étapes supplémentaires spécifiques à réaliser
Puis, pendant un certain temps, vous continuerez à maintenir la précédente version Major, en incluant des patches (afin de résoudre les bugs, failles de sécurités, etc…). De cette manière, les autres développeurs auront le temps d’adapter leur code, et pourront bénéficier des patches en attendant d’avoir terminé.
Comment générer un numéro de version ?
Vous utilisez très probablement un outil de gestion de version, tel que Git. Si ce n’est pas le cas, vous devriez très sérieusement le faire, même si vous travaillez seul, car les intérêts sont nombreux, et pas seulement pour le travail collaboratif.
Avec Git, il existe un système de « tag ». Le but étant de pouvoir ajouter un libellé unique, associé à un commit précis. Ces tags peuvent avoir différentes utilités, mais dans notre cas, c’est pour définir des numéros de version que nous allons les utiliser.
Mais comment faire concrètement ? Je vous propose un processus relativement simple :
- Vous effectuez le(s) commit(s) correspondant aux modifications que vous souhaitez apporter à votre projet
- Arrive le moment où vous souhaitez générer une nouvelle version : créez un commit, dans lequel vous n’effectuerez qu’une seule et unique modification, visant à modifier le(s) fichier(s) contenant le numéro de version actuel, afin de le remplacer par le nouveau numéro de version (je précise qu’il peut y avoir plusieurs fichiers, dans le cas par exemple du développement mobile, où l’on pourrait avoir un numéro pour Android, un autre pour iOS, et même encore un autre pour une éventuelle version Web)
- Effectuez votre commit, que vous nommerez simplement en précisant le nouveau numéro (ex: «
chore(release): X.Y.Z
« ) - Taguez votre commit «
vX.Y.Z
» (ex: «v1.2.3
« ) - Pushez votre commit, ainsi que le tag
Quel « modèle de branche » utiliser ?
« master », « main », « develop », etc… Vous avez dû en entendre des noms de branches. A quoi sert la branche « master » / « main » ? Pourquoi en créer d’autres ?
Il existe de nombreuses méthodes organisationnelles concernant le nombre et le nom des branches que l’on utilise. Utiliser plusieurs branches a certains avantages :
- Pouvoir faire évoluer chaque branche à son propre rythme (par exemple, une branche « develop » intègrerait les dernières fonctionnalités, tandis que la branche « master » / « main » ne les intègrerait qu’une fois prêtes et parfaitement testées)
- Pouvoir gérer plusieurs versions en parallèle (très utile dans le cadre d’un package par exemple, pour lequel il faudrait maintenir plusieurs versions Major en parallèle, en corrigeant les failles de sécurité/bugs/etc…
- Pouvoir développer tranquillement, et sans risque, avant d’intégrer les modifications sur la branche principale
A Odeven, nous employons un modèle relativement simple, composé d’une branche principale « develop », et de 4 types de branches :
- develop : Il s’agit de la branche principale, dans laquelle les modifications sont intégrées au fur-et-à-mesure
- feature : Il peut y avoir plusieurs « feature branches » en parallèle. Elles sont éphémères et visent à effectuer des développements correspondant à une tâche précise (généralement un ticket), avant de l’intégrer dans la branche « develop »
- release : Il peut y avoir plusieurs « release branches » en parallèle. Chaque branche de type « release » évolue indépendamment des autres à partir du moment où elle est créée. Le but est de pouvoir la faire évoluer en incluant des patches uniquements
- hotfix : Il peut y avoir plusieurs « hotfix branches » en parallèle. Elles sont identiques aux « feature branches », mis à part qu’elles consistent à effectuer un correctif présent « en production » (dans une/des releases)
- junk : Il s’agit de branches temporaires, visant à tester la CI, ou autre, mais dont la finalité n’est pas d’être intégrées dans la branche « develop »
La branche « develop » est donc unique. C’est la branche principale. Dans ce modèle, vous remarquez probablement qu’il n’y a pas de branche « master » ou « main ». En effet, les branches de type « release » la remplace. Mais au lieu de n’avoir qu’une seule branche « master » / « main », on crée ici autant de branche qu’il n’existe de version Major ou Minor, afin de pouvoir en maintenir potentiellement plusieurs en parallèle.
Pourquoi ? Sur un modèle de versioning comme celui-ci, on essai d’avoir des versions Minor et Major ne présentant aucun bug, et aucune faille de sécurité (dans l’idéal). L’idée n’est pas de maintenir éternellement chaque release, mais plutôt d’avoir cette possibilité. Dans un projet de type « package », ou une application proposant une API, cela est quasiment indispensable.
Voici la manière dont nous nommons nos branches à Odeven (libre à vous d’adapter suivant vos préférences) :
- develop : Cette branche est unique, et ne change pas de nom dans le temps
- feature/{ISSUE}/{TITLE} : (ex: « feature/54/user-authentication »)
- Avec « {ISSUE} » : le numéro du ticket/fonctionnalité (optionnel)
- Avec « {TITLE} » : un titre court et explicite
- release/{X}.{Y} : (ex: « release/1.0 »)
- Avec « X » : le numéro de version MAJOR
- Avec « Y » : le numéro de version MINOR
- hotfix/{ISSUE}/{TITLE} : (ex: « hotfix/104/fix-authentication »)
- Avec « {ISSUE} » : le numéro du ticket/fonctionnalité (optionnel)
- Avec « {TITLE} » : un titre court et explicite
- junk/{TITLE} : (ex: « junk/test-ci »)
- Avec « {TITLE} » un titre court et explicite
Que se passe-t-il lorsqu’on développe une nouvelle fonctionnalité ?
Supposons qu’un développeur souhaite traiter le ticket numéro 54, visant à mettre en place un système d’authentification utilisateur :
- Le développeur crée une nouvelle branche à partir de la branche « develop », et la nomme « feature/54/user-authentication«
- Le développeur effectue les modifications demandées dans le ticket numéro 54
- Le développeur commit et push sur la branche « feature/54/user-authentication«
- Le développeur demande éventuellement à ce que quelqu’un fasse une « Code Review », des tests, etc…
- Si tout est bon, que les éventuels tests sont valides, que les différentes parties prenantes valident les modifications, etc… : les modifications sont intégrées dans la branche « develop » (on passe généralement par une Merge Request/Pull Request)
- Dans le cas où des modifications seraient nécessaires, le développeur travaillerait à nouveau sur sa branche, et reprendrait à partir de l’étape 2
- Lorsque les modifications sont intégrées dans la branche « develop », on peut ensuite supprimer la branche « feature/54/user-authentication«
Que se passe-t-il lorsqu’on effectue un correctif ?
Il y a deux cas distincts à identifier. Dans un cas, le problème à résoudre est présent en production (dans une branche de release donc). Dans l’autre, il n’est présent que sur la branche « develop ».
Cas d’un problème présent en production
Généralement, on part d’un ticket détaillant le problème rencontré (la faille de sécurité, le bug, etc…). Supposons donc qu’un développeur souhaite traiter le ticket numéro 92, visant à corriger le problème empêchant l’authentification :
- Le développeur crée une nouvelle branche à partir de la branche « develop », et la nomme « hotfix/92/broken-authentication«
- Le développeur effectue les modifications demandées dans le ticket numéro 92
- Le développeur commit et push sur la branche « hotfix/92/broken-authentication«
- Le développeur demande éventuellement à ce que quelqu’un fasse une « Code Review », des tests, etc…
- Si tout est bon, que les éventuels tests sont valides, que les différentes parties prenantes valident les modifications, etc… : les modifications sont intégrées dans la branche « develop » (on passe généralement par une Merge Request/Pull Request)
- Dans le cas où des modifications seraient nécessaires, le développeur travaillerait à nouveau sur sa branche, et reprendrait à partir de l’étape 2
- Lorsque les modifications sont intégrées dans la branche « develop », on peut ensuite supprimer la branche « hotfix/92/broken-authentication«
- Important : les modifications ont bien été intégrées dans « develop » (cela est indispensable afin que le problème ne revienne pas dans la prochaine version Minor ou Major), mais il faut également intégrer ces modifications dans les branches de type « release » ayant ce problème, et qui sont encore maintenues. Dans ce cas, le développeur utilise généralement la technique du « cherry-pick », visant à copier des commits dans une autre branche
- S’il n’y a qu’un seul commit qui a été intégré dans la branche « develop », le développeur effectue généralement un cherry-pick de ce commit là dans la branche « release/1.0 » (si il s’agit de la seule et unique branche de release maintenue, et ayant le bug). Il est également possible de créer une nouvelle branche à partir de la branche « release/1.0 », de faire le cherry-pick, puis de soumettre les modifications à travers une Merge Request/Pull Request. Cela peut-être très utile dans le cas où il y aurait d’autres modifications à faire dans la branche de release (s’il y a des conflits par exemple), afin de s’assurer que le patch ne causera pas d’autres problèmes. Dans ce cas, la procédure à suivre serait la même, exceptée que celle-ci serait mergée dans une « release branch », et non dans « develop ». Notez bien qu’il s’agit d’un commit distinct sur chaque « release branches ». Le cherry-pick copie un commit. Mais le commit précédent étant différent, le commit sera différent lui aussi (voir fonctionnement de Git).
Note : dans certains cas, afin de gagner en rapidité pour résoudre un problème très gênant, on effectue le correctif à partir de la branche de release, puis on l’intègre dans un second temps dans la branche « develop ». En général, c’est l’affaire de quelques secondes. Mais si les modifications à faire dans la branche « develop » et dans la/les branche(s) de « release » ne sont pas les mêmes, il peut être pertinent de privilégier la/les release(s).
Cas d’un problème présent uniquement sur « develop »
Dans ce cas là, le problème n’est pas urgent. Le problème n’étant pas présent en production (donc sur les branches de « release »), il est possible de suivre la même procédure que pour le développement d’une nouvelle fonctionnalité.
Autres cas ?
Il existe d’autres cas, moins fréquents, mais possibles, si vous souhaitez par exemple tester le comportement de votre outil d’intégration continue, ou tout simplement si vous savez que les modifications que vous faites n’ont pas pour but d’être intégrées à terme dans la branche « develop ».
Il n’y a pas de procédure particulière pour ce type de cas, si ce n’est de préfixer la branche par « junk », afin d’éviter toute intégration involontaire.
Automatiser la génération de version
Vous venez de voir qu’il n’y avait pas grand chose à faire pour générer une nouvelle version. Néanmoins, pas grand chose signifie quand même qu’on peut faire des erreurs. De plus, ces quelques actions devront être « souvent » réalisées (en tout cas aussi souvent que vous souhaitez générer de versions). Alors pourquoi pas automatiser ce que vous pouvez automatiser ? Cela vous évitera d’éventuelles erreurs, et vous simplifiera la tâche !
Ce que je vous propose, c’est d’utiliser votre système d’intégration continue. A Odeven, nous utilisons GitLab CI/CD, car celui-ci est gratuit, s’intègre parfaitement avec le code source, et est bien assez puissant pour l’usage que nous avons. Néanmoins, vous pouvez utiliser d’autres outils, le principe reste le même.
Sur GitLab CI/CD, nous avons un système de Pipeline. Chaque fois qu’un développeur push des commits, il est possible de faire en sorte qu’une liste de jobs soit déclenchée, ou simplement qu’ils soient déclenchables manuellement.
Vous pouvez ainsi définir un job, nommé par exemple « release:major », un autre « release:minor », et enfin un « release:patch », visant à effectuer les tâches précédentes. Ce qui est intéressant avec GitLab CI/CD, c’est qu’il est possible de préciser les conditions permettant de déclencher un job ou non. Ainsi, on peut faire en sorte que les jobs « release:major » et « release:minor » soient uniquement présents sur la branche « develop », et le job « release:patch » uniquement présent sur une branche de « release ».
En effet, lorsque vous souhaitez générer une version Minor ou Major, cela signifie qu’il y a de nouvelles fonctionnalités en jeu. Vous devriez donc générer cette version depuis votre branche principale. Par contre, dans le cas d’éventuels patches, ceux-ci devraient être générés depuis une branche de release, afin de ne pas embarquer de nouvelles fonctionnalités ou de modifications entraînant des ruptures de rétrocompatibilité.
Le script déclenché devra réalisé les opérations suivantes :
- Lire le numéro de version actuel présent sur la branche
- Incrémenter le numéro de version, suivant le type de version (Major, Minor, Patch)
- Modifier le numéro de version dans le(s) fichier(s) correspondant (ex: « package.json », « composer.json », etc…)
- Effectuer un commit (intitulé par exemple « chore(release): 1.2.3 ») et embarquant les modifications
- Tagguer le commit avec le nouveau numéro de version
- Pusher le commit et le tag
Si vous utilisez GitLab, ou une solution équivalente, n’hésitez pas à créer des templates, surtout si vous gérez plusieurs projets. A titre d’exemples, voici quelques templates que nous avons créés à Odeven :
- Microservice Symfony
- Bundle Symfony
- Vue
- React
- Expo
L’avantage d’avoir un template par type de projet, c’est qu’on peut par la suite configurer l’intégration continue d’un projet en un instant. Vous pourrez ainsi ajouter toutes les étapes qui vous semblent pertinentes (tests, build, deploy, etc…).
Avec le versioning, un autre job pertinent est celui du build. S’il s’agit d’un package, vous pouvez le générer à ce moment là. Si votre application est « dockerisée » également. Ainsi, le build pourrait être déclenché automatiquement et uniquement lorsqu’un tag est pushé, de manière à avoir à disposition une version « toute prête » à chaque fois qu’un nouveau numéro de version est généré.
Conclusion
Il existe une infinité de projets différents, mais chacun gagnerait à employer un système de version clair et précis. Ce modèle est largement adopté par la communauté. Il a fait ses preuves, et apporte beaucoup, avec peu d’efforts. Il faut certes un court temps d’adaptation, mais le jeu en vaut la chandelle. Certains développeurs adaptent ce système à leur besoin, en ajoutant un éventuel suffixe par exemple, mais le simple fait de garder ces trois numéros est magique, et permet d’évaluer le risque, le travail nécessaire, et les délais en jeu.