Articles

Wechsel von mehreren Repositories zu einem lerna-js Mono-repo

At mitter.io , wir haben ein paar öffentlich zugängliche npm Pakete, die wir veröffentlichen müssen, und wir sind kürzlich zu einer Mono-Repo-Struktur gewechselt, die von Lerna verwaltet wird, da wir für jedes einzelne von ihnen separate Repositorys haben. Heute möchte ich unsere Erfahrungen mit dieser Migration und unserem Setup mit der neuen Monorepo-Struktur teilen. Alle unsere Pakete sind entweder SDK-Ziele oder Abhängigkeiten zu unseren SDK-Zielen:

  • @mitter-io/core – Die Kernfunktionalität des mitter.io SDKs
  • @mitter-io/models – Die Typescript-Modelle (Klassen, Typaliase, Schnittstellen usw.) für die SDKs
  • @mitter-io/web – Das Web SDK
  • @mitter-io/react-native – Das React Native SDK
  • @mitter-io/node – Der Knoten.js SDK (für Knoten verwendet.js-Backends)
  • @mitter-io/react-scl – Die Standardkomponentenbibliothek für ReactJS-Anwendungen

Alle unsere Pakete sind in TypeScript geschrieben und Typisierungen werden mit den Paketen selbst gebündelt, und wir verteilen keine separaten Typisierungspakete (die normalerweise mit @types/ beginnen). Wir verwenden Rollup, um diese Pakete sowohl im UMD- als auch im ES5-Modulformat zu bündeln. Darüber hinaus verwenden wir TypeDoc, um unsere Dokumentation zu generieren, die dann in einem öffentlichen Bucket in AWS S3 veröffentlicht wird.

Bevor wir Lerna benutzten, hatten wir ein separates Repository für jedes unserer Pakete und es funktionierte gut, während wir nur das Web SDK hatten. Als wir Fortschritte machten und mehr Entwickler am SDK arbeiteten, begannen wir mit ein paar Problemen mit unserem Setup:

  1. Da sich der größte Teil der SDK-Logik in @mitter-io/core , fast jede Änderung, die im core Paket und allen anderen Paketen auftrat, musste aktualisiert werden, um auf die neue Version zu verweisen. Selbst wenn es einen Fehler gäbe, der beispielsweise für React Native behoben werden sollte, würde die Änderung in core , aber das Update müsste sich jetzt in allen anderen Zielen widerspiegeln, dh webnode und react-native. Es war durchaus üblich, dass ein Entwickler ein Ziel verfehlte.
  2. Fast jede Änderung im SDK würde zu Änderungen in mindestens 3 der 5 Pakete führen.
  3. Wir sahen einen großen Vorteil darin, die gleiche Version über alle Pakete hinweg beizubehalten (was es Entwicklern erleichtert, die neueste Version von target zu erraten), aber das manuelle Verfolgen wurde immer umständlicher.
  4. npm link (oder yarn link wenn Sie es vorziehen) hatte seine eigenen Probleme damit, sicherzustellen, dass alle Abhängigkeiten verknüpft und dann nicht verknüpft waren, um die richtige von npm und zurück zum lokalen Link für die Entwicklung zu verwenden.
  5. Es war durchaus üblich, Skripte über Pakete hinweg auszuführen (z., um die Typescript-Dokumente zu veröffentlichen), und wir verwendeten ein fragiles Gerüst aus Symlinks und Bash-Skripten, um dasselbe zu verwalten.

Ungefähr zu dieser Zeit stießen wir auf Lerna und es schien die perfekte Lösung für unsere Anforderungen zu sein.

Wir haben uns für den einfachsten Weg entschieden und versucht, die Standardeinstellungen so weit wie möglich zu verwenden. Nach allem, was wir erlebt haben, war die Migration nach Lerna ein Kinderspiel. Beginnen Sie mit der Erstellung eines neuen Lerna-Repos:

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

