Articles

przejście z wielu repozytoriów do lerna-js mono-repo

w mitter.io, mamy kilka publicznych pakietów npm, które musimy opublikować, a niedawno przenieśliśmy się do struktury mono-repo zarządzanej przez Lernę z posiadania oddzielnych repozytoriów dla każdego z nich. Dzisiaj chciałbym podzielić się naszym doświadczeniem z tą migracją i naszą konfiguracją z nową strukturą monorepo. Wszystkie nasze pakiety są celami SDK lub zależnościami dla naszych celów SDK:

  • @mitter-io/core – podstawowa funkcjonalność Mittera.io SDK
  • @mitter-io/models – modele maszynopisu (klasy, aliasy typów, interfejsy itp.) dla zestawów SDK
  • @mitter-io/web – Web SDK
  • @mitter-io/react-native – React Native SDK
  • @mitter-io/node – węzeł.JS SDK (używany dla węzła.js backends)
  • @mitter-io/react-scl – standardowa biblioteka komponentów dla aplikacji ReactJS

wszystkie nasze pakiety są napisane w TypeScript i typy są dołączone do samych pakietów, a my nie dystrybuujemy oddzielnych pakietów do pisania (te, które zwykle widzisz zaczynając od@types/). Używamy rollup do łączenia tych pakietów zarówno w formatach UMD, jak i ES5. Oprócz tego używamy TypeDoc do generowania naszej dokumentacji, która jest następnie publikowana na publicznym wiadrze w AWS S3.

przed użyciem Lerny mieliśmy osobne repozytorium dla każdego z naszych pakietów i działało dobrze, podczas gdy mieliśmy tylko web SDK. W miarę postępu prac nad SDK i coraz więcej programistów pracowało nad nim, zaczęliśmy borykać się z kilkoma problemami z naszą konfiguracją:

  1. biorąc pod uwagę, że większość logiki SDK znajduje się w @mitter-io/core, prawie każda zmiana, która nastąpiła w core pakiet i wszystkie inne pakiety musiały zostać zaktualizowane, aby wskazać nową wersję. Tak więc, nawet jeśli był błąd, który miał zostać naprawiony, powiedzmy React Native, zmiana pojawiłaby się w core, ale aktualizacja musiała teraz odzwierciedlać wszystkie inne cele, tj. webnode I react-native. To było dość powszechne dla dewelopera, aby przegapić cel.
  2. prawie każda zmiana w SDK spowodowałaby zmiany w co najmniej 3 z 5 pakietów.
  3. zauważyliśmy ogromną korzyść w utrzymaniu tej samej wersji pomiędzy pakietami (ułatwia programistom odgadnięcie, jaka będzie najnowsza wersja target), ale ręczne śledzenie tego stało się kłopotliwe.
  4. npm link(lubyarn linkjeśli wolisz) miał swój własny zestaw problemów z upewnieniem się, że wszystkie zależności są połączone, a następnie odłączony, aby użyć poprawnego znpm I z powrotem do lokalnego łącza do rozwoju.
  5. dość często uruchamiano skrypty w pakietach (np., aby opublikować dokumenty typescript), a my używaliśmy Delikatnego rusztowania dowiązań symbolicznych i skryptów bash do zarządzania tym samym.

w tym czasie natknęliśmy się na Lernę i wydawało się, że idealnie pasuje do naszych wymagań.

zdecydowaliśmy się podążać najprostszą ścieżką, starając się jak najwięcej użyć domyślnych. Z tego, czego doświadczyliśmy, migracja do Lerny była bardzo prosta. Zacznij od utworzenia nowego Lerna repo:

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

Odpowiedz na kilka prostych pytań (gdzie zawsze uciekamy się do domyślnego) i wszystko gotowe. Przeniesienie naszych starych pakietów z ich repo do nowego (czego obawialiśmy się, ponieważ myśleliśmy, że będzie to ogromny ból) było o wiele łatwiejsze niż oczekiwaliśmy:

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

zauważ, że --flatten może być wymagane, ale napotkaliśmy problemy bez niego.

niesamowite w Lernie jest to, że wprowadza wszystkie commity Gita wraz z nim (możesz stracić trochę historii z --flatten), tak że dla nowego repo historia wygląda na to, że rozwój zawsze miał miejsce w tym monorepo. Jest to absolutnie niezbędne, ponieważ będziesz musiał git blame kogoś do błędu, który odkryłeś po przejściu do monorepo.

aktualna konfiguracja

dzięki Lernie zarządzamy teraz jednym repozytorium dla wszystkich naszych pakietów, ze strukturą katalogów, która wygląda następująco:

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

aby opublikować zmienione Pakiety, musimy teraz po prostu:

lerna boostrap
lerna publish

