Original von: https://www.martindroessler.de/blog/index.php?/archives/83-Establishing-a-semi-automated-gitflowversioning-process-with-a-maven-project.html

Die Situation

Wir haben ein Maven-Projekt mit sehr vielen Submodulen. Wir nennen es liebevoll „Klotz“.
Es gibt ein VCS (mercurial) und sämtliche Entwicklung findet im Branch „default“ statt. Hin und wieder wird aus dem aktuellen Stand von „default“ ein Release-Branch erzeugt.
Die Version im default-Branch wird einmal im Jahr aktualisiert, und für die Release-Branches wird die Version in folgender Art und Weise angepasst:

find -name pom.xml -exec sed -i "s/19.0.0-SNAPSHOT/19.2.2.0-SNAPSHOT/g" {} \;

Die Probleme ..äh, Herausforderungen

  1. Der default-Branch ist hin und wieder instabil, da jeder Entwickler seinen work-in-progress in diesen Branch pushed
  2. Das Arbeiten mit Branches in mercurial ist ein Krampf! Vor allem da Branches global sind – also wird auch kaum mit Feature-Branches gearbeitet. (Die Bookmark-Extension ist vielleicht hinreichend für lokales Arbeiten, aber kein adäquater Ersatz!)
  3. Bugfixes werden direkt in den Release-Branch eingecheckt – ohne die Version anzupassen. Die in Produktion laufende Version kann sich also anders verhalten als erwartet.

Das Ziel

Es ist 2019 (zum Zeitpunkt da ich den ursprünglichen Artikel geschrieben habe)! Es sollte also wohl möglich sein, es besser zu machen!

Wir wollen:

  • ein sauberes Branching-Model samt Workflow
  • das Erstellen eines Release, Hotfix und Versions-Updates sollten mit einem Klick möglich sein
  • das Arbeiten mit Pull-Requests soll erzwungen werden

Das Unterfangen

Die Kombination von Maven und git(flow) birgt einige Schwierigkeiten.
Die Erste ist: den Code in wohl definierten Branches zu haben sorgt nicht automatisch dafür, dass die Versionen in den pom.xml Dateien aktualisiert werden. Also brauchen wir ein Tool dafür.
Und zweitens: wenn in den verschiedenen Branches unterschiedliche Versionen in den pom.xml Dateien stehen, dann macht es das Mergen nicht gerade leichter. Dafür brauchen wir also auch Unterstützung durch ein Tool.

Und letztlich ergibt sich noch eine weitere Hürde aufgrund des Deployment-Prozess. Der berücksichtigt nämlich nur die Maven-Artefakte im Firmen-Repository, und nicht den Code selbst. Wenn also die Änderungen eines Feature-Branch deployed werden sollen, dann müssen dafür die Versionen in den pom.xmls entsprechend angepasst werden. Was aber wiederum verhindert, dass dafür ein Pull-Request erstellt werden kann – weil die Versionen will man ja nicht mit mergen. Also – ihre erratet es – brauchen wir auch dafür Tool-Support.

Und wo wir gerade von Pull-Requests sprechen: natürlich müssen die Entwickler die verschiedenen Versionen in den Branches im Hinterkopf behalten, wenn sie einen PR erstellen. Einen Pull-Request gegen den development-Branch zu erstellen, wenn der Source-Branch auf bspw. dem aktuellen Hotfix-Branch basiert, ist keine so gute Idee!

Die Umsetzung

Tools…

Wie oben bereits erwähnt, brauchen wir Unterstützung von Tools – und zwar viel davon! Zum Glück gibt es auch eins, dass uns schon mal einen Großteil Arbeit abnimmt: gitflow-maven-plugin

Die Version aus dem offiziellen Repository, hat ein paar Nachteile, weshalb ich einen Fork erstellt habe, mit den entsprechenden Anpassungen und Fixes: https://github.com/Iridias/gitflow-maven-plugin/tree/current-status

