
Jenkins-Tutorial: Installation, Build- und Release-Jobs

Artikelserie: Jenkins-Tutorial
Teil 1: Build- und Release-Jobs
Teil 3: Programmierung eines Seed-Jobs
(Melde dich für den HappyCoders-Newsletter an, um sofort über neue Teile informiert zu werden.)
In dieser Artikelserie erfährst du alles über Jenkins, dessen wichtigsten Funktionen und die Jenkins Job DSL.
In diesem ersten Teil geht es um:
- Wie installiert man Jenkins?
- Wie konfiguriert man manuell Jenkins-Build- und Release-Jobs für ein Maven-Projekt?
- Wie programmiert man Jenkins-Build- und Release-Jobs mit der Jenkins Job DSL?
- Wie generiert man Views mit der Jenkins Job DSL?
- Wie konfiguriert man das Skript-Sicherheitssystem?
- Wie aktualisiert man bei Änderungen im Job-DSL-Code die bestehende Jobs automatisch?
- Wie generiert man für neue Java-Projekte vollautomatisch neue Jenkins-Jobs?
Installation von Jenkins als Docker Container mit Ansible
Im offiziellen Tutorial zur Installation von Jenkins werden für Linux zwei Installationsarten vorgestellt: als Docker Container und über den Package Manager. Die Installation über den Package Manager ist ziemlich schnell erledigt und empfiehlt sich für erste Experimente. Ich möchte Jenkins langfristig und reproduzierbar installieren und werde es deshalb – analog zu meiner WordPress-Installation – als Docker Container mit Ansible installieren.
Dazu sind folgende Schritte nötig:
- Anlegen einer Ansible-Rolle
- Erstellung einer
docker-compose.yml
-Datei auf dem Server - Start des Containers auf dem Server
Außerdem musst du die Jenkins-Installation zugänglich machen, dazu gibt es verschiedene Möglichkeiten:
- Zugriff auf den Container-Port über einen SSH-Tunnel (einfach und sicher)
- Öffnen des Jenkins-Ports des Servers (unsicher)
- Anlegen einer Subdomain (z. B. jenkins.happycoders.eu) und Konfiguration von haproxy, um Zugriffe auf die Subdomain auf den Jenkins-Container weiterzuleiten (aufwändig)
Schritt 1: Anlegen der Ansible-Rolle
Solltest du mit Ansible nicht vertraut sein, empfehle ich dir mein Tutorial zum Setup eines Root-Servers mit Ansible durchzuarbeiten. Alternativ kannst du diesen Schritt überspringen und die Schritte 2 bis 4 manuell ausführen. Der Vorteil von Ansible ist, dass die Schritte jederzeit automatisiert erneut ausgeführt werden können.
Ich lege in meinem Ansible-Projekt das Verzeichnis roles/jenkins/
und darunter die Unterverzeichnisse files/
und tasks/
an.
Schritt 2: Erstellung einer docker-compose-Datei auf dem Server
Aus dem unter https://www.jenkins.io/doc/book/installing/#on-macos-and-linux angegebenen docker run
-Kommando erstelle ich folgende docker-compose.yml
-Datei:
version: '3.3'
services:
jenkins:
ports:
- 127.0.0.1:8080:8080
volumes:
- jenkins_home:/var/jenkins_home
image: jenkins/jenkins
restart: always
volumes:
jenkins-data:
Code-Sprache: YAML (yaml)
Wichtig ist hier beim Port-Mapping die IP-Adresse 127.0.0.1 mit anzugeben, da Port 8080 ansonsten offen aus dem Internet erreichbar wäre (es sei denn, du willst das).
Manuelle Installation der docker-compose-Datei ohne Ansible
Bei der manuellen Installation wird die oben gezeigte Datei unter /opt/docker/jenkins/docker-compose.yml
gespeichert.
Automatisierte Installation der docker-compose-Datei mit Ansible
Zur Installation mit Ansible wird die Datei zunächst im Unterverzeichnis roles/jenkins/files/
deines Ansible-Repositories als docker-compose.yml
abgelegt. Danach legst du die Tasks-Datei roles/jenkins/tasks/main.yml
mit folgendem Inhalt an:
- name: Create Jenkins docker directory
file:
path: /opt/docker/jenkins
state: directory
owner: root
group: root
mode: 0755
- name: Copy Jenkins docker-compose file
copy:
src: docker-compose.yml
dest: /opt/docker/jenkins/
owner: root
group: root
mode: 0644
Code-Sprache: YAML (yaml)
Nun musst du nur noch die Rolle "jenkins" zum Ansible Playbook deines Servers hinzufügen und es ausführen. In folgendem Screenshot habe ich der Einfachheit halber alle anderen Rollen meines Servers auskommentiert:
Schritt 3: Start des Containers auf dem Server
Manueller Start des Containers ohne Ansible
Um den Container manuell zu starten, führt man im /opt/docker/jenkins/
-Verzeichnis folgendes Kommando aus:
docker-compose up -d
Code-Sprache: Klartext (plaintext)
Automatisierter Start des Containers mit Ansible
Um den Container durch Ansible automatisch zu starten, muss folgender Task ans Ende der main.yml
angehängt werden:
- name: Run docker-compose up -d
shell: docker-compose up -d
args:
chdir: /opt/docker/jenkins/
Code-Sprache: YAML (yaml)
Ich führe das Playbook erneut aus:
Auf dem Server prüfe ich die Log-Ausgaben des neuen Containers:
cd /opt/docker/jenkins
docker-compose logs -f
Code-Sprache: Klartext (plaintext)
Das Log zeigt an, dass der Container erfolgreich erstellt wurde:
Die Jenkins-Installation steht nun auf Port 8080 des Servers zur Verfügung, und zwar – wie gewünscht – nur auf localhost, sodass dieser nicht offen aus dem Internet erreichbar ist. Im folgenden Schritt stelle ich drei Optionen vor, um von außerhalb auf die Installation zuzugreifen.
Schritt 4: Jenkins-Installation erreichbar machen
Variante a: Zugriff auf den Container-Port über einen SSH-Tunnel
Die einfachste und sicherste Variante auf unseren Server zuzugreifen ist über einen SSH-Tunnel. Serverseitig muss dazu in der /etc/ssh/sshd_config
-Datei die Option AllowTcpForwarding yes
gesetzt sein.
Je nachdem, ob du Windows oder Linux verwendest, öffnest du den Tunnel wie folgt:
Windows:
In deiner Putty-Session wählst du links Connection/SSH/Tunnels aus und gibst dann rechts einen lokalen Port ein (im Beispiel: 8081) und das Ziel aus Sicht des Servers (im Beispiel: localhost:8080) und klickst auf “Add”:
Falls du schon eine SSH-Verbindung offen hast, musst du diese nun schließen und eine neue öffnen, um den Tunnel aufzubauen. Über http://localhost:8081 kannst du dann auf die Jenkins-Installation zugreifen:
Linux:
Von Linux aus sellst du über das ssh
-Kommando eine Verbindung her und gibst dabei folgenden Parameter mit an:
-L <lokaler Port>:<Host-Name aus Sicht des Servers>:<Port des Docker Containers>
Code-Sprache: Klartext (plaintext)
Analog zur oben beschriebenen Putty-Konfiguration wäre das Kommando:
ssh -L 8081:localhost:8080 [email protected]
Code-Sprache: Klartext (plaintext)
Hierdurch wird ein Tunnel von meinem lokalen Port 8081 zum Port 8080 des Servers hergestellt (“localhost” aus Sicht des Servers) und meine Jenkins-Installation ist wiederum unter http://localhost:8081 erreichbar.
Variante b: Öffnen des Jenkins-Ports des Servers
Über die docker-compose
-Datei haben wir auf dem Server die Adresse 127.0.0.1:8080 auf den Docker-Container gemappt. Wenn wir an dieser Stelle die IP-Adresse 127.0.0.1 weglassen, wird stattdessen auf 0.0.0.0, d. h. auf alle IP-Adressen des Servers, einschließlich der externen gebunden. Da Docker alle gemappten Adressen automatisch auch in der Firewall öffnet, wäre unser Jenkins offen aus dem Internet erreichbar. Auch wenn Jenkins durch eine User-Authentifizierung abgesichert ist, ist dies unter Umständen nicht gewünscht. Außerdem ist die Verbindung unverschlüsselt.
Variante c: Anlegen einer (Sub-)Domain
Die dritte Variante wäre das Anlegen einer (Sub-)Domain, wie z. B. jenkins.happycoders.eu, verbunden mit der Konfiguration eines virtuellen Hosts in haproxy, der den eingehenden Traffic an localhost:8080 (also den Jenkins-Docker-Container) weiterleitet. Dieser Schritt ist aufwändig, insbesondere die Einrichtung eines SSL-Zertifikats, das bei einer solchen Lösung nicht fehlen sollte. Auch bei dieser Variante ist die Jenkins-Installation über das Internet erreichbar, es sei denn du konfigurierst die Firewall so, dass die IP-Adresse, an die der virtuelle Host gebunden ist, nur von festgelegten IP-Adressen aus erreichbar ist. Das ist sinnvoll, wenn du nur von einem eingeschränkten Kreis von IP-Adressen, z. B. deinem Büro (sofern dies eine feste IP-Adresse hat) auf den Server zugreifst.
Die Konfiguration erfolgt analog zur Konfiguration der HappyCoders.eu-Webseite, die ich in meinem Ansible-Tutorial zum Setup von HAProxy und einem HTTPS-Zertifikats von Let’s Encrypt Schritt für Schritt beschrieben habe. Dies hier im Detail zu beschreiben würde den Rahmen dieses Artikels sprengen, denn hier soll es ja in erster Linie um Jenkins gehen – nicht um Ansible, haproxy oder Let’s Encrypt.
Schritt 5: Abschließen der Installation
Zur Fertigstellung der Installation wirst du aufgefordert den Inhalt der Datei /var/jenkins_home/secrets/initialAdminPassword
in das angezeigte Formular einzutragen. Das Passwort haben wir auch schon in der Log-Ausgabe weiter oben gesehen und können es von dort kopieren.
Alternativ kannst du es natürlich auch aus der angegebenen Datei auslesen. Der Dateipfad existiert so allerdings nicht auf dem Host, sondern im Docker-Container. Es gibt zwei Wege den Inhalt der Datei anzuzeigen:
Variante 1: Auslesen der Passwort-Datei mittels docker exec
:
Mit docker exec
können wir Kommandos auf dem Container ausführen, also auch cat
, um den Inhalt einer Datei anzuzeigen:
docker exec -it <Container ID> cat /var/jenkins_home/secrets/initialAdminPassword
Code-Sprache: Klartext (plaintext)
Variante 2: Auslesen der Passwort-Datei aus dem gemappten Volume
Da wir in der docker-compose.yml
das Verzeichnis /var/jenkins_home/
als Volume definiert haben, ist es auf ein Verzeichnis des Hosts gemappt. Welches das ist, finden wir über docker inspect
heraus:
docker inspect -f '{{ .Mounts }}' <Container ID>
Code-Sprache: Klartext (plaintext)
Auf meinem Server erscheint folgende Ausgabe:
[{volume jenkins_jenkins_home /var/lib/docker/volumes/jenkins_jenkins_home/_data /var/jenkins_home local rw true }]
Das initiale Admin-Passwort finden wir somit auch über folgendes Kommando (wir müssen hier sudo
verwenden, da der reguläre User keinen Zugriff auf das Docker-Volume hat):
sudo cat /var/lib/docker/volumes/jenkins_jenkins_home/_data/secrets/initialAdminPassword
Code-Sprache: Klartext (plaintext)
Nach der Eingabe des Admin-Passworts in den Webbrowser musst du dir für eine initiale Plugin-Auswahl entscheiden. Ich wähle hier die empfohlenen Plugins. Als nächstes legst du den initialen Admin-User an. Später kannst du Jenkins auch mit einem LDAP-Server verbinden. Die Jenkins-URL kannst du auf http://localhost:8081/ lassen. Falls du später die Art des Zugriffs auf den Server änderst, kannst du die URL jederzeit unter Manage Jenkins → Configure System → Jenkins Location anpassen.