nie musisz robićlerna bootstrap za każdym razem; tylko jeśli jest to pierwszy raz, gdy sprawdzasz repo. To, co robi, to po prostu instaluje wszystkie zależności każdego z pakietów w tym repo.

jednocześnie zdecydowaliśmy się nieco usprawnić nasz proces i dodaliśmy wszystkie zadania pakowania w ramach samego cyklu życia npm. Zauważ, że to nie ma nic wspólnego z Lerną; jest to coś, co idealnie powinno znaleźć się w każdym pakiecie npm, niezależnie od struktury repo. Dla każdego z pakietów, następujące skrypty są obecne w poszczególnych pacakge.json:

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

to buduje pakiet za pomocą kompilatora typescript, łączy go z rollupem i generuje dokumenty za pomocą typedoc:

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

posiadanie pojedynczej struktury repo pozwala również na przechowywanie wspólnych skryptów w jednym miejscu, tak aby zmiany miały zastosowanie we wszystkich pakietach (powinniśmy również przenieść skrypt kompilacji do osobnego skryptu, biorąc pod uwagę, że stał się on dość złożonym poleceniem bash).

przepływ dewelopera

przepływ dewelopera poza wydaniami pozostaje niezmieniony. Programista tworzy sprawę na GitLab( lub jest jej przypisany), tworzy nową gałąź dla sprawy, a następnie Scala zmiany do master po przejrzeniu kodu. Cykl życia wydania przebiega teraz zgodnie z niezwykle ustrukturyzowanym procesem:

  1. gdy kamień milowy zostanie ukończony i planujemy wydanie nowej wersji, jeden z programistów (odpowiedzialny za tę konkretną wersję) tworzy nową wersję, uruchamiając lerna version.
  2. Lerna dostarcza niezwykle pomocny i łatwy w użyciu monit do znalezienia następnej wersji
