In meinem aktuellem Projekt nutzen wir sehr intensiv Feature Branches . Unser Master-Branch soll stets sauber, stabil und deploybar sein. Entwicklung, Code-Reviews und sogar die ersten Fachbereichstests finden in den Feature Branches statt.
Viele Projekte nutzen eine Menge von Jenkins-Jobs, um die Stabilität und Qualität sicherstellen zu können. Builds, Tests und Code-Metriken werden dabei häufig nur für den Master-Branch ausgeführt bzw. ermittelt.
Unser Ziel war die Reduzierung des Integrations-Risikos beim Zurückmergen eines Feature Branch in den Master-Branch. Die gleichen Jenkins-Jobs, die für den Master-Branch genutzt werden, können idealerweise auch für die Feature Branches genutzt werden. Entwickler sollten z.B. ihr Oberflächen-Test-Fehler oder Sonar-Violations direkt im Feature Branch fixen, nicht erst im Master-Branch. Als Nebeneffekt erhält das Produkt Owner Team eine klare Übersicht über alle Feature Branches und deren jeweiligen Status. Das Produkt Owner Team beginnt keinen Test, bevor nicht alle Feature-Branch-Jobs grün sind.
Jenkins hat keine eingebauten Features zur Unterstützung von Feature Branches. Bamboo zum Beispiel hat ein Feature mit dem Namen „plan branches“, aber für den Jenkins gibt es nichts Vergleichbares. In der Community findet man einige skizzierte Lösungen. Die meisten Ansätze basieren auf Job-Kopien und sind teilweise abhängig von einigen Jenkins-Plugins. Als Beispiel möchte ich einen Blog Post und eine zugehörige Thesis von zeroturnaround nennen. Hier wird das Thema Feature Branches in Verbindung mit Continuous Integration nochmal beschrieben und ebenfalls eine möglich Lösung genannt. Uns fehlten jedoch einige Punkte, z.B. eine einfache Branch-Status-Übersicht für den Fachbereich.
Also haben wir eine andere Lösung gesucht und auf Basis des Job-DSL-Plugin erstellt. Dabei haben wir gleich mehrere Verbesserungen umgesetzt.
- Branch-Dashboard mit einem klar sichtbarem Status zu jedem Feature Branch
- Automatisches erstellen/löschen von Jobs für Feature Branches, keine manuellen Aufwände
- versionskontrolliere Job-Definitionen
- Reduzierte Korrektur-Aufwände im Master-Branch nach der Integration von Feature Branches
- Reduzierte Aufwände für Master-in-Feature-Branch-Merges (siehe Tipps & Tricks: Automatisches Merging)
Was haben wir gemacht?
- Ausdrücken der Jobs in DSL-Scripts
Wir wollten schon immer unsere Job-Definition im SCM gesichert haben. Änderungen sollten nachvollziehbar und versionskontrolliert sein, genau wie Änderungen am restlichen Code. Also haben wir unsere Job-Definitionen in Form von DSL-Skripten des Job-DSL-Plugin ausgedrückt und im SCM abgelegt.1// basic example-job, which checks out sources from mercurial, 2// runs a maven build and sends mails 3job { 4 name('build.job') 5 logRotator(-1,3) 6 scm { 7 hg('http://mercurial.example.com/project123/','default') 8 } 9 triggers { scm('* 7-20 * * 1-5') } 10 steps { 11 maven { 12 rootPOM('parent/pom.xml') 13 goals('clean install -T 1C') 14 property('skipITs','true') 15 property('maven.test.failure.ignore','true') 16 } 17 } 18 publishers { 19 mailer('devs@example.com',true,false) 20 archiveJunit('**/target/surefire-reports/*.xml') 21 } 22}
- Setup des Seed-Job
Der Seed-Job generiert die einzelnen Jobs anhand der DSL-Skripte. Hier gibt es ein gutes Tutorial zum einrichten des Seed-Jobs. Abweichend zum Tutorial nutzen wir jedoch die Option „Look on Filesystem“ mit einem regulärem Ausdruck, um die DSL-Skripte aus dem SCM zu nutzen.
Der Seed-Job wird durch SCM-Änderungen angetriggert. Zusätzlich läuft der Job einmal am Tag (früh morgens), falls z.B niemand im Master-Branch arbeitet, aber ein neuer Feature Branch hinzugefügt wurde. - Erzeugen einer „new-line-separated“ Datei mit offenen Branches
Um zu jedem Feature Branch eine eigenen Job zu generieren, müssen wir wissen, welche Branches existieren. Wir nutzen ein kleines Shell-Skript, um die Branch-Name jeweils in eine Zeile in eine Datei zu schreiben. Das Skript wird als zusätzlicher Build-Step vom Seed-Job angestoßen. Wir nutzen Mercurial auf dem gleicher Server, wie unser Jenkins. Daher können wir recht einfach in das Verzeichnis unseres Mercurial-Repository wechseln und Mercurial nach den offenen Branches fragen.1WORKING_DIR=$PWD 2cd /opt/mercurial/hg-repo 3hg branches | column -t | awk '{printf "%s\n",$1}' | sort > ${WORKING_DIR}/branches.txt 4cd $WORKING_DIR
Da alle unsere Shell-Skripte ebenfalls im SCM abgelegt sind, befindet sich das Skript in einer eigenen Datei. Man könnte die einzelnen Befehle auch direkt in das Eingabefeld des Shell-Build-Step eintragen. - Umstellen der DSL zur Erzeugung eines Jobs für jeden Branch
Aktuell haben wir ein DSL-Skript für einen einfachen Build Job und eine Datei, die alle unsere aktiven Branches enthält. Jetzt erweitern wir das Skript, so dass es für jeden Branch läuft.1// read the text-file containing the branch list 2def branches = [] 3readFileFromWorkspace("seed-job","branches.txt").eachLine { branches << it } 4 5// for every branch... 6branches.each { 7 def branchName = it 8 9 job { 10 // use the branchName-variable for the job-name and SCM-configuration 11 name("branch.${branchName}.build.job") 12 // ... 13 scm { 14 hg('http://mercurial.example.com/project123/',"${branchName}") 15 } 16 // ... 17 } 18}
Die Job-Konsole im Jenkins sollte in etwa so aussehen:
1Processing DSL script build.groovy 2Adding items: 3 GeneratedJob{name='branch.featureFoo.build.job'} 4 GeneratedJob{name='branch.featureBar.build.job'}
Jetzt wird für jeden Branch ein Job erzeugt. Gleichzeitig werden die Jobs nach zurückmergen der Branches in den master branch automatisch gelöscht. Hier ist keine manuelle Arbeit notwendig.
Einige Tipps & Tricks, falls ihr etwas ähnliches implementieren wollt:
- „Repository Cache“
Wir nutzen im Projekt Mercurial. Normalerweise würde jeder Job eine eigene Repository-Kopie anlegen. Im Mercurial-Jenkins-Plugin gibt es jedoch zwei Funktionen, die das verhindern. In den globalen Jenkins-Einstellungen finden man dazu die beiden Punkte „Use Repository Caches“ und „Use Repository Sharing“. - Views erzeugen
Wenn man viele Branches hat oder vielleicht mehrere Jobs pro Branch generieren will, wird es schnell unübersichtlich im Jenkins. Hier ist es eine gute Idee, ein paar Views zu erzeugen. Das Job-DSL-Plugin hilft hier ebenfalls.1// example-view containing all jobs starting with "branch" 2view(type: ListView) { 3 name 'Builds per Branch' 4 jobs { regex("branch.*") } 5 columns { 6 status() 7 weather() 8 name() 9 lastSuccess() 10 lastFailure() 11 lastDuration() 12 buildButton() 13 } 14}
- Auswahl-Parameter nutzen?
Wir haben einen Deploy-Job, der von unserem Product Owner Team genutzt wird, um einen bestimmten Branch auf einen Test-Server zu deployen. Hier generieren wir keinen Deployment-Job für jeden Branch, sondern erzeugen einen Job, der die vorhandenen Branches als Auswahl-Parameter anbietet. Hierzu können wir unsere Datei mit den Branch einfach wiederverwenden.1def branches = [] 2readFileFromWorkspace("seed-job","branches.txt").eachLine { branches << it } 3 4job { 5 name('deploy.test') 6 parameters { 7 // create a parameter "BRANCH", containing all entries from "branches" 8 // default-value is the first entry from "branches" 9 choiceParam('BRANCH',branches,'Which branch do you want to deploy?') 10 } 11 scm { 12 hg('http://mercurial.example.com/project123/',"$BRANCH") 13 } 14 //... 15}
- Automatisches Mergen?
Nachdem wir die ersten Jobs für Branches erzeugt hatten, dachten wir über automatisches Mergen vom Master-Branch in die Feature Branches nach. In unserem Projekt ändern sich die Schnittstellen zu einigen benötigten Sub-Systemen recht häufig. Anschließend sind dann Änderungen im Zugriffscode notwendig, die von den Entwicklern direkt im Master-Branch durchgeführt werden. Diese Änderungen müssen jedoch auch in die Feature-Branches nachgezogen werden. Wir verbrachten viel Zeit mit einfachen Merges und dennoch kam es häufig zu Frustrationen beim Product Owner Team, wenn ein Feature in einem Branch getestet werden sollte, der Branch aber nicht auf dem aktuellem Schnittstellen-Stand war.
Basierend auf den oben genannten Techniken haben wir also einen automatischen Merge implementiert. Solang es keine Merge-Konflikte gibt wird jede Änderung im Master Branch automatisch in die Feature-Branches gemerged. Der Merge wird über ein Shell-Skript durchgeführt, welches durch einen weiteren generierten Job ausgeführt wird. Falls es einen Merge-Konflikt gibt, muss der Entwickler manuell mergen. Im Jenkins ist damit jederzeit sichtbar, ob ein Branch gemerged werden konnte und wann die letzte Aktualisierung stattgefunden hat.1# Job-DSL Automerge 2 3def branches = [] 4// we use another file here, to filter some branches which should not get automerged 5readFileFromWorkspace("seed-job","branchesAutomerge.txt").eachLine { branches << it } 6 7branches.each { 8 def branchName = it 9 10 job { 11 name("branch.${branchName}.automerge") 12 triggers { cron('H 5 * * 1-5') } 13 wrappers { 14 environmentVariables { 15 env('BRANCH', "${branchName}") 16 } 17 18 // we are using a single repository for the automerge-jobs. So we have to be sure, that only one job is using the repository 19 exclusionResources('AUTOMERGE_REPO') 20 } 21 steps { 22 criticalBlock { 23 shell(readFileFromWorkspace('parent/jenkinsJobs/scripts/automerge.sh')) 24 } 25 } 26 } 27}
1# automerge.sh 2 3# Jenkins uses "-e" parameter, but we want to handle the exit-code manually 4set +e 5 6WORKING_DIR=$PWD 7cd /var/lib/jenkins/repoAutomerge 8 9# reset local changes 10hg update -C . 11# get changes from repository 12hg pull 13# update to branch 14hg update -C ${BRANCH} 15 16# try the merge 17hg merge develop --config "ui.merge=internal:merge" 18mergereturn=$? 19 20case $mergereturn in 21 0) 22 echo '##################################' 23 echo '##### Merge successfully #####' 24 echo '##################################' 25 26 # commit and push 27 hg commit -m 'Automerge' -u 'AutoMerger' 28 hg push 29 30 rc=0 31 ;; 32 1) 33 echo '####################################################' 34 echo '##### Merge-Conflict, manual merge needed! #####' 35 echo '####################################################' 36 rc=1 37 ;; 38 255) 39 echo '############################################' 40 echo '##### No Changes (Return-Code 255) #####' 41 echo '############################################' 42 rc=0 43 ;; 44 *) 45 echo '###############################################' 46 echo "##### Merge-Returncode : $mergereturn #####" 47 echo '###############################################' 48 rc=1 49 ;; 50esac 51 52# reset local changes 53hg update -C . 54 55exit $rc
Weitere Beiträge
von Daniel Reuter
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Weitere Artikel in diesem Themenbereich
Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
Daniel Reuter
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.