Articles

verplaatsen van meerdere repositories naar een lerna-js mono-repo

At mitter.io, we hebben een paar publiek gerichte npm pakketten die we moeten publiceren, en we zijn onlangs verhuisd naar een mono-repo structuur beheerd door Lerna van het hebben van aparte repositories voor elk van hen. Vandaag wil ik graag onze ervaring van deze migratie en onze opzet delen met de nieuwe monorepo-structuur. Al onze pakketten zijn ofwel SDK – doelen of zijn afhankelijkheden voor onze SDK-doelen:

  • @mitter-io/core – de kernfunctionaliteit van de mitter.io SDKs
  • @mitter-io/models – de typescript-modellen (klassen, type aliassen, interfaces enz.) voor de SDK ‘ s
  • @mitter-io/web – de web SDK
  • @mitter-io/react-native – de React Native SDK
  • @mitter-io/node – de node.js SDK (gebruikt voor node.js-backends)
  • @mitter-io/react-scl – de standaard componentenbibliotheek voor ReactJS-toepassingen

al onze pakketten worden geschreven in TypeScript en typings worden gebundeld met de pakketten zelf, en we distribueren geen aparte typepakketten (degene die u gewoonlijk ziet beginnen met @types/). We gebruiken rollup om deze pakketten te bundelen in zowel de UMD als ES5 module formaten. Daarnaast gebruiken we TypeDoc om onze documentatie te genereren die vervolgens wordt gepubliceerd op een publieke emmer in AWS S3.

voordat Lerna werd gebruikt, hadden we een aparte repository voor elk van onze pakketten en het werkte prima terwijl we alleen de web SDK hadden. Naarmate we vorderden en er meer ontwikkelaars aan de SDK werkten, begonnen we met een paar problemen met onze setup:

  1. aangezien de meeste SDK-logica zich in @mitter-io/core bevindt, moesten bijna elke verandering die zich voordeed in het core pakket en alle andere pakketten worden bijgewerkt om naar de nieuwe versie te wijzen. Dus, zelfs als er een bug was die moest worden opgelost voor, Zeg React Native, zou de verandering gaan in core, maar de update moest nu worden weerspiegeld in alle andere doelen, dat wil zeggen, webnode en react-native. Het was heel gebruikelijk voor een ontwikkelaar om een doel te missen.
  2. bijna elke verandering in de SDK zou resulteren in veranderingen over ten minste 3 van de 5 pakketten.
  3. we zagen een enorm voordeel in het behouden van dezelfde versie over pakketten (maakt het makkelijker voor ontwikkelaars om te raden wat de nieuwste versie van target zou zijn), maar het handmatig volgen hiervan werd omslachtig.
  4. npm link (of yarn link indien gewenst) had zijn eigen set van problemen met het controleren of alle afhankelijkheden waren gekoppeld, vervolgens ontkoppeld om de juiste te gebruiken van npm en terug naar de lokale link voor ontwikkeling.
  5. het was vrij gebruikelijk om scripts uit te voeren over pakketten (bijv., om het typescript docs te publiceren), en we gebruikten een fragiele steiger van symlinks en bash scripts om hetzelfde te beheren.

rond die tijd kwamen we Lerna tegen en het leek de perfecte pasvorm voor onze vereisten.

we hebben besloten om het eenvoudigste pad te volgen dat er is, waarbij we proberen de standaardwaarden zo veel mogelijk te gebruiken. Van wat we ervoeren, was migreren naar Lerna een eitje. Begin met het maken van een nieuwe Lerna repo:

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

beantwoord een paar eenvoudige vragen (waarbij we altijd gebruik maakten van de standaard) en je bent helemaal klaar. Het verplaatsen van onze oude pakketten van hun repo ‘ s naar de nieuwe (waar we bang voor waren omdat we dachten dat het een enorme pijn zou zijn) was veel gemakkelijker dan verwacht:

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

merk op dat de --flatten misschien niet nodig is, maar we hadden problemen zonder.

wat verbazingwekkend is aan Lerna is dat het alle Git commits meebrengt (je zou wat geschiedenis kunnen verliezen met --flatten), zodat voor de nieuwe repo, de geschiedenis eruit ziet alsof ontwikkeling altijd in deze monorepo heeft plaatsgevonden. Dit is absoluut essentieel omdat je git blame iemand nodig hebt voor een bug die je ontdekt hebt na het verplaatsen naar de monorepo.

huidige instellingen

met Lerna beheren we nu een enkele repository voor al onze pakketten, met een mapstructuur die er zo uitziet:

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

om de gewijzigde pakketten te publiceren, moeten we nu gewoon:

lerna boostrap
lerna publish

