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ą:
- biorąc pod uwagę, że większość logiki SDK znajduje się w
@mitter-io/core
, prawie każda zmiana, która nastąpiła wcore
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ę wcore
, ale aktualizacja musiała teraz odzwierciedlać wszystkie inne cele, tj.web
node
Ireact-native
. To było dość powszechne dla dewelopera, aby przegapić cel. - prawie każda zmiana w SDK spowodowałaby zmiany w co najmniej 3 z 5 pakietów.
- 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.
-
npm link
(lubyarn link
jeś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. - 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:
- 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
. - 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 uruchamiamy
lerna 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:
- Build
- 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
Igit 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. refHEAD
nie wskazuje nigdzie. Lerna używaHEAD
, 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 dla
devDependencies
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 konfiguracyjnego
dependencies
, posiadanie sposobu wyrażania zmiennych dostępnych w projektach byłoby miłe. Na przykład w naszym Monorepo Java/Kotlin używamygradle.properties
, aby zawierać zmienne takie jakspringBootVersion
springCoreVersion
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ę.