JS monorepos en prod 3 : validation de commits et generation du changelog

JS monorepos en prod 3 : validation de commits et generation du changelog

WORMS David

By WORMS David

2 févr. 2021

Conventional Commits introduit un format structuré pour les message de commit. Il standardise les messages entre tous les contributeurs. Cela les rend plus lisibles et plus faciles à automatiser. Il simplifie la gestion d’un monorepo et contribue à de meilleures pratiques DevOps. De plus, il permet la génération automatique de fichiers changelog.

Dans l’article précédent sur la gestion des versions et de la publication, j’ai brièvement abordé la structure des messages de validation. Cet article montre comment les appliquer et les valider automatiquement avec Conventional Commits et comment les utiliser pour la génération des fichiers de changelog. Dans les articles suivants, nous continuerons avec les tests unitaires et l’intégration CI/CD :

Qu’est que Conventional Commits

Plusieurs fois dans les articles précédants, nous avons mentionné les spécifications de Conventional Commits. C’est une convention simple, ou une spécification, pour structurer vos messages de commit pour les rendre lisibles par l’homme et interprétables par des machines.

Voici à quoi ressemble un message de commmit :

[optional scope]: 

[optional body]

[optional footer(s)]

Vous avez peut-être remarqué que nos messages de commit respectent un format. Les types correspondent à des valeurs prédéfinies et ont une signification particulière pour Semantic Versioning (SemVer) :

  • patch corrige un bug et correspond à CORRECTIF dans SemVer.
  • feat quand il y a ajout de fonctionnalités rétrocompatibles et correspond à MINEUR dans SemVer.
  • BREAKING CHANGE introduit un changement dans l’API et correspond à MAJEUR dans SemVer.

D’autres types sont autorisés. La majorité respecte la convention Angular, qui definit les types build, chore, ci, docs, style, refactor, perf, test.

Nous avons déjà utilisé les types build, chore et docs. J’avoue utiliser ma propre interprétation de build qui couvre tout ce qui concerne la gestion du projet.

Les scopes doivent également correspondre aux valeurs prédéfinies. Ils agissent comme des sujets ou des catégories. Dans un monorepo, il est courant d’associer le scope aux noms des packages.

Utilisation de Conventional Commits et génération du changelog

Puisque Conventional Commits est interprétable par une machine, il est tentant de se fier au message de validation pour générer un changelog ou choisir le numéro de version approprié.

Le package gatsby-remark-title-to-frontmatter mérite également un README, créons-le :

echo \
 '# Package `gatsby-remark-title-to-frontmatter`' \
  > gatsby-remark/title-to-frontmatter/README.md
git add gatsby-remark/title-to-frontmatter/README.md
git commit -m "docs(title-to-frontmatter): new readme file"

Cette fois, nous exécutons lerna version avec l’option --conventional-commits. En se basant sur les types de commit, il propose la version la plus appropriée pour la validation.

lerna version --conventional-commits
info cli using local version of lerna
lerna notice cli v3.22.1
lerna info versioning independent
lerna info Looking for changed packages since gatsby-caddy-redirects-conf@0.1.0-alpha.2
lerna info ignoring diff in paths matching [ '**/test/**' ]
lerna info getChangelogConfig Successfully resolved preset "conventional-changelog-angular"

Changes:
 - gatsby-remark-title-to-frontmatter: 1.0.0-alpha.1 => 1.0.0-alpha.2

? Are you sure you want to create these versions? Yes
lerna info execute Skipping releases
lerna info git Pushing tags...
lerna success version finished

Un nouveau fichier est également créé dans notre package publié, le fichier CHANGELOG.md. Son contenu Markdown est extrait de l’historique des commits et ressemble à ceci :

# Change Log

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

# [1.0.0-alpha.2](https://github.com/adaltas/remark-gatsby-plugins/compare/gatsby-remark-title-to-frontmatter@1.0.0-alpha.1...gatsby-remark-title-to-frontmatter@1.0.0-alpha.2) (2020-12-03)

**Note:** Version bump only for package gatsby-remark-title-to-frontmatter

Des informations plus détaillées auraient été disponibles avec certains messages de commit patch, feat ou BREAKING CHANGE.

Il est bien sûr possible de combiner la génération du changelog et l’option --conventional-commits avec l’application d’une version particulière et d’un mot-clé SemVer.

