Articles

flytning fra flere arkiver til en Lerna-js mono-repo

At mitter.io, vi har et par offentlige npm pakker, som vi har brug for at offentliggøre, og vi flyttede for nylig til en mono-repo-struktur administreret af Lerna fra at have separate arkiver for hver enkelt af dem. I dag vil jeg gerne dele vores erfaring med denne migration og vores opsætning med den nye monorepo-struktur. Alle vores pakker er enten SDK mål eller er afhængigheder for vores SDK mål:

  • @mitter-io/core – kernen funktionalitet mitter.io SDKs
  • @mitter-io/models – typescript-modellerne (klasser, typealiaser, grænseflader osv.) for SDK ‘ erne
  • @mitter-io/web – internettet SDK
  • @mitter-io/react-native – React Native SDK
  • @mitter-io/node – noden.js SDK (bruges til node.js backends)
  • @mitter-io/react-scl – standardkomponentbiblioteket til ReactJS-applikationer

alle vores pakker er skrevet i TypeScript, og typinger er bundtet med selve pakkerne, og vi distribuerer ikke separate typepakker (dem, du normalt ser, starter med @types/). Vi bruger rollup til at samle disse pakker i både UMD-og ES5-modulformaterne. Derudover bruger vi TypeDoc til at generere vores dokumentation, som derefter offentliggøres på en offentlig spand i S3.

før vi brugte Lerna, havde vi et separat lager for hver af vores pakker, og det fungerede fint, mens vi kun havde SDK på nettet. Efterhånden som vi skred frem og havde flere udviklere, der arbejdede på SDK, begyndte vi at stå over for et par problemer med vores opsætning:

  1. i betragtning af at det meste af SDK-logikken findes i @mitter-io/core, næsten hver ændring, der opstod i core pakken og alle andre pakker skulle opdateres for at pege på den nye version. Så selvom der var en fejl, der skulle rettes til, siger React Native, ville ændringen gå i core, men opdateringen var nødvendig for nu at reflektere i alle andre mål, dvs. webnode og react-native. Det var ret almindeligt, at en udvikler gik glip af et mål.
  2. næsten hver ændring i SDK ville resultere i ændringer på tværs af mindst 3 af de 5 pakker.
  3. vi så en stor fordel ved at holde den samme version på tværs af pakker (gør det lettere for udviklere at gætte, hvad den nyeste version af target ville være), men manuelt at spore dette blev besværligt.
  4. npm link (eller yarn link hvis du foretrækker det) havde sit eget sæt problemer med at sikre, at alle afhængigheder var knyttet, derefter fjernet for at bruge den korrekte fra npm og tilbage til det lokale link til udvikling.
  5. det var ret almindeligt at køre scripts på tværs af pakker (f. eks., at offentliggøre typescript docs), og vi brugte et skrøbeligt stillads af symlinks og bash-scripts til at styre det samme.

omkring det tidspunkt stødte vi på Lerna, og det syntes at være den perfekte pasform til vores krav.

Vi besluttede at følge den enkleste vej der er, forsøger at bruge standardindstillinger så meget som muligt. Fra hvad vi oplevede, migrering til Lerna var en leg. Start med at oprette en ny Lerna repo:

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

Besvar et par enkle spørgsmål (hvor vi altid ty til standard), og du er klar. At flytte vores gamle pakker fra deres repos til den nye (som vi frygtede, da vi troede, det ville være en massiv smerte) var meget lettere end forventet:

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

Bemærk --flatten kan eller ikke være påkrævet, men vi stod over for problemer uden det.

det, der er fantastisk ved Lerna, er, at det bringer alle git-forpligtelserne sammen med det (du kan miste noget historie med--flatten), således at historien for den nye repo ser ud som om udvikling altid har fundet sted i denne monorepo. Dette er absolut nødvendigt, fordi du bliver nødt til at git blame nogen til en fejl, du opdagede efter at have flyttet til monorepo.

Aktuel opsætning

Med Lerna administrerer vi nu et enkelt lager for alle vores pakker med en mappestruktur, der ser sådan ud:

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

for at offentliggøre de ændrede pakker skal vi nu simpelthen:

lerna boostrap
lerna publish

