Jenkins Workflow - Pipeline de release

14 May 2015
| CI

Votre projet est prêt à être releasé. Cela implique traditionnellement un ensemble d’étapes qu’il convient d’orchestrer avec Jenkins en réutilisant si possible le pipeline de compilation -> test -> package tout en offrant un niveau d’automatisation satisfaisant.

Nous allons voir comment le plugin Workflow peut résoudre cette problématique.

Introduction

Votre projet est prêt à être releasé. Cela implique traditionnellement :

  • Une entrée utilisateur indiquant la version suivante
  • La suppression des qualifiers SNAPSHOT et la vérification que toutes les dépendances sont bien des versions non SNAPSHOT
  • Une execution du pipeline de compilation -> test -> package
  • Et quand tout ce passe bien, le passage à la version suivante
  • Et en cas d’erreur, un rollback à la version courante

Soit: prepare release -> compilation -> test -> package -> (next iteration | rollback)

Ces étapes peuvent se faire simplement en utilisant le plugin release de maven. La problématique est qu’il est une spécificité et qu’il bypass complétement notre pipeline de compilation -> test -> package avec toutes les spécificités qu’il peut contenir.

La difficulté avec Jenkins va être d’ajouter en queue et en tête de notre pipeline usuel les deux étapes susnommées. Jenkins ne permet pas, avec un systeme upstream/downstream basé sur des triggers de type post build, d’attendre la fin d’un pipeline (entendez avec une profondeur > 1).

Cela doit passer par :

  • Un freestyle job avec step de build bloquant de type “Trigger/call builds on other projects”
  • Un job de type MultiJob,
  • Un job de type Build flow plugin
  • Un job de type Workflow plugin qui semble remplacer à terme le Build flow plugin
  • Autre?

Pour cet article je vais utiliser le dernier: Workflow plugin.

Workflow plugin

Le workflow plugin ajoute un nouveau type de job nommé Workflow.

JobDsl

Un job de type Freestyle permet de décrire au travers d’une interface utilisateur les steps constituants un job. L’approche par interface utilisateur, si elle a l’avantage d’être visuelle et de guider l’utilisateur, a l’inconvénient d’une certaine rigidité. Le job de type workflow permet de décrire au travers d’un DSL Groovy les steps constituants un job. On retrouve donc les steps d’un job classique sous la forme d’une DSL augmenté de la puissance d’un langage de programmation (variable, condition, boucle…). Une approche donc plus flexible, mais plus technique. Le script peut, comme pour un job de type DSL, être stocké dans le job ou dans un SCM.

JobDsl

La documentation est spartiate voir inexistante, heureusement l’interface offre un snippet generator:

JobDsl

Pour plus d’information sur la big picture de ce plugin je vous invite à consulter la présentation de CloudBees Jenkins Workflow Webinar - Dec 10, 2014

Pipeline de release

Le pipeline de release sera articulé autour de 5 steps: prepare release -> compilation -> test -> package -> (next iteration | rollback). Ce pipeline utilisera un paramètre NEXT_VERSION qui est la version de la prochaine itération. Il sera saisi par l’utilisateur.

Step 1 : Prepare release

Ce step clone le projet, supprime le qualifier SNAPSHOT de l’ensemble des versions, dependances comprises, puis il commit les modifications. Pour ce faire je vais utiliser un script shell. Pourquoi ne pas utiliser le plugin versions de maven ?

Ce plugin ne permet pas de supprimer le qualifier SNAPSHOT de la version du projet ni de supprimer ce qualifier dans un projet multi module définissant une version au travers d’une propriété définie dans le POM root.

1
2
3
4
5
6
sh 'rm -Rf * .git'
git url:REPOSITORY
sh 'git checkout master'
sh 'find . -name "pom.xml" | xargs -I file sed -i.bak file -e "s/-SNAPSHOT//"'
sh 'git commit --allow-empty -am "Release"'
sh 'git push'

Ce snippet utilise 2 commandes DSL,sh et git, dont le nom est suffisamment explicite pour se passer d’explication.

La ligne 1 est l’équivalent d’un clean workspace. Les lignes 2 et 3 sont atypiques. Pourquoi ne pas simplement cloner le répertoire via un git clone? Jenkins crée des dot répertoires dans le workspace courant et git n’apprécie pas de cloner dans un répertoire non vide. A l’inverse la commande DSL git se positionne sur la référence du master en mode détaché. Bref…

Step 2,3,4 : compilation -> test -> package

Les jobs construits dans la partie 1 de cette suite utilisent des relations upstream/downstream basées sur des triggers de type post build. Comme précisé dans l’introduction, cette relation doit être deconstruite au profit d’une relation qui sera définie dans le job de release. Ormis cette différence, les jobs restent inchangés.

1
2
3
build 'Project 1 - Compile'
build 'Project 1 - Test'
build 'Project 1 - Package'

La commande DSL build invoque un build. Elle peut prendre différents paramètres comme Wait for completion, Propagate errors ainsi que les paramètres du job. Par exemple, et cela est à ma connaissance la seule manière de faire:

1
build job:'Foo', parameters: [[$class: 'StringParameterValue', name: 'FOO', value: 'BAR']]

En passant [['FOO' : 'BAR']] serait plus élégant.

Le step suivant, (next iteration | rollback), est conditionné au résultat du présent step. Si les trois jobs passent, le projet est releasé, si un des trois failed, le projet est rollbacké à sa version courante.