Conventional Commits depuis la ligne de commande - Commitizen

Nos messages de commit sont devenus très importants et nos collaborateurs doivent obtenir de l’aide pour les créer si nous voulons conserver des messages cohérents dans tous nos commits.

Commitizen est un outil CLI qui vous guide dans le processus de création de messages de commit conformes. L’utilisateur est invité à remplir tous les champs du commit requis au moment de la création du commit.

Il existe plusieurs façons d’utiliser Commitizen. L’exécution de npm install -g commitizen installe le package globalement et se connecte à Git. Maintenant, vous pouvez simplement utiliser git cz au lieu de git commit.

Je préfère installer mes dépendances localement, voici comment initialiser votre repo avec Commitizen :

npx commitizen init cz-conventional-changelog -D -E

Il installe la dépendance commitizen et la configure avec l’adaptateur Commitizen de votre choix, Conventional Commits dans notre cas. L’option -D sert à enregistrer l’adaptateur dans devDependencies et l’option -E sert à définir une version exacte au lieu d’une plage.

diff --git a/package.json b/package.json
index f853695..15e431f 100644
--- a/package.json
+++ b/package.json
@@ -12,5 +12,13 @@
   "workspaces": [
     "gatsby/*",
     "gatsby-remark/*"
-  ]
+  ],
+  "devDependencies": {
+    "cz-conventional-changelog": "^3.3.0"
+  },
+  "config": {
+    "commitizen": {
+      "path": "./node_modules/cz-conventional-changelog"
+    }
+  }
 }

Commitizen travaille sur les changements enregistrés. Avant de le tester, apportez quelques modifications et enregistrez les avec git add. Puisque Commitizen a modifié notre fichier package.json, nous pouvons simplement enregistrer et utiliser la commande npx cz au lieu de git commit :

git add package.json
npx cz

Tout d’abord, il commence par demander le type de changement. Les valeurs possibles sont :

  • feat
    Une nouvelle fonctionalité
  • fix
    Un correctif
  • docs
    Modifications dans la documentation
  • style
    Modifications qui n’affectent pas le code (espace blanc, formatage, points-virgules manquants, etc.)
  • refactor
    Un changement de code qui ne corrige pas de bug ni n’ajoute de fonctionnalité
  • perf
    Un changement de code qui améliore les performances
  • test
    Ajout de tests manquants ou correction de tests existants
  • build
    Modifications qui affectent le système de compilation ou les dépendances externes (exemples : gulp, brocoli, npm)
  • ci
    Modifications de nos fichiers et scripts de configuration CI (exemples : Travis, Circle, BrowserStack, SauceLabs)
  • chore
    Autres changements qui ne modifient pas les fichiers src ou de test
  • revert
    Annule un commit précédent

Notez que mon utilisation du build dans les articles précédents peut ne pas être conventionnelle puisque je l’utilise pour configurer le projet.

La deuxième étape consiste à nous demander un scope optionnel. Ceci est couvert plus tard et il est logique d’aligner les scopes avec les noms de vos packages.

Puis vient le message et la description du commit principal.

Enfin, Commitizen doit savoir si on introduit un changement non rétrocompatibles et ce changement est lié à un ticket ouvert.

Voici à quoi ressemble la commande finale npx cz :

npx cz
cz-cli@4.2.2, cz-conventional-changelog@3.3.0

? Select the type of change that you’re committing: build:    Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
? What is the scope of this change (e.g. component or file name): (press enter to skip) 
? Write a short, imperative tense description of the change (max 93 chars):
 (32) declare and configure commitizen
? Provide a longer description of the change: (press enter to skip)
 
? Are there any breaking changes? No
? Does this change affect any open issues? No
[master 9e33979] build: declare and configure commitizen
 1 file changed, 10 insertions(+), 2 deletions(-)

Validation des commits - Commitlint

Nos collaborateurs ont maintenant accès à un outil CLI qui les aide et les guide dans la création de messages. Cependant, cela ne nous préserve pas d’erreurs. Quiconque n’utilise pas git cz ou npx cz, par exemple ceux qui utilisent leur éditeur graphique préféré, entreront le message de leur choix. Comment nous assurer que les bonnes pratiques sont respectées et ne pas laisser des commits invalides être créés et poussés ?