du behøver ikke at gørelerna bootstrap hver gang; kun hvis dette er første gang, du tjekker repoen. Hvad det gør er simpelthen at installere alle afhængigheder af hver af pakkerne under denne repo.

samtidig besluttede vi også at strømline vores proces lidt og tilføjede alle emballageopgaverne inden fornpm livscyklus selv. Bemærk, at dette ikke har noget at gøre med Lerna; dette er noget, der ideelt set bør være der i enhver npm-pakke uanset repostrukturen. For hver af pakkerne er følgende scripts til stede i den enkelte pacakge.json:

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

dette bygger pakken med typescript compiler, bundter den med rollup og genererer docs med typedoc:

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

at have en enkelt repostruktur giver dig også mulighed for at holde almindelige scripts på et enkelt sted, så ændringer gælder på tværs af alle pakker (vi skal også flytte build-scriptet til et separat script, da det nu er blevet en ganske kompleks bash-kommando).

udviklerstrømmen

udviklerstrømmen bortset fra udgivelser er uændret. En udvikler opretter et problem på GitLab (eller er tildelt et), opretter en ny filial for problemet og fletter derefter ændringerne til master efter en kodeanmeldelse. Udgivelseslivscyklussen følger nu en ekstremt struktureret proces:

  1. når en milepæl er afsluttet, og vi planlægger at lave en ny udgivelse, opretter en af udviklerne (ansvarlig for den pågældende udgivelse) en ny version ved at køre lerna version.
  2. Lerna giver en yderst hjælpsom og nem at bruge prompt til at finde ud af den næste 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 er valgt, ændrer Lerna versionerne af pakkerne, opretter et tag i remote repo og skubber ændringerne til vores GitLab-forekomst. Ud over dette er udviklere ikke forpligtet til at gøre noget andet. Vores CI er konfigureret til at opbygge alle tags, der har et navn, der ligner et semantisk versionsnummer.

Bemærk Vi kører lerna version med --force-publish fordi vi ønsker, at alle pakker skal have nøjagtig samme slægt af versioner. Så nogle gange har vi pakker, der ikke adskiller sig mellem forskellige versioner. Afhængigt af dine præferencer, kan du vælge ikke at gøre det.

ci-opsætningen

Vi bruger Gitlabs integrerede CI til opbygning, test og udgivelse på tværs af alle vores projekter (JS og Java). For den nye JS monorepo har vi to faser:

  1. Build
  2. Publicer

byggefasen er ekstremt enkel og kører følgende to scripts:

lerna bootstrap
lerna run build

denne fase kører på hver enkelt forpligtelse til i det væsentlige at validere pakkenes sundhed. Udgivelsesfasen kører på den anden side følgende:

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

vi regnede ud, at vi var nødt til at lave en git checkout master og en git reset --hard fordi GitLab-kloner (eller henter, afhængigt af konfigurationen) det er nødvendigt at lave en repo, og tjekker derefter den forpligtelse, der skal bygges. Dette indstiller arbejdsmappen i en’ løsrevet hoved ‘ – tilstand, dvs.ref HEAD peger ikke nogen steder. Lerna bruger HEAD til at finde ud af den aktuelle version af pakken og fejl i den løsrevne hovedtilstand.

Vi skal også køre lerna publish from-package i modsætning til lerna publish, som udførelse af en simpel lerna publish ville have Lerna klager over, at den aktuelle version allerede er offentliggjort, da metadataene er blevet blev opdateret, da udvikleren løb lerna version lokalt. Argumentetfrom-package fortæller Lerna at offentliggøre alle versioner, der ikke i øjeblikket er til stede i npm for en given pakke. Dette hjælper også, hvis en udgivelse mislykkedes af en eller anden grund, og du prøver igen på rørledningen.

udgivelsesfasen er konfigureret til kun at køre på tags, der matcher følgende regeks credit:

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

Dette er lidt fancy, og for de fleste hold og til de fleste formål skal simpelthen^v*$ arbejde. 🙂

Bemærk Selvom vi ikke har gjort det endnu, da vi er et lille team, kan man også markere alle tags, der følger ovenstående regeks som beskyttet i GitLab for at begrænse, hvem der kan offentliggøre pakker til npm.

Du kan tjekke vores monorepo påhttps://github.com/mitterio/js-sdk (dette afspejles fra vores interne GitLab repo).

