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:
- med tanke på att det mesta av SDK-logiken finns i
@mitter-io/core
, nästan varje förändring som inträffade icore
– 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å icore
, men uppdateringen behövde nu reflektera i alla andra mål, dvsweb
node
ochreact-native
. Det var ganska vanligt att en utvecklare missade ett mål. - nästan varje ändring i SDK skulle resultera i ändringar över minst 3 av de 5 paketen.
- 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.
-
npm link
(elleryarn 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ånnpm
och tillbaka till den lokala länken för utveckling. - 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:
- 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
. - 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:
- Bygg
- 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 engit 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 refHEAD
pekar inte någonstans. Lerna använderHEAD
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 roten
package.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 alladevDependencies
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 vigradle.properties
för att innehålla variabler somspringBootVersion
springCoreVersion
, 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.