Articles

trecerea de la mai multe depozite la un mono-repo lerna-js

la mitter.io, avem câteva pachete publice npm pe care trebuie să le publicăm și ne-am mutat recent într-o structură mono-repo gestionată de Lerna de la a avea depozite separate pentru fiecare dintre ele. Astăzi, aș dori să împărtășesc experiența noastră despre această migrare și configurarea noastră cu noua structură monorepo. Toate pachetele noastre sunt fie obiective SDK sau sunt dependențe pentru obiectivele noastre SDK:

  • @mitter-io/core – funcționalitatea de bază a mitter.io SDK-uri
  • @mitter-io/models – modelele typescript (clase, pseudonime de tip, interfețe etc.) pentru SDK-urile
  • @mitter-io/web – web SDK
  • @mitter-io/react-native – React Native SDK
  • @mitter-io/node – nodul.JS SDK (folosit pentru nod.JS backends)
  • @mitter-io/react-scl – biblioteca de componente standard pentru aplicațiile ReactJS

toate pachetele noastre sunt scrise în TypeScript și dypings sunt incluse cu pachetele în sine și nu distribuim pachete de tastare separate (cele pe care le vedeți de obicei începând cu@types/). Folosim rollup pentru a lega aceste pachete atât în formatele modulului UMD, cât și în cele ale modulului ES5. În plus, folosim TypeDoc pentru a genera documentația noastră, care este apoi publicată pe o găleată publică în AWS S3.

înainte de a utiliza Lerna, am avut un depozit separat pentru fiecare dintre pachetele noastre și a funcționat bine în timp ce aveam doar SDK-ul web. Pe măsură ce am progresat și am avut mai mulți dezvoltatori care lucrau la SDK, am început să ne confruntăm cu câteva probleme cu configurarea noastră:

  1. având în vedere că cea mai mare parte a logicii SDK se află în @mitter-io/core, aproape fiecare schimbare care a avut loc în pachetul core și toate celelalte pachete trebuiau actualizate pentru a indica noua versiune. Deci, chiar dacă ar exista o eroare care urma să fie remediată, să zicem React Native, schimbarea ar intra în core, dar actualizarea trebuia să reflecte acum în toate celelalte ținte, adică webnode și react-native. Era destul de obișnuit ca un dezvoltator să rateze o țintă.
  2. aproape fiecare modificare a SDK-ului ar duce la modificări în cel puțin 3 din cele 5 pachete.
  3. am văzut un avantaj imens în păstrarea aceleiași versiuni pe pachete (face mai ușor pentru dezvoltatori să ghicească care ar fi cea mai recentă versiune a target), dar urmărirea manuală a acestui lucru a devenit greoaie.
  4. npm link (sauyarn link dacă preferați) a avut propriul set de probleme cu asigurându-vă că toate dependențele au fost legate, apoi deconectat pentru a utiliza cea corectă de lanpm și înapoi la link-ul local pentru dezvoltare.
  5. era destul de obișnuit să rulezi scripturi între pachete (de ex., pentru a publica documentele typescript) și am folosit o schelă fragilă de legături simbolice și scripturi bash pentru a gestiona la fel.

în acea perioadă, am dat peste Lerna și părea să se potrivească perfect cerințelor noastre.

am decis să urmăm cea mai simplă cale care există, încercând să folosim valorile implicite cât mai mult posibil. Din ceea ce am experimentat, migrarea la Lerna a fost o briză. Începeți prin crearea unui nou repo Lerna:

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

răspundeți la câteva întrebări simple (unde am recurs întotdeauna la implicit) și sunteți gata. Mutarea pachetelor noastre vechi de la repo-uri la cele noi (de care ne temeam pentru că am crezut că va fi o durere masivă) a fost mult mai ușoară decât ne așteptam:

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

notă --flatten poate fi sau nu necesară, dar ne-am confruntat cu probleme fără ea.

ceea ce este uimitor la Lerna este că aduce toate comiterile git împreună cu acesta (s-ar putea pierde o istorie cu--flatten), astfel încât pentru noul repo, istoria pare că dezvoltarea a avut loc întotdeauna în acest monorepo. Acest lucru este absolut esențial, deoarece veți avea nevoie de git blame cineva pentru o eroare pe care ați descoperit-o după ce ați trecut la monorepo.

configurare curentă

cu Lerna, gestionăm acum un singur depozit pentru toate pachetele noastre, cu o structură de directoare care arată astfel:

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

pentru a publica pachetele modificate, acum trebuie doar să:

lerna boostrap
lerna publish