commitlint valide les messages basés sur Conventional Commits. Nous devons installer les outils CLI avec leur adaptateur Conventional Commits. Nous devons également créer le fichier commitlint.config.js à la racine de notre projet.

yarn add -D -W @commitlint/{config-conventional,cli}
cat <<CONFIG > commitlint.config.js
module.exports = {
  extends: [
    "@commitlint/config-conventional"
  ]
};
CONFIG

L’option -W contourne une vérification Yarn qui vous empêche de déclarer des dépendances dans le paquet racine.

Essayons de voir comment cela fonctionne. Un message non valide, tel qu’un type invalid, doit générer une erreur :

echo 'invalid: error are expected sometimes' | yarn commitlint
yarn run v1.22.5
$ /Users/david/projects/github/remark-gatsby-plugins/node_modules/.bin/commitlint
⧗   input: invalid: error are expected sometimes
✖   type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] [type-enum]

✖   found 1 problems, 0 warnings
ⓘ   Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint

error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Notre message de commit est invalide, merveilleux ! La sortie nous a même suggéré les types valides pris en charge par Conventional Commits.

Le fichier package.json est modifié avec les nouvelles dépendances et un nouveau fichier commitlint.config.js est prêt à être validé. Voici comment faire un commit si seulement la validation est réussie :

echo 'build: enable commitlint' | yarn commitlint || exit 1
git add commitlint.config.js package.json
git commit -m 'build: enable commitlint'

Automatiser la validation des commits - Husky

Une solution est désormais à notre disposition pour valider les messages de commit. Cependant, nous ne pouvons pas nous attendre à ce que chaque utilisateur émette la commande. Le faire plus tard, comme à l’intérieur de votre CI/CD, sera trop tard. Le commit est déjà là et synchronisé avec votre dépôt Git distant.

La validation des commits doit être automatisée. C’est là que Husky entre en jeu. Il se connecte à la configuration du hook Git pour valider les règles de lint avant de commiter.

Nous utiliserons la dernière version de Husky, la version 5. Son agencement diffère de la version 4, ne soyez pas surpris. Notez cependant qu’au moment de la rédaction de cet article (décembre 2020) la licence de la version 5 autorise uniquement les utilisations Open Source de la bibliothèque à moins que vous ne deveniez sponsor. Vous pouvez également continuer à utiliser la version 4 dans votre environnement professionnel.

# Install the dependency
yarn add -D -W husky@next
# Enable Git hooks
yarn husky install

Maintenant, regardez votre fichier .git/config, il a été mis à jour avec :

cat .git/config | grep hook
	hooksPath = .husky

Husky est maintenant configuré, nous continuons à le configurer pour vérifier notre message de commit. Nous voulons déclencher commitlint juste avant qu’un commit ne se produise :

yarn husky add .husky/commit-msg 'yarn commitlint --edit $1'

Un nouveau dossier .husky est créé et il ne doit pas être ignoré par Git. Seul le dossier .husky/_ à l’intérieur doit être ignoré et il y a déjà le fichier .husky/.gitignore juste pour cela. Puisque le nom du dossier commence par ., nous devons modifier nos règles d’origine .gitignore :

echo '!.husky' >> .gitignore

Dans le dossier ..husky, les fichier gérés par Husky sont nommés d’après le nom du hook Git. Le dossier .git/hooks contient des exemples. Husky a créé un fichier pre-commit exécutant la commande yarn commitlint --edit $ 1 lors du commit. Validons le dossier avec nos modifications dans les fichiers .gitignore et package.json :

git add .gitignore .husky package.json
git commit -a -m 'disable ignore rule for husky configuration'
yarn run v1.22.5
$ /Users/david/projects/github/gatsby/node_modules/.bin/commitlint --edit
⧗   input: disable ignore rule for husky configuration
✖   subject may not be empty [subject-empty]type may not be empty [type-empty]

✖   found 2 problems, 0 warnings
ⓘ   Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint

error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
husky - commit-msg hook exited with code 1 (error)

Oups, j’ai oublié. Conventional Commits est désormais automatiquement actif et il n’y avait aucun type dans le message du commit :

git commit -a -m 'build: disable ignore rule for husky configuration'

Ça marche. Et non seulement cela fonctionne sur la ligne de commande avec git commit et npx cz, mais partout, y compris dans votre éditeur préféré.