Was ist daran anders:

  • pull dev-branch before pushing to avoid rejection due to remote-changes
  • support the option to not merge the release-branch into dev-branch
  • support processAllModules of versions-maven-plugin (important for child-modules without parent specified)
  • support separate additional merge-options on hotfix-finish and release-finish, for merging into production-branch and dev-branch respectively

Mit diesem Plugin an der Hand, sind wir gut vorbereitet um die erste Anforderung anzugehen.

Die „Erstellen mit einem Klick“-Anforderung können wir mit JenkinsCI lösen. Wer ein anderes Tool als Jenkins einsetzt, hat damit aber sicherlich auch die Möglichkeit entsprechend frei konfigurierte Jobs zu erstellen. Und wenn nicht: warum zur Hölle benutzt ihr es dann?

Um das Benutzen von Pull-Requests zu erzwingen, machen wir Gebrauch von BitBuckets „branching-restrictions“ Feature – aus dem einfachen Grund, weil wir eh schon ein BitBucket zur Verfügung haben. Die allermeisten Alternativen bieten aber ähnliche Features (und meistens auch für weniger Geld!)

Jetzt haben wir unsere Tools also zusammen.

Auf gehts!

Migration…

Zunächst müssen wir ein git-Repository aufsetzen. Um das mercurial-Repository nach git zu migrieren, benutzen wir https://github.com/frej/fast-export
Dann brauchen wir einen development-Branch: git branch development
Und schließlich fügen wir noch BitBucket als Remote hinzu und pushen das ganze:

git remote add origin ssh://[email protected]/project.git 
git push -u origin --all

Berechtigungen

Nun da das Projekt im neuen git-Repository ist, können wir die Branch-Berechtigungen setzen:

…sowie die merge-checks:

Wie schon zu sehen ist, benötigen wir hier einen CI-User Account. Irgendjemand muss die Änderungen ja in die entsprechenden Branches mergen, wenn wir das gitflow-maven-plugin benutzen.
Und als Admin will man solche Berechtigungen ggfs. auch haben – bspw. um Sachen zu fixen, falls doch mal etwas schief gelaufen ist!

Jenkins-Jobs

Für den nächsten Schritt benötigen wir verschiedene Jenkins-Jobs.
Um die Applikation selbst durch zu bauen brauchen wir Build-Jobs für Pull-Requests und die verschiedenen Branches:

  • Build-PR
  • Build-Develop
  • Build-Master
  • Build-Hotfix
  • Build-Feature
  • Build-Release

Dann brauchen wir noch die Jobs zum Aufrufen des gitflow-maven-plugin – diese ermöglichen später, die entsprechenden Aktionen „mit einem Klick“ durchzuführen (ok, manchmal auch mit 2 oder 3 Klicks, aber wir wollen ja nicht päpstlicher sein, als der Papst):

  • Version-Create-Feature-Branch
  • Version-Finish-Feature-Branch
  • Version-Create-Hotfix-Branch
  • Version-Finish-Hotfix-Branch
  • Version-Create-Release-Branch
  • Version-Finish-Release-Branch

