JS monorepos en prod 5 : fusion de plusieurs dépôts Git et préservation des commits

JS monorepos en prod 5 : fusion de plusieurs dépôts Git et préservation des commits

KUDINOV Sergei

By KUDINOV Sergei

21 mai 2021

Catégories : DevOps & SRE, Node.js | Tags : Bash, DevOps, Git, GitHub, JavaScript, Packaging, GitOps, Monorepo [plus][moins]

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 :

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.

Stratégies de logs Git

  • 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 :

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 remote origin.
  • 3. Migration des dépôts
    • 3.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 avec node- 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 format chore() : , 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épertoire packages 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.

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.