Encore une chose avec Husky, pour configurer automatiquement les hooks Git lors de l’installation, éditez le fichier package.json.

{
  "scripts": {
    "postinstall": "husky install"
  }
}

Et effectuer un commit :

git commit -a -m 'build: husky activation on install'

Personalisation du scope

Il a été mentionné plus tôt à quel point le scope du commit convient bien aux monorepos. En nommant le scope avec le nom du package, le changelog global peut informer les packages concernés dans les messages de commit.

commitlint fournit l’adaptateur de package @commitlint/config-lerna-scopes pour Lerna :

yarn add -D -W @commitlint/config-lerna-scopes

Il doit également être enregistré dans le fichier commitlint.config.js :

module.exports = {
  extends: [
    "@commitlint/config-conventional",
    "@commitlint/config-lerna-scopes"
  ]
};

Cependant, je trouve que la liste des scopes fournie par le nom du package est un peu restrictive. En effet, nous avons déjà un problème. Essayons d’effectuer un commit notre changement et de faire une version.

Tout d’abord, nous avons besoin de quelques modifications dans les packages pour déclencher une nouvelle version. Nous ajoutons une instruction aux fichiers readme :

git commit -a -m 'build: commitlint with lerna scopes'
echo '' >> gatsby/caddy-redirects-conf/README.md
echo 'Generate a Caddy compatible config file.' >> gatsby/caddy-redirects-conf/README.md
git commit -a -m 'docs(gatsby-caddy-redirects-conf): introduction'
echo '' >> gatsby-remark/title-to-frontmatter/README.md
echo 'Move the title from the content to the frontmatter' >> gatsby-remark/title-to-frontmatter/README.md
git commit -a -m 'docs(gatsby-remark-title-to-frontmatter): introduction'

Nous pouvons maintenant essayer de créer de nouvelles versions :

lerna version --conventional-commits
info cli using local version of lerna
lerna notice cli v3.22.1
lerna info versioning independent
lerna info Looking for changed packages since gatsby-remark-title-to-frontmatter@1.0.0-alpha.2
lerna info ignoring diff in paths matching [ '**/test/**' ]
lerna info getChangelogConfig Successfully resolved preset "conventional-changelog-angular"

Changes:
 - gatsby-remark-title-to-frontmatter: 1.0.0-alpha.2 => 1.0.0-alpha.3
 - gatsby-caddy-redirects-conf: 0.1.0-alpha.2 => 0.1.0-alpha.3

? Are you sure you want to create these versions? Yes
...
lerna ERR! ✖   scope must be one of [gatsby-remark-title-to-frontmatter, gatsby-caddy-redirects-conf] [scope-enum]
...
lerna ERR! lerna husky - commit-msg hook exited with code 1 (error)
lerna ERR! lerna
# Do some cleanup
git reset --hard HEAD

La sortie est verbeuse mais la ligne importante est scope must be one of [gatsby-caddy-redirects-conf, gatsby-remarque-title-to-frontmatter] [scope-enum]. En effet, Lerna est configuré pour générer un message de commit à partir du template chore (release): publish. La liste des étendues prises en charge est appliquée par @commitlint/config-lerna-scopes et “release” n’en fait pas partie.

La solution est de se connecter à la fonction rules.scope-enum et d’ajouter nos scopes personnalisées. Modifiez le fichier commitlint.config.js en conséquence.

const {utils: {getPackages}} = require('@commitlint/config-lerna-scopes');