u hoeft niet elke keer lerna bootstrap te doen; alleen als dit de eerste keer is dat u de repo uitcheckt. Wat het doet is gewoon installeren alle afhankelijkheden van elk van de pakketten onder deze repo.

tegelijkertijd besloten we ons proces een beetje te stroomlijnen en voegden we alle verpakkingstaken toe binnen de npm levenscyclus zelf. Merk op dat dit niets te maken heeft met Lerna; dit is iets dat idealiter aanwezig zou moeten zijn in elk NPM-pakket, ongeacht de repo-structuur. Voor elk van de pakketten zijn de volgende scripts aanwezig in de individuele pacakge.json:

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

Dit bouwt het pakket met de typescript-compiler, bundelt het met rollup en genereert documenten met typedoc:

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

met een enkele repo-structuur kunt u ook gemeenschappelijke scripts op een enkele plaats houden, zodat wijzigingen van toepassing zijn op alle pakketten (we zouden ook het build-script naar een apart script moeten verplaatsen, gezien het feit dat het nu een behoorlijk complex bash-commando is geworden).

de ontwikkelaarsstroom

De ontwikkelaarsstroom afgezien van releases is onveranderd. Een ontwikkelaar maakt een probleem aan op GitLab (of krijgt er een toegewezen), maakt een nieuwe branch voor het probleem, en mergt dan de wijzigingen in master na een code review. De levenscyclus van de release volgt nu een zeer gestructureerd proces:

  1. wanneer een mijlpaal is voltooid en we van plan zijn een nieuwe release te maken, maakt een van de ontwikkelaars (verantwoordelijk voor die specifieke release) een nieuwe versie door lerna versionuit te voeren.
  2. Lerna biedt een zeer handige en eenvoudig te gebruiken prompt voor het uitzoeken van de volgende versie