nu trebuie să faceți lerna bootstrap de fiecare dată; numai dacă aceasta este prima dată când verificați repo-ul. Ceea ce face este să instalați pur și simplu toate dependențele fiecăruia dintre pachetele din acest repo.

în același timp, am decis, de asemenea, să ne eficientizăm puțin procesul și am adăugat toate sarcinile de ambalare înnpm ciclul de viață în sine. Rețineți că acest lucru nu are nimic de-a face cu Lerna; acest lucru ar trebui să fie în mod ideal în orice pachet npm, indiferent de structura repo. Pentru fiecare dintre pachete, următoarele scripturi sunt prezente în individ pacakge.json:

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

aceasta construiește pachetul cu compilatorul typescript, pachete cu rollup și generează documente cu typedoc:

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

având o singură structură repo vă permite, de asemenea, să păstrați script-uri comune într-un singur loc, astfel încât modificările să se aplice în toate pachetele (ar trebui să mutăm și scriptul de construire într-un script separat, având în vedere că acum a devenit o comandă Bash destul de complexă).

fluxul dezvoltatorului

fluxul dezvoltatorului în afară de versiuni este neschimbat. Un dezvoltator creează o problemă pe GitLab (sau i se atribuie una), creează o nouă ramură pentru problemă și apoi îmbină modificările la master după o revizuire a codului. Ciclul de viață al lansării urmează acum un proces extrem de structurat:

  1. când o piatră de hotar este finalizată și intenționăm să facem o nouă versiune, unul dintre dezvoltatori (responsabil de acea versiune specială) creează o nouă versiune rulândlerna version.
  2. Lerna oferă un prompt extrem de util și ușor de utilizat pentru a afla următoarea versiune
