Articles

flytta från flera förvar till en lerna-js mono-repo

At mitter.io, vi har ett par public-facing npm-paket som vi behöver publicera, och vi flyttade nyligen till en mono-repo-struktur som hanteras av Lerna från att ha separata repositorier för var och en av dem. Idag vill jag dela vår erfarenhet av denna migrering och vår inställning med den nya monorepo-strukturen. Alla våra paket är antingen SDK-mål eller är beroenden för våra SDK-mål:

  • @mitter-io/core – kärnfunktionaliteten hos mitter.Io SDK: er
  • @mitter-io/models – typescript-modellerna (klasser, typalias, gränssnitt etc.) för SDK: erna
  • @mitter-io/web – web SDK
  • @mitter-io/react-native – React Native SDK
  • @mitter-io/node – noden.js SDK (används för nod.JS backends)
  • @mitter-io/react-scl – standardkomponentbiblioteket för ReactJS-applikationer

alla våra paket är skrivna i TypeScript och typings levereras med själva paketen, och vi distribuerar inte separata skrivpaket (de som du vanligtvis ser börjar med @types/). Vi använder samlad att paketera dessa paket i både UMD och ES5 modulformat. Utöver detta använder vi TypeDoc för att generera vår dokumentation som sedan publiceras på en offentlig hink i AWS S3.

innan vi använde Lerna hade vi ett separat förråd för vart och ett av våra paket och det fungerade bra medan vi bara hade web SDK. När vi utvecklades och hade fler utvecklare som arbetade med SDK började vi möta några problem med vår inställning:

  1. med tanke på att det mesta av SDK-logiken finns i @mitter-io/core, nästan varje förändring som inträffade i core – paketet och alla andra paket måste uppdateras för att peka på den nya versionen. Så även om det fanns ett fel som skulle åtgärdas för, säg React Native, skulle ändringen gå i core, men uppdateringen behövde nu reflektera i alla andra mål, dvs webnode och react-native. Det var ganska vanligt att en utvecklare missade ett mål.
  2. nästan varje ändring i SDK skulle resultera i ändringar över minst 3 av de 5 paketen.
  3. vi såg en stor fördel med att hålla samma version över paket (gör det lättare för utvecklare att gissa vad den senaste versionen av target skulle vara), men manuellt spåra detta blev besvärligt.
  4. npm link (eller yarn link om du föredrar) hade sin egen uppsättning problem med att se till att alla beroenden var länkade, sedan länkade för att använda den rätta från npm och tillbaka till den lokala länken för utveckling.
  5. Det var ganska vanligt att köra skript över paket (t. ex., för att publicera typescript docs), och vi använde en bräcklig ställning av symlinks och bash-skript för att hantera detsamma.

runt den tiden kom vi över Lerna och det verkade vara den perfekta passformen för våra krav.

vi bestämde oss för att följa den enklaste vägen som finns, försöker använda standardvärden så mycket som möjligt. Från vad vi upplevde var det en bris att migrera till Lerna. Börja med att skapa en ny Lerna repo:

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

svara på ett par enkla frågor (där vi alltid tillgripit standard) och du är redo. Att flytta våra gamla paket från sina repor till den nya (som vi fruktade eftersom vi trodde att det skulle vara en enorm smärta) var mycket lättare än förväntat:

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

notera --flatten kan eller inte krävas, men vi mötte problem utan det.

vad är fantastiskt om Lerna är att det ger i alla git begår tillsammans med det (du kan förlora en del historia med --flatten), så att för den nya repo, historien ser ut som utveckling har alltid hänt i denna monorepo. Detta är absolut nödvändigt eftersom du kommer att behöva git blame någon för en bugg som du upptäckte efter att ha flyttat till monorepo.

aktuell inställning

med Lerna hanterar vi nu ett enda arkiv för alla våra paket, med en katalogstruktur som ser ut så här:

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

för att publicera de ändrade paketen måste vi nu helt enkelt:

lerna boostrap
lerna publish

