Der Umgang mit Maven-Abhängigkeiten bei der Umstellung auf Git
Matt Shelton
Developer Advocate
Wir wechseln jetzt also zu Git und finden Git-flow toll. Was nun? Probieren wir alle Funktionen aus! Mein Team ist großartig. Es hat auf Basis unserer Teamaktivitäten und allen seltsamen Dingen, die wir in Zukunft vielleicht machen werden, in Confluence eine Hitliste der Entwickler-Workflows zusammengestellt. Anschließend hat es in einer Projektstruktur, die unsere eigene widerspiegelt (aber ohne Code, nur mit einer pom.xml), jeden Workflow ausprobiert.
Dabei haben sich Maven-Abhängigkeiten als unser größtes Problem herausgestellt.
Bei der Maven-Build-Nummerierung produziert Maven 1.0.0-SNAPSHOT
-Builds bis zum Release. Beim Release wird -SNAPSHOT
entfernt und deine Versionsnummer lautet 1.0.0
. Dein Build-Prozess muss in der Lage sein, deine Nebenversionen im Nachhinein hochzuzählen, sodass anschließend releaster Code beim nächsten Mal zu Builds wie 1.1.0-SNAPSHOT
führen. Die Nummer muss nicht zwingend dreistellig sein. Das kannst du zu Beginn eines Projekts festlegen. Wir werden hier mit drei Stellen arbeiten. Jedenfalls ist es wirklich wichtig, das mit dem -SNAPSHOT zu verstehen. Das ist immer die aktuellste Momentaufnahme eines Projekts vor dem Release.
Artefakte
Wie du siehst haben wir uns bei allen diesen Workflows darum bemüht, sicherzustellen, dass unsere Projektversionen und projektübergreifenden Abhängigkeiten richtig verwaltet wurden.
Jedes Mal, wenn Maven-Abhängigkeiten abgerufen werden, wird standardmäßig ein Pull aus dem guten alten Internet durchgeführt. Diese Artefakte sind lokal gespeichert, sodass nachfolgende Builds schneller ausgeführt werden können. Eine Möglichkeit, dies ein wenig schmerzloser zu gestalten, ist die Verwendung eines Artefakt-Repositorys auf deinem lokalen Netzwerk, das als ein lokaler Cache für diese externen Abhängigkeiten fungiert. Ein LAN-Abruf ist fast immer schneller als ein Download, selbst vom schnellsten CDN. Wir verwenden Artifactory Pro als Artefakt-Repository. Zusätzlich speichern wir auch unsere eigenen Build-Artefakte in Artifactory, da unsere Struktur mehrmodular ist. Wenn wir eines unserer üblichen Pakete erstellen, können wir diese bestimmte Version über die Auflösung der Maven-Abhängigkeit pullen und das Artefakt direkt aus dem Artefakt-Repository abrufen.
Dies funktioniert bestens. Mit Artifactory lassen sich auch Artefakte zwischen Instanzen synchronisieren. Wenn du es also z. B. zur Replizierung deines Release-Repositorys in deine Rechenzentren für Produktions-Deployments verwenden möchtest, wäre dies ohne den Aufbau eines separaten Prozesses möglich.
Zugehöriges Material
Verschieben eines vollständigen Git-Repositorys
Lösung anzeigen
Git kennenlernen mit Bitbucket Cloud
Maven-Abhängigkeiten, Feature Branches und Pull-Requests
Alle unsere Builds fließen in Artifactory ein. Mit SVN haben wir ein Snapshot-Repository zum Speichern der beiden neuesten Snapshot-Builds, ein Staging-Repository für alle noch nicht genehmigten Release-Builds und ein Release-Repository nur für Builds zur Produktion verwendet.[1] Diese Builds werden wie zuvor beschrieben nummeriert und sind abrufbar über ein vorhersehbares URL-Muster basierend auf Repository und Version.
Der primäre Workflow jedes Entwicklers sah folgendermaßen aus: Erstellen eines Feature Branch vom Develop Branch für die jeweilige Aufgabe, Abschließen der Aufgabe, Absetzen eines Pull-Requests zum Mergen des Arbeitsergebnisses in den Develop Branch. In einem einzelnen Projekt funktioniert dies meist ohne Schwierigkeiten, aber wir sind mit vollem Karacho auf ein Problem gestoßen, das uns ernsthaft an der gesamten Migration zweifeln ließ:
Wie ich bereits erwähnte, verfügen wir über mehrere Abhängigkeitsebenen zwischen unseren Projekten. Dafür gibt es bei unseren Produkten gute Gründe – historisch gewachsene und strategische. Wir haben über die Einführung anderer Architekturen nachgedacht, die dieses Problem beseitigen würden, aber dadurch würden wieder neue Probleme entstehen. Wir können uns das Leben erleichtern (und haben dies auch getan, aber dazu mehr in einem anderen Beitrag), doch momentan ist es für uns strategisch besser, unsere Struktur beizubehalten.
Entwickler A – sagen wir, es ist Angela – beginnt, an einem Feature in Jira zu arbeiten. Dazu braucht sie zwei Branches: einen ausgehend vom gemeinsamen Projekt und einen ausgehend von ProduktX. Die Version des gemeinsamen Projekts lautet 2.1.0-SNAPSHOT. Die Version von ProduktX ist 2.4.0-SNAPSHOT. Sie arbeitet eine Zeit lang lokal und pusht dann zu Bitbucket Data Center. Bamboo greift diese Änderungen auf, erstellt das gemeinsame Paket, lädt common-2.1.0-SNAPSHOT zu Artifactory hoch und erstellt ProduktX mit einer Abhängigkeit zu common-2.1.0-SNAPSHOT. Dabei wird auch productX-2.4.0-SNAPSHOT hochgeladen. Die Unit-Tests sind erfolgreich.
Entwickler B – nennen wir ihn Bruce – beginnt, an einem anderen Feature in Jira zu arbeiten, das zu einem anderen Projekt gehört: ProduktY. Dazu braucht er ebenfalls zwei Branches: einen ausgehend von unserem gemeinsamen Projekt und einen ausgehend von ProduktY. Die Version des gemeinsamen Projekts ist, wie schon oben erwähnt, 2.1.0-SNAPSHOT. Die Version von Produkt Y ist 2.7.0-SNAPSHOT. Er arbeitet eine Zeit lang lokal und pusht seine Änderungen dann zu Bitbucket Data Center. Bamboo greift diese Änderungen auf, erstellt das gemeinsame Paket, lädt common-2.1.0-SNAPSHOT zu Artifactory hoch und erstellt ProduktX mit einer Abhängigkeit zu common-2.1.0-SNAPSHOT. Dabei wird auch productX-2.4.0-SNAPSHOT hochgeladen. Die Unit-Tests sind erfolgreich.
In der Zwischenzeit findet Angela einen kleinen Bug in ihrem ProduktX-Code und überprüft ihren Bugfix mit einem Unit-Test. Der lokal ausgeführte Test ist erfolgreich. Sie pusht ihre Änderungen zu Bitbucket. Bamboo greift die Änderung auf und führt den Build für ProduktX aus. Die Builds laufen erfolgreich, aber einige der Unit-Tests schlagen fehl. Schwierigkeiten machen nicht ihre neuen Tests, sondern die ersten Tests ihrer anfänglichen Änderungen des Features. Wurde im Bamboo-Build etwa eine Regression gefunden, die dem lokalen Build entgangen ist? Wie ist das möglich?
Der Grund: Ihre übliche Abhängigkeit – die, die Bamboo beim Erstellen von ProduktX übernommen hat – war keine Kopie mehr. Bruce hat common-2.1.0-SNAPSHOT in Artifactory beim Abschluss seines Feature-Builds überschrieben. Es gab keinen Quellcode-Konflikt, denn beide Entwickler haben isoliert voneinander in eigenen Branches gearbeitet. Jedoch war die Quelle, aus der Maven Artefakte abruft, beschädigt.
Da möchte man den Kopf gegen den Schreibtisch schlagen.
Nachdem wir das Problem ausgemacht hatten, haben wir rund einen Monat lang alles versucht, um eine Lösung zu finden. Über unseren TAM[2] haben wir uns an das Bamboo-Team gewendet, das mit Git-flow arbeitet, und haben mit dem Entwickler gesprochen, der Git-flow, eine Java-Implementierung von Git-flow, wartet. Sie waren alle sehr hilfsbereit, kannten aber keinen Prozess mit einzelnen manuellen Schritten, die jeder Entwickler jedes Mal beim Bearbeiten eines Features befolgen könnte. Wir konnten also keine vertretbare Lösung finden.
Falls dich interessiert, was wir alles ausprobiert haben:
1. Modifiziere die Versionsnummer bei der Branch-Erstellung oder direkt danach.
- Dazu können wir mit
mvn jgitflow:feature-start
einen Branch erstellen. - Wir können einen Bitbucket Data Center-Hook oder einen lokalen Git-Hook verwenden.
- Mit
mvn version:set-version
können wir die Version manuell festlegen, nachdem wir den Branch erstellt haben. - Wir können die Änderung mit dem [maven-external-version]-Plug-in automatisieren.
2. Modifiziere die Versionsnummer, nachdem du den Branch abgeschlossen und ihn in den develop-Branch zurückgeführt hast.
- Dazu können wir mit
mvn jgitflow:feature-finish
einen Branch abschließen. - Verwende einen "git merge"-Treiber, um POM-Konflikte zu bewältigen.
- Verwende einen asynchronen post-receive-Hook in Bitbucket Data Center.
3. Mach alles manuell. (Kleiner Scherz. War nur eine kurze Überlegung.)
Mögliche Workflows
Vor dem Hintergrund dieses Kernkonzepts wirst du verstehen, dass submodule
einige Workflows gut und andere weniger optimal unterstützen. Es gibt mindestens drei Szenarien, in denen Untermodule eine gute Wahl sind:
-
Wenn eine Komponente oder ein Unterprojekt sich zu schnell ändert oder bevorstehende Änderungen nicht mit der API kompatibel sind, kannst du zur eigenen Sicherheit den Code in einen bestimmten Commit sperren.
-
Wenn eine Komponente nicht sehr oft aktualisiert wird und du sie als Anbieterabhängigkeit verfolgen möchtest. Ich mache das zum Beispiel bei meinen Vim-Plugins.
-
Wenn du einen Teil des Projekts an eine Drittpartei delegierst und deren Arbeitsergebnisse zu einem bestimmten Zeitpunkt oder beim Release integrieren möchtest. Dies funktioniert bei nicht zu häufigen Updates.
Danke an Finch für die gut erklärten Szenarien.
Jede dieser Optionen hatte ihre eigenen negativen Nebenwirkungen. In erster Linie entstanden jedes Mal, wenn die Entwickler einen Feature Branch benötigten, manuelle Schritte. Wo wir doch möchten, dass sie ständig Feature Branches erstellen. In den meisten Fällen konnten wir außerdem Pull-Request nicht effektiv einsetzen – dies kam für uns nicht in Frage.
Damit waren ein bis zwei Mitarbeiter fast zwei Monate vollkommen ausgelastet, bis wir letztendlich in einem Moment der Erleuchtung feststellten, dass wir das Problem von der falschen Seite angegangen sind.
Eine Version für alle
Im Jahre 2020 werden wir sicher zurückblicken und denken: Unser größter Fehler war, uns zu sehr auf Git-flow-Tools zu konzentrieren, statt die Tools zu nutzen, die wir längst hatten, um den Workflow zu implementieren, den wir uns wünschten. Wir hatten:
- Jira
- Bamboo Data Center
- Maven
- Artifactory Pro
Wie es sich herausstellte, benötigten wir lediglich diese Tools.
Einer unserer Entwickler hatte eine clevere Idee: Da der Grund des Problems nicht beim Build-Management an sich lag, sondern eher bei den zu überschreibenden Artefakten, sollten wir den Fehler besser in Artifactory beheben. Er schlug vor, sich eine Maven-Eigenschaft zunutze zu machen und für die Repository-URL des Snapshots eine selbst festgelegte URL mit der Vorgangs-ID von Jira zu definieren. Anschließend wollte er die Artefakte mit einer individuellen Vorlage zu einem dynamisch erstellten Repository in Artifactory überführen. Bei der Auflösung von Abhängigkeiten in Maven sind Artefakte im Entwicklungs-Snapshot-Repository auffindbar, wenn wir für sie keine Branches erstellt haben, z. B. bei der Arbeit an nur einem Produkt und nicht am gemeinsamen Projekt.
Wir haben diese praktische kleine Eigenschaftsvariable in unsere Build-Einstellungsdatei aufgenommen und ein Maven-Plug-in erstellt, um die Variable im frühsten Abschnitt des Maven-Build-Lebenszyklus einzusetzen. Auf dem Papier hörte sich das unglaublich gut an und hat das Team wieder darin bestärkt, härter an der Lösung des Problems zu arbeiten. In der Realität konnten wir das allerdings nicht umsetzen. Die früheste Phase des Maven-Lebenszyklus ist das Validieren. Zu dem Zeitpunkt, an dem Plug-ins zur Validierung ausgeführt werden, sind die Repository-URLs bereits aufgelöst worden. Daher wurde unsere Variable nie eingesetzt und die URL enthält schließlich nicht den Namen des Branches. Selbst wenn wir ein Layout in einem Repository verwendet hätten, das von unseren Entwicklungs-Snapshots getrennt gewesen wäre, wäre es nicht derart isoliert gewesen, um eine Parallelentwicklung zu ermöglichen.
Und wieder einmal schlagen wir unsere Köpfe gegen den Tisch.
Nach einem Bier suchte der eben erwähnte Entwickler nach einem anderen Weg, Maven mit zusätzlichen Funktionen auszustatten: Erweiterungen.
"Auf den Alkohol, den Ursprung und die Lösung sämtlicher Lebensprobleme." – Homer Simpson
Erweiterungen, wie auch Plug-ins, sind starke Werkzeuge zur Verbesserung deines Maven-Workflows. Sie werden jedoch vor dem Lebenszyklus-Ziel ausgeführt und greifen besser in die Maven-Mechanismen. Wir haben in Maven mit dem Paket RepositoryUtils erzwungen, dass die URLs mit einem speziellen Parser neu ausgewertet und dann mit unseren aktualisierten Werten neu festgesetzt werden.[3]
Nachdem die Erweiterung vorhanden und auch getestet war, arbeiteten wir uns zügig durch die Vorbereitung der Migration und "Das wird niemals Wirklichkeit werden" wurde zu "Das WIRD am Montag Wirklichkeit ... und ich muss bis morgen zehn Seiten Dokumentation verfassen". Ich werde in Kürze noch mehr darüber schreiben, wie die Tools in unserem neuen Entwicklungs-Workflow zusammenarbeiten und was wir während des gesamten Prozesses gelernt haben.
Fußnoten
[1]: Eine Kehrseite dieser Medaille: Ich musste ein Skript erstellen, damit die Artifactory-REST-API die Builds vom Staging in den Release "beförderte". Die Geschwindigkeit ist ausreichend, aber eine Automatisierung wäre dringend angebracht.
[2]: Technical Account Manager. Weitere Informationen findest du hier.
[3]: Nach den anfänglichen Entwicklungsarbeiten dachten wir, wir müssten mehr Arbeit investieren, damit alles immer funktioniert. Wenn z. B. ein Snapshot in Artifactory (von einem anderen Entwickler) neuer ist als der lokale Snapshot, dann schnappt sich Maven das Remote-Artefakt, weil es NEUER ist – und NEUER heißt BESSER, oder etwa nicht?
Diesen Artikel teilen
Nächstes Thema
Lesenswert
Füge diese Ressourcen deinen Lesezeichen hinzu, um mehr über DevOps-Teams und fortlaufende Updates zu DevOps bei Atlassian zu erfahren.