
JS monorepos en prod 3 : validation de commits et generation du changelog
By WORMS David
2 févr. 2021
- Catégories
- DevOps & SRE
- Front End
- Tags
- CI/CD
- Git
- JavaScript
- Monorepo
- Node.js
- Versions et évolutions
- Tests unitaires [plus][moins]
Ne ratez pas nos articles sur l'open source, le big data et les systèmes distribués, fréquence faible d’un email tous les deux mois.
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 :
- Partie 1 : initialisation de project
- Partie 2 : gestion des versions et de la publication
- Partie 3 : messages de commit et génération du changelog
- Partie 4 : tests unitaires
- Partie 5 : fusion de plusieurs dépôts Git et préservation des commits
- Partie 6 : CI/CD, intégration et déploiement continus avec Travis CI
- Partie 7 : CI/CD, intégration et déploiement continus avec GitHub Actions
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 correctifdocs
Modifications dans la documentationstyle
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 performancestest
Ajout de tests manquants ou correction de tests existantsbuild
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 testrevert
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 :Modifiezyarn add -D -W @commitlint/{config-conventional,cli}
commitlint.config.js
avec les scopes Conventional Commits et Lerna :Faire un commit des changements :module.exports = { extends: [ "@commitlint/config-conventional", "@commitlint/config-lerna-scopes" ] };
git add commitlint.config.js package.json git commit -m 'build: enable commitlint'
- Automatiser la validation de Conventional Commits lors du commit
Installer Husky :Enregistrer le commit du hook :yarn add -D -W husky@next yarn husky install
Modifieryarn husky add .husky/commit-msg 'yarn commitlint --edit $1'
package.json
configure automatiquement les hooks Git lors de l’installation :Faire un commit des changements :{ ... "scripts": { ... "postinstall": "husky install" } }
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 (utiliserpremajor
,prepatch
, ouprepatch
) :Passer à un état en prerelease avec incrémentation automatique de la version :lerna version --conventional-commits --preid beta premajor
Finaliser la montée de version après une prerelease :lerna version --conventional-commits --conventional-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.