Du behöver inte göra lerna bootstrap varje gång; bara om det här är första gången du checkar ut reporäntan. Vad det gör är att helt enkelt installera alla beroenden för vart och ett av paketen under denna repo.

samtidigt bestämde vi oss också för att effektivisera vår process lite och lade till alla förpackningsuppgifter inom npm livscykeln själv. Observera att detta inte har något att göra med Lerna; detta är något som helst borde vara där i något npm-paket oavsett repo-struktur. För varje paket finns följande skript i individen pacakge.json:

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

detta bygger paketet med TypeScript-kompilatorn, buntar det med samlad och genererar dokument med typedoc:

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

med en enda repo-struktur kan du också hålla vanliga skript på ett enda ställe så att ändringar gäller i alla paket (vi bör också flytta byggskriptet till ett separat skript, eftersom det nu har blivit ganska komplext bash-kommando).

utvecklarflödet

utvecklarflödet bortsett från utgåvor är oförändrat. En utvecklare skapar ett problem på GitLab (eller tilldelas en), skapar en ny filial för problemet och sammanfogar sedan ändringarna till master efter en kodgranskning. Utgivningslivscykeln följer nu en extremt strukturerad process:

  1. när en milstolpe är klar och vi planerar att göra en ny version skapar en av utvecklarna (ansvarig för den specifika versionen) en ny version genom att köra lerna version.
  2. Lerna ger en mycket hjälpsam och lättanvänd uppmaning för att räkna ut nästa 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

När en ny version har valts ändrar Lerna versionerna av paketen, skapar en tagg i fjärrrepo och skjuter ändringarna i vår GitLab-instans. Utöver detta är utvecklare inte skyldiga att göra något annat. Vår CI är inställd för att bygga alla taggar som har ett namn som liknar ett semantiskt versionsnummer.

OBS Vi kör lerna version med --force-publish eftersom vi vill att alla paket ska ha exakt samma linje av versioner. Så ibland har vi paket som inte skiljer sig mellan olika versioner. Beroende på vad du föredrar kan du välja att inte göra det.

CI setup

Vi använder Gitlabs integrerade CI för att bygga, testa och publicera i alla våra projekt (JS och Java). För den nya JS monorepo har vi två steg:

  1. Bygg
  2. publicera

byggfasen är extremt enkel och kör följande två skript:

lerna bootstrap
lerna run build

denna fas körs på varje enskilt åtagande för att i huvudsak validera paketets sanity. Publiceringsfasen å andra sidan kör följande:

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

vi räknade ut att vi var tvungna att göra en git checkout master och en git reset --hard eftersom GitLab kloner (eller hämtar, beroende på konfigurationen) repo, och kontrollerar sedan det åtagande som ska byggas. Detta ställer in arbetskatalogen i ett ”fristående Huvud” – läge, dvs ref HEAD pekar inte någonstans. Lerna använder HEAD för att ta reda på den aktuella versionen av paketet och fel i det fristående huvudläget.

Vi måste också köra lerna publish from-package I motsats till lerna publish, som utför en enkel lerna publish skulle ha Lerna klagar på att den aktuella versionen redan publicerats, som metadata uppdaterades när utvecklaren körde lerna version lokalt. Argumentetfrom-package säger till Lerna att publicera alla versioner som för närvarande inte finns i npm för ett visst paket. Detta hjälper också om en publicering misslyckades av någon anledning och du försöker igen rörledningen.

publiceringsfasen är konfigurerad för att endast köras på taggar som matchar följande regex-kredit:

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

det här är lite snyggt, och för de flesta lag och för de flesta ändamål borde helt enkelt^v*$ fungera. 🙂

OBS även om vi inte har gjort det ännu, eftersom vi är ett litet team, kan man också markera alla taggar som följer ovanstående regex som skyddade i GitLab för att begränsa vem som kan publicera paket till npm.

Du kan kolla in vår monorepo på https://github.com/mitterio/js-sdk (detta speglas från vår interna GitLab repo).

Quick notes

