Tech up! zu Jenkins Pipeline Scripts

In unserer neuen Blogpost-Serie „Tech up!“ dreht sich alles um technische Themen – unsere System Technicians und Software Engineers zeigen ihre Expertise. Angela (Senior Software Engineer) schreibt in ihrem Beitrag zu Jenkins Pipeline Scripts. 

Im Rahmen eines Jenkins Techlabs, welches bei Puzzle durchgeführt wurde, wurde gezeigt wie Pipeline Scipts funktionieren und wie diese geschrieben werden. 

Jenkins ist für den Build, Release und Deploy Prozess zuständing:

Seit Version 2.0 von Jenkins werden Pipelines unterstützt. Die Jenkins Pipelines unterstützen 2 Syntaxe: Scripted Pipeline und Declarative Pipelines.

Im folgenden Blogpost möchte ich zeigen, wie ein Pipeline Script in der deklarativen Syntax geschrieben wird und wie die wichtigsten Funktionen genutzt werden koennen. Folgende Themen werden abgedeckt:

  • Java
  • Yarn/Gulp
  • Android/Cordova
  • Nightly Sonar
  • Deploy auf JBoss

Jenkinsfile erstellen & Jenkins einrichten

  • Jenkinsfile im Root des Projekts definieren. Der Defaultname ist `Jenkinsfile`
  • Im Jenkins ein neues Multibranch Pipeline Projekt erstellen, das entsprechende Git-Repository und Pipeline-Script File angeben

Nun wird in jedem Branch des Projektes automatisch nach Jenkinsfiles gescannt und wenn welche gefunden werden, wird der Buildprozess gestartet.

Pipeline Script

Das Grundgerüst für das Pipeline Script sieht wiefolgt aus.

pipeline {

// der Job wird auf einem beliebigen Node ausgeführt

agent any

options {

}

triggers {

}

environment {

}

stages {

}

}

Welche Parameter möglich sind, wird ausführlich in der Jenkins Doku beschrieben: https://jenkins.io/doc/book/pipeline/syntax/

Options

options {

buildDiscarder(logRotator(numToKeepStr: '5'))

timeout(time: 10, unit: 'MINUTES')

}
  • 5 ältere Builds werden behalten
  • Das Timeout beträgt 10 Minuten

Triggers

triggers {

pollSCM('H/5 * * * *')

cron('@midnight')

}
  • Das SCM wird alle 5 Minuten gepollt
  • Um Mitternacht wird der Buildprozess durch einen Cronjob gestartet

Environment

environment {

NVM_HOME = tool('NVM')

YARN_HOME = tool('YARN')

ANDROID_HOME = tool('ANDROID')

GRADLE_HOME = tool('GRADLE4')

MVN_HOME = tool('MAVEN3')

JAVA_HOME = tool('JDK8_OPENJDK')

}
  • Alle benötigten Tools werden importiert. Dazu müssten diese vorgängig installiert worden sein. Bei uns wird dies mit dem CustomTools Plugin gemacht

Backend & Client Build (Java/Maven & Yarn)

  • Der Backend und der Client Build sowie das Ausführen von deren Tests laufen parallel ab
  • Da die Custom Tools von Pipelines noch nicht vollständig unterstützt werden, müssen dem Java/Maven Build die entsprechenden Environment Variablen mitgegeben werden:
  • Im `post` Schritt werden die Artefakte archiviert
stage('Build backend & client') {
	steps {
		 parallel (
			"Build backend" : {                      
				withEnv(["PATH+MAVEN=${MVN_HOME}/bin:${env.JAVA_HOME}/bin"]) {
					sh 'mvn --batch-mode -V -U -e clean verify -DskipTests=true -Dsurefire.useFile=false'
				}
			},
			
			"Building client" : {
				 sh """#!/bin/bash +x
					source $NVM_HOME/nvm.sh
					nvm install v7.10.0
					nvm use --delete-prefix v7.10.0
					${YARN_HOME}/bin/yarn install
					./node_modules/.bin/gulp build
				"""
			}
		)
	}
	post {
	   success {
		   archiveArtifacts '**/target/*.?ar'
		   archiveArtifacts '**/target/*.zip'			  
		}
	}
}