(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

zodra een nieuwe versie is geselecteerd, verandert Lerna de versies van de pakketten, maakt een tag aan in de remote repo, en pusht de wijzigingen naar onze GitLab instantie. Verder zijn ontwikkelaars niet verplicht om iets anders te doen. Onze CI is ingesteld om alle tags te bouwen die een naam hebben die lijkt op een semantisch versienummer.

NOTE we draaienlerna version met--force-publish omdat we willen dat alle pakketten exact dezelfde lineage van versies hebben. Dus soms hebben we pakketten die niet verschillen tussen verschillende versies. Afhankelijk van uw voorkeur, kunt u ervoor kiezen om het niet te doen.

de CI setup

We gebruiken GitLab ‘ s geïntegreerde CI voor het bouwen, testen en publiceren in al onze projecten (JS en Java). Voor de nieuwe js monorepo hebben we twee fasen:

  1. Build
  2. Publish

de build fase is uiterst eenvoudig en draait de volgende twee scripts:

lerna bootstrap
lerna run build

Deze fase draait op elke commit om in wezen de sanity van het pakket te valideren. De publicatiefase daarentegen loopt als volgt::

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

We kwamen erachter dat we een git checkout master en een git reset --hard moesten doen omdat GitLab de repo klonen (of fetcht, afhankelijk van de configuratie), en dan de commit die is te bouwen. Dit stelt de werkdirectory in in een ‘losgekoppelde HEAD’ modus, d.w.z. de ref HEAD wijst nergens naar. Lerna gebruikt HEAD om de huidige versie van het pakket en fouten uit te zoeken in de staat van het losstaande hoofd.

we moeten ook lerna publish from-package uitvoeren in tegenstelling tot lerna publish, omdat het uitvoeren van een eenvoudige lerna publish Lerna zou doen klagen dat de huidige versie al gepubliceerd is, omdat de metagegevens bijgewerkt werden toen de ontwikkelaar lerna version lokaal. Het argumentfrom-package vertelt Lerna om alle versies te publiceren die momenteel niet aanwezig zijn in npm voor een bepaald pakket. Dit helpt ook als een publicatie mislukt om wat voor reden dan ook en je bent opnieuw proberen de pijplijn.

De publicatiefase is geconfigureerd om alleen te draaien op tags die overeenkomen met de volgende Regex credit:

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

Dit is een beetje fancy, en voor de meeste teams en voor de meeste doeleinden, gewoon ^v*$ zou moeten werken. 🙂

NOTE Hoewel we het nog niet gedaan hebben, omdat we een klein team zijn, kan men ook tags na de bovenstaande regex markeren als beschermd in GitLab om te beperken wie pakketten kan publiceren naar npm.

u kunt onze monorepo bekijken op https://github.com/mitterio/js-sdk (dit wordt gespiegeld van onze interne GitLab repo).

Quick notes

bij het uitvoeren van veelgebruikte scripts (zoals we doen voor het publiceren van typescript-documenten), is het erg handig om de details te kennen van het pakket dat het script draait. Dit geldt voor scripts in de NPM-levenscyclus, evenals scripts die kunnen worden uitgevoerd met lerna run of lerna exec. Voor een gegeven pakket in npm maakt npm het gehele package.json beschikbaar voor een script met omgevingsvariabelen. Dus, voor een bepaald pakket met de volgende package.json:

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

De volgende variabelen zijn beschikbaar tijdens het uitvoeren van een levenscyclus script:

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

Eigenaardigheden/Issues

Een paar dingen die we zijn nog steeds bezig met de nieuwe setup (sommigen van hen zijn problemen, terwijl we waarschijnlijk gewoon niet beter weten):

  • Niet zeker of het is mogelijk, maar we willen in staat zijn om gemeenschappelijk levenscyclus van scripts voor al onze pakketten. Declareren in de root package.json werkt niet.
  • het is uiterst moeilijk om uw Lerna setup volledig te testen zonder daadwerkelijk iets naar npm te publiceren. Niet zeker of er ergens een --dry-run is.
  • Lerna heeft een manier om een gemeenschappelijk configuratieblok voor devDependencies te behouden, zodat alle devDependencies van dezelfde versie zijn voor elk van de subpakketten. Dit is nogal een coole functie, maar zou ons enige tijd kosten om onkruid uit alle gemeenschappelijke degenen.
  • hetzelfde kan ook gelden voor andere afhankelijkheden, dus hoewel we geen gemeenschappelijk dependencies configuratieblok willen, zou het leuk zijn om variabelen beschikbaar te hebben voor de projecten. Bijvoorbeeld, in onze Java/Kotlin monorepo gebruiken we gradle.properties om variabelen te bevatten zoals springBootVersionspringCoreVersion, enz., die vervolgens worden gebruikt door de individuele gradle scripts.onze gedachten over monorepos het is nogal een verhitte discussie geweest de laatste tijd met monorepos en of we zien een groot aantal springen op de bandwagon weer, nogal doet denken aan de tijd toen microservices was alle de rage.

    de structuur die we hier volgen heeft meerdere monorepos, en dit is niet de eerste keer dat we monorepos beheren. Ons hele platform en backend is een monorepo die private, inzetbare code en meerdere openbare-gerichte pakketten die worden gepubliceerd om bintray bevat. We hebben ook onze hoofdwebsite draaien met een Spring backend, met de frontend gebundeld met webpack ondersteunen hot herladen (webpack watch), enz. We hebben nooit besloten om te gaan met een enkele mono-repo in de hele organisatie, omdat de tooling was er gewoon niet.

    het hebben van het grootste deel van onze Java-code in een enkele repo werkt geweldig omdat gradle alle tooling biedt die nodig is voor de Java monorepo en lerna en de NPM lifecycle de tooling voor de js SDK monorepo. Dus, simpel gezegd, monorepos zijn geweldig zodra u de dekking van de veranderingen die gaan in uw repo identificeren. Voor onze Java-backend, zagen we meerdere MRs-projecten voor een enkele functie, die ons geneigd om te verhuizen naar een monorepo alleen voor dit specifieke project, met al onze andere code nog steeds in afzonderlijke repo ‘ s. En toen we een soortgelijk patroon zagen ontstaan voor onze js SDK ‘ s, verhuisden we naar Lerna.

    merk op dat we een klein team van ongeveer 9 ingenieurs zijn; dus wat voor ons werkt, werkt misschien niet voor teams van verschillende grootte. Waar we vooral op willen wijzen is dat het aannemen van een oplossing niet binair hoeft te zijn, waarbij we het doen zoals voorgeschreven of helemaal niet doen.

    sommige van de motivaties die we zagen voor een monorepo waren zeker van toepassing op ons en veel van hen niet. Bijvoorbeeld, we kunnen gewoon niet de tijd om de tooling te bouwen als onze hele codebase werd verplaatst naar een enkele repo — ongeacht het voordeel dat we kunnen of niet ervaren. Dus het debat gaat echt niet over het hebben van een “enkele repo” — op zichzelf is het niets meer dan een nieuwe directory structuur. Het voorschrift van hen is om bepaalde kwesties te verlichten en zoals met elke “zilveren kogel”, zijn er voorbehouden.

    het debat gaat over gemeenschappelijke problemen waarmee de software-industrie wordt geconfronteerd en welke oplossingen vaak zijn gekozen; “gemeenschappelijk” is het sleutelwoord. Het gebied waar je afwijkt van de” gemeenschappelijke ” toepassing is waar je krijgt om te innoveren, veranderingen aan te brengen en een beetje op te bouwen.