När du kör vanliga skript (som vi gör för att publicera typescript-dokument) är det ganska användbart att känna till detaljerna i paketet som kör skriptet. Detta gäller för skript i npm-livscykeln, liksom skript som man kan köra med lerna run eller lerna exec. För ett givet paket i npm gör npm hela package.json tillgängligt för ett skript med miljövariabler. Så för ett givet paket med följande package.json:

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

följande variabler kommer att finnas tillgängliga när du kör något livscykelskript:

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

Quirks/Issues

ett par saker som vi fortfarande arbetar med med den nya installationen (vissa av dem är problem, medan vissa vi förmodligen bara inte vet bättre):

  • inte säker på om det är möjligt, men vi skulle vilja kunna ha gemensamma livscykelskript för alla våra paket. Att deklarera dessa i rotenpackage.json fungerar inte.
  • Det är extremt svårt att testa din Lerna-installation helt utan att faktiskt publicera något till npm. Inte säker på om det finns ett --dry-run någonstans.
  • Lerna har ett sätt att hålla ett gemensamt konfigurationsblock för devDependencies så att alla devDependencies har samma version för varje delpaket. Det här är en ganska cool funktion men skulle ta oss lite tid att rensa ut alla vanliga.
  • detsamma kan gälla för andra beroenden också, så medan vi inte vill ha ett gemensamt dependencies config-block, skulle det vara trevligt att ha ett sätt att uttrycka variabler tillgängliga över projekten. Till exempel i vår Java/Kotlin monorepo använder vi gradle.properties för att innehålla variabler som springBootVersionspringCoreVersion, etc., som sedan används av de enskilda gradle-skripten.

våra tankar om monorepos
det har varit en ganska het debatt nyligen med monorepos och om vi ser ett stort antal hoppar på tåget igen, ganska påminner om den tid då microservices var på modet.

strukturen vi följer här har flera monorepos, och detta är inte
vår första gång hantera monorepos. Hela vår plattform och backend är en monorepo som innehåller privat, distribuerbar kod och flera publika inför paket som publiceras till bintray. Vi har också vår huvudwebbplats som körs med en Fjäderbackend, med frontend buntad med webpack som stöder hot reloading (webpack watch), etc. Vi bestämde oss aldrig för att gå med en enda mono-repo över hela organisationen eftersom verktyget helt enkelt inte var där.

att ha det mesta av vår Java-kod i en enda repo fungerar bra eftersom gradle ger alla verktyg som behövs för Java monorepo och lerna och npm-livscykeln som ger verktyget för JS SDK: s monorepo. Så enkelt uttryckt är monorepos bra när du identifierar täckningen av förändringar som går i din repo. För vår Java backend såg vi flera Fru över projekt för en enda funktion, vilket lutade oss att flytta till en monorepo endast för det här projektet, med all vår andra kod fortfarande i separata repor. Och när vi såg ett liknande mönster dyka upp för våra JS SDK också, vi flyttade till Lerna.

notera att vi är ett litet team på cirka 9 ingenjörer; så det som fungerar för oss kanske inte fungerar för team av olika storlekar. Vad vi mest vill påpeka är att antagandet av någon lösning inte behöver vara binär, där vi antingen gör det som föreskrivet eller inte gör det alls.

några av de motivationer vi såg för en monorepo tillämpade definitivt på oss och många av dem gjorde det inte. Till exempel kan vi helt enkelt inte spara tid att bygga verktyget om hela vår kodbas flyttades till en enda repo — oavsett vilken fördel vi kan eller inte kan uppleva. Så debatten handlar verkligen inte om att ha en ”enda repo” — i sig är det inget annat än en ny katalogstruktur. Receptet på dem är att lindra vissa problem och som med varje ”Silverkula” finns det varningar.

debatten handlar om vanliga problem inom mjukvaruindustrin och vilka lösningar som vanligtvis har tagits; ”vanligt” är nyckelordet. Området där du avviker från den ”vanliga” applikationen är där du får förnya, göra ändringar och bygga lite.