08.04.2015 von Daniel Reuter
6 Kommentare
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.// basic example-job, which checks out sources from mercurial,
// runs a maven build and sends mails
job {
name('build.job')
logRotator(-1,3)
scm {
hg('http://mercurial.example.com/project123/','default')
}
triggers { scm('* 7-20 * * 1-5') }
steps {
maven {
rootPOM('parent/pom.xml')
goals('clean install -T 1C')
property('skipITs','true')
property('maven.test.failure.ignore','true')
}
}
publishers {
mailer('devs@example.com',true,false)
archiveJunit('**/target/surefire-reports/*.xml')
}
} |
// basic example-job, which checks out sources from mercurial, // runs a maven build and sends mails job { name('build.job') logRotator(-1,3) scm { hg('http://mercurial.example.com/project123/','default') } triggers { scm('* 7-20 * * 1-5') } steps { maven { rootPOM('parent/pom.xml') goals('clean install -T 1C') property('skipITs','true') property('maven.test.failure.ignore','true') } } publishers { mailer('devs@example.com',true,false) archiveJunit('**/target/surefire-reports/*.xml') } }
- 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.

Build-Step-Definition für den Seed-Job
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.WORKING_DIR=$PWD
cd /opt/mercurial/hg-repo
hg branches | column -t | awk '{printf "%s\n",$1}' | sort > ${WORKING_DIR}/branches.txt
cd $WORKING_DIR |
WORKING_DIR=$PWD cd /opt/mercurial/hg-repo hg branches | column -t | awk '{printf "%s\n",$1}' | sort > ${WORKING_DIR}/branches.txt cd $WORKING_DIR

Shell-Step in Seed-Job
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.// read the text-file containing the branch list
def branches = []
readFileFromWorkspace("seed-job","branches.txt").eachLine { branches << it }
// for every branch...
branches.each {
def branchName = it
job {
// use the branchName-variable for the job-name and SCM-configuration
name("branch.${branchName}.build.job")
// ...
scm {
hg('http://mercurial.example.com/project123/',"${branchName}")
}
// ...
}
} |
// read the text-file containing the branch list def branches = [] readFileFromWorkspace("seed-job","branches.txt").eachLine { branches << it } // for every branch... branches.each { def branchName = it job { // use the branchName-variable for the job-name and SCM-configuration name("branch.${branchName}.build.job") // ... scm { hg('http://mercurial.example.com/project123/',"${branchName}") } // ... } }
Die Job-Konsole im Jenkins sollte in etwa so aussehen:
Processing DSL script build.groovy
Adding items:
GeneratedJob{name='branch.featureFoo.build.job'}
GeneratedJob{name='branch.featureBar.build.job'} |
Processing DSL script build.groovy Adding items: GeneratedJob{name='branch.featureFoo.build.job'} 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.// example-view containing all jobs starting with "branch"
view(type: ListView) {
name 'Builds per Branch'
jobs { regex("branch.*") }
columns {
status()
weather()
name()
lastSuccess()
lastFailure()
lastDuration()
buildButton()
}
} |
// example-view containing all jobs starting with "branch" view(type: ListView) { name 'Builds per Branch' jobs { regex("branch.*") } columns { status() weather() name() lastSuccess() lastFailure() lastDuration() buildButton() } }
- 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.def branches = []
readFileFromWorkspace("seed-job","branches.txt").eachLine { branches << it }
job {
name('deploy.test')
parameters {
// create a parameter "BRANCH", containing all entries from "branches"
// default-value is the first entry from "branches"
choiceParam('BRANCH',branches,'Which branch do you want to deploy?')
}
scm {
hg('http://mercurial.example.com/project123/',"$BRANCH")
}
//...
} |
def branches = [] readFileFromWorkspace("seed-job","branches.txt").eachLine { branches << it } job { name('deploy.test') parameters { // create a parameter "BRANCH", containing all entries from "branches" // default-value is the first entry from "branches" choiceParam('BRANCH',branches,'Which branch do you want to deploy?') } scm { hg('http://mercurial.example.com/project123/',"$BRANCH") } //... }
- 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.# Job-DSL Automerge
def branches = []
// we use another file here, to filter some branches which should not get automerged
readFileFromWorkspace("seed-job","branchesAutomerge.txt").eachLine { branches << it }
branches.each {
def branchName = it
job {
name("branch.${branchName}.automerge")
triggers { cron('H 5 * * 1-5') }
wrappers {
environmentVariables {
env('BRANCH', "${branchName}")
}
// we are using a single repository for the automerge-jobs. So we have to be sure, that only one job is using the repository
exclusionResources('AUTOMERGE_REPO')
}
steps {
criticalBlock {
shell(readFileFromWorkspace('parent/jenkinsJobs/scripts/automerge.sh'))
}
}
}
} |
# Job-DSL Automerge def branches = [] // we use another file here, to filter some branches which should not get automerged readFileFromWorkspace("seed-job","branchesAutomerge.txt").eachLine { branches << it } branches.each { def branchName = it job { name("branch.${branchName}.automerge") triggers { cron('H 5 * * 1-5') } wrappers { environmentVariables { env('BRANCH', "${branchName}") } // we are using a single repository for the automerge-jobs. So we have to be sure, that only one job is using the repository exclusionResources('AUTOMERGE_REPO') } steps { criticalBlock { shell(readFileFromWorkspace('parent/jenkinsJobs/scripts/automerge.sh')) } } } }
# automerge.sh
# Jenkins uses "-e" parameter, but we want to handle the exit-code manually
set +e
WORKING_DIR=$PWD
cd /var/lib/jenkins/repoAutomerge
# reset local changes
hg update -C .
# get changes from repository
hg pull
# update to branch
hg update -C ${BRANCH}
# try the merge
hg merge develop --config "ui.merge=internal:merge"
mergereturn=$?
case $mergereturn in
0)
echo '##################################'
echo '##### Merge successfully #####'
echo '##################################'
# commit and push
hg commit -m 'Automerge' -u 'AutoMerger'
hg push
rc=0
;;
1)
echo '####################################################'
echo '##### Merge-Conflict, manual merge needed! #####'
echo '####################################################'
rc=1
;;
255)
echo '############################################'
echo '##### No Changes (Return-Code 255) #####'
echo '############################################'
rc=0
;;
*)
echo '###############################################'
echo "##### Merge-Returncode : $mergereturn #####"
echo '###############################################'
rc=1
;;
esac
# reset local changes
hg update -C .
exit $rc |
# automerge.sh # Jenkins uses "-e" parameter, but we want to handle the exit-code manually set +e WORKING_DIR=$PWD cd /var/lib/jenkins/repoAutomerge # reset local changes hg update -C . # get changes from repository hg pull # update to branch hg update -C ${BRANCH} # try the merge hg merge develop --config "ui.merge=internal:merge" mergereturn=$? case $mergereturn in 0) echo '##################################' echo '##### Merge successfully #####' echo '##################################' # commit and push hg commit -m 'Automerge' -u 'AutoMerger' hg push rc=0 ;; 1) echo '####################################################' echo '##### Merge-Conflict, manual merge needed! #####' echo '####################################################' rc=1 ;; 255) echo '############################################' echo '##### No Changes (Return-Code 255) #####' echo '############################################' rc=0 ;; *) echo '###############################################' echo "##### Merge-Returncode : $mergereturn #####" echo '###############################################' rc=1 ;; esac # reset local changes hg update -C . exit $rc