Close

Postępowanie z zależnościami Maven podczas przechodzenia na Git

Zdjęcie portretowe Nicoli Paolucciego
Matt Shelton

Developer Advocate


A zatem migrujemy do Git i chętnie korzystamy z git-flow. Co teraz? Przetestujmy to! Mój zespół jest niezastąpiony. Skompilował w Confluence listę najlepszych programistycznych przepływów pracy na postawie naszej dotychczasowej współpracy oraz wszystkich dziwnych rzeczy, które być może będziemy musieli robić w przyszłości, a następnie przetestował każdy z nich na strukturze projektu odzwierciedlającej nasz (ale bez kodu — z samym pom.xml).

Zależności Maven miały okazać się największym problemem.

Numeracja kompilacji Maven Maven generuje kompilacje 1.0.0-SNAPSHOT aż do momentu wydania. Po wydaniu część -SNAPSHOT zostaje usunięta, a numerem wersji staje się 1.0.0. Twój proces kompilacji musi uwzględniać stopniowe zwiększanie numeru wersji pomocniczej, tak aby późniejsze prace nad kolejnym zadaniem generowały kompilacje takie jak 1.1.0-SNAPSHOT. Nie ma obowiązku używania trzech liczb — można to określić na początku projektu. My akurat używamy trzech. W każdym razie należy mieć świadomość znaczenia elementu -SNAPSHOT. Jest on wskazaniem najnowszej wersji projektu przed wydaniem.

Artefakty


Naszym największym zmartwieniem w przypadku wszystkich tych przepływów pracy jest zapewnienie, że wszystkie wersje projektu i zależności wewnątrzprojektowe są prawidłowo zarządzane.

Za każdym razem, gdy zależności Maven są pobierane do kompilacji, domyślnie ściągane są z Internetu. Artefakty te są przechowywane lokalnie, aby kolejne kompilacje mogły być generowane szybciej. Jednym z rozwiązań, które usprawnia cały ten proces, jest korzystanie z repozytorium artefaktów w sieci lokalnej, które działa jako pamięć lokalna dla zewnętrznych zależności. Pobieranie z sieci LAN niemal zawsze jest szybsze niż pobieranie z Internetu, nawet w przypadku najszybszych sieci CDN. Jako repozytorium artefaktów służy nam Artifactory Pro. Z racji struktury wielomodułowej w Artifactory przechowujemy również nasze własne artefakty kompilacji. Gdy tworzymy jeden z naszych wspólnych pakietów, możemy pobrać tę konkretną wersję za pomocą rozwiązania zależności Maven i wydobyć artefakt prosto z repozytorium.

To rozwiązanie się sprawdza. Artifactory umożliwia także synchronizację artefaktów między instancjami. Jeśli np. chcesz zreplikować repozytorium wydań w centrach danych dla potrzeb wdrożeń produkcyjnych, możesz to zrobić bez generowania oddzielnego procesu.

Bazy danych
materiały pokrewne

Jak przenieść pełne repozytorium Git

Logo Bitbucket
POZNAJ ROZWIĄZANIE

Poznaj środowisko Git z rozwiązaniem Bitbucket Cloud

Zależności Maven, gałęzie funkcji i pull requesty


Wszystkie nasze kompilacje trafiają do Artifactory. W przypadku SVN używaliśmy repozytorium migawek do przechowywania 2 najświeższych kompilacji migawek, repozytorium tymczasowego do wszystkich niezatwierdzonych jeszcze kompilacji wydań oraz repozytorium wydań tylko do kompilacji, które przeszły do fazy produkcji[1]. Te kompilacje są numerowane w opisany przeze mnie powyżej sposób i można je wydobyć za pomocą przewidywalnego wzorca adresu URL opartego na repozytorium i wersji.

Podstawowym przepływem pracy dla każdego programisty było utworzenie gałęzi funkcji z gałęzi programowania, ukończenie jej, a następnie zgłoszenie pull requestu celem scalenia z powrotem z gałęzią programowania. W przypadku pojedynczego projektu to może się sprawdzić bez zarzutu, ale pozwolę sobie przedstawić pewien problem, z którym się zderzyliśmy tak mocno, że zaczęliśmy wątpić w sens migracji:

