DevOps, CI/CD et conteneurs
Jenkins en pratique : rapports de tests, quality gates, artefacts et images
Intermédiaire9 min de lectureMis à jour
Cette page a été traduite de l'anglais avec l'aide de l'IA, puis vérifiée par rapport à l'original. Lire la version anglaise
Un pipeline qui se contente de compiler ne vous apprend presque rien. Ce guide ajoute ce qui rend un build digne de confiance : rapports JUnit et couverture de code, analyse statique, quality gate SonarQube capable d'arrêter le build, artefacts avec empreinte, release Maven vers un gestionnaire de dépôts, images de conteneur construites et poussées avec des tags corrects, le tout branché sur un job multibranch.
Ce que vous allez apprendre
- Publier les résultats JUnit, la couverture de code et les résultats d'analyse statique pour que les échecs soient lisibles
- Intégrer SonarQube à un pipeline et bloquer la promotion sur la quality gate
- Archiver les artefacts avec leur empreinte et déployer les releases dans un gestionnaire de dépôts
- Construire, taguer, scanner et pousser des images Docker depuis un pipeline sans exposer les credentials du registre
- Configurer un pipeline multibranch pour que branches et pull requests soient buildées automatiquement
Faire compiler le code par un pipeline représente les dix premiers pour cent du travail. Le reste consiste à transformer un build en source de preuves : quels tests ont tourné et lesquels ont échoué, si la couverture a bougé, si le nouveau code a introduit des défauts, quel binaire exactement est sorti et où il est allé. Sans ces preuves, un build vert signifie seulement que le compilateur était content, et les équipes finissent par se rabattre sur une checklist manuelle.
Ce guide reprend là où s'arrête le guide sur la syntaxe du Jenkinsfile. Chaque exemple est un fragment à coller dans un pipeline déclaratif, et l'exemple fil rouge est un service Maven qui finit dans un gestionnaire de dépôts et dans un registre de conteneurs.
Résultats de tests : rendre les échecs lisibles
Le step junit lit n'importe quel XML au format JUnit, que presque tous les frameworks de test de tous les langages savent produire, et en tire le graphe de tendance des tests, l'historique de chaque test et le résumé des échecs sur la page du build.
stage('Test') {
steps {
sh 'mvn -B -ntp verify'
}
post {
always {
junit testResults: '**/target/surefire-reports/*.xml, **/target/failsafe-reports/*.xml',
skipPublishingChecks: false,
keepLongStdio: true
}
}
}
Trois détails comptent. Placez le step dans post { always } et non dans steps, sinon une suite de tests en échec interrompt le stage avant la collecte du rapport, et vous obtenez un build rouge sans aucune raison visible. Incluez à la fois les motifs Surefire (tests unitaires) et Failsafe (tests d'intégration). Enfin, laissez allowEmptyResults à sa valeur par défaut, false, sur les projets qui ont forcément des tests : un motif qui ne trouve rien sans rien signaler, c'est exactement ainsi qu'un pipeline finit par annoncer un succès pour un build dont les tests n'ont jamais tourné.
Un rapport publié change la façon de traiter un échec : au lieu de faire défiler un log de console, le développeur ouvre le build, lit « 3 échecs, dont 1 nouveau » et va directement à la stack trace et à l'historique du test concerné. Le Test Results Analyzer et la détection des tests instables des versions récentes du plugin s'appuient sur les mêmes données, ce qui permet de repérer les tests qui échouent de façon intermittente plutôt que systématique.
Couverture et analyse statique
La couverture est produite par l'outil de build et publiée par Jenkins. Avec Maven et JaCoCo, branchez l'agent et le goal de rapport :
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<execution><id>prepare</id><goals><goal>prepare-agent</goal></goals></execution>
<execution><id>report</id><phase>verify</phase><goals><goal>report</goal></goals></execution>
</executions>
</plugin>
Publiez ensuite avec le plugin Coverage, qui remplace l'ancien plugin JaCoCo et comprend les formats Cobertura, JaCoCo et de type JUnit grâce à un parser commun :
recordCoverage(
tools: [[parser: 'JACOCO', pattern: '**/target/site/jacoco/jacoco.xml']],
sourceCodeRetention: 'EVERY_BUILD',
qualityGates: [
[threshold: 70.0, metric: 'LINE', baseline: 'PROJECT', criticality: 'UNSTABLE'],
[threshold: 80.0, metric: 'LINE', baseline: 'MODIFIED_LINES', criticality: 'FAILURE']
]
)
La seconde gate est la plus intéressante : elle exige 80 % de couverture sur les lignes touchées par la modification, tout en tolérant la moyenne actuelle du projet, quelle qu'elle soit. C'est la seule règle de couverture qui fonctionne sur une base de code existante, parce qu'elle demande une amélioration à la marge plutôt qu'une réécriture rétroactive.
Warnings du compilateur, Checkstyle, PMD, SpotBugs et des dizaines d'autres analyseurs sont regroupés par le plugin Warnings Next Generation dans une vue unique, avec détection des nouveaux problèmes par rapport à un build de référence :
recordIssues(
tools: [java(), checkStyle(pattern: '**/target/checkstyle-result.xml'),
spotBugs(pattern: '**/target/spotbugsXml.xml')],
qualityGates: [[threshold: 1, type: 'NEW', criticality: 'UNSTABLE']],
referenceJob: 'shop-api/main'
)
C'est en plaçant la gate sur les nouveaux problèmes plutôt que sur leur total que l'analyse statique devient adoptable dans une base de code qui traîne déjà 4 000 warnings.
La quality gate SonarQube
SonarQube agrège problèmes, couverture, duplication et security hotspots, et applique une quality gate configurable. Une intégration correcte repose sur deux éléments du pipeline : lancer l'analyse, puis attendre le verdict.
Configurez le serveur une fois pour toutes dans Manage Jenkins → System (nom, URL et token d'authentification stocké comme credential de type secret text). Ensuite, dans le pipeline :
stage('Analyse') {
steps {
withSonarQubeEnv('sonarqube-main') {
sh '''mvn -B -ntp sonar:sonar \
-Dsonar.projectKey=shop-api \
-Dsonar.qualitygate.wait=false'''
}
}
}
stage('Quality gate') {
steps {
timeout(time: 10, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
withSonarQubeEnv injecte l'URL du serveur et le token, si bien qu'aucun credential n'apparaît dans la commande. L'analyse est asynchrone : le scanner envoie les données et rend la main, et SonarQube calcule le résultat ensuite. waitForQualityGate bloque jusqu'à ce que le serveur rappelle Jenkins, ce qui suppose un webhook configuré dans SonarQube vers https://jenkins.example.com/sonarqube-webhook/. Sans ce webhook, le step attend jusqu'au timeout et tout le monde en conclut que l'intégration est cassée.
Séparer l'analyse et la gate en deux stages permet de faire tourner d'autres tâches entre les deux, et placer le stage de la gate sur agent none libère l'executor pendant l'attente.
Configurez la gate elle-même sur la définition du nouveau code : aucun nouveau problème bloquant ou critique, couverture du nouveau code au-dessus d'un seuil, duplication du nouveau code en dessous d'un autre, aucun nouveau security hotspot laissé sans revue. Une gate définie ainsi peut être imposée sur main dès le premier jour, sans chantier de nettoyage préalable.
Artefacts : archive, stash, fingerprint
Trois mécanismes font circuler des fichiers, et ils ne sont pas interchangeables.
stash et unstash transmettent des fichiers d'un stage à l'autre au sein d'un même build. Ils sont stockés sur le controller : gardez-les donc légers, un jar et quelques manifestes, pas un workspace entier.
stage('Build') { steps { sh 'mvn -B -ntp package -DskipTests'; stash name: 'app', includes: 'target/*.jar' } }
stage('Deploy') { agent { label 'deploy' }; steps { unstash 'app'; sh './deploy.sh' } }
archiveArtifacts rattache des fichiers au build de façon permanente, téléchargeables depuis la page du build et via l'API REST. Ajoutez fingerprint: true pour que Jenkins enregistre la somme de contrôle du fichier et puisse vous dire plus tard quel build a produit un binaire donné et quels builds l'ont consommé.
archiveArtifacts artifacts: 'target/*.jar, target/classes/META-INF/build-info.properties',
fingerprint: true,
onlyIfSuccessful: true
La place des releases, c'est un gestionnaire de dépôts : Nexus, Artifactory, un registre de paquets cloud. Les archives Jenkins servent au débogage et à la conservation à court terme ; le gestionnaire de dépôts est le système de référence, avec politiques de rétention, immuabilité et contrôle d'accès.
Livrer une release vers un gestionnaire de dépôts
Donnez au pipeline un settings.xml dont les credentials serveur proviennent de variables d'environnement, afin que le fichier lui-même ne contienne aucun secret :
<settings>
<servers>
<server>
<id>nexus-releases</id>
<username>${env.NEXUS_USR}</username>
<password>${env.NEXUS_PSW}</password>
</server>
</servers>
</settings>
Liez ensuite le credential et déployez. Évitez le classique maven-release-plugin en CI : il crée deux commits, un tag et deux builds complets, et échoue à mi-chemin assez souvent pour laisser un dépôt dans un état bizarre. L'alternative adaptée à la CI fixe la version depuis l'extérieur :
environment { NEXUS = credentials('nexus-deploy') }
stage('Release') {
when { buildingTag() }
steps {
sh '''
VERSION="${TAG_NAME#v}"
mvn -B -ntp -s ci/settings.xml \
-Drevision="$VERSION" \
-DskipTests \
deploy
'''
}
}
Avec <version>${revision}</version> dans le POM, le pipeline fournit la version, le tag la définit, et un seul build produit une seule release immuable. Les snapshots issus de main partent vers le dépôt de snapshots ; seuls les builds de tags atteignent le dépôt de releases, que la plupart des gestionnaires de dépôts savent configurer pour refuser le redéploiement d'une version existante.
Construire et pousser des images de conteneur
Le plugin Docker Pipeline enveloppe le daemon dans des steps pratiques et gère l'authentification au registre à partir du magasin de credentials :
stage('Image') {
agent { label 'docker' }
environment {
REGISTRY = 'registry.example.com'
IMAGE = "${REGISTRY}/acme/shop-api"
TAG = "${env.GIT_COMMIT.take(12)}"
}
steps {
unstash 'app'
script {
def image = docker.build("${IMAGE}:${TAG}", "--build-arg JAR_FILE=target/app.jar .")
docker.withRegistry("https://${REGISTRY}", 'registry-credentials') {
image.push()
if (env.BRANCH_NAME == 'main') { image.push('latest') }
}
}
}
}
Pour des images multi-architectures, ou pour construire sans daemon Docker sur l'agent, utilisez directement buildx :
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag "$IMAGE:$TAG" \
--label "org.opencontainers.image.revision=$GIT_COMMIT" \
--label "org.opencontainers.image.source=https://git.example.com/acme/shop-api" \
--provenance=true \
--push .
Ces labels OCI valent la peine d'être ajoutés : ils permettent à quiconque inspecte une image en cours d'exécution de retrouver le commit qui l'a produite. Le guide Docker : les fondamentaux couvre le versant Dockerfile, notamment les builds multi-stage qui gardent l'image poussée légère.
Ajoutez un scan avant le push, et faites échouer le build sur les vulnérabilités sérieuses :
sh 'trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed "$IMAGE:$TAG"'
--ignore-unfixed garde la gate actionnable : elle n'échoue que sur les vulnérabilités pour lesquelles une version corrigée existe, c'est-à-dire sur ce que l'équipe peut réellement corriger dès aujourd'hui.
Pipelines multibranch et pull requests
Un pipeline multibranch analyse un dépôt, crée un job pour chaque branche contenant un Jenkinsfile, le builde à chaque push et supprime le job quand la branche est supprimée. Sa variante organization folder fait de même pour chaque dépôt d'une organisation Git. C'est ce qui remplace l'habitude de copier un job et d'en modifier le nom de branche.
Pointez-le sur le dépôt, donnez-lui des credentials, puis configurez :
- Discover branches : en général « exclure les branches qui font aussi l'objet d'une PR », pour qu'une pull request ne soit pas buildée deux fois.
- Discover pull requests from origin and forks : la stratégie utile consiste à fusionner la PR avec la branche cible, car elle teste ce qui se trouverait sur
mainaprès le merge, et non ce qui est sur la branche à l'instant. - Orphaned item strategy : supprimer les jobs quelques jours après la disparition de la branche.
- Scan triggers : un scan périodique comme filet de sécurité, le vrai travail étant fait par un webhook.
Dans le Jenkinsfile, env.BRANCH_NAME, env.CHANGE_ID, env.CHANGE_TARGET et env.TAG_NAME permettent à un seul fichier de servir tous les contextes :
stage('Publish snapshot') {
when { allOf { branch 'main'; not { changeRequest() } } }
steps { sh 'mvn -B -ntp -s ci/settings.xml deploy -DskipTests' }
}
Les builds de pull request doivent être rapides et exhaustifs sur la correction ; les builds de branche sur main ajoutent le travail plus lent : suites d'intégration complètes, construction et push de l'image, déploiement sur un environnement partagé. Gardez la boucle de retour des pull requests sous les dix minutes, sinon les gens cessent de l'attendre.
Assembler le tout
Un pipeline réaliste pour un service ressemble, dans les grandes lignes, à ceci : build et tests unitaires sur un agent conteneurisé ; publication des résultats et de la couverture dans post ; analyse statique et Sonar en parallèle des tests d'intégration ; attente de la quality gate ; construction et scan de l'image ; push avec un tag immuable ; déploiement automatique en staging ; déploiement en production derrière une validation input ou depuis un build de tag.
Chacune de ces étapes produit des preuves rattachées au build : tendances des tests, delta de couverture, verdict Sonar, artefacts avec empreinte, digest de l'image. Quand quelqu'un demande « qu'est-ce qui tourne en production, et d'où ça vient ? », la réponse est un lien et non une enquête.
Erreurs courantes
- Rapports collectés dans
stepsplutôt que danspost. Un stage en échec les saute, et le build affiche un échec sans le moindre détail. - Gates de couverture sur tout le projet. Elles sont intenables sur du code legacy et finissent désactivées. Mettez la gate sur les lignes nouvelles ou modifiées.
waitForQualityGatesans le webhook SonarQube. Le step bloque jusqu'au timeout et l'équipe accuse Jenkins.- Tags Docker mobiles. Déployer
:latestrend l'image qui tourne en production impossible à identifier. Poussez un tag immuable et déployez celui-là. - Credentials interpolés dans des chaînes Groovy. Utilisez des chaînes shell entre apostrophes simples, pour que ce soit le shell qui développe la variable et que la valeur n'entre jamais dans la commande assemblée par Jenkins.
- Archivage de workspaces entiers. Des builds de plusieurs gigaoctets remplissent le disque du controller. Archivez l'artefact et les rapports, rien d'autre.
- Release depuis un build de branche. Deux builds de la même branche peuvent produire deux binaires différents portant la même version. Livrez depuis les tags, et déployez l'artefact que vous avez déjà construit.
Points clés à retenir
- Publiez les résultats
junitdans un blocpost { always }, et faites d'un motif de rapport vide un échec plutôt qu'un succès silencieux. - Enregistrez la couverture et les résultats d'analyse statique avec des gates sur le code nouveau ou modifié : c'est la seule formulation qui fonctionne sur une base de code existante.
- Branchez SonarQube avec
withSonarQubeEnvet un stagewaitForQualityGateséparé, et configurez le webhook qui permet à l'attente de se terminer. - Utilisez
stashau sein d'un build,archiveArtifacts ... fingerprint: truepour la traçabilité, et un gestionnaire de dépôts comme système de référence des releases. - Fournissez la version depuis le pipeline au lieu de laisser un plugin de release réécrire le POM, et ne déployez les releases que depuis des builds de tags.
- Construisez les images sur un agent Docker dédié ou avec un builder sans daemon, taguez-les par commit, ajoutez les labels OCI, scannez avec
--ignore-unfixedet poussez avant de déployer. - Un pipeline multibranch donne automatiquement un build à chaque branche et à chaque pull request, et supprime le job quand la branche disparaît.
Sources et lectures complémentaires
Questions fréquentes
- Pourquoi mon build reste-t-il vert alors que des tests échouent ?
- Presque toujours parce que la commande de build a avalé l'échec : une étape shell qui se termine par un pipe, un appel Maven avec -Dmaven.test.failure.ignore=true, ou un step junit dont le motif ne trouve aucun fichier alors que allowEmptyResults vaut true. Vérifiez que l'outil de test renvoie un code de sortie non nul et que le step junit remonte le nombre de tests attendu. Un motif de rapport qui ne trouve rien doit faire échouer le build, pas le laisser passer.
- La quality gate doit-elle casser le build ?
- Sur la branche principale, oui, à condition que la gate porte sur le nouveau code et non sur l'ensemble de la base de code. Une gate sur la couverture globale d'un projet legacy échoue indéfiniment et finit désactivée en moins d'un mois. Une gate sur le nouveau code (couverture des lignes ajoutées, aucun nouveau problème bloquant, aucun nouveau bloc dupliqué) est tenable dès le premier jour et empêche la base de code de se dégrader.
- Que fait concrètement le fingerprinting d'un artefact ?
- Jenkins enregistre le MD5 du fichier, et retient quel build l'a produit et quels builds l'ont ensuite utilisé. Face à un jar trouvé sur un serveur, vous pouvez demander à Jenkins quel build l'a fabriqué, et depuis un build vous voyez partout où sa sortie est passée. L'activer sur archiveArtifacts ne coûte rien, et c'est la forme de traçabilité la moins chère qu'un pipeline interne puisse avoir.
- Comment taguer les images de conteneur depuis la CI ?
- Taguez chaque image avec une valeur immuable et traçable, le hash court du commit ou la version sémantique du build, et poussez ce tag. N'ajoutez un tag mobile, comme le nom de la branche ou latest, que pour le confort des humains. Les déploiements doivent référencer le tag immuable ou, mieux, le digest, afin que redéployer la même référence donne toujours exactement les mêmes octets.