Articles

Déplacement de plusieurs dépôts vers un mono-dépôt lerna-js

À mitter.io , nous avons quelques paquets publics npm que nous devons publier, et nous sommes récemment passés à une structure mono-dépôt gérée par Lerna d’avoir des référentiels séparés pour chacun d’eux. Aujourd’hui, j’aimerais partager notre expérience de cette migration et notre configuration avec la nouvelle structure monorepo. Tous nos packages sont soit des cibles SDK, soit des dépendances pour nos cibles SDK :

  • @mitter-io/core – La fonctionnalité de base du mitter.SDK io
  • @mitter-io/models – Les modèles typescript (classes, alias de type, interfaces, etc.) pour les SDK
  • @mitter-io/web – Le SDK web
  • @mitter-io/react-native – Le SDK natif React
  • @mitter-io/node – Le nœud.SDK js (utilisé pour le nœud.js backends)
  • @mitter-io/react-scl – La bibliothèque de composants standard pour les applications ReactJS

Tous nos paquets sont écrits en TypeScript et les typages sont fournis avec les paquets eux-mêmes, et nous ne distribuons pas de paquets de frappe séparés (ceux que vous voyez habituellement commençant par @types/). Nous utilisons rollup pour regrouper ces paquets dans les formats UMD et ES5. De plus, nous utilisons TypeDoc pour générer notre documentation qui est ensuite publiée sur un compartiment public dans AWS S3.

Avant d’utiliser Lerna, nous avions un référentiel séparé pour chacun de nos paquets et cela fonctionnait bien alors que nous n’avions que le SDK Web. Au fur et à mesure que nous progressions et que de plus en plus de développeurs travaillaient sur le SDK, nous avons commencé à faire face à quelques problèmes avec notre configuration :

  1. Étant donné que la majeure partie de la logique du SDK réside dans @mitter-io/core, presque toutes les modifications survenues dans le package core et tous les autres packages ont dû être mis à jour pour pointer vers la nouvelle version. Ainsi, même s’il y avait un bogue à corriger, par exemple React Native, la modification irait dans core, mais la mise à jour devait maintenant se refléter dans toutes les autres cibles, c’est-à-dire webnode et react-native. Il était assez courant pour un développeur de manquer une cible.
  2. Presque toutes les modifications apportées au SDK entraîneraient des modifications sur au moins 3 des 5 packages.
  3. Nous avons vu un énorme avantage à conserver la même version sur tous les packages (ce qui permet aux développeurs de deviner plus facilement quelle serait la dernière version de target), mais le suivi manuel devenait fastidieux.
  4. npm link (ou yarn link si vous préférez) avait son propre ensemble de problèmes pour s’assurer que toutes les dépendances étaient liées, puis dissociées pour utiliser la bonne de npm et revenir au lien local pour le développement.
  5. Il était assez courant d’exécuter des scripts sur des paquets (par ex., pour publier les documents typescript), et nous utilisions un échafaudage fragile de liens symboliques et de scripts bash pour gérer la même chose.

À cette époque, nous sommes tombés sur Lerna et il semblait être la solution parfaite pour nos besoins.

Nous avons décidé de suivre le chemin le plus simple qui soit, en essayant d’utiliser autant que possible les valeurs par défaut. D’après ce que nous avons vécu, migrer vers Lerna a été un jeu d’enfant. Commencez par créer un nouveau dépôt Lerna:

mkdir my-new-monorepo && cd my-new-monorepo
git init .
lerna init

Répondez à quelques questions simples (où nous avons toujours eu recours à la valeur par défaut) et vous êtes prêt. Déplacer nos anciens paquets de leurs dépôts vers le nouveau (que nous redoutions car nous pensions que ce serait une douleur énorme) était beaucoup plus facile que prévu:

lerna import ~/projects/my-single-repo-package-1 --flatten

NOTEZ que le --flatten peut ou non être nécessaire, mais nous avons rencontré des problèmes sans cela.

Ce qui est étonnant avec Lerna, c’est qu’il apporte tous les commits git avec lui (vous risquez de perdre un peu d’historique avec --flatten), de sorte que pour le nouveau dépôt, l’historique semble avoir toujours été développé dans ce monorepo. C’est absolument essentiel car vous allez avoir besoin de git blame quelqu’un pour un bug que vous avez découvert après le passage au monorepo.

Configuration actuelle

Avec Lerna, nous gérons désormais un référentiel unique pour tous nos paquets, avec une structure de répertoires qui ressemble à ceci:

packages/
core/
models/
node/
react-native/
web/
lerna.json
package.json

Pour publier les paquets modifiés, il suffit maintenant de :

lerna boostrap
lerna publish

Vous n’avez pas à le faire lerna bootstrap à chaque fois ; seulement si c’est la première fois que vous consultez le dépôt. Ce qu’il fait, c’est simplement installer toutes les dépendances de chacun des paquets sous ce dépôt.