Basis-Konfiguration
Installation von Plugins
Nun müssen wir ein paar benötigte Plugins installieren. Dazu klickst du in der Navigation links auf Manage Jenkins → Manage Plugins und wählst folgende Plugins zur Installation aus (du kannst dazu über die Filter-Funktion nach "authorize", "git", "groovy", "dsl" und "maven" suchen):
- Authorize Project (wird für das Ausführen von aus einem Git-Repository geladenen Groovy-Code benötigt)
- Git Parameter (ermöglicht die Auswahl eines Branches)
- Groovy (erlaubt es uns Groovy-Code innerhalb eines Jobs zu verwenden)
- Job DSL (ermöglicht das Erstellen von Jobs über eine DSL; mehr dazu im zweiten Teil der Artikelserie)
- Maven Integration (stellt den Job-Typ "Maven" zur Verfügung)
Setup von Maven
Der Jenkins-Docker-Container enthält noch keine Maven-Installation. Glücklickerweise kann Jenkins Maven eigenständig herunterladen und installieren. Dazu musst du im Hauptmenü Manage Jenkins → Global Tool Configuration auswählen. Dann scrollst du zu "Maven" herunter, klickst auf "Add Maven", und gibst als Namen "Latest" ein. Das Häkchen bei "Install automatically" ist standardmäßig gesetzt und unter Version wird die aktuelle Version angezeigt. Mit einem Klick auf "Save" speicherst du die Änderungen.
Setup von Git
Nun müssen wir noch einen Username für Git-Commits durch Jenkins festlegen. Dies geschieht unter Manage Jenkins → Configure System, im Abschnitt "Git plugin". Ich trage hier "Jenkins" als User mit der E-Mail-Adresse "[email protected]" ein:
Für den Zugriff auf unser Git-Repository per SSH müssen wir ein Key-Paar erstellen. Jenkins bietet die Möglichkeit im Hauptmenü unter Credentials Key-Paare zu hinterlegen. Allerdings können diese Key-Paare nur für das Clonen von Repositories durch das Git-Plugin verwendet werden. Um Änderungen ins Repository zu pushen (was für den Release-Job nötig sein wird), werden wir das Maven Goal scm:checkin
verwenden. Dieses kann nicht auf die in Jenkins hinterlegten Key-Paare zugreifen, sondern verwendet die im Jenkins-Container im Home-Verzeichnis des jenkins-Users liegenden Keys. Auf diese kann wiederum auch das Git-Plugin zugreifen, sodass dieses eine Key-Paar ausreicht. Wir erstellen es, in dem wir im laufenden Jenkins-Docker-Container ssh-keygen
aufrufen:
docker exec -it <Container ID> /bin/bash -c "ssh-keygen -t ecdsa -b 521
-C <Kommentar, z. B. '[email protected]<Server Name>'>
-N ''
-f ~/.ssh/id_ecdsa"
docker exec -it <Container ID> /bin/bash -c "cat ~/.ssh/id_ecdsa.pub"
Code-Sprache: Klartext (plaintext)
Auf meinem Server sieht das wie folgt aus:
Den öffentlichen Schlüssel hinterlege ich in meinem Git-Repository (auf GitLab ist die URL dafür https://gitlab.com/profile/keys):
Manuelle Jenkins-Job-Konfiguration für Maven-Projekte
Ich zeige dir im Folgenden wie du für zwei voneinander abhängige Maven-Projekte je einen Build- und einen Release-Job erstellst. Die Beispiel-Maven-Projekte (eine Demo-Applikation und eine von dieser verwendete Demo-Library) liegen in folgendem Git-Monorepo: https://gitlab.com/SvenWoltmann/jenkins-tutorial-demo
Warum ich ein Monorepo verwende, kannst du in meinem Artikel über die Vorteile von Monorepos nachlesen. Was du anders machen musst, um einen Job für ein Multirepo zu erstellen, hebe ich an den entsprechenden Stellen hervor.
Jenkins Build-Job
Ein Build-Job läuft üblicherweise nach jedem Commit im Source-Code-Management; er lädt den Quellcode aus dem Source-Code-Management-Tool, baut das Projekt und führt alle Unit- und Integration-Tests aus. Der Build-Job erstellt dabei SNAPSHOT-Artefakte (z. B. JAR-, WAR- oder EAR-Dateien) und lädt diese in ein Artefakt-Repository (ein zentraler Speicherplatz für die erstellten Artefakte, wie z. B. https://mvnrepository.com/) hoch.
Die Build-Jobs für die zwei Projekte im oben verlinkten Git-Repository erstelle ich nun wie folgt:
Als erstes klicke ich links oben auf "New Item":
Auf der folgenden Seite gebe ich einen Namen ein, wähle "Maven project" aus (diese Option wird durch das Maven Integration-Plugin zur Verfügung gestellt) und klicke auf "OK":
Im "General"-Tab gebe ich eine kurze Beschreibung ein, aktiviere "Discard old builds" und gebe als "Max # of builds to keep" 5 an. Builds benötigen teilweise eine Menge Speicherplatz, und mit dieser Einstellung bewirke ich, dass nur die letzten fünf Builds aufgehoben werden.
Immer noch auf dem "General"-Tab aktiviere ich "This project is parameterized", klicke auf "Add Parameter" und dann auf "Git Parameter":
In dem neu erscheinenden Feld gebe ich als Name "Branch" ein, dazu eine Beschreibung, als Parameter Type wähle ich ebenfalls "Branch" aus und als default trage ich "origin/master" ein. Diese Einstellung ermöglicht es mir einen bestimmten Branch des Projekts zu bauen. Lasse ich den vorangegangenen und diesen Schritt weg, wird immer der Master-Branch gebaut.
Ich scrolle weiter zu "Source Code Management" und wähle "Git" aus. Als Repository-URL hinterlege ich die Clone-URL meines Repositories, "[email protected]:SvenWoltmann/jenkins-tutorial-demo.git". Wenn du keinen GitLab-Account hast oder es Probleme mit der Authentifizierung gibt, kannst du auch die HTTPS-URL "https://gitlab.com/SvenWoltmann/jenkins-tutorial-demo.git" eintragen, die ein Clonen ohne Authentifizierung ermöglicht.
Unter "Branch Specifier" gebe ich "$Branch" an, dies wird zur Build-Zeit durch den in den vorherigen zwei Schritten definierten Branch-Parameter ersetzt.
Da ich nur ein Teilprojekt meines Git-Monorepos auschecken möchte, klicke ich bei "Additional Behaviours" auf "Add" und wähle "Sparse Checkout paths" aus. (Dieser letzte Schritt kann bei einem Multirepo übersprungen werden.)
Unter "Sparse Checkout paths / Path" trage ich den Pfad des zu bauenden Projekts, "library1/" ein. Ich klicke noch einmal auf "Add" und auf "Polling ignored commits in certain paths". (Auch diese Schritte sind bei einem Multirepo nicht nötig.)
Unter "Polling ignored commits in certain paths / Included Regions" trage ich "library1/.*" ein. Hiermit bewirke ich, dass Jenkins das Projekt nur dann automatisch baut, wenn sich auch etwas an diesem Projekt ändert. Ohne diese Einstellung würde Jenkins das Projekt immer dann neu bauen, wenn sich in einem beliebigen Teilprojekt des Monorepos etwas ändern würde. (Auch dieser Schritt fällt bei einem Multirepo weg.)
In der Kategorie "Build Triggers" musst du der Option "Build whenever a SNAPSHOT dependency is built" besondere Aufmerksamkeit schenken. Diese Option ist ansich sinnvoll, führt aber bei großen Dependency-Graphen dazu, dass ein kleiner Commit ggf. Hunderte Build-Jobs triggert. Da es in meiner Demo lediglich zwei Projekte gibt, lasse ich die Option aktiviert.
Außerdem aktiviere ich die Option "Poll SCM" und trage unter "Schedule" "H/15 * * * *" ein, was dazu führt, dass Jenkins jede Viertelstunde in GitLab nachschaut, ob es neue Commits für das Projekt gibt und das Projekt neu baut, wenn das der Fall ist.
Die Kategorien "Build Environment" und "Pre Steps" können wir überspringen. Unter "Build" muss der "Root POM"-Pfad angepasst werden. Dieser ist standardmäßig "pom.xml", doch in unserem Monorepo liegen die pom.xml-Dateien in den Projekt-Verzeichnissen. Daher ändere ich den Eintrag auf "library1/pom.xml". (Bei einem Multirepo mit der pom.xml im Root-Verzeichnis lässt du diesen Eintrag unverändert.)
Unter "Goals and options" trage ich "clean install" ein, sodass Jenkins das Projekt letztendlich durch einen Aufruf von mvn clean install
bauen und im lokalen Maven-Repository ablegen wird.
Im Anschluss an den Build könnten wir die erstellte JAR noch in ein Artefakt-Repository, wie z. B. den Nexus Repository Manager hochladen. Dazu würde man unter "Post-build Actions" auf "Add post-build action" klicken, "Deploy artifacts to Maven repository" auswählen, und die Repository-URL eintragen.
Da ich kein Artefakt-Repository installiert habe und mir die JAR im lokalen Maven-Repository des Jenkins-Servers ausreicht, überspringe ich diesen Schritt und klicke zum Abschluss der Konfiguration auf "Save". Wir landen auf der Seite des soeben angelegten Jobs. Um den Job auszuführen, klicke ich auf "Build with Parameters":
Es erscheint das folgende Formular, in dem wir den Git-Branch auswählen können. Momentan gibt es nur den Master-Branch, also wähle ich diesen aus und klicke zum Fortfahren auf "Build":
Der Job läuft nun, wie man am Fortschrittsbalken in der Build-History links unten sieht:
Nach kurzer Zeit ist der Job abgeschlossen. Der Fortschrittsbalken verschwindet und die zuvor graue Kugel wird blau. Falls du grün vorziehst (und du im Team keine Personen mit Rot-Grün-Sehschwäche hast), hilft dir das Plugin "Green Balls" weiter.
Ein Klick auf die "#1" führt dich zur Konsolen-Ausgabe des Jobs:
Am Ende der Konsolen-Ausgabe sehen wir, dass der Job 7,5 Sekunden benötigt hat und erfolgreich abgeschlossen wurde:
Analog kannst du den Build-Job für das "application1"-Projekt des Monorepos konfigurieren.
Im folgenden Abschnitt zeige ich dir, wie du den Build-Job zu einem Release-Job erweitert.
Jenkins Release-Job
Was unterscheidet einen Release-Job von einem Build-Job? Ein Release-Job wird manuell angestoßen. Er setzt die Versionsnummer des Projekts auf eine Release-Version (also ohne "SNAPSHOT"), baut das Projekt, lädt es ggf. in ein Artefakt-Repository hoch und setzt abschließend die Versionsnummer auf die nächste SNAPSHOT-Version.
Zunächst legen wir den Release-Job als Kopie des Build-Jobs an. Dazu klicken wir erneut links oben auf "New Item". Als Namen trage ich "Jenkins Tutorial Demo - Library 1 - Release" ein. Einen Projekt-Typ brauchen wir nicht auszuwählen – stattdessen tragen wir ganz unten bei "Copy from" den Namen des Jobs, den wir kopieren wollen, ein – also "Jenkins Tutorial Demo - Library 1". Abschließend klicken wir auf "OK".
Wir landen wieder im Konfigurationsformular, wo ich zunächst die Beschreibung anpasse ("Release job" anstatt "Build job"):
Ich scrolle hinunter zu "This project is parameterized" und entferne den Git Parameter "Branch" über das Kreuz rechts oben, da ich Releases ausschließlich vom Master-Branch erstellen möchte.
Stattdessen füge ich über "Add Parameter" zwei "String Parameter" hinzu:
- "releaseVersion": hiermit kann ich die Release-Version setzen. Lasse ich das Feld bei der Ausführung des Job frei, wird standardmäßig die aktuelle SNAPSHOT-Version, ohne den "-SNAPSHOT"-Suffix, verwendet.
- "nextSnapshotVersion": hiermit kann ich die Snapshot-Version nach dem Release festlegen. Lasse ich dieses Feld frei, wird die Minor-Stelle der Release-Version um eins erhöht.
Im Abschnitt "Source Code Management" ändern wir den Branch Specifier auf "origin/master". Den "Branch"-Parameter hatten wir im vorangegangenen Schritt entfernt, da wir Releases nur vom Master-Branch aus bauen wollen.
Im selben Abschnitt muss unter "Additional Behaviours" eine weitere Aktion eingetragen werden, und zwar "Check out to specific local branch" mit dem Branch-Namen "master". Andernfalls wird später das Maven Goal scm:checkin
mit der Fehlermeldung "Detecting the current branch failed: fatal: ref HEAD is not a symbolic ref" fehlschlagen.
Im Abschnitt "Build Triggers" deaktiviere ich jegliche Trigger, da ich meinen Release-Job ausschließlich manuell starten will:
Im Abschnitt "Pre Steps" füge ich über den Button "Add pre-build step" einen Schritt vom Typ "Execute system Groovy script" hinzu:
Mit diesem Script fülle ich die Variablen "releaseVersion" und "nextSnapshotVersion", falls diese bei der Ausführung des Jobs leer gelassen wurden:
Hier ist der Groovy-Code, du kannst ihn gerne kopieren (achte darauf für eigene Projekte den pomPath anzupassen):
import hudson.model.StringParameterValue
import hudson.model.ParametersAction
def env = build.getEnvironment(listener)
String releaseVersion = env.get('releaseVersion')
String nextSnapshotVersion = env.get('nextSnapshotVersion')
if (!releaseVersion) {
String pomPath = build.workspace.toString() + '/library1/pom.xml';
def pom = new XmlSlurper().parse(new File(pomPath))
releaseVersion =
pom.version.toString().replace('-SNAPSHOT', '')
println "releaseVersion (calculated) = $releaseVersion"
def param = new StringParameterValue('releaseVersion', releaseVersion)
build.replaceAction(new ParametersAction(param))
}
if (!nextSnapshotVersion) {
def tokens = releaseVersion.split('\.')
nextSnapshotVersion =
tokens[0] + '.' + (Integer.parseInt(tokens[1]) + 1) + '-SNAPSHOT'
println "nextSnapshotVersion (calculated) = $nextSnapshotVersion"
def param1 = new StringParameterValue('releaseVersion', releaseVersion)
def param2 =
new StringParameterValue('nextSnapshotVersion', nextSnapshotVersion)
build.replaceAction(new ParametersAction(param1, param2))
}
Code-Sprache: Groovy (groovy)
Ich füge noch drei weitere Pre Steps hinzu. Mit dem ersten Pre Step setze ich die Versionsnummer in der pom.xml
auf die gewünschte Release-Version. Ich füge den Pre Step wie folgt hinzu (releaseVersion
ist dabei die als String-Parameter eingetragene bzw. im Groovy-Skript gesetzte Variable):
- Typ: "Invoke top-level Maven targets"
- Maven Version: "Latest"
- Goals:
versions:set -DnewVersion=${releaseVersion} -DgenerateBackupPoms=false
- Advanced → POM: "library1/pom.xml"
Der nächste Pre Step setzt alle Snapshot-Dependencies auf Release-Versionen. Falls es von einer Dependency noch keine Release-Version gibt, wird der Build im nächsten Schritt mit einer entsprechenden Fehlermeldung abbrechen. Dadurch stelle ich sicher, dass mein Release nur von anderen Releases abhängt und nicht von Snapshots:
- Typ: "Invoke top-level Maven targets"
- Maven Version: "Latest"
- Goals:
versions:use-releases -DgenerateBackupPoms=false -DprocessDependencyManagement=true
- Advanced → POM: "library1/pom.xml"
Der letzte Pre Step stellt sicher, dass die pom.xml keine Referenzen auf Snapshot-Versionen mehr enthält. Dies ist notwendig, da der vorherige Schritt u. a. keine Snapshot-Referenzen auf Parent-Projekte anpasst. Falls wir vergessen haben den Parent zu releasen, wird der Build somit an dieser Stelle abbrechen:
- Typ: "Execute shell"
- Command:
if find library1/ -name 'pom.xml' | xargs grep -n "SNAPSHOT"; then
echo 'SNAPSHOT versions not allowed in a release'
exit 1
fi
Achtung: im folgenden Screenshot sieht man die "POM"-Felder nicht. Diese befinden sich hinter den "Advanced..."-Buttons. Für den Screenshot habe ich die "Advanced"-Felder geschlossen, da diese neben dem "POM"-Feld auch noch eine Reihe andere Eingabefelder enthalten und ziemlich viel Platz in Anspruch nehmen.
Am Build-Schritt müssen wir nichts ändern. Im darauf folgenden Abschnitt "Post Steps" wähle ich ganz oben "Run only if build succeeds" aus, sodass im Falle eines Build-Fehlers der Release-Job an dieser Stelle abgebrochen wird. Ich füge die folgenden vier "Post Steps" hinzu, um 1. die Release-Version in Git zu committen, 2. diese zu taggen, 3. die Version auf die nächste Snapshot-Version hochzusetzen und 4. auch diese in Git zu committen.
Alle vier Post Steps sind vom selben Typ, benötigen dieselbe Maven-Version und arbeiten auf derselben pom.xml:
- Typ: "Invoke top-level Maven targets"
- Maven Version: "Latest"
- Advanced → POM: "library1/pom.xml"
Im folgenden liste ich zu jedem Schritt nur noch die jeweiligen Maven-Goals auf:
Erster Post Step – Commit der Release-Version in Git (releaseVersion
ist die als String-Parameter eingetragene bzw. im Groovy-Skript gesetzte Variable):
scm:checkin -Dmessage="Release version ${project.artifactId}:${releaseVersion}" -DdeveloperConnectionUrl=scm:git:[email protected]:SvenWoltmann/jenkins-tutorial-demo.git
Zweiter Post Step – Taggen des Releases in Git (project.artifactId
wird aus der pom.xml ausgelesen):
scm:tag -Dtag=${project.artifactId}-${releaseVersion} -DdeveloperConnectionUrl=scm:git:[email protected]:SvenWoltmann/jenkins-tutorial-demo.git
Dritter Post Step – Hochsetzen der Version auf die nächste Snapshot-Version (nextSnapshotVersion
ist die als String-Parameter eingetragene bzw. im Groovy-Skript gesetzte Variable):
versions:set -DnewVersion=${nextSnapshotVersion} -DgenerateBackupPoms=false
Vierter Post Step – Commit der Snapshot-Version in Git:
scm:checkin -Dmessage="Switch to next snapshot version: ${project.artifactId}:${nextSnapshotVersion}" -DdeveloperConnectionUrl=scm:git:[email protected]:SvenWoltmann/jenkins-tutorial-demo.git
Auch im folgenden Screenshot habe ich die "POM"-Felder der Übersicht halber wieder ausgeblendet:
Damit ist auch der Release-Job fertiggestellt, ich speichere ihn mit einem Klick auf "Save". Ich lande auf der Job-Seite und starte den Job mit "Build with Parameters":
Auf der folgenden Seite kann ich die Release-Version und die nächste Snapshot-Version eintragen. Ich lasse die Felder jedoch leer, vertraue auf meinen in Schritt 8 geschriebenen Groovy-Code und klicke auf "Build":
Während der Release-Job läuft, können wir uns die Ausgaben durch einen Klick auf "#1" in der Build History anschauen:
Der Release-Job läuft in nur 3,2 Sekunden erfolgreich durch (der erste Build-Job hat länger gedauert, weil Maven zunächst noch Dependencies runterladen musste):
Um zu überprüfen, ob der Release-Job korrekt gearbeitet hat, schaue ich in die Commit-History meines GitLab-Projekts unter https://gitlab.com/SvenWoltmann/jenkins-tutorial-demo/commits/master und sehe dort die erfolgreich durchgeführten Release-Commits:
Somit ist auch der Release-Job abgeschlossen und wir können analog dazu den Release-Job für das „application1“-Projekt konfigurieren.
Fazit und Ausblick
In diesem ersten Teil des Jenkins-Tutorials habe ich dir gezeigt, wie du Jenkins über Ansible installierst und initial konfigurierst und wir du für Maven-Projekte in einem Git-Monorepo – vorerst manuell – Build- und Release-Jobs erstellst.
Das ist schon ein ziemlich hoher manueller Aufwand. Stell dir vor, du müsstest das für zehn oder 100 Projekte machen. Oder stell dir vor, du willst in zehn oder 100 Projekten einen einzelnen Schritt im Build- oder Release-Prozess ändern. Nicht nur, dass du stundenlang beschäftigt wärst – dieser manuelle Prozess ist zudem überaus fehleranfällig.
Deshalb werde ich dir im zweiten Teil der Artikelserie zeigen, wir du die gleichen Jobs mit Hilfe der Jenkins Job DSL als Programmcode schreibst und diesen in Jenkins importierten kannst. Im dritten Teil zeige ich dir, wie du einen sogenannten "Seed Job" schreibst, der für neue Projekte im Monorepo vollautomatisch neue Build- und Release-Jobs generiert, bzw. bei Änderungen des Job-DSL-Codes die bestehenden Jobs automatisch anpasst.
Wenn dir dieser Artikel geholfen hat, hinterlasse gerne einen Kommentar und teile ihn über einen der Share-Buttons am Ende.
Möchtest du informiert werden, wenn neue Artikel auf HappyCoders.eu veröffentlicht werden? Dann klick hier, um dich für den HappyCoders.eu Newsletter anzumelden.