Beantworten Sie ein paar einfache Fragen (wobei wir immer auf den Standard zurückgegriffen haben) und Sie sind fertig. Das Verschieben unserer alten Pakete von ihren Repos auf das neue (was wir fürchteten, da wir dachten, es wäre ein massiver Schmerz) war viel einfacher als erwartet:

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

BEACHTEN Sie, dass die --flatten möglicherweise erforderlich ist oder nicht, aber wir hatten Probleme ohne sie.

Das Erstaunliche an Lerna ist, dass es alle Git-Commits mitbringt (Sie könnten etwas Geschichte mit --flatten verlieren), so dass für das neue Repo die Geschichte so aussieht, als hätte die Entwicklung in diesem Monorepo immer stattgefunden. Dies ist absolut notwendig, da Sie git blame jemanden für einen Fehler benötigen, den Sie nach dem Wechsel zum Monorepo entdeckt haben.

Aktuelles Setup

Mit Lerna verwalten wir jetzt ein einziges Repository für alle unsere Pakete mit einer Verzeichnisstruktur, die wie folgt aussieht:

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

Um die geänderten Pakete zu veröffentlichen, müssen wir jetzt einfach:

lerna boostrap
lerna publish

Sie müssen nicht jedes Mal lerna bootstrap tun; nur wenn dies das erste Mal ist, dass Sie das Repo auschecken. Was es tut, ist einfach alle Abhängigkeiten von jedem der Pakete unter diesem Repo zu installieren.

Gleichzeitig haben wir beschlossen, unseren Prozess ein wenig zu rationalisieren und alle Verpackungsaufgaben innerhalb des npm Lebenszyklus selbst hinzuzufügen. Beachten Sie, dass dies nichts mit Lerna zu tun hat; dies sollte idealerweise in jedem npm-Paket vorhanden sein, unabhängig von der Repo-Struktur. Für jedes der Pakete sind die folgenden Skripte in den einzelnen pacakge.json :

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

Dies erstellt das Paket mit dem Typescript-Compiler, bündelt es mit Rollup und generiert Dokumente mit typedoc:

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

Wenn Sie eine einzige Repo-Struktur haben, können Sie auch allgemeine Skripte an einem einzigen Ort aufbewahren, sodass Änderungen für alle Pakete gelten (wir sollten auch das Build-Skript in ein separates Skript verschieben, da es jetzt zu einem ziemlich komplexen Bash-Befehl geworden ist).

Der Entwicklerfluss

Der Entwicklerfluss ist, abgesehen von Releases, unverändert. Ein Entwickler erstellt ein Problem in GitLab (oder erhält eines zugewiesen), erstellt einen neuen Zweig für das Problem und führt die Änderungen nach einer Codeüberprüfung mit dem Master zusammen. Der Release Lifecycle folgt nun einem extrem strukturierten Prozess:

  1. Wenn ein Meilenstein erreicht ist und wir eine neue Version planen, erstellt einer der Entwickler (verantwortlich für diese bestimmte Version) eine neue Version, indem er lerna version .
  2. Lerna bietet eine äußerst hilfreiche und einfach zu bedienende Eingabeaufforderung, um die nächste Version herauszufinden