Dans le même temps, nous avons également décidé de rationaliser un peu notre processus et d’ajouter toutes les tâches d’emballage au sein du cycle de vie npm lui-même. Notez que cela n’a rien à voir avec Lerna; c’est quelque chose qui devrait idéalement être présent dans n’importe quel package npm, quelle que soit la structure de dépôt. Pour chacun des paquets, les scripts suivants sont présents dans l’individu pacakge.json :

"scripts": {
...
"prepare": "yarn run build",
"prepublishOnly": "./../../ci-scripts/publish-tsdocs.sh"
...
}

Cela construit le paquet avec le compilateur typescript, le regroupe avec rollup et génère des documents avec typedoc:

"scripts": {
...
"build": "tsc --module commonjs && rollup -c rollup.config.ts && typedoc --out docs --target es6 --theme minimal --mode file src"
...
}

Avoir une structure de dépôt unique vous permet également de conserver les scripts communs en un seul endroit afin que les modifications s’appliquent à tous les paquets (nous devrions également déplacer le script de construction vers un script séparé, étant donné qu’il est maintenant devenu une commande bash assez complexe).

Le flux de développeurs

Le flux de développeurs en dehors des versions est inchangé. Un développeur crée un problème sur GitLab (ou s’en voit attribuer un), crée une nouvelle branche pour le problème, puis fusionne les modifications dans master après un examen du code. Le cycle de vie des versions suit désormais un processus extrêmement structuré:

  1. Lorsqu’un jalon est terminé et que nous prévoyons de faire une nouvelle version, l’un des développeurs (en charge de cette version particulière) crée une nouvelle version en exécutant lerna version.
  2. Lerna fournit une invite extrêmement utile et facile à utiliser pour déterminer la prochaine version
(master) mitter-js-sdk ツ lerna version --force-publish
lerna notice cli v3.8.1
lerna info current version 0.6.2
lerna info Looking for changed packages since v0.6.2
? Select a new version (currently 0.6.2) (Use arrow keys)
❯ Patch (0.6.3)
Minor (0.7.0)
Major (1.0.0)
Prepatch (0.6.3-alpha.0)
Preminor (0.7.0-alpha.0)
Premajor (1.0.0-alpha.0)
Custom Prerelease
Custom Version

Une fois qu’une nouvelle version est sélectionnée, Lerna modifie les versions des paquets, crée une balise dans le dépôt distant et pousse les modifications vers notre instance GitLab. Au-delà de cela, les développeurs ne sont pas tenus de faire autre chose. Notre CI est configuré pour construire toutes les balises qui ont un nom similaire à un numéro de version sémantique.

REMARQUE Nous exécutons lerna version avec --force-publish parce que nous voulons que tous les paquets aient exactement la même lignée de versions. Donc, parfois, nous aurons des paquets qui ne diffèrent pas entre les différentes versions. Selon vos préférences, vous pouvez choisir de ne pas le faire.

La configuration CI

Nous utilisons le CI intégré de GitLab pour la construction, les tests et la publication de tous nos projets (JS et Java). Pour le nouveau monorepo JS, nous avons deux étapes :

  1. Build
  2. Publish

La phase de construction est extrêmement simple et exécute les deux scripts suivants :

lerna bootstrap
lerna run build

Cette phase s’exécute sur chaque commit pour valider essentiellement la santé mentale du paquet. La phase de publication, d’autre part, exécute les opérations suivantes:

git checkout master
lerna bootstrap
git reset --hard
lerna publish from-package --yes

Nous avons compris que nous devions faire un git checkout master et un git reset --hard car GitLab clone (ou récupère, selon la configuration) le dépôt, et vérifie ensuite le commit à construire. Cela définit le répertoire de travail en mode « TÊTE détachée », c’est-à-dire que la ref HEAD ne pointe nulle part. Lerna utilise HEAD pour déterminer la version actuelle du paquet et les erreurs dans l’état de tête détaché.

Nous devons également exécuter lerna publish from-package par opposition à lerna publish, car l’exécution d’un simple lerna publish aurait Lerna se plaignant que la version actuelle est déjà publiée, en tant que métadonnées a été mis à jour lorsque le développeur a exécuté lerna version localement. L’argument from-package indique à Lerna de publier toutes les versions qui ne sont pas actuellement présentes dans npm pour un paquet donné. Cela aide également si une publication a échoué pour une raison quelconque et que vous réessayez le pipeline.

La phase de publication est configurée pour s’exécuter uniquement sur des balises correspondant au crédit d’expression régulière suivant :

^v(0|\d*)\.(0|\d*)\.(0|\d*)(-(0|\d*|\d**)(\.(0|\d*|\d**))*)?(\++(\.+)*)?$

C’est un peu chic, et pour la plupart des équipes et à la plupart des fins, simplement ^v*$ devrait fonctionner. 🙂

REMARQUE Bien que nous ne l’ayons pas encore fait, puisque nous sommes une petite équipe, on peut également marquer toutes les balises suivant l’expression régulière ci-dessus comme protégées dans GitLab pour restreindre qui peut publier des paquets dans npm.

Vous pouvez consulter notre monorepo à https://github.com/mitterio/js-sdk (Ceci est reflété à partir de notre dépôt GitLab interne).