Jak wspomniałem wcześniej, między naszymi projektami mamy wiele poziomów zależności. Jest ku temu istotny powód natury zarówno historycznej, jak i strategicznej. Rozważaliśmy użycie innych architektur, które wyeliminowałyby ten problem, ale one wiązały się z kolejnymi. Mogliśmy ułatwić sobie życie (i finalnie to zrobiliśmy, ale to temat na inny wpis), jednakże póki co strategiczne znaczenie miało dla nas zachowanie istniejącej struktury bez zmian.

Przykładowo programistka A (powiedzmy, że Angela) zaczyna pracę nad funkcją w Jirze. Wymaga to dwóch gałęzi: jednej z naszego wspólnego projektu i jednej z produktu X. Wersja dla wspólnego projektu to 2.1.0-SNAPSHOT. Wersja dla produktu X to 2.4.0-SNAPSHOT. Angela pracuje lokalnie przez jakiś czas, a następnie wypycha dokonane zmiany do Bitbucket Data Center. Bamboo odbiera te zmiany, kompiluje pakiet wspólny i przekazuje common-2.1.0-SNAPSHOT do Artifactory, a następnie kompiluje produkt X z zależnością do common-2.1.0-SNAPSHOT, przekazując także productX-2.4.0-SNAPSHOT. Testy jednostkowe dają wynik pozytywny.

Programista B (powiedzmy, że Bruce) zaczyna pracę nad inną funkcją w Jirze dla innego produktu, zwanego produktem Y. Wymaga to również dwóch gałęzi: jednej z naszego wspólnego projektu i jednej z produktu Y. Wersja dla wspólnego projektu to, jak powyżej, 2.1.0-SNAPSHOT. Wersja dla produktu Y to 2.7.0-SNAPSHOT. Bruce pracuje lokalnie przez jakiś czas, a następnie wypycha dokonane zmiany do Bitbucket Data Center. Bamboo odbiera te zmiany, kompiluje pakiet wspólny i przekazuje common-2.1.0-SNAPSHOT do Artifactory, a następnie kompiluje produkt X z zależnością do common-2.1.0-SNAPSHOT, przekazując także productX-2.4.0-SNAPSHOT. Testy jednostkowe dają wynik pozytywny.

W międzyczasie Angela znajduje drobny błąd w kodzie produktu X i pisze test jednostkowy, aby sprawdzić swoją poprawkę. Uruchamia go lokalnie i test daje wynik pozytywny. Przesyła swoje zmiany do Bitbucket, a Bamboo odbiera zmianę i kompiluje produkt X. Kompilacja jest poprawna, ale niektóre z testów jednostkowych kończą się niepowodzeniem. Nie te nowe, które napisała, ale te pierwsze z czasu początkowych zmian w funkcji. W jakiś sposób kompilacja Bamboo znalazła regresję, a lokalna nie? Jak to możliwe?

Otóż stało się tak dlatego, że jej wspólna zależność, ta pobrana przez Bamboo podczas kompilacji produktu X, nie stanowiła już jej kopii. Bruce nadpisał common-2.1.0-SNAPSHOT w Artifactory wskutek ukończenia kompilacji swojej funkcji. Nie doszło do konfliktu kodu źródłowego — programiści pracowali oddzielnie nad własnymi gałęziami, ale źródło danych do wydobycia artefaktu Maven było uszkodzone.

Głowo, oto biurko.

Przez cały miesiąc od momentu odkrycia tego problemu próbowaliśmy wszystkiego, aby to obejść. Za pośrednictwem naszego TAM[2] dyskutowaliśmy z zespołem Bamboo korzystającym z git-flow, jak i z programistą zarządzającym implementacją git-flow w java. Wszyscy byli niezwykle pomocni, ale poza procesem, który wymagałby od każdego programisty ręcznego wykonania całego szeregu kroków przy każdej pracy nad daną funkcją, nie mogliśmy znaleźć żadnego satysfakcjonującego rozwiązania.

Na wypadek gdyby Cię interesowało, podajemy wszystkie metody, które wypróbowaliśmy:

1. Zmiana numeru wersji podczas lub bezpośrednio po utworzeniu gałęzi.

  • Można to osiągnąć używając polecenia mvn jgitflow:feature-start celem utworzenia gałęzi.
  • Można użyć hooka Bitbucket Data Center lub lokalnego githooka.
  • Można ręcznie ustawić numer wersji za pomocą polecenia mvn version:set-version po utworzeniu gałęzi.
  • Można zautomatyzować zmianę za pomocą wtyczki [maven-external-version].

2. Zmiana numeru wersji po zakończeniu tworzenia gałęzi i scaleniu jej z gałęzią programowania.

  • Można to zrobić, używając polecenia mvn jgitflow:feature-finish celem zakończenia gałęzi.
  • Można użyć sterownika git merge do obsługi konfliktów POM.
  • Można użyć asynchronicznego hooka post-receive w Bitbucket Data Center.

3. Zrobienie tego wszystkiego ręcznie. (Oczywiście żartujemy. Nie rozważaliśmy tej opcji zbyt długo).

Możliwe przepływy pracy


Pamiętając o tej kluczowej koncepcji i mając ją na uwadze, możesz uzmysłowić sobie, że funkcja submodule sprawdza się dobrze w niektórych przepływach pracy, a w innych jest mniej optymalny. Istnieją co najmniej trzy scenariusze, w których moduły podrzędne są dobrym wyborem:

  • Gdy komponent lub projekt podrzędny zmieniają się zbyt szybko lub nadchodzące zmiany spowodują uszkodzenie interfejsu API, możesz dla bezpieczeństwa zablokować kod na konkretnym commicie.

  • Gdy masz komponent, który nie jest aktualizowany zbyt często, a chcesz go śledzić jako zależność dostawcy. Sam robię tak na przykład ze swoimi wtyczkami vim.

  • Gdy delegujesz fragment projektu do innej osoby i chcesz zintegrować wykonaną przez nią pracę w konkretnym czasie lub wydaniu. Także tutaj ten sposób sprawdza się wówczas, gdy aktualizacje nie są zbyt częste.

Podziękowania dla fincha za dobrze wyjaśnione scenariusze.

Każda z tych opcji wiązała się z jakimś negatywnym skutkiem ubocznym. Przede wszystkim programista musiał ręcznie wykonać całą procedurę za każdym razem, gdy potrzebował gałęzi funkcji. A my chcieliśmy, by możliwe było tworzenie gałęzi funkcji na bieżąco. W większości przypadków nie mogliśmy też efektywnie korzystać z pull requestów, co było kwestią decydującą.

Jedna lub dwie osoby zajmowały się tym problemem przez niemal dwa miesiące — aż do momentu, gdy doznaliśmy objawienia, że podchodzimy do niego z niewłaściwej strony.

Jedna wersja, by wszystkimi rządzić


Z perspektywy czasu widzę, że naszym największym błędem było to, że przy wdrażaniu pożądanego przepływu pracy skupiliśmy się na narzędziach git-flow, zamiast wykorzystać narzędzia, które już posiadaliśmy wcześniej. Oto, co mieliśmy do dyspozycji:

  • Jira
  • Bamboo Data Center
  • Maven
  • Artifactory Pro

Okazało się, że były to wszystkie narzędzia, jakich potrzebowaliśmy.

Jeden z naszych inżynierów wpadł na genialny pomysł, że skoro problemem nie jest samo zarządzanie kompilacją, ale raczej nadpisywanie artefaktów, to powinniśmy naprawić Artifactory. Jego pomysł zakładał użycie właściwości Mavena do ustawienia w repozytorium migawek niestandardowego adresu URL, który zawierałby identyfikator zgłoszenia Jira, oraz zapisywanie artefaktów w dynamicznie utworzonym repozytorium w Artifactory za pomocą niestandardowego szablonu. Mechanizm rozwiązywania zależności Maven miałby znajdować artefakty w repozytorium migawek w przypadku, gdyby nie było konieczne tworzenie dla nich gałęzi, na przykład przy pracy nad produktem, ale nie projektem wspólnym.

