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 :
- La release comporte uniquement des corrections de bugs, compatibles avec les versions précédentes : on incrémente
Z
(patch). - La release comporte des nouvelles fonctionnalités, compatibles avec les versions précédentes : on incrémente
Y
(minor), et on réinitialiseZ
. - 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éinitialiseY
etZ
.
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
ouZ
) - 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 :
Exemple de Changelog :
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 fichierpackage.json
(dans le cas d'un projetnpm
)
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 modificationstagFormat
: 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 fichierCHANGELOG.md
.@semantic-release/npm
: permet de mettre à jour le numéro de version présent dans le fichierpackage.json
et de publier la nouvelle release surnpm
(si le projet est public).@semantic-release/git
: permet de générer un commit incluant notamment la mise à jour duCHANGELOG.md
et dupackage.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 formevX.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 commandenpx 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 leCHANGELOG.md
). Il vous faudra dans ce cas remplacer la variable d'environnementGITHUB_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.
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. 🤝🏼