Notes rapides

Lors de l’exécution de scripts courants (comme nous le faisons pour la publication de documents typescript), il est très utile de connaître les détails du paquet exécutant le script. Cela s’applique aux scripts du cycle de vie npm, ainsi qu’aux scripts que l’on peut exécuter en utilisant lerna run ou lerna exec. Pour un paquet donné dans npm, npm rend l’intégralité de package.json disponible pour un script utilisant des variables d’environnement. Ainsi, pour un paquet donné avec le package.json:

{
"name": "@mitter-io/core",
"version": "0.6.28",
"repository": {
"type": "git"
}
}

Les variables suivantes seront disponibles lors de l’exécution de n’importe quel script de cycle de vie :

npm_package_name=@mitter-io/core
npm_package_version=0.6.28
npm_package_repository_type=git

Bizarreries/problèmes

Quelques choses sur lesquelles nous travaillons toujours avec le nouveau script de cycle de vie setup (certains d’entre eux sont des problèmes, alors que d’autres que nous ne connaissons probablement pas mieux):

  • Je ne sais pas si c’est possible, mais nous aimerions pouvoir avoir des scripts de cycle de vie communs pour tous nos paquets. Les déclarer dans la racine package.json ne fonctionne pas.
  • Il est extrêmement difficile de tester complètement votre configuration Lerna sans publier quelque chose dans npm. Je ne sais pas s’il y a un --dry-run quelque part.
  • Lerna a un moyen de conserver un bloc de configuration commun pour devDependencies afin que tous les devDependencies soient de la même version pour chacun des sous-paquets. C’est une fonctionnalité assez cool, mais cela nous prendrait un certain temps pour éliminer tous les courants.
  • La même chose pourrait également s’appliquer pour d’autres dépendances, donc bien que nous ne voulions pas d’un bloc de configuration dependencies commun, avoir un moyen d’exprimer les variables disponibles à travers les projets serait bien. Par exemple, dans notre monorepo Java/Kotlin, nous utilisons gradle.properties pour contenir des variables comme springBootVersionspringCoreVersion, etc., qui sont ensuite utilisés par les scripts gradle individuels.

Nos réflexions sur les monorepos
Il y a eu un débat assez houleux récemment avec les monorepos et si nous voyons un grand nombre de personnes sauter à nouveau dans le train en marche, ce qui rappelle tout à fait l’époque où les microservices étaient à la mode.

La structure que nous suivons ici comporte plusieurs monorepos, et ce n’est pas la première fois que nous gérons des monorepos. L’ensemble de notre plate-forme et de notre backend est un monorepo qui contient du code privé déployable et plusieurs packages publics publiés sur bintray. Nous avons également notre site Web principal fonctionnant avec un backend Spring, avec le frontend fourni avec webpack prenant en charge le rechargement à chaud (webpack watch), etc. Nous n’avons jamais décidé d’opter pour un seul mono-repo dans toute l’organisation car l’outillage n’était tout simplement pas là.

Avoir la majeure partie de notre code Java dans un seul dépôt fonctionne très bien car gradle fournit tous les outils nécessaires pour le monorepo Java et lerna et le cycle de vie npm fournissant les outils pour le monorepo du SDK JS. Donc, en termes simples, les monorepos sont parfaits une fois que vous avez identifié la couverture des modifications apportées à votre dépôt. Pour notre backend Java, nous avons vu plusieurs MRS à travers des projets pour une seule fonctionnalité, ce qui nous a incités à passer à un monorepo uniquement pour ce projet particulier, avec tout notre autre code toujours dans des dépôts séparés. Et une fois que nous avons vu un modèle similaire émerger pour nos SDK JS, nous avons déménagé à Lerna.

Notez que nous sommes une petite équipe d’environ 9 ingénieurs; donc ce qui fonctionne pour nous pourrait ne pas fonctionner pour des équipes de tailles différentes. Ce que nous voudrions surtout souligner, c’est que l’adoption de toute solution ne doit pas nécessairement être binaire, dans laquelle soit nous le faisons comme prescrit, soit nous ne le faisons pas du tout.

Certaines des motivations que nous avons vues pour un monorepo s’appliquaient définitivement à nous et beaucoup d’entre elles ne l’ont pas fait. Par exemple, nous ne pouvons tout simplement pas perdre le temps de construire l’outillage si l’ensemble de notre base de code a été déplacé vers un seul dépôt — quel que soit l’avantage que nous pouvons ou non ressentir. Le débat n’est donc pas vraiment d’avoir un « dépôt unique » — en soi, ce n’est rien de plus qu’une nouvelle structure de répertoires. La prescription d’entre eux est d’atténuer certains problèmes et comme pour chaque « solution miracle », il y a des mises en garde.

Le débat porte sur les problèmes communs rencontrés dans l’industrie du logiciel et sur les solutions couramment adoptées; « commun » étant le mot-clé. Le domaine où vous vous écartez de l’application « commune » est celui où vous pouvez innover, apporter des modifications et construire un peu.