Automatisez Vos Montées de Versions Grâce à Semantic Release

Automatisez Vos Montées de Versions Grâce à Semantic Release

·

13 min read

Votre application utilise la convention SemVer pour ses numéros de versions (X.Y.Z) ? Si oui, j'ai une bonne nouvelle pour vous : il est possible d'automatiser les étapes lancées lors de vos montées de versions.

Si vous n'utilisez pas SemVer, tant mieux ! Dans cet article, vous découvrirez une convention claire et simple à mettre en place pour versionner vos releases. Elle permet aux utilisateurs de votre application ou de votre librairie de déterminer les types de changements effectués entre deux releases uniquement à partir du numéro de version.

Si vous êtes déjà à l'aise avec ces notions, vous pouvez sauter les parties qui les expliquent en détail et vous rendre directement à la section qui vous intéresse. 💪🏼

SemVer : une convention largement répandue

Principes

Avoir des numéros de versions c'est bien. Ce qui l'est moins, c'est d'avoir un unique numéro : v1, v2, etc. Il en est de même pour les releases dont l'identifiant se base sur une date : v1-2022-02, v2-2022-08, etc.

Pourquoi ? Parce que vous n'avez aucun moyen de savoir quels types de modifications ont été effectués d'une release à l'autre juste en comparant ces nombres. Ainsi, si vous développez une librairie, les développeurs qui l'utilisent ne sauront pas déterminer simplement si les mises à jour que vous proposerez pourront être appliquées sur leurs applications sans provoquer de régressions. Ils ne pourront pas détecter facilement non plus si votre mise à jour comporte des nouvelles fonctionnalités, ou si elle ne fait que corriger des bugs.

De ce fait, avoir une numérotation aussi naïve montre rapidement ses limites. Et c'est alors que SemVer intervient.

SemVer est un ensemble de règles qui vont dicter la façon dont les numéros de versions sont attribués et incrémentés. En adoptant ces règles, ils suivront un format qui ne doit pas vous être inconnu : X.Y.Z (par exemple, 1.0.0 ou 2.1.4).

C'est une convention largement répandue et utilisée par de nombreuses applications. L'avantage principal est la transparence vis à vis de nos utilisateurs : les numéros de versions ont désormais un sens. En effet, ce format décrit clairement les impacts des modifications effectuées : correction de bugs, ajout de fonctionnalités mineures, ajout de fonctionnalités majeures…

Comment est-ce qu'on incrémente ces numéros ? C'est ce qu'expliquent ces 11 règles définies par la spécification SemVer.

Elles peuvent être résumées simplement de la façon suivante : prenons un numéro de version sous la forme X.Y.Z. Lors de la publication d'une nouvelle release, on distingue 3 cas :

  1. La release comporte uniquement des corrections de bugs, compatibles avec les versions précédentes : on incrémente Z (patch).
  2. La release comporte des nouvelles fonctionnalités, compatibles avec les versions précédentes : on incrémente Y (minor), et on réinitialise Z.
  3. La release comporte des nouvelles fonctionnalités ou des corrections de bugs, non compatibles avec les versions précédentes : on incrémente X (major), et on réinitialise Y et Z.

Illustrons cette définition formelle avec un exemple plus concret.

Cas concret

L'API Rest de notre site de vidéos en streaming est en version 1.4.2. On remarque un bug lors de l'ajout d'une nouvelle vidéo : elle n'est pas sauvegardée en base de données. On effectue alors les corrections nécessaires, et comme le contrat d'interface n'a pas été modifié et qu'il s'agit d'une correction de bug, on incrémente Z : notre nouvelle version est 1.4.3.

Quelques jours plus tard, on décide d'ajouter une route POST /like pour pouvoir liker une vidéo. On introduit une nouvelle fonctionnalité, qui est compatible avec les anciennes versions (les applications qui l'utilisaient déjà n'ont rien à modifier de leur côté) : cette fois-ci, on incrémente Y, et on n'oublie pas de réinitialiser Z. Notre numéro de version devient 1.5.0.

On aimerait ensuite avoir la possibilité de publier des vidéos privées, visibles uniquement par leur auteur. Désormais, lorsqu'on voudra publier une vidéo, un nouveau paramètre obligatoire devra être fourni : visibility. On change le contrat d'interface, et notre mise à jour devient incompatible avec les précédentes : une erreur 400 est renvoyée en cas d'absence de ce nouveau paramètre. De ce fait, on incrémente X, et on réinitialise Y et Z. On passe donc à la version 2.0.0.