(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

odată selectată o nouă versiune, Lerna modifică versiunile pachetelor, creează o etichetă în repo la distanță și împinge modificările la instanța noastră GitLab. Dincolo de aceasta, dezvoltatorii nu sunt obligați să facă altceva. CI nostru este de configurare pentru a construi toate etichetele care au un nume similar cu un număr versionat semantic.

notă rulămlerna version cu--force-publish pentru că dorim ca toate pachetele să aibă exact aceeași linie de versiuni. Deci, uneori, vom avea pachete care nu diferă între diferite versiuni. În funcție de preferințele dvs., puteți alege să nu o faceți.

configurarea CI

folosim CI integrat GitLab pentru construirea, testarea și publicarea în toate proiectele noastre (JS și Java). Pentru noul JS monorepo, avem două etape:

  1. Build
  2. Publish

faza de construire este extrem de simplă și rulează următoarele două scripturi:

lerna bootstrap
lerna run build

această fază rulează pe fiecare angajament pentru a valida în esență sănătatea pachetului. Faza de publicare, pe de altă parte, rulează următoarele:

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

ne-am dat seama că trebuie să facem un git checkout masterși un git reset --harddeoarece clonele GitLab (sau preluări, în funcție de configurație) repo-ul, și apoi verifică comiterea care urmează să fie construită. Aceasta setează directorul de lucru într-un mod”cap detașat”, adică ref HEAD nu indică nicăieri. Lerna folosește HEAD pentru a afla versiunea curentă a pachetului și erorile în starea capului detașat.

de asemenea, trebuie să rulăm lerna publish from-package spre deosebire de lerna publish, ca executând un simplu lerna publish Lerna s-ar plânge că versiunea curentă este deja publicată, deoarece metadatele au fost actualizate când dezvoltatorul a rulat lerna version local. Argumentulfrom-package îi spune lui Lerna să publice toate versiunile care nu sunt prezente în prezent în npm pentru un pachet dat. Acest lucru ajută, de asemenea, dacă o publicare a eșuat din anumite motive și încercați din nou conducta.

faza de publicare este configurată pentru a rula numai pe etichete care se potrivesc cu următorul credit regex:

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

acesta este un pic fantezist, iar pentru majoritatea echipelor și pentru majoritatea scopurilor, pur și simplu^v*$ ar trebui să funcționeze. 🙂

Notă Deși nu am făcut-o încă, deoarece suntem o echipă mică, s-ar putea marca, de asemenea, orice etichete după regex de mai sus ca protejate în GitLab pentru a restricționa cine poate publica pachete la npm.

puteți consulta monorepo-ul nostru lahttps://github.com/mitterio/js-sdk (acest lucru este reflectat din repo-ul nostru intern GitLab).

note rapide

când rulați scripturi comune (așa cum facem pentru publicarea documentelor typescript), este destul de util să cunoașteți detaliile pachetului care rulează scriptul. Acest lucru se aplică pentru scripturile din ciclul de viață npm, precum și scripturile pe care le puteți rula folosind lerna run sau lerna exec. Pentru un pachet dat în npm, npm face întregul package.json disponibil pentru un script folosind variabile de mediu. Deci, pentru un pachet dat cu următoarele package.json:

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

următoarele variabile vor fi disponibile în timp ce rulează orice script ciclu de viață:

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

Quirks/probleme

câteva lucruri suntem încă de lucru pe cu noua configurare (unele dintre ele sunt probleme, în timp ce altele probabil că nu știm mai bine):

  • nu sunt sigur dacă este posibil, dar am dori să putem avea scripturi comune pentru ciclul de viață pentru toate pachetele noastre. Declararea acestora în rădăcina package.json nu funcționează.
  • este extrem de dificil de a testa configurarea Lerna complet fără a publica de fapt ceva la npm. Nu sunt sigur dacă există un --dry-run undeva.
  • Lerna are o modalitate de a păstra un bloc de configurare comun pentrudevDependencies astfel încât toatedevDependencies Sunt de aceeași versiune pentru fiecare dintre subpachete. Aceasta este o caracteristică destul de interesantă, dar ne-ar lua ceva timp pentru a elimina toate cele comune.
  • același lucru s-ar putea aplica și pentru alte dependențe, așa că, deși nu vom dori un bloc de configuraredependencies, ar fi frumos să avem o modalitate de a exprima variabilele disponibile în cadrul proiectelor. De exemplu, în Monorepo-ul nostru Java/Kotlin, folosim gradle.properties pentru a conține variabile precum springBootVersionspringCoreVersion etc., care sunt apoi utilizate de scripturile individuale gradle.

gândurile noastre despre monorepos
A fost o dezbatere destul de aprinsă recent cu monorepos și dacă vedem un număr imens sărind din nou pe bandwagon, amintind destul de mult de vremea când microserviciile erau la modă.

structura pe care o urmăm aici are mai multe monorepos, iar aceasta nu este prima dată când gestionăm monorepos. Întreaga noastră platformă și backend este un monorepo care conține cod privat, implementabil și mai multe pachete publice care sunt publicate în bintray. Avem, de asemenea, site-ul nostru principal rulează cu un backend de primăvară, cu frontend la pachet cu webpack sprijinirea reîncărcare la cald (webpack watch), etc. Nu am decis niciodată să mergem cu un singur mono-repo în întreaga organizație, deoarece uneltele pur și simplu nu erau acolo.

având cea mai mare parte a codului nostru Java într-un singur repo funcționează excelent, deoarecegradle oferă toate instrumentele necesare pentru Monorepo Java șilerna și ciclul de viață npm care oferă instrumentele pentru MONOREPO SDK JS. Deci, pur și simplu, monorepos sunt minunate odată ce identificați acoperirea modificărilor care apar în repo. Pentru backend nostru Java, am văzut mai multe MRs peste proiecte pentru o singură caracteristică, care ne-a înclinat să se mute la un monorepo numai pentru acest proiect special, cu toate celelalte codul nostru încă în repo separate. Și odată ce am văzut un model similar emerge pentru SDK-urile noastre JS, de asemenea, ne-am mutat la Lerna.

rețineți că suntem o echipă mică de aproximativ 9 ingineri; deci ceea ce funcționează pentru noi s-ar putea să nu funcționeze pentru echipe de diferite dimensiuni. Ceea ce am dori mai ales să subliniem este că adoptarea oricărei soluții nu trebuie să fie binară, în care fie o facem așa cum este prescris, fie nu o facem deloc.

unele dintre motivațiile pe care le-am văzut pentru un monorepo ni s-au aplicat cu siguranță și multe dintre ele nu. De exemplu, pur și simplu nu putem economisi timp pentru a construi sculele dacă întreaga noastră bază de cod a fost mutată într — un singur repo-indiferent de beneficiul pe care îl putem experimenta sau nu. Deci, dezbaterea nu este cu adevărat despre a avea un” repo unic ” — de la sine, nu este altceva decât o nouă structură de directoare. Prescrierea acestora este de a atenua anumite probleme și, ca și în cazul fiecărui „glonț de argint”, există avertismente.

dezbaterea este despre problemele comune cu care se confruntă în industria de software și ce soluții au fost luate în mod obișnuit; „comun” fiind cuvântul cheie. Zona în care vă abateți de la aplicația „comună” este locul în care ajungeți să inovați, să faceți schimbări și să construiți puțin.