📦🚀 Semantic Release

  • 15 septembre 2024
  • 19 novembre 2024
  • 11 min de lecture
/devicon/semanticrelease-original.svg

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éfaut v${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 est semantic-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 :

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 :

Quel résultat cela peut donner ?

Voici un exemple de ce qu’une release sur GitHub peut donner :

La release v24.0.0 de semantic-release

La release v24.0.0 de semantic-release

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 :

gh-release

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.

release-drafter

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.

release-please

À 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.