Gestion manuelle : les inconvénients

Maintenant que nous avons compris cette nouvelle convention, nous pouvons l'utiliser au sein de nos projets. Dans cette partie, on prend le cas d'une application web qui utilise npm comme gestionnaire de dépendances.

La gestion des numéros de versions sera faite via le champ version du fichier package.json.

{
  "name": "my-project",
  "version": "1.3.2"
}

Lorsque l'on effectuera des modifications, nous penserons à mettre à jour ce dernier, avant de publier notre application (npm publish).

On en profitera également pour ajouter un tag au commit correspondant à ces nouvelles modifications. On pourra aussi créer une release via GitHub, qui précisera en détail toutes les modifications qui ont été faites depuis la dernière version. Si votre application comporte un CHANGELOG, il faudra aussi penser à le mettre à jour.

On comprend vite que ce mode de fonctionnement est assez pénible et comporte plusieurs inconvénients :

  • Possibilité d'oubli de l'une des étapes (ou réalisation dans le mauvais ordre)
  • Possibilité d'erreur de numérotation (exemple : oubli de réinitialisation de Y ou Z)
  • Réflexion à chaque release : "Quel numéro dois-je incrémenter ?"

Intuitivement, on pourrait alors penser à développer un script bash permettant d'automatiser ce processus, et ça ne serait pas une mauvaise idée du tout.

Cependant, permettez-moi de vous présenter Semantic Release, un outil qui va grandement vous simplifier la vie.

Semantic Release à la rescousse

Semantic Release a pour but d'automatiser le processus de release que l'on a vu précédemment. En analysant les commits qui ont été réalisés, il va pouvoir déterminer le type de release à effectuer (major, minor ou patch), et dérouler les différentes étapes que l'on aura renseignées, dans l'ordre et sans faire d'erreurs ! 😉

Comment ça fonctionne ?

Pour pouvoir gérer automatiquement les montées de version, Semantic Release se base sur 2 choses :

  • Le tag git de la release précédente
  • Les commits réalisés depuis ce tag

Une convention devra donc être respectée pour le nommage des commits : dans le cas contraire, l'outil ne pourra pas déterminer quel numéro de version doit être incrémenté : il est puissant certes, mais il ne fait pas de magie. 😄

Cette convention, par défaut, c'est [Conventional Commits conventional_commits (mais elle peut être personnalisée). Lorsque l'on va créer un nouveau commit, Semantic Release va analyser le format de son titre, qui doit être le suivant :

<type>(<scope>): <description>

Exemples :

  • feat(blog): add a new post
  • fix: decrease font size

En fonction du type de commit réalisé, l'un des numéros de versions de notre format X.Y.Z sera incrémenté. Avec un commit de type fix, c'est Z qui sera incrémenté, alors qu'avec un commit de type feat, c'est Y qui le sera.

Par défaut, tous les autres types (chore, docs, refactor, …) n'incrémentent aucun numéro, mais ce comportement peut être personnalisé grâce à un plugin.

Mais… comment est-ce qu'on incrémente X alors !? 🤨

Pour incrémenter X, le fonctionnement est un peu différent. Comme ce dernier peut être incrémenté à la fois suite à un commit de type fix ou feat, une information supplémentaire devra être fournie : le footer du commit doit comporter BREAKING CHANGE.

Exemple de commit :

feat: add visibility attribute

Add a new required attribute : visibility ("public" or "private").

BREAKING CHANGE: the input is required, which breaks previous versions.

Si plusieurs commits ont été réalisés, c'est celui qui incrémente le numéro le plus à gauche qui l'emporte. Par exemple, avec 2 commits de type fix et 1 commit de type feat, c'est ce dernier qui est pris en compte pour l'incrémentation du numéro de version, peu importe l'ordre dans lequel ces commits ont été effectués. Dans ce cas de figure, c'est donc Y qui sera incrémenté.

Ses super-pouvoirs

Maintenant que nous avons découvert les bases de Semantic Release, parlons de ses super-pouvoirs… 🦸🏻‍♂️

En effet, l'outil va beaucoup plus loin que la simple incrémentation du bon numéro de version.

