Articles

Passare da più repository a un lerna-js mono-repo

At mitter.io, abbiamo un paio di pacchetti npm che dobbiamo pubblicare, e recentemente ci siamo trasferiti in una struttura mono-repo gestita da Lerna da avere repository separati per ognuno di essi. Oggi, vorrei condividere la nostra esperienza di questa migrazione e la nostra configurazione con la nuova struttura monorepo. Tutti i nostri pacchetti sono target SDK o sono dipendenze per i nostri target SDK:

  • @mitter-io/core – La funzionalità principale del mitter.io SDK
  • @mitter-io/models – I modelli dattiloscritti (classi, alias di tipo, interfacce ecc.) per gli SDK
  • @mitter-io/web – Il web SDK
  • @mitter-io/react-native – Il React Native SDK
  • @mitter-io/node – Il nodo.js SDK (utilizzato per il nodo.js backend)
  • @mitter-io/react-scl La libreria di componenti per ReactJS applicazioni

Tutti i nostri pacchetti sono scritti a Macchina e tipizzazioni sono in bundle con pacchetti di per sé, e noi non distribuire separato digitando pacchetti (quelli che si vedono solitamente iniziano con @types/). Usiamo rollup per raggruppare questi pacchetti in entrambi i formati di modulo UMD e ES5. Oltre a questo, usiamo TypeDoc per generare la nostra documentazione che viene poi pubblicata su un bucket pubblico in AWS S3.