Ustawiliśmy tę poręczną małą zmienną właściwości w naszym pliku ustawień kompilacji i napisaliśmy wtyczkę Maven, która wypełniałaby ją wstępnie podczas najwcześniejszej fazy cyklu życia kompilacji Maven. W teorii brzmiało to niesamowicie i zachęciło zespół do cięższej pracy nad rozwiązaniem problemu. Kłopot w tym, że w praktyce nie mogliśmy tego zrobić. Najwcześniejszym etapem cyklu życia Maven jest walidacja. Zanim wtyczki związane z walidacją zostaną uruchomione, adresy URL repozytoriów już zostają ustalone. Z tego względu nie następowało wypełnienie naszej zmiennej, a adres URL nie był oparty na nazwie gałęzi. Choć używaliśmy układu znajdującego się w odrębnym repozytorium względem naszych migawek, nie był on odizolowany na potrzeby równoległego wprowadzania zmian.

Głowo, oto ponownie biurko, twój NAJLEPSZY PRZYJACIEL.

Wypiwszy sobie piwo wspomniany inżynier poszperał jeszcze trochę i zbadał inny sposób dodawania funkcjonalności do Mavena: rozszerzenia.

„Niech żyje piwo: przyczyna, jak i rozwiązanie wszystkich problemów życiowych”. — Homer Simpson

Rozszerzenia, podobnie jak wtyczki, dają całą masę możliwości usprawnienia pracy Mavena, jednakże są one wykonywane przed celami cyklu życia i mają większy dostęp do wewnętrznych zasobów Mavena. Wykorzystując pakiet RepositoryUtils, zmusiliśmy Mavena do ponownej oceny swoich adresów URL przy użyciu niestandardowego parsera, a następnie do ponownego ich ustawienia z wykorzystaniem zaktualizowanych przez nas wartości[3].

Po zainstalowaniu i przetestowaniu rozszerzenia zaczęliśmy po kolei wykonywać zadania przedmigracyjne... i stopniowo zaczęliśmy przechodzić z trybu „to nie ma prawa się udać” do „to się uda w poniedziałek... więc teraz muszę stworzyć dziesięć stron dokumentacji na jutro rano”. Wkrótce napiszę nieco więcej o tym, jak owe narzędzia ze sobą współpracują celem wykształcenia naszego nowego przepływu pracy, a także o niektórych nauczkach, jakie wyciągnęliśmy z tego procesu.


Przypisy

[1]: Jednym z minusów była konieczność napisania skryptu do zmuszenia Artifactory REST API do przepchnięcia kompilacji od stanu z przechowalni do etapu wydania. To dość szybki proces, ale aż się prosi o większą automatyzację.

[2]: Menedżer techniczny ds. klientów Więcej informacji można znaleźć tutaj.

[3]: Po zakończeniu pierwszych prac programistycznych zorientowaliśmy się, że musimy zrobić znacznie więcej, aby to rozwiązanie działało w 100% przypadków; np. kiedy migawka w Artifactory (pochodząca od innego inżyniera) jest nowsza od naszej lokalnej, Maven sięga po artefakt zdalny, bo skoro jest NOWSZY, to siłą rzeczy musi być LEPSZY, prawda?

Matt Shelton
Matt Shelton

Matt Shelton jest specjalistą i praktykiem DevOps, który nadzoruje dostarczanie usług DevOps (i innych powiązanych usług) dla firmy Atlassian w Ameryce Północnej i Południowej. Ostatnio rzadko bloguje. Skupia się na kształceniu następnego pokolenia zwolenników lepszych sposobów pracy.


Udostępnij ten artykuł

Zalecane lektury

Dodaj te zasoby do zakładek, aby dowiedzieć się więcej na temat rodzajów zespołów DevOps lub otrzymywać aktualności na temat metodyki DevOps w Atlassian.

Ludzie współpracujący przy ścianie pełnej narzędzi

Blog Bitbucket

Ilustracja DevOps

Ścieżka szkoleniowa DevOps

Demonstracje funkcji z ekspertami Atlassian

Zobacz, jak Bitbucket Cloud współpracuje z Atlassian Open DevOps

Zapisz się do newslettera DevOps

Thank you for signing up