Cela peut s’exprimer simplement par un block try-catch car l’echec d’un build lance une exception:

1
2
3
4
5
6
7
8
def success = true
try {
    build 'Project 1 - Compile'
    build 'Project 1 - Test'
    build 'Project 1 - Package'
} catch(e){
    success = false
}

Il y a différente manière de procéder, par exemple en utilisant le retour de build qui est de type RunWrapper:

1
2
    def compileBuild = build job: 'Project 1 - Compile', propagate: false
    def success = 'SUCCESS' == compileBuild.result 

Le propagate mis à false est indispensable pour bloquer le lancement d’une exception en cas d’échec.

Notons également la commande catchError qui a une portée plus globale au bloc en cours d’exécution, voir l’aide du snippet generator

Job : Next Iteration

Ce step modifie la version en utilisant celle saisie par l’utilisateur. Pour ce faire j’utilise simplement le plugin Maven Versions:

1
2
3
4
def mvnHome = tool 'Maven 3.2.2'
sh "${mvnHome}/bin/mvn versions:set -DnewVersion=${NEXT_VERSION}  -DgenerateBackupPoms=false"
sh 'git commit --allow-empty -am "Next Version"'
sh 'git push'

La ligne 1 permet de déclarer et d’utiliser un outil (ici Maven) préalablement défini dans les settings Jenkins

JobDsl

Job : Rollback

Ce step rollback la version en utilisant celle initiale.

1
2
3
4
def mvnHome = tool 'Maven 3.2.2'
sh "${mvnHome}/bin/mvn versions:set -DnewVersion=${currentVersion}  -DgenerateBackupPoms=false"
sh 'git commit --allow-empty -am "Rollback"'
sh 'git push'

Orchestrateur

Les 5 steps sont mis en musique par un job de type Workflow qui se charge de l’orchestration. Ce job possède un paramètre qui sera à saisir par l’utilisateur: NEXT_VERSION est la version de la prochaine itération.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
node {
    //workspace cleanup
    sh 'rm -Rf * .git'

    //checkout the repository
    git url: 'https://github.com/nithril/jenkins-jobdsl-project1.git'
    sh 'git checkout master'

    //extract the current version
    def pom = readFile 'pom.xml'
    def currentVersion = new XmlParser().parseText(pom).version.text()

    //remove the snapshot qualifier
    sh 'find . -name "pom.xml" | xargs -I file sed -i.bak file -e "s/-SNAPSHOT//"'

    //push the change
    commitAndPush("Release ${currentVersion}")


    def success = true

    //compile -> test -> package
    try {
        build 'Project 1 - Compile'
        build 'Project 1 - Test'
        build 'Project 1 - Package'
    }
    catch (e) {
        success = false
        echo "Error during the compile -> test -> package : ${e}"
        echo 'The release will be rollbacked'
    }

    if (success) {
        mavenSetVersion(NEXT_VERSION)
        commitAndPush("Next Version ${NEXT_VERSION}")
    } else {
        mavenSetVersion(currentVersion)
        commitAndPush("Rollback to ${currentVersion}")
        error 'Error during the compile -> test -> package'
    }
}


def commitAndPush(message) {
    sh "git commit --allow-empty -am \"${message}\""
    sh 'git push'
}

def mavenSetVersion(newVersion) {
    def mvnHome = tool 'Maven 3.2.2'
    sh "${mvnHome}/bin/mvn versions:set -DnewVersion=${newVersion}  -DgenerateBackupPoms=false"
}

La commande node permet d’allouer un executor et un workspace sur un noeud Jenkins. L’extraction de la version courante à partir du POM se fait au travers un parsing XML Groovy du résultat de la commande readFile (ligne 10 et 11). On retrouve ensuite les snippet élaborés dans les steps ci-dessus. Pour le besoin de ce job j’ai créé deux fonctions commitAndPush et mavenSetVersion.

Résultat

L’execution du job donne le resultat suivant en sortie de console.

Le menu Running Steps permet de voir les steps executés.

JobDsl

Le clique sur l’icone de la console associé à un step affiche les logs du step correspondant. Seul petit bémol, la sortie d’un step de type build se résume à l’affichage de Starting building project: Project 1 - Compile et non pas à la sortie du job sous jacent.

Conclusion

Il m’a demandé de revoir la conception classique que j’avais des pipelines Jenkins à base de relations upstream/downstream non bloquantes. Le code est relativement concis et surtout localisé et auto suffisant pour comprendre l’entièreté du workflow sans avoir à naviguer dans les relations downstreams ou à utiliser des plugins pour mettre en oeuvre des conditions.

Le manque de documentation rend la conception fastidieuse. C’est encore un plugin jeune et la compatibilité avec les plugins existants n’est pas automatique mais va en s’améliorant

For architectural reasons, plugins providing various extensions of interest to builds cannot be made automatically compatible with Workflow. Typically they require use of some newer APIs, large or small.

La visualisation de l’ensemble pourrait être travaillée. La vue Running Steps pourrait compléter une vue de plus haut niveau où l’utilisateur aurait la capacité de définir des steps de haut niveau à l’image du pipeline prepare release -> compilation -> test -> package -> (next iteration | rollback) et du plugin Build Pipeline.

Dans la partie 3, je reprendrai la partie 1 et la partie 2 suivant ce nouveau paradigme pour générer le pipeline nominal compilation -> test -> package et celui de release prepare release -> compilation -> test -> package -> (next iteration | rollback)

Published 14 May 2015
blog comments powered by Disqus