Prima di usare Lerna, avevamo un repository separato per ciascuno dei nostri pacchetti e funzionava bene mentre avevamo solo l’SDK web. Come abbiamo progredito e più sviluppatori al lavoro sul SDK, abbiamo iniziato ad affrontare alcuni problemi con il nostro programma di installazione:

  1. Dato che la maggior parte dell’SDK logica risiede nel @mitter-io/core, quasi ogni cambiamento che si è verificato nel core pacchetto e tutti gli altri pacchetti dovuto essere aggiornato per scegliere la nuova versione. Quindi, anche se ci fosse un bug che doveva essere risolto, ad esempio React Native, la modifica andrebbe in core, ma l’aggiornamento doveva ora riflettere in tutti gli altri target, cioè webnode e react-native. Era abbastanza comune per uno sviluppatore perdere un obiettivo.
  2. Quasi ogni cambiamento nell’SDK comporterebbe cambiamenti in almeno 3 dei 5 pacchetti.
  3. Abbiamo visto un enorme vantaggio nel mantenere la stessa versione tra i pacchetti (rende più facile per gli sviluppatori indovinare quale sarebbe stata l’ultima versione di target), ma il tracciamento manuale stava diventando ingombrante.
  4. npm link (o yarn link se preferisci) aveva il suo set di problemi con l’assicurarsi che tutte le dipendenze fossero collegate, quindi scollegate per usare quella corretta da npm e tornare al link locale per lo sviluppo.
  5. Era abbastanza comune eseguire script tra pacchetti (ad esempio, pubblicare i documenti dattiloscritti, e stavamo usando una impalcatura fragile di symlinks e scripts bash per maneggiare lo stesso.

In quel periodo, ci siamo imbattuti in Lerna e sembrava essere la soluzione perfetta per le nostre esigenze.

Abbiamo deciso di seguire il percorso più semplice che ci sia, cercando di utilizzare i valori predefiniti il più possibile. Da quello che abbiamo vissuto, migrare a Lerna è stato un gioco da ragazzi. Inizia creando un nuovo repo Lerna:

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

Rispondi a un paio di semplici domande (dove abbiamo sempre fatto ricorso al default) e sei a posto. Spostare i nostri vecchi pacchetti dai loro repository a quello nuovo (che temevamo come pensavamo sarebbe stato un enorme dolore) è stato molto più facile del previsto:

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

NOTA Il --flatten potrebbe essere richiesto o meno, ma abbiamo affrontato problemi senza di esso.

La cosa sorprendente di Lerna è che porta con sé tutti i commit git (potresti perdere un po ‘ di storia con--flatten), in modo tale che per il nuovo repository, la storia sembra che lo sviluppo sia sempre avvenuto in questo monorepo. Questo è assolutamente essenziale perché avrai bisogno digit blame qualcuno per un bug che hai scoperto dopo esserti trasferito al monorepo.

Configurazione corrente

Con Lerna, ora gestiamo un singolo repository per tutti i nostri pacchetti, con una struttura di directory simile a questa:

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

Per pubblicare i pacchetti modificati, ora dobbiamo semplicemente:

lerna boostrap
lerna publish

Non devi farelerna bootstrap ogni volta; solo se questa è la prima volta che stai controllando il repository. Quello che fa è semplicemente installare tutte le dipendenze di ciascuno dei pacchetti sotto questo repository.

Allo stesso tempo, abbiamo anche deciso di semplificare un po ‘ il nostro processo e abbiamo aggiunto tutte le attività di packaging all’interno del ciclo di vita npm stesso. Si noti che questo non ha nulla a che fare con Lerna; questo è qualcosa che dovrebbe idealmente essere presente in qualsiasi pacchetto npm indipendentemente dalla struttura del repository. Per ciascuno dei pacchetti, i seguenti script sono presenti nel singolopacakge.json:

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

Questo crea il pacchetto con il compilatore typescript, lo raggruppa con rollup e genera documenti con typedoc:

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

Avere una singola struttura repo consente anche di mantenere script comuni in un unico posto in modo che le modifiche si applichino a tutti i pacchetti (dovremmo anche spostare lo script di compilazione in uno script separato, dato che ora è diventato un comando bash piuttosto complesso).

Il flusso di sviluppo

Il flusso di sviluppo a parte le versioni è invariato. Uno sviluppatore crea un problema su GitLab (o ne viene assegnato uno), crea un nuovo ramo per il problema e quindi unisce le modifiche a master dopo una revisione del codice. Il ciclo di vita del rilascio segue ora un processo estremamente strutturato:

  1. Quando una pietra miliare è completata e stiamo progettando di creare una nuova versione, uno degli sviluppatori (responsabile di quella particolare versione) crea una nuova versione eseguendo lerna version.
  2. Lerna fornisce un prompt estremamente utile e facile da usare per capire la prossima versione
(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

Una volta selezionata una nuova versione, Lerna cambia le versioni dei pacchetti, crea un tag nel repository remoto e invia le modifiche alla nostra istanza GitLab. Oltre a questo, gli sviluppatori non sono tenuti a fare altro. Il nostro CI è impostato per costruire tutti i tag che hanno un nome simile a un numero con versione semantica.

NOTA Eseguiamo lerna versioncon --force-publish perché vogliamo che tutti i pacchetti abbiano lo stesso identico lignaggio di versioni. Quindi a volte avremo pacchetti che non differiscono tra le diverse versioni. A seconda delle preferenze, si potrebbe scegliere di non farlo.

Il setup CI

Usiamo il CI integrato di GitLab per costruire, testare e pubblicare su tutti i nostri progetti (JS e Java). Per il nuovo JS monorepo, abbiamo due fasi:

  1. Build
  2. Publish

La fase di build è estremamente semplice ed esegue i seguenti due script:

lerna bootstrap
lerna run build

Questa fase viene eseguita su ogni singolo commit per convalidare essenzialmente la sanità mentale del pacchetto. La fase di pubblicazione, d’altra parte, esegue quanto segue:

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

Abbiamo capito che dovevamo fare un git checkout master e un git reset --hard perché GitLab cloni (o recupera, a seconda della configurazione) repo, e poi controlla il commit che verrà costruito. Questo imposta la directory di lavoro in una modalità “TESTA distaccata”, cioè il ref HEAD non punta da nessuna parte. Lerna usa HEAD per capire la versione corrente del pacchetto e gli errori nello stato head distaccato.

Abbiamo anche bisogno di eseguire lerna publish from-package anziché lerna publish, come l’esecuzione di un semplice lerna publish avrebbe Lerna, lamentandosi che la versione attuale è già pubblicato, come i metadati è stato aggiornato quando lo sviluppatore ran lerna version a livello locale. L’argomentofrom-package dice a Lerna di pubblicare tutte le versioni che non sono attualmente presenti in npm per un determinato pacchetto. Questo aiuta anche se una pubblicazione non è riuscita per qualche motivo e stai riprovando la pipeline.

La fase di pubblicazione è configurata per essere eseguita solo su tag che corrispondono al seguente credito regex:

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

Questo è un po ‘ stravagante, e per la maggior parte dei team e per la maggior parte degli scopi, semplicemente^v*$ dovrebbe funzionare. 🙂

NOTA Anche se non l’abbiamo ancora fatto, dato che siamo una piccola squadra, si potrebbe anche contrassegnare qualsiasi tag seguendo la regex sopra come protetto in GitLab per limitare chi può pubblicare pacchetti su npm.

Puoi controllare il nostro monorepo ahttps://github.com/mitterio/js-sdk (Questo è rispecchiato dal nostro repository GitLab interno).

Note rapide

Quando si eseguono script comuni (come facciamo per la pubblicazione di documenti dattiloscritti), è molto utile conoscere i dettagli del pacchetto che esegue lo script. Questo vale per gli script nel ciclo di vita di npm, così come gli script che si potrebbero eseguire usando lerna run o lerna exec. Per un dato pacchetto in npm, npm rende l’interopackage.json disponibile per uno script che utilizza variabili di ambiente. Così, per un dato pacchetto con il seguente package.json:

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

Le seguenti variabili saranno disponibili durante l’esecuzione di qualsiasi ciclo di vita script:

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

Stranezze/Problemi

Un paio di cose che stiamo ancora lavorando con il nuovo setup (alcuni di loro sono problemi, mentre alcuni di noi probabilmente non lo sanno meglio):

  • Non sono sicuro se è possibile, ma ci piacerebbe essere in grado di avere un comune ciclo di vita di script per tutti i nostri pacchetti. Dichiararli nella radice package.json non funziona.
  • È estremamente difficile testare completamente la configurazione di Lerna senza pubblicare effettivamente qualcosa su npm. Non sono sicuro se c’è un --dry-run da qualche parte.
  • Lerna ha un modo di mantenere un blocco di configurazione comune perdevDependencies in modo che tutti idevDependencies siano della stessa versione per ciascuno dei sotto-pacchetti. Questa è una caratteristica piuttosto interessante, ma ci vorrebbe un po ‘ di tempo per estirpare tutti quelli comuni.
  • Lo stesso potrebbe valere anche per altre dipendenze, quindi anche se non vogliamo un blocco di configurazione comune dependencies, avere un modo per esprimere le variabili disponibili tra i progetti sarebbe bello. Ad esempio, nel nostro monorepo Java/Kotlin, usiamo gradle.properties per contenere variabili come springBootVersionspringCoreVersion, ecc., che sono poi usati dai singoli scripts gradle.

I nostri pensieri su monorepos
Recentemente è stato un dibattito piuttosto acceso con monorepos e se stiamo vedendo un numero enorme saltare di nuovo sul carro, che ricorda molto il tempo in cui microservices era di gran moda.

La struttura che seguiamo qui sta avendo più monorepos, e questa non è la nostra prima volta che gestiamo monorepos. La nostra intera piattaforma e backend è un monorepo che contiene codice privato, distribuibile e più pacchetti rivolti al pubblico che vengono pubblicati su bintray. Abbiamo anche il nostro sito web principale in esecuzione con un backend a molla, con il frontend in bundle con il webpack che supporta il ricaricamento a caldo (webpack watch), ecc. Non abbiamo mai deciso di andare con un singolo mono-repo in tutta l’organizzazione perché gli utensili semplicemente non c’erano.

Avere la maggior parte del nostro codice Java in un singolo repository funziona alla grande perchégradle fornisce tutti gli utensili necessari per il monorepo Java elerna e il ciclo di vita npm che fornisce gli utensili per il monorepo di JS SDK. Quindi, in poche parole, i monorepos sono fantastici una volta identificata la copertura dei cambiamenti che vanno nel tuo repository. Per il nostro backend Java, abbiamo visto più MR tra i progetti per una singola funzionalità, che ci ha spinto a passare a un monorepo solo per questo particolare progetto, con tutto il nostro altro codice ancora in repository separati. E una volta che abbiamo visto un modello simile emergere anche per i nostri SDK JS, ci siamo trasferiti a Lerna.

Si noti che siamo un piccolo team di circa 9 ingegneri; quindi ciò che funziona per noi potrebbe non funzionare per squadre di dimensioni diverse. Quello che vorremmo principalmente sottolineare è che l’adozione di qualsiasi soluzione non deve essere binaria, in cui o lo facciamo come prescritto o non lo facciamo affatto.

Alcune delle motivazioni che abbiamo visto per un monorepo sicuramente applicate a noi e molti di loro non ha fatto. Ad esempio, non possiamo semplicemente risparmiare tempo per costruire gli utensili se l’intera base di codice è stata spostata in un singolo repository, indipendentemente dal vantaggio che potremmo o meno sperimentare. Quindi il dibattito non riguarda davvero un “singolo repository” — di per sé, non è altro che una nuova struttura di directory. La prescrizione di loro è quello di alleviare alcuni problemi e come con ogni “proiettile d’argento”, ci sono avvertimenti.

Il dibattito riguarda i problemi comuni affrontati nell’industria del software e quali soluzioni sono state comunemente prese; “comune” è la parola chiave. L’area in cui si devia dall’applicazione “comune” è dove si arriva a innovare, apportare modifiche e costruire un po’.