Generierte Jenkins Jobs und automatisches Branch Merging für Feature Branches

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?

  1. 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')
        }
    }
  2. 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

    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.
  3. 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

    Shell-Step in Seed-Job

    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.
  4. 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}") 
            }
            // ...
        }
    }

    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'}

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()
        }
    }
  • 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") 
        }
        //...
    }
  • 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'))
    	    }
    	}
        }
    }
    # 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
Daniel Reuter

Daniel gehört seit April 2009 zum Team der codecentric. Seine Schwerpunkte liegen in der Entwicklung von Enterprise-Anwendungen im Versicherungsumfeld. Daniel ist Allroundcodehacker, Architekturgedankenpfleger, Sauberkeitsprogrammierenthusiast, Teamzusammenarbeitshelfer und Versicherungsfachdomänenfreund.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentare

  • Eberhard Wolff

    8. April 2015 von Eberhard Wolff

    Feature Branches widersprechen leider Continuous Integration. Durch die Branches wird die Integration so lange verzögert, bis die Branches wieder gemergt werden. Und genau da treten dann Konflikte auf und müssen gelöst werden. Daher sollten man meiner Meinung nach eher auf Continuous Delivery, Continuous Integration und Feature Toggles zum Deaktivieren neuer Features in Produktion setzten.

  • Daniel Reuter

    Da kann ich nur zustimmen. Feature Branches sollten sich bei idealen Projektbedingungen vermeiden lassen.

    Dennoch ist mir die Aussage zu pauschal. Es gibt sicherlich Konstellationen, in denen man Codeänderungen isoliert behandeln möchte.

    In unserem Projekt bietet sich die Vorgehensweise an. Das Team ist z.B. stark verteilt und unterschiedlich qualifiziert. Wir setzen hier verstärkt auf Reviews und es kommt häufig nachträglich zu größeren Veränderungen innerhalb der Branches. Um hier den Überblick zu behalten, nutzen uns isolierte Branches sehr. Des Weiteren ist das Projekt hinreichend groß und die Anforderungen weit verteilt, so dass die einzelnen Branches kaum untereinander zu Konflikten führen. Und durch den automatischen Merge haben wir zumindest „Reverse Continuous Integration“ 😉 Alle Branches beinhalten so stets den aktuellen Entwicklungsstand aus dem Master und Konflikte werden hier schnell deutlich.

    Ich vergleiche diese Vorgehensweise gerne mit Pull Requests in Open Source Projekten, wo Codeänderungen vor dem eigentlichen Merge auch gerne diskutiert, getestet, angepasst werden und nur wirklich „sauberer“ Code hinsichtlich Testabdeckung, Architektur, Design usw. tatsächlich im Master landet.

  • Daniel Gräfen

    Feature Branches widersprechen nicht zwingend Continuous Integration. Es hängt stark von der Lebensdauer der Feature-Branches ab. Existieren Feature-Branches über Wochen oder Monate mag das ein Antipattern sein. Bei Feature-Branches die für Stunden bis mehrere Tage existieren sind diese durchaus hilfreich.

    In unserem Projekt werden wir das Vorgehen noch weiter auf die Spitze treiben. Wir setzen bereits auf Bamboo mit einer eigenen generierten Pipeline pro Feature-Branch. Der nächste Schritt wird ein eigenes via Dockercontainer generiertes Testsystem pro Feature-Branch. Dies hat den Vorteil, das man jedes Feature bevor es integriert wird isoliert Testen kann. Nich nur dies, man kann es auch fachlich gegenprüfen lassen. Wir stehen oft vor der Situation, dass die fachlichen Anforderungen an den Userworkflow schwer zu 100% ausspezifizieren lassen. Manchmal ist es einfacher man erlebt den Workflow. Wir entwickeln auf Basis der Anforderungen einen ersten groben Entwurf, bauen diesen auf dem Featurebranch, spielen diesen auf ein Feature-Testsystem, schicken dem Product Owner ein Link auf das Testsystem und sprechen über den konkreten Stand. Dies funktionert auch sehr gut wenn Entwickler und Product Owner verteilt an unterschiedlichen Orten arbeiten.

    Man beeinträchtigt mit seinem experimentellen Stand nicht den Releaseprozess anderer Feature und es kann weiter kontinuierlich am Produkt gearbeitet werden.

  • Daniel

    29. April 2016 von Daniel

    Wir arbeiten aehnlich und neue features werden normalerweise auschliesslich in feature Branches entwickelt (Stichwort Gitflow). Eure Loesung mit dem DSL plugin und der Job config im Git gefaellt mir sehr gut, das kennt man ja auch von GitHub (.travis.yml) werde ich auch mal bei uns einsetzen.

    Ich habe mir zum automatischen anlegen der Jobs eine Jenkins Plugin geschrieben, dort definiert man einen Template Job und einen regulaeren Ausdruck auf den die Branches matchen muessen fuer die man automatisiert Jobs verwaltet, z.B. .*/feature/.*
    Das Plugin habe ich auf Github hochgeladen: https://github.com/OSTHUS/jenkins-gitflow-plugin

    Im uebrigen finde ich nicht das feature Branches und CI nicht unter einen Hut zu bekommen sind, das CI setup ist halt etwas aufwendiger. Zusaetzlich haben allen feature branch builds noch die option „Merge before build“ aktiviert, somit schlaegt der build fehl wenn merge konflikte auftreten, die developer sollten dann ein rebase durchfuehren und beim finalen merge des features gibt es keine boesen Ueberraschungen mehr 😉

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.