(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

Sobald eine neue Version ausgewählt ist, ändert Lerna die Versionen der Pakete, erstellt ein Tag im Remote-Repo und überträgt die Änderungen an unsere GitLab-Instanz. Darüber hinaus müssen Entwickler nichts anderes tun. Unser CI ist so eingerichtet, dass alle Tags erstellt werden, deren Name einer semantischen Versionsnummer ähnelt.

HINWEIS Wir führen lerna version mit --force-publish aus, weil wir möchten, dass alle Pakete genau die gleiche Versionslinie haben. Manchmal haben wir also Pakete, die sich nicht zwischen verschiedenen Versionen unterscheiden. Abhängig von Ihren Vorlieben können Sie dies möglicherweise nicht tun.

Das CI-Setup

Wir verwenden gitlabs integriertes CI zum Erstellen, Testen und Veröffentlichen in all unseren Projekten (JS und Java). Für den neuen JS Monorepo haben wir zwei Phasen:

  1. Build
  2. Publish

Die Build-Phase ist extrem einfach und führt die folgenden zwei Skripte aus:

lerna bootstrap
lerna run build

Diese Phase wird bei jedem einzelnen Commit ausgeführt, um die Richtigkeit des Pakets im Wesentlichen zu überprüfen. In der Veröffentlichungsphase wird dagegen Folgendes ausgeführt:

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

Wir haben herausgefunden, dass wir ein git checkout master und ein git reset --hard weil GitLab das Repo klont (oder abruft, abhängig von der Konfiguration) und dann das Commit überprüft, das ausgeführt werden soll gebaut. Dadurch wird das Arbeitsverzeichnis in einen „detached HEAD“ -Modus versetzt, dh der ref HEAD zeigt nirgendwo hin. Lerna verwendet HEAD , um die aktuelle Version des Pakets und Fehler im Zustand des getrennten Kopfes herauszufinden.

Wir müssen auch lerna publish from-package im Gegensatz zu lerna publish ausführen, da die Ausführung eines einfachen lerna publish Lerna beschwert hätte, dass die aktuelle Version bereits veröffentlicht wurde, da die Metadaten aktualisiert wurden, als der Entwickler lerna version lokal. Dasfrom-package Argument weist Lerna an, alle Versionen zu veröffentlichen, die derzeit nicht in npm für ein bestimmtes Paket vorhanden sind. Dies hilft auch, wenn eine Veröffentlichung aus irgendeinem Grund fehlgeschlagen ist und Sie die Pipeline erneut versuchen.

Die Veröffentlichungsphase ist so konfiguriert, dass sie nur für Tags ausgeführt wird, die dem folgenden regulären Ausdruck entsprechen:

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

Dies ist ein bisschen schick und für die meisten Teams und für die meisten Zwecke sollte einfach ^v*$ funktionieren. 🙂

HINWEIS Obwohl wir es noch nicht getan haben, da wir ein kleines Team sind, könnte man auch alle Tags, die dem obigen regulären Ausdruck folgen, in GitLab als geschützt markieren, um einzuschränken, wer Pakete in npm veröffentlichen kann.

Sie können unser Monorepo unter https://github.com/mitterio/js-sdk (Dies wird von unserem internen GitLab-Repo gespiegelt) .

Kurznotizen

Wenn Sie gängige Skripte ausführen (wie wir es bei der Veröffentlichung von Typescript-Dokumenten tun), ist es sehr nützlich, die Einzelheiten des Pakets zu kennen, in dem das Skript ausgeführt wird. Dies gilt sowohl für Skripte im npm-Lebenszyklus als auch für Skripte, die mit lerna run oder lerna exec . Für ein bestimmtes Paket in npm stellt npm einem Skript mithilfe von Umgebungsvariablen die gesamte package.json zur Verfügung. Also, für ein gegebenes Paket mit den folgenden package.json:

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

Die folgenden Variablen sind verfügbar, während ein beliebiges Lifecycle-Skript ausgeführt wird:

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

Macken/ Probleme

Ein paar Dinge, an denen wir noch mit dem neuen Setup arbeiten (einige davon sind wir es wahrscheinlich nicht besser wissen):

  • Ich bin mir nicht sicher, ob dies möglich ist, aber wir möchten in der Lage sein, gemeinsame Lebenszyklusskripte für alle unsere Pakete zu haben. Das Deklarieren dieser im Stamm package.json funktioniert nicht.
  • Es ist extrem schwierig, Ihr Lerna-Setup vollständig zu testen, ohne tatsächlich etwas in npm zu veröffentlichen. Ich bin mir nicht sicher, ob es irgendwo eine --dry-run gibt.
  • Lerna hat eine Möglichkeit, einen gemeinsamen Konfigurationsblock für devDependencies zu behalten, so dass alle devDependencies für jedes der Unterpakete die gleiche Version haben. Dies ist ein ziemlich cooles Feature, aber wir würden einige Zeit brauchen, um alle gängigen auszusortieren.
  • Das gleiche könnte auch für andere Abhängigkeiten gelten, also während wir keinen gemeinsamen dependencies Konfigurationsblock wollen, wäre es schön, eine Möglichkeit zu haben, Variablen über die Projekte hinweg auszudrücken. In unserem Java/ Kotlin-Monorepo verwenden wir beispielsweise gradle.properties , um Variablen wie springBootVersionspringCoreVersion usw. zu enthalten., die dann von den einzelnen Gradle-Skripten verwendet werden.

Unsere Gedanken zu Monorepos
Es war in letzter Zeit eine ziemlich hitzige Debatte mit Monorepos und ob wir wieder eine große Anzahl auf den Zug aufspringen sehen, die ziemlich an die Zeit erinnert, als Microservices der letzte Schrei waren.

Die Struktur, der wir hier folgen, besteht darin, mehrere Monorepos zu haben, und dies ist nicht
unser erstes Mal, dass wir Monorepos verwalten. Unsere gesamte Plattform und unser Backend sind ein Monorepo, das privaten, bereitstellbaren Code und mehrere öffentlich zugängliche Pakete enthält, die in Bintray veröffentlicht werden. Wir haben auch unsere Hauptwebsite mit einem Spring-Backend, wobei das Frontend mit einem Webpack gebündelt ist, das das Hot-Reloading unterstützt (webpack watch) usw. Wir haben uns nie für ein einzelnes Mono-Repo in der gesamten Organisation entschieden, weil das Tooling einfach nicht vorhanden war.

Den größten Teil unseres Java-Codes in einem einzigen Repo zu haben, funktioniert hervorragend, da gradle alle für den Java-Monorepo erforderlichen Werkzeuge und lerna und der npm-Lebenszyklus die Werkzeuge für den Monorepo des JS SDK bereitstellen. Einfach gesagt, Monorepos sind großartig, sobald Sie die Abdeckung der Änderungen in Ihrem Repo identifiziert haben. Für unser Java-Backend sahen wir mehrere Änderungen in Projekten für ein einzelnes Feature, was uns dazu veranlasste, nur für dieses bestimmte Projekt zu einem Monorepo zu wechseln, wobei sich unser gesamter anderer Code noch in separaten Repos befand. Und als wir ein ähnliches Muster auch für unsere JS SDKs sahen, zogen wir zu Lerna.

Beachten Sie, dass wir ein kleines Team von etwa 9 Ingenieuren sind; Was für uns funktioniert, funktioniert möglicherweise nicht für Teams unterschiedlicher Größe. Wir möchten vor allem darauf hinweisen, dass die Annahme einer Lösung nicht binär sein muss, wobei wir es entweder wie vorgeschrieben tun oder überhaupt nicht.

Einige der Motivationen, die wir für einen Monorepo sahen, trafen definitiv auf uns zu und viele von ihnen nicht. Zum Beispiel können wir einfach nicht die Zeit aufwenden, um die Werkzeuge zu erstellen, wenn unsere gesamte Codebasis in ein einziges Repo verschoben wurde — unabhängig davon, welchen Nutzen wir haben oder nicht. In der Debatte geht es also wirklich nicht darum, ein „einzelnes Repo“ zu haben — an sich ist es nichts anderes als eine neue Verzeichnisstruktur. Die Verschreibung von ihnen ist es, bestimmte Probleme zu lindern und wie bei jeder „Silberkugel“ gibt es Vorbehalte.

In der Debatte geht es um häufige Probleme in der Softwareindustrie und welche Lösungen häufig gefunden wurden; „common“ ist das Schlüsselwort. Der Bereich, in dem Sie von der „üblichen“ Anwendung abweichen, ist der Ort, an dem Sie Innovationen entwickeln, Änderungen vornehmen und ein wenig aufbauen können.