Grâce à l'utilisation de plugins, il sera possible de :

  • Générer automatiquement une release GitHub (accompagnée d'une release notes)
  • Générer automatiquement un CHANGELOG (basé sur la release notes)
  • Écrire le nouveau numéro de version dans le fichier de configuration du package manager (package.json pour npm, pom.xml pour maven…)
  • Publier une nouvelle version sur le package manager
  • Envoyer une notification Slack
  • Et bien plus encore.

Exemple de Release notes : Release notes générée par Semantic Release

Exemple de Changelog : Changelog généré par Semantic Release

Avantages

Pour quelles raisons devriez-vous utiliser Semantic Release ?

  • Simple à mettre en place
  • Agnostique du langage utilisé
  • Génération automatique de release notes
  • Création automatique de release GitHub
  • Diminution du risque d'erreurs
  • Gain de temps

Limites

Cet outil est génial, mais rien ni personne n'est parfait, et Semantic Release n'échappe pas à la règle. Une limite que j'ai trouvé à cet outil est qu'il nécessite une utilisation stricte de SemVer (pas de variation possible à 4 numéros ou autres suffixes dynamiques).

J'aurais aimé trouver d'autres inconvénients à cet outil pour que vous puissiez vous faire votre propre avis, mais je n'en ai pas trouvé. Si vous avez déjà utilisé Semantic Release et que vous en voyez d'autres, n'hésitez-pas à m'en faire part dans les commentaires, et j'en profiterai pour mettre à jour cet article. 😊

Mise en place

Assez parlé, passons maintenant à la pratique : voyons comment mettre en place Semantic Release. Que votre projet soit développé en Java, en PHP, en Python ou n'importe quel autre langage, peu importe : comme nous l'avons vu, l'outil est agnostique du langage utilisé.

Si votre project utilise npm, une première façon de mettre en place Semantic Release est d'utiliser la commande suivante :

npx semantic-release-cli setup

Dans le cadre de cet article, nous allons créer nous-même le fichier de configuration afin de comprendre son fonctionnement et de supporter n'importer quel langage.

Fichier de configuration

Il peut s'agir :

  • D'un fichier .releaserc utilisant la syntaxe YAML ou JSON (avec éventuellement une extension : .yml, .yaml, .json)
  • D'un fichier release.config.js dont l'export par défaut est un objet
  • D'une clé release à l'intérieur du fichier package.json (dans le cas d'un projet npm)

Vous pouvez retrouver une référence complète vers cette configuration sur le dépôt GitHub de Semantic Release.

Les options qui nous intéresseront principalement sont :

  • branches : la liste des branches sur lesquelles une nouvelle release doit être créée en cas de modifications
  • tagFormat : le format de tag à utiliser (X.Y.Z, vX.Y.Z…)
  • plugins : la liste des plugins à utiliser

Voici un exemple de configuration :

{
  "branches": ["main"],
  "tagFormat": "${version}",
  "plugins": [
    "@semantic-release/commit-analyzer",
    "@semantic-release/release-notes-generator",
    "@semantic-release/changelog",
    "@semantic-release/npm",
    "@semantic-release/git",
    "@semantic-release/github"
  ]
}

Analysons ensemble ce fichier de configuration. On indique d'abord que seule la branche main doit pouvoir créer des nouvelles releases, et que les tags git qui seront créés doivent suivre le format X.Y.Z. On déclare enfin la liste des différents plugins que l'on va utiliser :

  • @semantic-release/commit-analyzer : permet d'examiner les commits réalisés depuis la version précédente afin d'identifier le numéro de version qui devra être incrémenté.
  • @semantic-release/release-notes-generator : permet de générer une release notes complète à partir de cette même liste de commits.
  • @semantic-release/changelog : permet de mettre à jour le fichier CHANGELOG.md.
  • @semantic-release/npm : permet de mettre à jour le numéro de version présent dans le fichier package.json et de publier la nouvelle release sur npm (si le projet est public).
  • @semantic-release/git : permet de générer un commit incluant notamment la mise à jour du CHANGELOG.md et du package.json.
  • @semantic-release/github : permet de créer une release sur GitHub et d'ajouter un commentaire sur les pull requests et les issues incluses dans la nouvelle version publiée.

⚠️ Attention: l'exécution des plugins est déclenchée de manière séquentielle. Leur ordre de déclaration dans la configuration a donc son importance, car chaque plugin sera lancé l'un après l'autre (en réalité, c'est même un peu plus compliqué que ça, mais on en parlera dans un autre article).

Désormais, il ne nous reste plus qu'à réaliser un ou plusieurs commits, et à lancer la commande suivante lorsque l'on souhaite générer une nouvelle release :

npx semantic-release

⚠️ Attention : si vous intégrez Semantic Release à un projet existant, et que ce dernier ne comporte aucun tag git, le dernier commit réalisé devra être taggé avec le numéro de version actuelle. Si vous oubliez cette étape, Semantic Release n'aura pas de tag sur lequel s'appuyer, et créera donc la version 1.0.0. Si votre projet utilise déjà les tags git, assurez-vous de renseigner le même format dans le fichier de configuration (exemple : v${version} si vos tags sont de la forme vX.Y.Z). Si le format ne correspond pas, le même sort vous attend. 😅

GitHub Actions

Semantic Release n'a de réel intérêt que s'il est utilisé au sein d'une intégration continue.

Dans cet exemple, nous réaliserons l'implémentation avec GitHub Actions, mais n'importe quelle autre plateforme aurait pu faire l'affaire : GitLab CI, CircleCI, Jenkins...

Nous allons déclencher son exécution après chaque push d'un ou plusieurs commits sur la branche principale : main.

L'intérêt d'utiliser GitHub Actions, c'est qu'il existe un grand nombre de "briques" (actions) réutilisables ayant déjà été développées par d'autres développeurs. Nous n'aurons donc pas besoin de réinventer la roue : il nous suffira d'utiliser l'action cycjimmy/semantic-release-action.

Le workflow ressemble donc au fichier suivant (.github/workflows/release.yaml) :

name: Generate Release 🚀

on:
  push:
    branches: [main]

jobs:
  release:
    name: Create Release 🚀
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repo 📥
        uses: actions/checkout@v3

      - name: Semantic Release 🔥
        uses: cycjimmy/semantic-release-action@v3
        with:
          extra_plugins: |
            @semantic-release/commit-analyzer
            @semantic-release/release-notes-generator
            @semantic-release/changelog
            @semantic-release/npm
            @semantic-release/git
            @semantic-release/github
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Pour l'implémenter dans d'autres systèmes d'intégration continue, rien de bien compliqué. Il suffit d'installer npm, d'installer les différents plugins que vous souhaitez utiliser et de lancer la commande npx semantic-release que nous avons vue précédemment.

Désormais, à chaque push sur la branche main, une nouvelle release sera générée automatiquement (pour peu que de nouvelles modifications aient été apportées).

Généralement, ce n'est pas directement sur la branche main que l'on va réaliser des commits, mais on passera par des pull requests. Dans l'idéal, la branche main est d'ailleurs protégée contre les push.

⚠️ Attention : si vous protégez votre branche main, Semantic Release risque de ne pas pouvoir créer de commits lors de la release (notamment pour modifier le CHANGELOG.md). Il vous faudra dans ce cas remplacer la variable d'environnement GITHUB_TOKEN par un token d'accès personnel (PAT), qui aura les permissions nécessaires et que vous stockerez dans les secrets de votre dépôt GitHub.

Dans cette configuration, Semantic Release sera lancé à chaque nouveau merge, ce qui peut impliquer une fréquence de releases très élevée (et donc des numéros de versions qui évoluent rapidement).

Si vous souhaitez plutôt décider vous-même quand une nouvelle version doit être générée, vous pouvez remplacer la partie on: du workflow ci-dessus de la façon suivante :

on: workflow_dispatch

Cet événement correspond au lancement manuel du workflow via l'onglet Actions de l'interface GitHub.

Workflow manuel

De cette manière, vous déclenchez une nouvelle montée de version quand bon vous semble. Cependant, ce n'est pas un mode de fonctionnement que je vous recommande d'utiliser.

Pour plus d'informations : Is it really a good idea to release on every push?

Conclusion

J'espère que cet article vous a plu et que Semantic Release vous utiles pour vos projets. À titre personnel, je m'en sers désormais dans la quasi totalité de mes projets tellement je le trouve pratique et simple à mettre en place. Je n'ai ainsi plus à me prendre la tête pour les numéros de versions ni pour toutes les étapes qui suivent : je mets en place Semantic Release, et je n'y pense plus. 😁

N'hésitez-pas à me partager votre avis sur cet outil dans les commentaires, et si vous envisagez de le mettre en place ou non dans vos projets. Si vous l'utilisez déjà, je serai ravi d'avoir votre retour d'expérience également. De même, si vous avez des questions, j'y répondrai avec plaisir. 🤝🏼

Liens utiles

Did you find this article valuable?

Support Ludal by becoming a sponsor. Any amount is appreciated!