Tests

  • Tests für Backend und Client werden parallel ausgeführt
  • Mit dem junit Plugin werden die Testresultate publiziert
stage('Testing backend & client') {
steps {
parallel(
"Test backend" : {
withEnv(["PATH+MAVEN=${MVN_HOME}/bin:${env.JAVA_HOME}/bin"]) {
sh 'mvn --batch-mode -V -U -e test -Dsurefire.useFile=false'
}
},
 
"Test client" : {
sh """#!/bin/bash +x
source $NVM_HOME/nvm.sh
nvm install v7.10.0
nvm use --delete-prefix v7.10.0
./node_modules/.bin/gulp test
"""
}
)
}
post {
   always {
   junit '**/target/**/*.xml'  // backend
   junit 'reports/*.xml'  // client
}
}
}

Android/Cordova Build

  • Mit dem Android SDK Manager kann die Version der Build-Tools ausgewählt werden.
  • GRADLE wird für den Cordova/Android ebenfalls benötigt und muss in der PATH Variable hinzugefügt werden.
stage('Build apk release file ') {

steps {

withEnv(["TARGET=TEST","PATH+GRADLE=${GRADLE_HOME}/bin:${ANDROID_HOME}/tools/bin:${ANDROID_HOME}/platform-tools"]) {

sh """#!/bin/bash +x

sdkmanager "build-tools;26.0.0"

source $NVM_HOME/nvm.sh

nvm install v7.10.0

nvm use --delete-prefix v7.10.0

${YARN_HOME}/bin/yarn install

./node_modules/.bin/gulp cordova-clean

./node_modules/.bin/gulp cordova-build-release

"""

}

}

}

Sonar Analyse

  • Jede Nacht wird mit der Cron Expression der Build getriggert, um die Sonar Analyse durchzuführen
  • Mit der in der Expression angegebenen Validierung wird geschaut, ob der Build durch einen Trigger ausgeloest wurde und nur dann werden auch die entsprechenden steps ausgeführt
stage('Nightly sonar analysis') {

when {

expression {

return currentBuild.rawBuild.getCause(hudson.triggers.TimerTrigger.TimerTriggerCause.class) != null

}

}

steps {

echo "sonar analysis"

withEnv(["PATH+MAVEN=${MVN_HOME}/bin:${env.JAVA_HOME}/bin"]) {

sh 'mvn --batch-mode -V -U -e clean install sonar:sonar -Dsurefire.useFile=false'

}

}

}

Shared Libraries

Was genau Shared Libraries sind und ich welchen Fällen man diese benutzt, kann im Puzzle Techlab nachgelesen werden:

In unserem Beispiel haben wir eine Shared Library implementiert, welche die E-Mail des letzten Git Commiters ausliest und im Falle eines broken oder unstable Builds ein E-Mail an diesen versendet.

Code der Shared Library:

def call() {

node {

sh 'git log --format="%ae" | head -1 > commit-author.txt'

return readFile('commit-author.txt').trim()

}

}

Eingebunden wird diese im Jenkinsfile mit dem folgenden Aufruf:

@Library('jenkins-libraries') _

Verwendet wird diese nun in den post Actions:

post {

unstable {

mail to: getLastGitCommiterEmail(),

subject: "Unstable Pipeline: ${currentBuild.fullDisplayName}",

body: "Something is wrong with ${env.BUILD_URL}"

}

failure {

mail to: getLastGitCommiterEmail(),

subject: "Failed Pipeline: ${currentBuild.fullDisplayName}",

body: "Something is wrong with ${env.BUILD_URL}"

}

}

Deployment

