JS monorepos en prod 5 : fusion de plusieurs dépôts Git et préservation des commits
21 mai 2021
- Catégories
- DevOps & SRE
- Node.js
- Tags
- Bash
- DevOps
- GitHub
- Packaging
- Git
- GitOps
- JavaScript
- Monorepo [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.
Chez Adaltas, nous maintenons plusieurs projets open-source Node.js organisés en monorepos Git et publiés sur NPM. Nous avons développé notre expérience avec les monorepos Lerna que nous partageons dans une série d’articles :
- Partie 1 : initialisation du projet
- Partie 2 : stratégies de version et de publication
- Partie 3 : application des commit et génération de changelog
- Partie 4 : tests unitaires avec Mocha et Should.js
- 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
C’est maintenant au tour de notre projet open-source populaire Node CSV d’être migré vers un monorepo. Cet article vous guidera à travers les approches, les techniques et les outils utilisés pour migrer plusieurs projets Node.js hébergés sur GitHub vers un monorepo Lerna. A la fin de cet article, nous fournissons le script bash que nous avons utilisé pour migrer notre projet Node CSV. Ce script peut être appliqué à d’autres projets avec de simples modifications.
Exigences pour la migration
Le projet Node CSV combine 4 packages NPM qui permettent de travailler avec des fichiers CSV dans Node.js. Ils sont tous les 4 inclus dans un seul package principal, csv
. Chaque package NPM possède un historique de commit riche dont nous voulons sauver un maximum d’informations. Nos exigences pour réaliser la migration sont les suivantes :
- préserver l’historique des commits avec un maximum d’informations (telles que les tags, les messages et les merges) ;
- améliorer les messages de commit pour suivre la spécification Conventional Commits ;
- préserver les issues sur GitHub.
Structure du monorepo
Nous avons 5 packages NPM à migrer vers le monorepo Lerna :
Nous voulons obtenir une structure de répertoire qui ressemble à celle-ci :
packages/
csv/
csv-generate/
csv-parse/
csv-stringify/
stream-transform/
lerna.json
package.json
Choix de la stratégie de journalisation de Git
Lorsque vous migrez plusieurs dépôts dans un monorepo, vous fusionnez leurs historiques de commit. Pour ce faire, il existe 3 stratégies différentes présentées dans l’image ci-dessous.
- Branche unique
Elle fournit un historique simple contenant uniquement les commits des branches par défaut (master) de chaque package. Les différents logs sont joints séquentiellement en prenant le dernier commit du package N comme parent du premier commit du package N+1. Cette stratégie rompt le classement par date des commits. - Branches multiples avec un parent commun
Cette stratégie améliore la perception visuelle de l’historique en répartissant chaque dépôt dans une branche différente. Un commit parent, relié à tous les premiers commits de chaque branche est créé au début de l’historique. A la fin, toutes les branches sont à nouveau fusionnées dans la branche par défaut. - Branches multiples avec des parents différents
Cette stratégie ne réécrit pas les premiers commits des anciens dépôts. Elle nécessite une intervention minimale dans l’historique des commits et semble plus logique car initialement les dépôts n’avaient pas de parent commun.
Fusionner les journaux de commit
Lerna possède un mécanisme intégré permettant de rassembler des packages NPM indépendant déjà existants dans un monorepo tout en préservant leurs historiques de commits. La commande lerna import
permet d’importer un package depuis un dépôt externe dans les dossier packages/
. La séquence de commandes à entrer est assez simple. Vous devez : initialiser les dépôts Git et Lerna, faire le premier commit puis commencer à importer des packages depuis des dépôts Git clonés localement. Vous pouvez trouver les instructions d’utilisation de base dans la documentation ici.
L’utilisation de lerna import
, ne vous permet que de suivre la 1ère ou 2ème stratégie décrite ci-dessus. Pour la 2ème stratégie, vous devez créer une branche séparée par dépôt à importer comme ceci :
# Importation du 1er package
git checkout -b package-1
lerna import /path/to/package-1
# Retour à la branche par défaut
git checkout master
# Importation du 2ème package
git checkout -b package-2
lerna import /path/to/package-2
# Puis fusionner les branches dans la branche par défaut...
lerna import
est un outil facile à utiliser pour migrer des dépôts vers un monorepo Lerna. Cependant, il simplifie l’historique des commit en supprimant les merge. Il ne migre pas non plus les tags et leurs messages. Malheureusement, ces limitations ne répondent pas à l’une de nos contraintes initiales : la sauvegarde d’un maximum d’informations des dépôts existants. Nous devons donc utiliser un autre outil.
La commande native git merge
permet de fusionner des historiques non liés en utilisant l’option --allow-unrelated-histories
. Elle préserve l’historique complet des commit d’une branche ciblée ainsi que ses tags. Cette commande nous permettra de réaliser la 3ème stratégie.
Fusionner l’historique d’un dépôt externe dans un dépôt courant avec --allow-unrelated-histories
tient en 2 commandes :
# Ajout du dépôt externe comme dépôt distant
git remote add -f <nom-du-repo-externe> <chemin-du-repo-externe>.
# Fusion de l'historique des commits
git merge --allow-unrelated-histories <external-repo-name>/<branch-name>
Réécriture des messages de commit
Pour mettre plus d’ordre et de transparence dans l’historique fraîchement combinés, nous pouvons préfixer les messages de chaque commit par le nom de leur package. De plus, cela nous permet de les rendre compatibles avec la spécification Conventional Commits que nous suivons dans nos derniers projets. Cette spécification standardise les messages de commit, les rendant plus lisibles et plus faciles à automatiser.
Pour mettre en œuvre cette spécification, nous devons réécrire tous les messages de commit en les préfixant par une chaîne de caractères telle que chore(
.
Nous avons choisi le préfixe
chore
pour rendre le commit compatible avec la spécification. Nous ne voulions pas faire d’expressions régulières complexes pour l’implémenter à 100%.
Il existe 2 outils qui permettent de réécrire les messages de commit :
git filter-branch
Une commande native de Git. Elle n’est pas recommandée officiellement et génère un avertissementWARNING: git-filter-branch has a glut of gotchas generating mangled history rewrites.
(“la commande comprend une série de pépins entraînant des réécritures de l’historique tronquées”, mentionné dans ce post).git filter-repo
Un outil tiers polyvalent qui permet de réécrire l’historique, officiellement recommandé par Git.
En suivant les recommandations de Git, nous choisissons la commande git filter-repo
. Après l’avoir installée en suivant ces instructions, la commande pour réécrire les messages de commit d’un dépôt est la suivante :
git filter-repo --message-callback 'return b "chore(<nom-du-package>) : " + message'
Pour voir plus d’exemples d’utilisation de réécriture d’historique avec git filter-repo
, vous pouvez suivre cette documentation.
Transfert des problèmes GitHub
Après avoir migré les dépôts et publié un nouveau monorepo sur GitHub, nous voulons désormais transférer les issues GitHub existantes sur les anciens dépôts. Les issues peuvent être transférées d’un dépôt à l’autre directement depuis l’interface GitHub. Vous pouvez suivre ce guide pour en savoir plus sur la démarche à suivre.
Malheureusement, au moment ou nous rédigeons cet article, il n’existe aucun moyen de réaliser un transfert groupé d’issues. Elles ne peuvent être transférées que une par une. Cela pourra cependant vous donner une excuse pour “oublier” de transférer certaines issues ennuyeuses en attente, créés par la communauté du projet ;)
Qu’en est-il des pull requests de GitHub ? Il y aura forcément une perte dont nous devrons nous accommoder. Le bon côté est que les liens entre certains problèmes mentionnés dans les commentaires ainsi que les pull requests qui y sont liées seront sauvegardés grâce à des redirections.
Script de migration
Le script bash de migration s’appuie sur les approches et les outils décrits ci-dessus. Il permet de générer le répertoire ./node-csv
contenant les fichiers du projet Node CSV réorganisés en monorepo Lerna.
#!/bin/sh
# 1. Configuration
repos=(
https://github.com/adaltas/node-csv
https://github.com/adaltas/node-csv-generate
https://github.com/adaltas/node-csv-parse
https://github.com/adaltas/node-csv-stringify
https://github.com/adaltas/node-stream-transform
)
monorepoDir=node-csv
packagesDir=packages
# 2. Initialisation d'un nouveau dépôt
rm -rf $monorepoDir && mkdir $monorepoDir && cd $monorepoDir
git init .
git remote add origin ${repos[0]}
# 3. Migration des dépôts
for repo in ${repos[@]}; do
# 3.1. Récupération du nom du package
splited=(${repo//// })
package=${splited[${#splited[@]}-1]/node-/}
# 3.2. Réécritures des messages de commit grâce à un dépôt temporaire
rm -rf $TMPDIR/$package && mkdir $TMPDIR/$package && git clone $repo $TMPDIR/$package
git filter-repo \
--source $TMPDIR/$package \
--target $TMPDIR/$package \
--message-callback "return b'chore(${package}): ' + message"
# 3.3. Fusion du dépôt avec le monorepo
git remote add -f $package $TMPDIR/$package
git merge --allow-unrelated-histories $package/master -m "chore(${package}): merge branch 'master' of ${repo}"
# 3.4. Déplacement des fichiers du dépôt vers le dossier packages
mkdir -p $packagesDir/$package
files=$(find . -maxdepth 1 | egrep -v ^./.git$ | egrep -v ^.$ | egrep -v ^./${packagesDir}$)
for file in ${files// /[@]}; do
mv $file $packagesDir/$package
done
git add .
git commit -m "chore(${package}): move all package files to ${packagesDir}/${package}"
# 3.5. Creation d'une nouvelle branche, eg "init/my_package"
git branch init/$package $package/master
done
# 4. Suppression des fichiers périmés des packages
rm $packagesDir/**/LICENSE
rm $packagesDir/**/CONTRIBUTING.md
rm $packagesDir/**/CODE_OF_CONDUCT.md
rm -rf $packagesDir/**/.github
git add .
git commit -m "chore: remove outdated packages files"
Pour exécuter ce script, il suffit de créer un fichier exécutable, par exemple avec le nom migrate.sh
, d’y coller le contenu du script, et de le lancer avec la commande :
chmod u+x ./migrate.sh
./migrate.sh
Remarque ! N’oubliez pas d’installer
git-filter-repo
avant d’exécuter le script.
Chaque étape du script comporte une explication :
1.
Configuration
Les variables de configuration définissent la liste des dépôts à migrer, le répertoire de destination du nouveau monorepo Lerna, et le dossier des packages qui s’y trouvent. Vous pouvez modifier ces variables pour réutiliser ce script pour votre projet.2.
Initialisation d’un nouveau dépôt
Nous créons le nouveau dépôts. Le premier dépôts référencé est aussi enregistré en tant que remoteorigin
.3.
Migration des dépôts3.1.
Récupération du nom du package
Il extrait les noms des packages à partir des liens de leurs dépôts. Dans notre cas, les dépôts sont préfixés avecnode-
que nous ne voulons pas garder.3.2.
Réécritures des messages de commit grâce à un dépôt temporaire
Pour ajouter un préfixe aux commits de chaque package en utilisant le formatchore(
, nous devons le faire individuellement pour chaque dépôt. C’est possible en clonant localement un dépôt dans un dossier temporaire.) : 3.3.
Fusion du dépôt avec le monorepo
Tout d’abord, nous ajoutons le dépôt cloné localement comme dépôt distant pour le monorepo. Ensuite, nous fusionnons son historique de commit en spécifiant un message pour le commit de merge.3.4.
Déplacement des fichiers du dépôt vers le dossier packages
Les fichiers du dépôt apparaissent sous le répertoire racine du monorepo après avoir été fusionnés. Pour suivre la structure que nous voulons atteindre, nous déplaçons ces fichiers vers le répertoirepackages
avant de réaliser un commit.
4.
Suppression des fichiers périmés des packages Dans un souci d’illustration, nous nettoyons les fichiers des packages qui sont désormais périmés suite à la migration. Certains de ses fichiers doivent être déplacer à la racine du dépôt.
Etapes suivantes
Le dépôt GIT est désormais créé et peut être qualifié de monorepo. Pour le rendre utilisable, des fichiers additionnels doivent être importés tels que le fichier package.json
racine, le fichier lerna.json
pour configuration de Lerna et un fichier README
. Référez-vous à notre premier article de cette série pour appliquer les changements requis et initialiser votre monorepo avec Lerna.
Conclusion
La migration de projets open-source existants doit être réalisée de façon ordonnée et méticuleuse. En effet, la moindre petite erreur pourra ruiner le travail de vos utilisateurs. Chaque étape doit être soigneusement analysée et surtout bien testée. Dans cet article, nous avons pu couvrir le travail nécessaire à la migration de plusieurs projets Node.js vers un monorepo Lerna. Nous avons présenté différentes approches, techniques et outils disponibles pour automatiser la migration sur l’exemple de notre projet open-source Node CSV.