📦🚀 Semantic Release
- 15 septembre 2024
- 19 novembre 2024
- 11 min de lecture
Lorsque l’on construit un logiciel, on cherche généralement à gérer des versions pour simplifier le suivi ou la maintenance de ce qui a été livré ou déployé. Dans cette démarche, en creusant la CICD (Continuous Integration, Continuous Delivery, Continuous Deployment) sur l’une de mes missions, pour gérer les versions de mes projets personnels, je suis tombé sur semantic-release.
En creusant un peu, je me suis rendu compte que la capacité de l’outil à être applicable peu importe le langage du logiciel était intéressante et que son système d’extensions permettait d’ajuster les comportements de l’outil pour la publication des versions.
Qu’est-ce que semantic-release ?
C’est un outil Open Source gérant la publication des tags pour des logiciels maintenus avec git.
Développé en JavaScript, semantic-release est facilement extensible avec des extensions
et configurable au travers d’une CLI (Command Line Interface) ou d’un fichier (.releaserc
au format .yaml
, .json
ou .js
).
Une version est calculée à partir des nouveaux commits (de la branche qui doit être publiée) réalisés depuis la dernière version publiée.
Sur quoi se base t’il pour calculer une version ?
Le calcul d’une version se base sur du Semantic Versioning (ou semver), c’est une spécification (assez complète) pour le nommage de version. Voici quelques exemples :
1.0.0
12.16.1788
1.0.0-beta.1
1.0.0+702c7fcc879cf8cd0401e70fc083386e07ff0a35
1.0.0+702c7fcc879
1.0.0-dev.702c7fcc879
Il est commun en supplément d’ajouter le préfixe “v” aux versions,
c’est d’ailleurs pour cela que semantic-release propose une option de configuration spécifique (tagFormat
) pour paramétrer le format du tag qui sera créé.
Quel est le processus de publication ?
Dans les détails de l’outil, le processus de publication consiste en suite d’étapes (fonctions) qui sont exécutées dans l’ordre suivant :
- Verify conditions : Vérification de certaines conditions (tokens d’accès par exemple)
- Get last release : Récupération des commits réalisés depuis la dernière version
- Analyze commits : Détermination de la nouvelle version (prerelease, patch, mineur, majeur, next, etc.)
- Verify release : Étape libre pour les extensions afin de déterminer la conformité de la release
- Generate notes : Génération des notes de la release (titre du commit, notes supplémentaires, organisation en section)
- Create git tag : Création du tag git
- Prepare : Préparation de la release
- Publish : Publication de la release
- Notify : Notification du succès ou de l’échec de la release
Lorsqu’une étape lève une exception / erreur, alors les suivantes ne sont pas exécutées (sauf la notification d’échec) et l’exécution s’arrête avec l’erreur rencontrée. En réalité, semantic-release délègue une majeure partie du flow d’exécution aux extensions (abordé plus bas), qui peuvent se greffer à chacune des étapes. Bien sûr, par défaut, un certain nombre d’extensions sont définies pour que l’outil ait une vraie plus-value sans configuration particulière.
Comment configurer l’outil ?
Comme abordé plus haut, semantic-release peut être configuré avec plusieurs options :
--extends
: La liste de configurations à étendre--branches
: La liste des branches qui peuvent être publiées--tagFormat
: Le format du tag à créer (par défautv${version}
)--plugins
: La liste d’extensions avec leur configuration associée--repositoryUrl
: URL vers le dépôt avec votre code (optionnel, par défaut récupéré au travers de l’URL git)--dry-run
: Exécute en dry run la création de la release--ci
/--no-ci
: Bypass des vérifications liées à un environnement CI (Continuous Integration) pour publier de nouvelles versions depuis un environnement local--debug
: Ajoute les logs réalisés avec debug dont le namespace estsemantic-release:
Personnellement, je ne suis pas très fan de l’option --extends
puisque pour qu’une configuration puisse être étendue,
celle-ci doit être publiée sur un registre npm.
En comparaison, une extension de configuration comme peut le faire renovate
est très pratique, puisqu’il suffit simplement de préciser le chemin de la configuration.
En quoi consiste la configuration des branches ?
Si on regarde de plus près certaines configurations, comme celle des branches, cela consiste en deux choses :
- Préciser quelles branches peuvent être publiées
- Préciser si une branche spécifique est dite de prerelease et son identifiant de prerelease
Ci-dessous, un exemple complet, il faut aussi noter que le nom d’une branche peut être une chaîne de caractères fixe, ou un glob qui respecte le format micromatch.
1branches:
2 # 1.12.x, 1.x, 1.x.x
3 # https://semantic-release.gitbook.io/semantic-release/usage/workflow-configuration#maintenance-branches
4 - +([0-9])?(.{+([0-9]),x}).x
5 - master
6 - main
7 - next
8 - next-major
9 # la branche nommée "beta" est catégorisée en prerelease
10 # le tag créé sera de la forme 1.12.5-beta.X
11 # le X sera incrémenté en fonction du nombre de prerelease réalisées
12 # sur la version actuelle pointée par la beta
13 - name: beta
14 prerelease: true
15 # la branche nommée "staging" est catégorisée en prerelease
16 # le tag créé sera de la forme 1.12.5-beta.X
17 # le X sera incrémenté en fonction du nombre de prerelease réalisées
18 # sur la version actuelle pointée par la beta
19 - name: staging
20 prerelease: beta
À quoi servent les extensions ?
Par défaut, semantic-release ne gère que la création d’un tag git, le reste du processus de publication doit être géré par les extensions et au moins une extension doit être présente pour gérer l’étape d’analyse des commits.
Les extensions peuvent ajouter des comportements comme par exemple :
- La création de notes de version (qui pourraient être intégrées à une page de release)
- La création d’une release GitHub, GitLab ou Gitea
- La publication d’un package npm, ou maven sur un registre
- La publication d’une image Docker sur un registre
- La fusion de la branche publiée dans une autre branche
Malgré tout, sans configuration particulière, semantic-release intègre les extensions suivantes :
- @semantic-release/commit-analyzer :
- Analyse les commits réalisés depuis la dernière version et détermine la nouvelle version
- @semantic-release/release-notes-generator :
- Crée les notes de la release organisées en sections à partir de la liste des commits de la nouvelle version
- @semantic-release/npm :
- Incrémente la version présente dans le
package.json
avec la nouvelle version (attention ce n’est pas poussé sur le dépôt git) - Publie le package npm sur un registre approprié (par défaut npmjs.org) si besoin
- Incrémente la version présente dans le
- @semantic-release/github :
- Crée la release GitHub et notifie les pull requests / issues que ces sujets ont été publiés
Comme l’étape d’analyse des commits est obligatoire dans le processus de publication de l’outil, je recommande de garder à minima l’extension @semantic-release/commit-analyzer qui peut être configurée pour modifier les modalités du calcul de la nouvelle version :
1plugins:
2 - - "@semantic-release/commit-analyzer"
3 - # le parser global pour les commits
4 # https://www.conventionalcommits.org/en/v1.0.0/#specification
5 preset: conventionalcommits
6
7 # les règles pour définir quel type de commit engendre quel type de release
8 releaseRules:
9 - { breaking: true, release: "major" }
10 - { revert: true, release: "patch" }
11 - { type: "feat", release: "minor" }
12 - { type: "fix", release: "patch" }
13 - { type: "revert", release: "patch" }
14 - { type: "docs", release: "patch" }
15 - { type: "refactor", release: "minor" }
16 - { scope: "release", release: false }
17
18 # la présence de BREAKING CHANGES ou BREAKING dans un commit
19 # indiquera à semantic-release de réaliser un version majeur
20 # peu importe les types de commits présents dans la release attendue
21 parserOpts:
22 noteKeywords: [ "BREAKING CHANGES", "BREAKING" ]
L’une des fonctionnalités intéressantes sur laquelle gravitent plusieurs extensions concerne la génération des notes de release.
En effet on peut retrouver @semantic-release/release-notes-generator (abordé plus haut)
et @semantic-release/changelog qui se sert de la précédente extension
pour construire ou mettre à jour le fichier CHANGELOG.md
(afin de suivre les changements réalisés à chaque version) :
1plugins:
2 - - "@semantic-release/release-notes-generator"
3 - # le parser global pour les commits
4 # https://www.conventionalcommits.org/en/v1.0.0/#specification
5 preset: conventionalcommits
6
7 # une configuration pour définir quel type de commit va
8 # dans quelle section des notes de release
9 presetConfig:
10 types:
11 # chaque type de commit est positionné dans une section spécifique
12 - { type: "feat", section: "Features" }
13 - { type: "fix", section: "Bug Fixes" }
14 - { type: "revert", section: "Reverts" }
15 - { type: "docs", section: "Documentation" }
16 - { type: "refactor", section: "Code Refactoring" }
17 - { type: "test", section: "Tests", hidden: true } # il est possible de masquer une section
18
19 parserOpts:
20 # les notes derrière BREAKING ou BREAKING CHANGES dans un commit
21 # seront positionnées dans une section spécifique supplémentaire
22 # dans les notes de la release
23 noteKeywords: [ "BREAKING CHANGES", "BREAKING" ]
24
25 - "@semantic-release/changelog"
En complément des deux exemples précédents et des extensions par défaut présentées, voici quelques extensions Open Source ajoutant différents comportements :
- Créer des release en fonction de la plateforme git :
- @saithodev/semantic-release-gitea :
- Crée la release Gitea et notifie les pull requests / issues
- @semantic-release/gitlab :
- Crée la release GitLab et notifie les pull requests / issues
- @saithodev/semantic-release-gitea :
- Créer et publier un package maven ainsi que mettre à jour la version dans le
pom.xml
: - Fusionner la branche publiée dans d’autres branches :
- @kilianpaquier/semantic-release-backmerge :
- Crée une pull request en cas de conflits
- @saithodev/semantic-release-backmerge :
- Méthodologie de fusion (fast forward, rebase, etc.) configurable
- @kilianpaquier/semantic-release-backmerge :
- Exécuter des scripts shell personnalisés sur certaines (ou toutes) étapes d’exécution : @semantic-release/exec
- Ajouter un commit à la branche publiée avec différents assets modifiés durant le processus de publication
(
package.json
,pom.xml
,CHANGELOG.md
,LICENSE
, etc.) : @semantic-release/git
Quel résultat cela peut donner ?
Voici un exemple de ce qu’une release sur GitHub peut donner :
Comment développer une extension ?
On a parlé plus haut des possibilités d’extension, mais finalement, comment développer une extension pour apporter de la valeur ajoutée supplémentaire à semantic-release ?
Une extension est forcément un package npm qui exporte (au sens JavaScript ou TypeScript) au moins une des étapes d’exécution de semantic-release et qui soit “enregistré” ou “déployé” dans un registre npm.
Pour que ce soit plus simple pour vos utilisateurs, je vous recommande d’utiliser le registre npmjs.org plutôt qu’un autre registre car cela nécessiterait de la configuration supplémentaire pour l’authentification et droits d’accès.
1import { SuccessContext, VerifyConditionsContext, ... } from 'semantic-release'
2
3export interface Config {
4 debug: boolean
5 dryRun: boolean
6 repositoryUrl: string
7
8 // un moyen simple en TypeScript de récupérer n'importe quelle clé envoyée en entrée
9 // bien sûr pour le développement d'une extension avec une configuration spécifique,
10 // je vous recommande de préciser les noms et types des propriétés
11 [k: string]: any
12}
13
14// fonction exécutée pour vérifier certaines conditions comme par exemple
15// le bon format de la configuration du plugin
16// ou encore la vérification des variables d'environnement (token d'accès, URL d'API, etc.)
17export const verifyConditions = async (globalConfig: Config, context: VerifyConditionsContext) => {}
18
19// fonction exécutée pour l'analyse des commits depuis la dernière release
20export const analyzeCommits = async (globalConfig: Config, context: AnalyzeCommitsContext) => {}
21
22// fonction exécutée pour vérifier la conformité de la release
23export const verifyRelease = async (globalConfig: Config, context: VerifyReleaseContext) => {}
24
25// fonction exécutée pour / lors de la génération des notes de la release
26export const generateNotes = async (globalConfig: Config, context: GenerateNotesContext) => {}
27
28// fonction exécutée pour ajouter un channel de release,
29// je n'ai pas plus de contexte car je n'ai jamais poussé la réflexion sur cette fonctionnalité
30export const addChannel = async (globalConfig: Config, context: AddChannelContext) => {}
31
32// fonction exécutée pour préparer la release comme
33// mettre à jour certains fichiers ou pousser un commit
34export const prepare = async (globalConfig: Config, context: PrepareContext) => {}
35
36// fonction exécutée pour publier la release
37export const publish = async (globalConfig: Config, context: PublishContext) => {}
38
39// fonction exécutée quand la publication de la release s'est correctement déroulée
40export const success = async (globalConfig: Config, context: SuccessContext) => {}
41
42// fonction exécutée quand la publication de la release n'a pas fonctionné
43export const fail = async (globalConfig: Config, context: FailContext) => {}
Existe-t-il des alternatives ?
C’est vrai que le sujet abordé ici était semantic-release, mais il existe aussi des solutions alternatives, qui résolvent aussi cette problématique de suivi et de maintenance des livraisons et déploiements :
C’est une Action GitHub configurable qui se base principalement sur les pull requests réalisées depuis la dernière version publiée.
L’aspect intéressant concerne la génération des notes de release
puisque l’outil peut se baser sur le fichier .github/release.yml
qui est aussi le fichier par défaut
qu’utilise GitHub pour générer les notes de release quand celle-ci est
créée à la main.
Un autre point intéressant concerne la création d’une discussion GitHub (optionnel) lors de la création de la release pour permettre aux utilisateurs de commenter / réagir aux changements.
C’est aussi une Action GitHub (existait aussi en GitHub App mais celle-ci a été dépréciée), qui se base aussi sur les pull requests réalisées depuis la dernière version publiée.
À la différence de gh-release, la configuration est un peu plus malléable puisqu’elle n’utilise pas le fichier
.github/release.yml
mais un fichier spécifique pour release-drafter.
Parmi les points plus malléables, on peut retrouver plus de customisation sur les notes de la release,
une fonctionnalité que j’aime beaucoup, l’autolabeler, qui à partir de la configuration va mettre les bons labels
automatiquement sur les pull requests ou encore la possibilité de préciser l’identifiant de prerelease.
À la différence de gh-release ou release-drafter, cet outil se base, comme semantic-release sur les commits et les conventional commits pour déterminer la nouvelle version ainsi que les notes de release.
C’est à la base une CLI qui est déclinée en Action GitHub, dans tous les cas la CLI n’est pour le moment (octobre 2024) disponible uniquement que pour GitHub.
L’un des gros atout de cet outil est de passer par une pull request mise à jour au fur et à mesure des fusions réalisées dans la branche ciblée par la release. Une fois la pull request principale fusionnée alors la release est créée au travers de l’action GitHub.