Das Deployment ist in einem zusätzlichen Job und Jenkinsfile erfasst, da dieses manuell ausgelöst wird. Dieser Job ist parametrisiert, sodass die Credentials und auch die Umgebung ausgewählt werden können.

@Library('jenkins-libraries') _

pipeline {

agent any

options {

buildDiscarder(logRotator(numToKeepStr: '5'))

timeout(time: 5, unit: 'MINUTES')

}

parameters {

credentials(credentialType: 'com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl', defaultValue: 'jboss-deployment-test', description: 'Credentials used to deploy the application to the JBoss Application Server', name: 'DEPLOY_CREDENTIALS_ID', required: true)

choice(name: 'DEPLOY_SERVER_ADDRESS', choices: 'test.puzzle.ch\nint.puzzle.ch', description: 'test.puzzle.ch: TEST\nint.puzzle.ch: INT')

choice(name: 'DEPLOY_JBOSS_SERVER_GROUP', choices: 'test-server-group\nint-server-group', description: 'test-server-group: Group name on TEST\nint-server-group: Group name on INT')

}

environment {

MVN_HOME = tool('MAVEN3')

JAVA_HOME = tool('JDK8_ORACLE')

DEPLOY_CREDENTIALS = credentials("${params.DEPLOY_CREDENTIALS_ID}")

}

stages {

stage('Deploy to JBoss') {

steps {

withEnv(["PATH+MAVEN=${MVN_HOME}/bin:${env.JAVA_HOME}/bin"]) {

sh 'mvn clean install'

sh 'mvn wildfly:deploy'

}

}

}

}

post {

unstable {

mail to: getLastGitCommiterEmail(),

subject: "Build unstable: ${currentBuild.fullDisplayName}",

body: "Build unstable: ${env.BUILD_URL}"

}

failure {

mail to: getLastGitCommiterEmail(),

subject: "Build failed: ${currentBuild.fullDisplayName}",

body: "Build failed: ${env.BUILD_URL}"

}

}

}
  • Das Plugin muss dann im pom.xml noch entsprechend konfiguriert werden
<plugin>

<groupId>org.wildfly.plugins</groupId>

<artifactId>wildfly-maven-plugin</artifactId>

<configuration>

<domain>

<server-groups>

<server-group>${env.DEPLOY_JBOSS_SERVER_GROUP}</server-group>

</server-groups>

</domain>

<hostname>${env.DEPLOY_SERVER_ADDRESS}</hostname>

<username>${env.DEPLOY_CREDENTIALS_USR}</username>

<password>${env.DEPLOY_CREDENTIALS_PSW}</password>

<force>true</force>

<name>fileToDeploy.war</name>

</configuration>

</plugin>
  • Wenn der Job nun gestartet wird, erscheint folgendes Fenster, in dem die Parameter angegeben werden können

Übersicht des Pipeline Scripts

  • Jenkins View: In der folgenden Grafik sind die einzelnen Schritte des Pipeline Scripts dargestellt. Pro Schritt wird auch die Dauer angegeben.

  • Blue Ocean Plugin View: Mit dem Blue Ocean Plugin sieht man auch welche Schritte parallel ausgeführt werden. Die Nightly Jobs wurden hier nicht ausgeführt, deshalb sind diese mit „?“ markiert.

Fazit

Persönlich finde die Job-Konfiguration via Jenkins-Pipeline Scripts sehr praktisch und übersichtlich. Desweiteren müssen Jobs nicht immer neu erfasst werden, sonden die Konfiguration ist im Projekt und GIT abgelegt. Mit dem Multibranch Job erübrigt sich auch das Erfassen der Jobs pro Branch. Diese werden automatisch erkannt, erstellt und ausgeführt.

2 Kommentare

  • Oliver Santschi, 1. September 2017

    Super Blogpost! Ich geh komm immer wieder darauf zurück um etwas nachzuschlagen. Merci

  • mm
    Angela Stempfel, 5. September 2017

    @Oliver: Danke vielmals!