hurtige noter

når du kører almindelige scripts (som vi gør for at offentliggøre typescript docs), er det ret nyttigt at kende detaljerne i pakken, der kører scriptet. Dette gælder for scripts i npm livscyklus, samt scripts man kan køre ved hjælp af lerna run eller lerna exec. For en given pakke i npm gør npm hele package.json tilgængelig for et script ved hjælp af miljøvariabler. Så for en given pakke med følgende package.json:

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

følgende variabler vil være tilgængelige, mens du kører et livscyklusscript:

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

særheder/problemer

et par ting, vi stadig arbejder på med den nye opsætning (nogle af dem er problemer, mens nogle vi sandsynligvis bare ikke ved bedre):

  • ikke sikker på, om det er muligt, men vi vil gerne kunne have fælles livscyklusskripter til alle vores pakker. Erklære disse i roden package.json virker ikke.
  • det er ekstremt vanskeligt at teste din Lerna-opsætning helt uden faktisk at offentliggøre noget til npm. Ikke sikker på, om der er et --dry-run et eller andet sted.
  • Lerna har en måde at holde en fælles config-blok for devDependencies så alle devDependencies er af samme version for hver af underpakningerne. Dette er en ganske cool funktion, men det ville tage os lidt tid at udrydde alle de almindelige.
  • det samme kunne også gælde for andre afhængigheder, så selvom vi ikke vil have en fælles dependencies config block, ville det være rart at have en måde at udtrykke variabler til rådighed på tværs af projekterne. For eksempel bruger vi i Vores Java/Kotlin monorepo gradle.properties til at indeholde variabler som springBootVersionspringCoreVersion osv., som derefter bruges af de enkelte gradle scripts.

vores tanker om monorepos
Det har været en ganske heftig debat for nylig med monorepos, og om vi ser et stort antal hoppe på båndvognen igen, der minder om den tid, hvor mikroservices var alle raseri.

strukturen vi følger her er at have flere monorepos, og det er ikke
vores første gang at styre monorepos. Hele vores platform og backend er en monorepo, der indeholder privat, implementerbar kode og flere offentligt vendende pakker, der offentliggøres til bintray. Vi har også vores vigtigste hjemmeside kører med en fjeder backend, med frontend bundtet med netpakke understøtter hot reloading (webpack watch), etc. Vi besluttede aldrig at gå med en enkelt mono-repo på tværs af organisationen, fordi værktøjet simpelthen ikke var der.

at have det meste af vores Java-kode i en enkelt repo fungerer godt, fordigradle giver alt det værktøj, der er nødvendigt for Java monorepo oglerna og npm-livscyklussen, der leverer værktøjet til JS SDK ‘ s monorepo. Så kort sagt, monorepos er gode, når du identificerer dækningen af ændringer, der går i din repo. For vores Java-backend så vi flere MRs på tværs af projekter for en enkelt funktion, som tilbøjelige os til at flytte til en monorepo kun for dette særlige projekt, med al vores anden kode stadig i separate repos. Og når vi så et lignende mønster dukke op for vores JS SDK ‘ er, flyttede vi til lerna.

bemærk, at vi er et lille team på omkring 9 ingeniører; så hvad der fungerer for os fungerer muligvis ikke for hold i forskellige størrelser. Det, vi mest gerne vil påpege, er, at vedtagelsen af en løsning ikke behøver at være binær, hvor vi enten gør det som foreskrevet eller slet ikke gør det.

Nogle af de motivationer, vi så for en monorepo, gjaldt bestemt os, og mange af dem gjorde det ikke. For eksempel kan vi simpelthen ikke spare tid til at bygge værktøjet, hvis hele vores kodebase blev flyttet til en enkelt repo — uanset hvilken fordel vi måske eller måske ikke oplever. Så debatten handler virkelig ikke om at have en “enkelt repo” — i sig selv er det intet andet end en ny katalogstruktur. Recepten på dem er at lindre visse problemer, og som med enhver “sølvkugle” er der advarsler.

debatten handler om almindelige problemer i programmelindustrien, og hvilke løsninger der ofte er taget;” fælles ” er nøgleordet. Det område, hvor du afviger fra den “almindelige” applikation, er, hvor du kommer til at innovere, foretage ændringer og bygge lidt.