(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

Po wybraniu nowej wersji, Lerna zmienia wersje pakietów, tworzy tag w zdalnym repozytorium i wypycha zmiany do naszej instancji GitLab. Poza tym programiści nie są zobowiązani do robienia niczego innego. Nasz CI jest skonfigurowany do budowania wszystkich tagów, które mają nazwę podobną do semantycznego numeru wersji.

Uwaga uruchamiamylerna version z--force-publish, ponieważ chcemy, aby wszystkie pakiety miały dokładnie tę samą linię wersji. Tak więc czasami mamy pakiety, które nie różnią się między różnymi wersjami. W zależności od preferencji możesz tego nie robić.

konfiguracja CI

używamy zintegrowanego CI GitLab do budowania, testowania i publikowania we wszystkich naszych projektach (JS i Java). Dla nowego js monorepo mamy dwa etapy:

  1. Build
  2. Publish

Faza budowania jest niezwykle prosta i uruchamia dwa następujące Skrypty:

lerna bootstrap
lerna run build

Ta faza działa na każdym commicie, aby zasadniczo sprawdzić poprawność działania pakietu. Z drugiej strony, Faza publikowania przebiega w następujący sposób:

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

stwierdziliśmy, że musimy zrobić git checkout master I git reset --hard ponieważ GitLab klonuje (lub pobiera, w zależności od konfiguracji) repo, a następnie sprawdza commit, który ma zostać zbudowany. Ustawia to katalog roboczy w trybie 'odłączonej głowicy’, tzn. ref HEAD nie wskazuje nigdzie. Lerna używa HEAD, aby sprawdzić aktualną wersję pakietu i błędy w odłączonym stanie head.

musimy również uruchomić lerna publish from-package w przeciwieństwie do lerna publish, ponieważ wykonanie prostego lerna publish spowodowałoby, że Lerna narzekałaby, że aktualna wersja jest już opublikowana, ponieważ metadane zostały zaktualizowano, gdy deweloper uruchomił lerna version lokalnie. Argument nakazuje lernie opublikować wszystkie wersje, które nie są obecnie obecne w npm dla danego pakietu. Pomaga to również, jeśli publikowanie nie powiodło się z jakiegoś powodu i próbujesz ponownie potoku.

Faza publikowania jest skonfigurowana tak, aby działała tylko na tagach, które odpowiadają następującym regex credit:

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

jest to nieco fantazyjne i dla większości zespołów i dla większości celów powinno działać po prostu^v*$. 🙂

Uwaga chociaż jeszcze tego nie zrobiliśmy, ponieważ jesteśmy małym zespołem, można również oznaczyć dowolne tagi po powyższym wyrażeniu regularnym jako chronione w GitLab, aby ograniczyć, kto może publikować pakiety do npm.

Możesz sprawdzić nasze monorepo pod adresemhttps://github.com/mitterio/js-sdk (jest to odzwierciedlone w naszym wewnętrznym repo GitLab).

szybkie notatki

podczas uruchamiania zwykłych skryptów (tak jak w przypadku publikowania dokumentów typescript), bardzo przydatne jest poznanie szczegółów pakietu uruchamiającego skrypt. Dotyczy to skryptów w cyklu życia npm, a także skryptów, które można uruchomić za pomocą lerna run lub lerna exec. Dla danego pakietu w npm, npm udostępnia całypackage.json skryptowi używającemu zmiennych środowiskowych. Tak więc dla danego pakietu z następującym package.json:

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

następujące zmienne będą dostępne podczas uruchamiania dowolnego skryptu cyklu życia:

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

dziwactwa/problemy

kilka rzeczy, nad którymi wciąż pracujemy dalej z nową konfiguracją (niektóre z nich to problemy, podczas gdy niektóre prawdopodobnie po prostu nie wiemy lepiej):

  • nie jesteśmy pewni, czy jest to możliwe, ale chcielibyśmy mieć wspólne skrypty cyklu życia dla wszystkich naszych pakietów. Deklarowanie ich w katalogu głównym package.json nie działa.
  • niezwykle trudno jest całkowicie przetestować konfigurację Lerna bez publikowania czegoś na npm. Nie wiem, czy gdzieś jest --dry-run.
  • Lerna ma sposób na utrzymanie wspólnego bloku konfiguracyjnego dladevDependencies tak, że wszystkiedevDependencies są tej samej wersji dla każdego z pakietów. Jest to dość fajna funkcja, ale zajmie nam trochę czasu, aby pozbyć się wszystkich typowych.
  • to samo może dotyczyć również innych zależności, więc chociaż nie chcemy wspólnego bloku konfiguracyjnegodependencies, posiadanie sposobu wyrażania zmiennych dostępnych w projektach byłoby miłe. Na przykład w naszym Monorepo Java/Kotlin używamy gradle.properties, aby zawierać zmienne takie jak springBootVersionspringCoreVersion itd., które są następnie używane przez poszczególne Skrypty gradle.

nasze przemyślenia na temat monorepos
To była dość gorąca debata ostatnio z monorepos i czy widzimy ogromną liczbę skaczących na modę ponownie, dość przypomina czasy, kiedy mikroserwisy były wściekłe.

struktura, którą tu obserwujemy, ma wiele monoreposów i nie jest to nasz pierwszy raz, gdy zarządzamy monoreposami. Cała nasza platforma i backend to monorepo, które zawiera prywatny, wdrożony kod i wiele publicznych pakietów, które są publikowane w bintray. Mamy również naszą główną stronę internetową działającą z backendem sprężynowym, z frontendem w pakiecie webpack obsługującym hot reloading (webpack watch) itp. Nigdy nie zdecydowaliśmy się na pojedynczy mono-repo w całej organizacji, ponieważ narzędzia po prostu nie było.

posiadanie większości naszego kodu Java w jednym repo działa świetnie, ponieważgradle zapewnia wszystkie narzędzia potrzebne do Monorepo Java ilerna oraz cykl życia npm zapewniający oprzyrządowanie dla MONOREPO JS SDK. Mówiąc najprościej, monorepos są świetne, gdy zidentyfikujesz zakres zmian, które trafiają do twojego repozytorium. W przypadku naszego zaplecza Java widzieliśmy wiele projektów MRs dla jednej funkcji, co skłoniło nas do przejścia do monorepo tylko dla tego konkretnego projektu, z całym naszym kodem nadal w osobnych repos. Kiedy zobaczyliśmy podobny wzór dla naszych zestawów SDK JS, przenieśliśmy się do Lerny.

zauważ, że jesteśmy małym zespołem około 9 inżynierów; więc to, co działa dla nas, może nie działać dla zespołów o różnych rozmiarach. Przede wszystkim chcielibyśmy zwrócić uwagę na to, że przyjęcie jakiegokolwiek rozwiązania nie musi być binarne, w którym albo robimy to zgodnie z zaleceniami, albo nie robimy tego w ogóle.

niektóre z motywacji, które widzieliśmy dla monorepo, zdecydowanie się do nas odnosiły, a wiele z nich nie. Na przykład, po prostu nie możemy poświęcić czasu na zbudowanie narzędzia, jeśli cała nasza baza kodu została przeniesiona do jednego repo — niezależnie od korzyści, jakie możemy lub nie możemy doświadczyć. Tak więc w debacie nie chodzi o posiadanie „pojedynczego repo” – samo w sobie jest to nic innego jak nowa struktura katalogów. Ich receptą jest złagodzenie pewnych problemów i jak w przypadku każdej „srebrnej kuli”, istnieją zastrzeżenia.

debata dotyczy typowych problemów w branży oprogramowania i tego, jakie rozwiązania zostały powszechnie przyjęte; „wspólne” jest słowem kluczowym. Obszar, w którym odbiegasz od” wspólnej ” aplikacji, to miejsce, w którym możesz wprowadzać innowacje, wprowadzać zmiany i budować trochę.