Für den „Build-PR“ Job brauchen wir noch ein Plugin für Jenkins: bitbucket-pullrequest-builder-plugin (https://github.com/nishio-dens/bitbucket-pullrequest-builder-plugin)
Der Grund ist ein Bug in Bitbucket-Server vor Version 7. Dort gibt es eigentlich entsprechende web-hooks – aber der triggert nicht, wenn ein PR geupdated wird: https://jira.atlassian.com/browse/BSERV-10279
Wenn ihr schon Version >= 7 im Einsatz habt, dann braucht ihr das Jenkins-Plugin vermutlich nicht. Ihr Glückspilze!

In den folgenden Jenkins-Jobs müssen außerdem Wildcards in den branch-specifiern vom Jenkins-git-plugin verwendet werden:

  • Build-Hotfix
  • Build-Feature
  • Build-Release

Das hängt damit zusammen, dass die entsprechenden Branches immer den selben Prefix haben, aber der aktuelle Name unterscheidet sich je nach aktueller Version dann halt doch.
Aber kein Problem! Das lässt sich einfach wie folgt angeben: */hotfix-*

Aber das wichtige dabei ist: der Workspace darf nach dem Build _nicht_ gelöscht werden!!!! (Post-Build action)
Den Workspace _vor_ dem Build zu löschen ist OK – weil dann der git-checkout schon durch ist.
Der Grund ist, dass Jenkins nicht auf Änderungen in den Branches prüfen kann, wenn ein Wildcard-Matching angegeben ist.

Maven-Artefakte deployen

Da wir Sonatype Nexus Repository als Maven-Repository benutzen, lag die Entscheidung nahe, nexus-staging-maven-plugin einzusetzen. Es ersetzt das default deploy-maven-plugin und macht den Prozess robuster, da es sicher stellt, dass alle Artefakte deployed werden oder keins!
Wenn also während des Upload etwas furchtbares passiert, endet man nicht direkt in einem inkonsistenten Zustand! Allerdings gibt es da einen Nachteil: das funktioniert nur bei SNAPSHOTs – für Release-Artefakte braucht man die kommerzielle Version.
Für den Job, der die Release-Artefakte erzeugt, brauchen wir also noch den Parameter -DskipStaging=true für mvn deploy – sonst fliegt uns der Build um die Ohren.
Aber immerhin ist es trotzdem eine Verbesserung.

Die Jenkins-Jobs im Detail:

Version-Create-Feature-Branch

Parameter:

Type: Text
Name: FeatureName

Shell Build-Step:

echo "Checking Feature-Name...";
if [[ ! "${FeatureName}" =~ ^[a-zA-Z0-9_\-]*$ ]]; then 
   echo "Invalid Feature-Name specified! Aborting!";
   exit 1;
fi

echo "Fetching repository...";
git clone ssh://[email protected]:9999/prj/repo-name.git
cd repo-name

# gitflow:feature-start checks for existing branch in refs/heads/ instead of refs/remotes/origin
git branch -r | grep -v '\->' | grep -v 'master' | while read remote; do git branch --track "${remote#origin/}" "$remote"; done
git pull --all
git checkout staging

echo "Starting feature-branch...";
mvn -U -B gitflow:feature-start -DversionProcessAllModules=true -DinstallProject=false -DskipFeatureVersion=false -DallowSnapshots=true -DpushRemote=true -DfeatureName="${FeatureName}"

Wir müssen das git clone im Script hier machen, weil wir noch diverse andere git-Operationen durchführen wollen. Würden wir das Jenkins-git-plugin nutzen, würde das zu einem Fehler wie dem hier führen:

fatal: could not read Username for ‚https://yourgitrepo‘

Wir im Kommentar im Script geschrieben, ist die Schleife zum Erzeugen der lokalen Tracking-Branches nötig. Das hängt damit zusammen, wie das gitflow-maven-plugin arbeitet. Das betrifft auch die folgenden Jobs, aber dort werde ich das nicht noch mal gesondert erwähnen!

Version-Finish-Feature-Branch

Parameter:

Type: Extensible Choice
Name: FeatureName
Groovy Script:

def p = "/path/to/fetch-feature-branches.sh".execute()
p.waitFor()
File versions = new File("/path/to/git-feature-branch-names.txt")
return versions.readLines()

fetch-feature-branches.sh:

#!/bin/bash

cd /path/to/copy/of/your/repo-name
git pull --rebase --all
git remote prune origin
# dfb == deployable feature branch
git branch -r | grep "dfb-" | sed -e 's/.*dfb-//gi' > /path/to/git-feature-branch-names.txt

Shell Build-Step:

echo "Checking Feature-Name...";
if [[ ! "${FeatureName}" =~ ^[a-zA-Z0-9_\-]*$ || -z "${FeatureName}" ]]; then 
   echo "Invalid Feature-Name specified! Aborting!";
   exit 1;
fi

echo "Fetching repository...";
git clone ssh://[email protected]:9999/prj/repo-name.git
cd repo-name
git branch -r | grep -v '\->' | grep -v 'master' | while read remote; do git branch --track "${remote#origin/}" "$remote"; done
git pull --all

echo "Finish feature-branch...";
mvn -U -B gitflow:feature-finish -DversionProcessAllModules=true -DinstallProject=false -DadditionalMergeOptions="-Xours" -DskipTestProject=true -DskipFeatureVersion=false -DallowSnapshots=true -DpushRemote=true -DfeatureName="${FeatureName}"

Hier sollten optimaler Weise noch die selben Merge-Optionen eingeführt werden, wie beim Job „Version-Finish-Hotfix-Branch“ – ist bei uns nicht der Fall, weil diese Art von Feature-Branches eh nicht benutzt werden. Aber ich wollte es dennoch erwähnt haben.
Das Extensible Choice plugin und das Script um die existierenden Feature-Branches zu finden ist nicht zwingend erforderlich – aber für die User ist es definitiv sehr viel angenehmer! Sie können den richtigen Feature-Branch einfach auswählen, statt den Namen selber herausfinden zu müssen und dann noch mit Tippfehlern zu kämpfen – und vertraut mir: sie kämpfen immer mit Tippfehlern! Also eliminieren wir diese Fehlerquelle doch direkt.

Version-Create-Hotfix-Branch

Shell Build-Step:

echo "Fetching repository...";
git clone ssh://[email protected]:9999/prj/repo-name.git
cd repo-name
git branch -r | grep -v '\->' | grep -v 'master' | while read remote; do git branch --track "${remote#origin/}" "$remote"; done
git pull --all

echo "check for existing hotfix-branch..."
amount=git branch -r |grep -i "hotfix-" | wc -l
if [ $amount -gt 0 ]; then
   echo "Found $amount existing hotfix-branche(s)! Aborting...";
   exit 2;
fi

echo "creating hotfix branch..."
mvn -B gitflow:hotfix-start -DversionProcessAllModules=true -DuseSnapshotInHotfix=true
git push -u origin --all

Exit code to set build unstable: 2

Es sollte immer nur einen Hotfix-Branch geben – deswegen braucht es hier auch keine Parameter!

Version-Finish-Hotfix-Branch

Parameter:

Type: Select
Name: MergeStrategy
Values:
AUTO
HOTFIX
STAGING
MANUAL

Shell Build-Step:

echo "Fetching repository...";
git clone ssh://[email protected]:9999/prj/repo-name.git
cd repo-name
git branch -r | grep -v '\->' | grep -v 'master' | while read remote; do git branch --track "${remote#origin/}" "$remote"; done
git pull --all

echo "check for existing hotfix-branch..."
amount=git branch -r |grep -i "hotfix-" | wc -l
if [ $amount -eq 0 ]; then
   echo "No existing hotfix-branch found! Aborting...";
   exit 2;
fi

hotfixBranchVersion=git branch -r |grep -i "hotfix-" | sed -e 's/.*hotfix-//gi'

skipMergeDevBranch="false"
keepHotfixBranch="false"
devBranchMergeOptions="";

if [ "${MergeStrategy}" == "HOTFIX" ]; then
	devBranchMergeOptions="-Xtheirs";
elif [ "${MergeStrategy}" == "STAGING" ]; then
	devBranchMergeOptions="-Xours";
elif [ "${MergeStrategy}" == "MANUAL" ]; then
	skipMergeDevBranch="true";
	keepHotfixBranch="true";
fi

echo "releasing hotfix branch..."
mvn -U -B gitflow:hotfix-finish -DversionProcessAllModules=true -DskipTestProject=true -DallowSnapshots=true -DuseSnapshotInHotfix=true -DproductionBranchMergeOptions="-Xtheirs" -DdevBranchMergeOptions="${devBranchMergeOptions}" -DkeepBranch="${keepHotfixBranch}" -DskipMergeDevBranch="${skipMergeDevBranch}" -DhotfixVersion="${hotfixBranchVersion}"

Exit code to set build unstable: 2

An dieser Stelle werden die Dinge etwas komplizierter.
Dieser Job muss Änderungen in zwei Branches mergen: den master und den development-branch. Das mergen in den master ist leicht! Da niemand direkt in den master schreiben darf, ist es praktisch ausgeschlossen, dass dort konfliktreiche Änderungen vorhanden sind. Also können wir auch erzwingen, dass immer die Änderungen vom Hotfix-Branch genommen werden.
Beim development-Branch ist das leider nicht so einfach. Je länger der Hotfix-Branch offen ist, um so wahrscheinlicher ist es, dass merge-Konflikte entstehen – und mit denen müssen wir umgehen!
Der default für die Merge-Strategy ist AUTO. Damit versucht das gitflow-maven-plugin die Branches ohne spezielle Behandlung zu mergen.
Wenn das fehlschlägt, dann muss ein Entwickler entscheiden, wie der Konflikt zu lösen ist – immerhin haben die Entwickler ihn ja auch eingebaut!
Um das aber zu vereinfachen, haben wir hier noch 3 nette Optionen:

  • Ignorieren der im Konflikt stehenden Änderungen im development-Branch und stumpfes Übernehmen der Änderungen aus dem Hotfix-Branch
  • Das selbe nur umgedreht: Konflikte im Hotfix-Branch ignorieren und die Änderungen im development-Branch beibehalten
  • Überhaupt nicht in den development-Branch mergen und den Hotfix-Branch behalten (also nach dem mergen in master nicht löschen)

In den ersten beiden Fällen kann man den Job einfach mit dem entsprechenden Parameter noch einmal laufen lassen und dann ist die Arbeit getan. Im letzten Fall …nunja, da müssen hinterher die Entwickler noch mal ran! Diese müssen dann sicher stellen, dass alle nötigen Änderungen aus dem Hotfix-Branch in den development-Branch übernommen werden. …alles kann man denen halt auch nicht abnehmen.

Version-Create-Release-Branch

Parameter:

Type: Extensible Choice
Name: ReleaseVersion
Groovy Script:

def p = "/path/to/fetch-git-version.sh".execute()
p.waitFor()
File versions = new File("/path/to/git-current-version.txt")
return versions.readLines()

fetch-git-version.sh:

#!/bin/bash

credentials=cat ~/.bitbucket_user
curl -u "$credentials" "https://yourgitrepo/projects/PRJ/repos/repo-name/raw/pom.xml?at=refs%2Fheads%2Fmaster" > /tmp/repo-pom.xml
grep -m 1 "" /tmp/repo-pom.xml | sed -e 's/.*version>\(.*\)<\/version>/\1/gi' > /path/to/git-current-version.txt

Parameter:

Type: Text
Name: DevelopmentVersion

Shell Build-Step:

echo "Checking target release-version...";
if [[ ! "${ReleaseVersion}" =~ ^[0-9]{2}\.[0-9]{1,2}\.[0-9](\.[0-9]+)?$ ]]; then 
   echo "Invalid Release-Version specified! Aborting!";
   exit 1;
fi

echo "Enforcing SNAPSHOT for dev-version...";
if [[ ! "${DevelopmentVersion}" =~ ^.*-SNAPSHOT$ ]]; then 
	DevelopmentVersion="${DevelopmentVersion}-SNAPSHOT"
fi

echo "Checking target dev-version...";
if [[ ! "${DevelopmentVersion}" =~ ^[0-9]{2}\.[0-9]{1,2}\.[0-9](\.[0-9]+)?-SNAPSHOT$ ]]; then 
   echo "Invalid Development-Version specified! Aborting!";
   exit 1;
fi

echo "Fetching repository...";
git clone ssh://[email protected]:9999/prj/repo-name.git
cd repo-name
git branch -r | grep -v '\->' | grep -v 'master' | while read remote; do git branch --track "${remote#origin/}" "$remote"; done
git pull --all

echo "Starting release-branch...";
mvn -B gitflow:release-start -DversionProcessAllModules=true -DpushRemote=true -DuseSnapshotInRelease=true -DallowSnapshots=true -DcommitDevelopmentVersionAtStart=true -DreleaseVersion="${ReleaseVersion}" -DdevelopmentVersion="${DevelopmentVersion}" -DversionDigitToIncrement=2

Bei uns in der Abteilung wollen die Kollegen hohe Flexibilität bzgl. der Versionen. Einfach hoch zählen ist also nicht!
Aber um ihnen das zu vereinfachen, nutzen wir das Extensible Choice Plugin von Jenkins um die aktuelle Version heraus zu finden und anzuzeigen.
Kann man sicherlich auch komplett in Groovy machen, ohne separates Shell-Script. Aber so war es zum einen schneller umzusetzen und das Script kann noch für andere Sachen wiederverwendet werden.

Im Build-Step ist der wichtige Teil, sicher zu stellen, dass für den development-Branch eine SNAPSHOT-Version verwendet wird. Ihr dürft euch da nicht darauf verlassen, dass die Entwickler das schon mit angeben werden!!!
Und keine Bange bzgl. des Release-Branches: gitflow-maven-plugin sorgt an der Stelle schon selber dafür, dass das SNAPSHOT gesetzt wird (via -DuseSnapshotInRelease=true)

Version-Finish-Release-Branch

Parameter:

Type: Select
Name: MergeStrategy
Values:
AUTO
HOTFIX
STAGING
MANUAL

Shell Build-Step:

echo "Fetching repository...";
git clone ssh://[email protected]:9999/prj/repo-name.git
cd repo-name
git branch -r | grep -v '\->' | grep -v 'master' | while read remote; do git branch --track "${remote#origin/}" "$remote"; done
git pull --all

echo "check for existing release-branch..."
amount=git branch -r |grep -i "hcf-" | wc -l
if [ $amount -eq 0 ]; then
   echo "No existing release-branch found! Aborting...";
   exit 2;
fi

skipMergeDevBranch="false"
keepReleaseBranch="false"
devBranchMergeOptions="";

if [ "${MergeStrategy}" == "HOTFIX" ]; then
	devBranchMergeOptions="-Xtheirs";
elif [ "${MergeStrategy}" == "STAGING" ]; then
	devBranchMergeOptions="-Xours";
elif [ "${MergeStrategy}" == "MANUAL" ]; then
	skipMergeDevBranch="true";
	keepReleaseBranch="true";
fi

echo "Finish release-branch...";
mvn -B gitflow:release-finish -DversionProcessAllModules=true -DpushRemote=true -DuseSnapshotInRelease=true -DskipTestProject=true -DallowSnapshots=true -DcommitDevelopmentVersionAtStart=true -DproductionBranchMergeOptions="-Xtheirs" -DdevBranchMergeOptions="${devBranchMergeOptions}" -DkeepBranch="${keepReleaseBranch}" -DskipMergeDevBranch="${skipMergeDevBranch}" -DversionDigitToIncrement=2

Im Prinzip wie bei „Version-Finish-Hotfix-Branch“ – nur dass halt auf einen Release-Branch geprüft wird und die Parameter für gitflow-maven-plugin entsprechend anders sind.
Auch hier gelten die selben Dinge bzgl. Merge-Konflikte!

Zusammenfassung

Mit den vorgestellten Tools und entsprechend konfigurierten Jenkins-Jobs, ist es möglich, dass auch eine technisch nicht so versierte Person das Release-Management übernimmt. Aber auch wenn sich die Entwickler noch selber darum kümmern, wird die ganze Sache doch bedeutend einfacher!
Und am aller wichtigsten: Repository und Releases bleiben immer konsistent und in einem reproduzierbaren Zustand!

Wir haben das jetzt seit über einem Jahr im Einsatz und es hat sich hervorragend bewährt und uns viel Arbeit abgenommen.