module.exports = {
  "extends": [
    "@commitlint/config-conventional",
    "@commitlint/config-lerna-scopes"
  ],
 rules: {
  'scope-enum': async ctx =>     [2, 'always', [...(await getPackages(ctx)),       // Insert custom scopes below:      'release'    ]] }
}

Nous pouvons maintenant valider le changement de commitlint.config.js et créer une nouvelle version :

git commit -a -m 'build: add the release scope generated by lerna'
lerna version --conventional-commits
...
Changes:
 - gatsby-remark-title-to-frontmatter: 1.0.0-alpha.2 => 1.0.0-alpha.3
 - gatsby-caddy-redirects-conf: 0.1.0-alpha.2 => 0.1.0-alpha.3
...

Prerelease version management

Nous avons utilisé des versions de prerelease précédemment lorsque nous utilisions lerna version. La commande propose parmi les choix disponibles les versions prepatch, preminor et premajor. Par example, si on est positionné sur une version 0.1.0-alpha.3, les choix sont :

lerna version                       
info cli using local version of lerna
lerna notice cli v3.20.2
lerna info versioning independent
lerna info Looking for changed packages since @nikitajs/core@0.9.7
lerna info version rooted leaf detected, skipping synthetic root lifecycles
? Select a new version for @nikitajs/db (currently 0.9.7) (Use arrow keys)
❯ Patch (0.9.8) 
  Minor (0.10.0) 
  Major (1.0.0) 
  Prepatch (0.1.1-alpha.0)   Preminor (0.2.0-alpha.0)   Premajor (1.0.0-alpha.0)   Custom Prerelease 
  Custom Version

Les états les plus communnes de prerelease sont alpha lors que le logiciel peut contenir des erreurs et pas encore toutes les fonctionnalités, beta quand le logiciel est complet mais qu’il peut rester des bugs et rc lorsqu’il porte le potentiel d’être une version stable.

Lerna automatise la sélection de l’état de prerelease avec l’option --preid. Par exemple, pour passer d’une version 0.1.0 à une version majeur 1.0.0 en mode béta :

lerna version --conventional-commits --preid beta premajor

Cependant, demander à Lerna d’incrémenter tout les packages vers une version majeure rentre en collision avec l’intérêt d’utiliser l’option --conventional-commits pour extraire la prochaine versions des messages de commits.

Il est possible de passer à un état de prerelease tout en laissant Lerna choisir la prochaine version à partir des messages de commit avec l’option --conventional-prerelease :

lerna version --conventional-commits --conventional-prerelease

Il est ensuite possible de clôturer et de sortir de l’état de prerelease pour graduer vers une version finale avec l’option --conventional-graduate :

lerna version --conventional-commits --conventional-graduate

Aide-mémoire

  • Validation du Commit avec commitlint
    Installer la dépendance dev :
    yarn add -D -W @commitlint/{config-conventional,cli}
    Modifiez commitlint.config.js avec les scopes Conventional Commits et Lerna :
    module.exports = {
      extends: [
        "@commitlint/config-conventional",
        "@commitlint/config-lerna-scopes"
      ]
    };
    Faire un commit des changements :
    git add commitlint.config.js package.json
    git commit -m 'build: enable commitlint'
  • Automatiser la validation de Conventional Commits lors du commit
    Installer Husky :
    yarn add -D -W husky@next
    yarn husky install
    Enregistrer le commit du hook :
    yarn husky add .husky/commit-msg 'yarn commitlint --edit $1'
    Modifier package.json configure automatiquement les hooks Git lors de l’installation :
    {
      ...
      "scripts": {
        ...
        "postinstall": "husky install"
      }
    }
    Faire un commit des changements :
    echo '!.husky' >> .gitignore
    git add .gitignore .husky package.json
    git commit -a -m 'build: disable ignore rule for husky configuration'
  • Prerelease
    Passer à un état en prerelease tout en incrémentant la version (utiliser premajor, prepatch, ou prepatch) :
    lerna version --conventional-commits --preid beta premajor
    Passer à un état en prerelease avec incrémentation automatique de la version :
    lerna version --conventional-commits --conventional-prerelease
    Finaliser la montée de version après une prerelease :
    lerna version --conventional-commits --conventional-graduate

Conclusion

Dans cet article nous avons découvert comment respecter Conventional Commits, valider les messages avec commitlint, et également automatiser cette validation avec Husky. Nous verrons ensuite comment exécuter des tests unitaires et comment automatiser la publication de packages dans un environnement CI/CD.

Canada - Maroc - France

International locations

10 rue de la Kasbah
2393 Rabbat
Canada

Nous sommes une équipe passionnée par l'Open Source, le Big Data et les technologies associées telles que le Cloud, le Data Engineering, la Data Science le DevOps…

Nous fournissons à nos clients un savoir faire reconnu sur la manière d'utiliser les technologies pour convertir leurs cas d'usage en projets exploités en production, sur la façon de réduire les coûts et d'accélérer les livraisons de nouvelles fonctionnalités.

Si vous appréciez la qualité de nos publications, nous vous invitons à nous contacter en vue de coopérer ensemble.