Intégration continue: la branche main
Date de l'article:01-07-2026
Grafana (K6) Docker Hub CI/CD SonarCloud JaCoCo Gitlab
Place à la stabilité pour la branche de production dont l'objectif change de paradigme. Découvrez comment configurer ce pipeline final pour garantir la livraison d'une version validée et publiable en toute confiance.
Mise en place du pipeline de production
Contrairement aux branches develop et staging, l'heure n'est plus à la découverte de bogues, mais à la validation absolue.
Les tests unitaires et d'intégration ayant déjà été validés en amont, ce pipeline de production se concentre sur l'étanchéité de la sécurité, la conformité de la qualité et la livraison finale.
Dans cette branche, nous verrons:
Une 3ème manière d'exécuter les tests, combinant H2 pour l'unitaire et PostgreSQL pour l'intégration.
Nous adopterons un "double rempart Trivy" incluant le scan de sécurité et l'analyse de l'image Docker.
Nous ferons la publication des rapports de tests sur SonarCloud (Free Plan) et nous verrons la mise à jour de la version de Sonar (v5) et ce qu'elle à apporter.
Pour valider l'image, nous exécuterons un Smoke-test en conteneur, et nous terminerons par la livraison de l'image finale sur Docker Hub, déclenchée uniquement par un push tag.
Voici les jobs de la branche [main]:
Analyse et feuille de route
Le workflow Github ne contient qu'un seul job
name: name: Prod Build..
job:
├─ Tests et génération du rapport JaCoCo
├─ Analyse CodeQL
├─ Scan Trivy (système de fichiers)
├─ Calcul d'un identifiant court du commit
├─ Construction de l'image Docker
├─ Scan de l'image Docker
├─ Publication du rapport SonarCloud
├─ Smoke tests du conteneur
│ ├─ Vérification de /actuator/health
│ └─ Vérification de /api/categories
└─ Publication de l'image sur Docker Hub
Définition des stages suivant les tests:
Stage test
----------------------------------
test spotless:check
clean verify
jacoco
quality sonarQube seuil min. 80% de couverture
security Trivy filesystem exit-code 1
security-image Trivy Docker Image exit-code 1
smoke smoke-test
publish Docker Hub sur push tag
Plan du pipeline GitLab
main
├── prod-build
├── prod-trivy-fs
├── prod-sonarqube
├── prod-docker-build
├── prod-trivy-image
├── prod-smoke
└── prod-publish
Nous le diviserons en étapes, contenant 7 jobs clairs et ordonnés.
Adaptation des stages
On ajoute les stages pour cette branche:
quality : sera utilisé pour la publication sur SonarCloud
smoke : pour le smoke-test
publish : pour la publication de l'image dans la registry Docker Hub
À ce stade, le fichier "
.gitlab-ci" ressemble à:
stages:
- test
- security
- quality
- docker
- security-image
- functional
- performance
- smoke
- publish
L'ordre des stages sera donc:
test: englobe Spotless, les tests (ut/it), CheckStyle et Spotbug
security: englobe les jobs tels que Trivy(FS), Semgrep
quality: Scan et publication dans SonarCloud
docker: pour les job exécutant un docker build de l'image
security-image: sera utilisé pour le scan de l'image Docker (Trivy)
smoke: sera utilisé pour le smoke-test
publish: sera utilisé pour la publication de l'image sur Docker Hub
* Leur ordre doit être déclaré dans l'ordre chronologique exact dans lequel on souhaite que les tâches s'exécutent.
[main] Build des tests
3 branches, 3 manières différentes d'exécuter les tests
Au fil du cycle de vie de notre code, notre stratégie de validation a évolué pour s'adapter aux objectifs de chaque environnement. La branche main marque la dernière étape de cette transition. Pour sécuriser cette étape sans multiplier les runtimes, l'exécution des tests reproduit le comportement nominal d'un poste de développement
- Les tests Unitaires (UT): Lancés sur une base temporaire H2 pour garantir une exécution rapide des validations de logique métier
- Les tests d'Intégration (IT): Connectés directement à une instance PostgreSQL dédiée afin de valider les requêtes, les transactions et les schémas de base de données réels avant la conteneurisation.
Cette consolidation en un seul job, permet d'optimiser le temps d'occupation des agents (GitLab Runners) tout en fournissant les rapports (JaCoCo, Spotless, CheckStyle) requis pour alimenter la suite du pipeline.
develop Focus sur la vélocité. Les tests unitaires sont exécutés rapidement en s'appuyant sur une base de données en mémoire H2.
staging Focus sur l'isolation et la conformité. Les tests sont séparés pour approfondir les validations (UT sur H2 et IT sur PostgreSQL).
main Focus sur la certification de production. Tous les tests ayant déjà été validés en amont, l'objectif est ici de rejouer la suite complète dans les conditions strictes de l'environnement cible, le tout centralisé au sein d'un unique job CI/CD.
Pour concrétiser cette stratégie sur la branche main, le job prod-build est configuré dans le stage test. Il orchestre le build de l'application tout en isolant la persistance des tests d'intégration. L'exécution de ce job suit une séquence optimisée pour sécuriser et accélérer le build.
prod-build:
stage: test
services:
- name: postgres:16.14-alpine3.23
alias: postgres
variables:
POSTGRES_DB: flashcardsdb
POSTGRES_USER: postgres
POSTGRES_PASSWORD: $POSTGRES_PASSWORD
DB_HOST: postgres
DB_PORT: "5432"
DB_USER: postgres
DB_PASSWORD: $POSTGRES_PASSWORD
cache:
key: maven
paths:
- .m2/repository
policy: pull-push
image: maven:3.9.11-eclipse-temurin-17
script:
- ./mvnw spotless:check
- sleep 10
- ./mvnw -B clean verify jacoco:report -Pcoverage-main
artifacts:
when: always
paths:
- target/*.jar
- target/classes/
- target/test-classes/
- target/site/jacoco/
- target/failsafe-reports/
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
Résumé des étapes
Le runner instancie le conteneur principal avec l'image Maven. En parallèle, un conteneur de service PostgreSQL est déployé sur le réseau interne du runner, accessible via l'alias postgres grâce aux variables d'environnement injectées.
Gestion du cache (Performance):
Avant l'exécution, le runner télécharge le cache Maven (.m2/repository).
Avec sa politique "pull-push", on garantit que les dépendances existantes ne sont pas re-téléchargées, et que toute nouvelle dépendance résolue sera réinjectée dans le cache global en fin de job.
Séquence de build (Script):
La commande spotless:check valide le formatage du code et peut bloquer le pipeline en cas de non-conformité
Avec sleep 10 on temporise pour s'assurer que le service PostgreSQL a terminé son initialisation et accepte les connexions socket.
clean verify jacoco:report nettoie, compile, exécute la suite de tests et génère le rapport de couverture de code via le profil "-coverage-main" défini dans le pom via <jacoco.minimum.coverage>0.80</jacoco.minimum.coverage>
Persistance des résultats (Artifacts):
Configurée sur when: always, cette directive force l'extraction et le téléversement des fichiers stratégiques vers GitLab, même si les tests échouent.
Sont ainsi préservés: le .jar compilé, les classes compilées (utiles pour les scans ultérieurs), ainsi que les rapports de tests et le livrable JaCoCo.
Bien que les conventions DevOps puristes suggèrent de nommer ce job mvn-test (car il s'exécute dans le stage: test), j'ai choisi de standardiser le premier jalon de chaque branche sous la nomenclature [nom_branche]-build (develop-build, staging-build, prod-build). Pour une application d'expérimentation, cette symétrie offre une lisibilité immédiate de l'architecture du pipeline à travers tout le projet.
Validation de la gestion des variables (Secrets)
Pour sécuriser l'accès à l'instance PostgreSQL durant les tests d'intégration, la variable $POSTGRES_PASSWORD exploite deux fonctionnalités de la plateforme GitLab.
Depuis le projet Settings → CI/CD → Variables
Le Masquage (Masked): L'injection du mot de passe via l'environnement GitLab garantit que la valeur de la variable $POSTGRES_PASSWORD n'apparaît jamais en clair dans le code source ou dans les fichiers de configuration du dépôt. Dans les logs elle apparaitra comme: [MASKED]
La Protection (Protected): En configurant la variable comme Protected, GitLab assure qu'elle ne sera injectée que sur les branches protégées. Un utilisateur ne peut donc pas créer une branche secondaire pour tenter d'écrire un script de dump et afficher ce secret dans les logs.
Résultat des logs
L'analyse des logs Spring Boot permet de vérifier que chaque phase utilise la bonne configuration et la bonne base de données.
→ Avant même de lancer les tests,
Spotless vérifie que le code Java respecte les normes de formatage de l'équipe.
les 34 fichiers sont parfaitement propres et la commande s'achève avec un
BUILD SUCCESS:
[INFO] Spotless.Java is keeping 34 files clean - 0 needs changes to be clean, 34 were already clean, 0 were skipped because caching determined
[INFO] BUILD SUCCESS they were already clean
$ sleep 10
$ ./mvnw -B clean verify jacoco:report -Pcoverage-main
=== Running Maven tests ===
[INFO] Scanning for projects...
..
[INFO] -----------------------< com.example:flashcards >-----------------------
[INFO] Building flashcards 1.1.0
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] Downloading from central:
..
[INFO] Results:
[INFO]
[INFO] Tests run: 59, Failures: 0, Errors: 0, Skipped: 0
[INFO]
..
[INFO] Replacing main artifact /builds/val7304/flashcards/target/flashcards-1.1.0.jar with repackaged archive, adding nested dependencies in BOOT-INF/.
[INFO] The original artifact has been renamed to /builds/val7304/flashcards/target/flashcards-1.1.0.jar.original
[INFO]
[INFO] --- failsafe:3.5.3:integration-test (default) @ flashcards ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
La commande exécute l'ensemble des tests et vérifie la couverture: il utilise le profil "-Pcoverage-main" qui vérifie que le seuil minimal des 80% à bien été atteint, si c'est la cas, il génère un rapport de couverture de code (JaCoCo) pour s'assurer qu'aucune régression n'est introduite sur la branche principale:
les 59 tests passent avec succès
L'extrait: "Replacing main artifact... et failsafe:3.5.3:integration-test" prépare l'artefact au tests: il package l'application puis démarre la phase des tests avec le plugin Failsafe.
→
Les tests Unitaire utilise le profil "test" ainsi qu'une base H2 temporaire. Ils doivent être rapides, isolés et sans effet de bord.
Voici comment identifier l'activation d'une base de données éphémère en mémoire :
INFO 454 --- [flashcards] [main] c.e.f.controller.CategoryControllerTest : The following 1 profile is active: "test"
INFO 454 --- [flashcards] [main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection conn0: url=jdbc:h2:mem:testdb user=SA
INFO 454 --- [flashcards] [main] org.hibernate.orm.connections.pooling : HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database version: 2.3.232
La présence de "jdbc:h2:mem" confirme qu'une base de données H2 volatile stockée en mémoire vive est utilisée.
La mention "testdb user=SA" prouve que le framework a correctement chargé le fichier d'environnement localisé dans "src/test/resources/application-test.properties" (contenant les clés spring.datasource.url et spring.datasource.username)
→ Une fois les tests unitaires validés, le pipeline passe aux
tests d'intégration (gérés par le plugin failsafe).
Ces tests vérifient le comportement de l'application avec un interpréteur de base de données identique à celui de la production (PostgreSQL):
INFO 556 --- [flashcards] [main] c.e.f.i.FlashcardIntegrationIT: The following 1 profile is active: "it"
INFO 556 --- [flashcards] [main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection org.[MASKED]ql.jdbc.PgConnection@4faf32a7
INFO 556 --- [flashcards] [main] org.hibernate.orm.connections.pooling : HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database version: 18.6
Le passage au profil "it" et l'apparition de l'objet "PgConnection" indiquent que l'application communique désormais avec une véritable instance PostgreSQL.
Vous remarquez également que le mot de passe à été remplacé par [MASKED]
Pour exécuter ces tests d'intégration de manière isolée, on utilise GitLab Services, qui instancie un conteneur éphémère (postgres:16.14-alpine3.23) lié au job. Grâce à l'alias, l'application peut interagir avec une vraie base de données PostgreSQL isolée pendant la phase it.
La fin du log illustre l'aboutissement de la stratégie de sécurisation: le moment où la CI confirme que le code de la branche main est sain, documenté, conforme et sauvegardé. il déroule une batterie d'analyses statiques de code avant de valider définitivement le build:
1. L'audit de qualité et de sécurité (Checkstyle & SpotBugs)
[INFO] Starting audit...
[INFO] You have 0 Checkstyle violations.
[INFO]...
[INFO] --- spotbugs:4.8.6.2:spotbugs (spotbugs) @ flashcards ---
[INFO] Fork Value is true
[INFO] BugInstance size is 0
[INFO] Error size is 0
[INFO] No errors/warnings found
Le pipeline s'assure que le code respecte les standards et ne contient pas de failles de sécurité ou de mauvaises pratiques :
Zéro violation Checkstyle: Le style architectural est validé.
Zéro BugInstance SpotBugs: L'analyse statique confirme l'absence de bugs potentiels ou de vulnérabilités connues dans les classes compilées.
2. La validation de la couverture de code (JaCoCo) Le plugin JaCoCo compile les métriques collectées durant les tests.
[INFO] --- jacoco:0.8.12:report (report) @ flashcards ---
[INFO] Loading execution data file /builds/val7304/flashcards/target/jacoco.exec
[INFO] Analyzed bundle 'flashcards' with 6 classes
[INFO] All coverage checks have been met.
[INFO] --- failsafe:3.5.3:verify (default) @ flashcards ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
La mention "All coverage checks have been met" indique que le seuil minimum de couverture de code imposé (80% de couverture) a été honoré sur les 6 classes du projet.
Le verdict final tombe alors avec succès: BUILD SUCCESS
On termine le job avec la sauvegarde du cache et des artefacts
Saving cache for successful job
Creating cache 0_pom_-...-4-protected...
.m2/repository/: found 4453 matching artifact files and directories
Uploading artifacts for successful job
target/*.jar: found 1 matching artifact files and directories
Uploading artifacts as "archive" to coordinator... 201 Created
Job succeeded
Le cache (protected) contenant les fichiers de dépendances du dossier .m2
Les artefacts contenant le jar les classes etc
Avoir la branche protégée permet de créer un cache sécurisé qui servira de base ultra-rapide pour les prochains builds
[main] Scan du FileSystem avec Trivy
Ce job intervient après la phase de test. La sécurité est ainsi traitée comme une étape de validation à part entière avant tout déploiement
prod-trivy-fs:
stage: security
needs:
- prod-build
artifact: true
image:
name: aquasec/trivy:0.72.0
entrypoint: [""]
cache:
- key: maven
paths:
- .m2/repository
policy: pull
- key: trivy-db
paths:
- .trivycache
policy: pull-push
variables:
TRIVY_CACHE_DIR: ".trivycache"
script:
trivy fs . \
--severity HIGH,CRITICAL \
--format table \
--offline-scan \
--scanners vuln \
--exit-code 1 \
--no-progress
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
Résumé des étapes
Avec "needs:" On indique que ce job commencera dès que le job "prod-build" est terminé avec succès, sans attendre que l'intégralité du stage précédent soit finalisée.
Scan du File System
La ligne "entrypoint: [""]" surcharge le point d'entrée par défaut de l'image Docker pour permettre à GitLab d'exécuter ses propres scripts Shell à l'intérieur du conteneur. Le scanner analyse ensuite le système de fichiers, c'est-à-dire l'intégralité du code source du projet situé dans le répertoire courant
Optimisations de performance (cache et mode offline)
En utilisant "--offline-scan" et "trivy-db" Trivy n'a pas besoin de télécharger la base de données des vulnérabilités à chaque exécution
Avec le "cache: maven", le job récupère le dossier ".m2/repository" en mode lecture seule.
Avec "--severity HIGH,CRITICAL" et "--exit-code 1", le scan filtre les résultats pour ne se concentrer que sur les menaces majeures (les vulnérabilités hautes et critiques)
Si Trivy trouve une seule faille, la commande renvoie un code d'erreur (1), ce qui fait échouer le job et bloque immédiatement le pipeline.
L'utilisation du paramètre "exit-code 1" combiné au filtrage des sévérités "HIGH,CRITICAL" incarne parfaitement la philosophie
Fail-Fast:
"Le pipeline fait office de douane stricte plutôt que de découvrir une faille de sécurité en production"
[main] Analyse et publication de la qualité du code avec SonarCloud
Ce troisième job du pipeline introduit une étape cruciale de gouvernance et de qualité logicielle. Placé après la validation fonctionnelle (test) et souvent en parallèle ou juste après la sécurité, ce stage est utilisé pour évaluer la dette technique du code en détectant des problèmes de maintenabilité, des bugs potentiels et des vulnérabilités. Il mesure la couverture du code par les tests lorsque le rapport de couverture est disponible.
L'intégration dans le pipeline GitLab CI permet d'exécuter cette analyse dans le cadre de la CI/CD et de transmettre les résultats à SonarCloud. Le contrôle du Quality Gate permet ensuite de déterminer si les conditions de qualité configurées dans le projet sont respectées.
prod-sonarqube:
variables:
GIT_DEPTH: "0"
stage: quality
image: maven:3.10.0-eclipse-temurin-21
needs:
- job: prod-build
artifacts: true
script:
- chmod +x mvnw
- ./mvnw -B sonar:sonar \
-Dsonar.host.url="$SONAR_HOST_URL" \
-Dsonar.login="$SONAR_TOKEN" \
-Dsonar.qualitygate.wait=true
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
Les scanners récents de SonarCloud exigent souvent une version de Java plus récente pour tourner, même si le projet lui-même est codé dans une version antérieure. Ce job utilise donc Java 21, alors que le build initial utilise Java 17.
→ La publication Sonar se fait à chaque "push" sur la branche main, elle nous permet de suivre son analyse et de vérifier que tout se passe sans issues éventuelles
En raison de l'utilisation du Plan Free de SonarCloud, ce job ne s'exécute que sur la branche main
Résumé des étapes
Le job télécharge les résultats de "prod-build" (le rapport de couverture de code JaCoCo et les fichiers .class compilés) afin de le transmettre à SonarCloud
Par défaut, GitLab CI ne télécharge que les derniers commits (shallow clone). En forçant la profondeur avec GIT_DEPTH: "0", on demande un clone complet de l'historique Git.
Le plugin Maven de SonarCloud est utilisé pour envoyer les rapports vers le serveur d'analyse
La variable SONAR_HOST_URL doit être définie en "Visible" et non protégée.
La variable SONAR_TOKEN doit être définie en "Masked" et être protégée.
Le contrôle qualité strict (Quality Gate) et la "commande maîtresse" pour sécuriser la production
Le paramètre "-Dsonar.qualitygate.wait=true" demande au scanner d'attendre le résultat du Quality Gate. Si celui-ci échoue, le job échoue également.
Si le code ne respecte pas les critères définis (moins de 80% de couverture de code, présence de "code smells" ou de failles), SonarCloud renvoie un statut d'échec (visibles depuis la MR), bloquant instantanément le pipeline.
Une partie de la configuration du projet SonarCloud est définie dans le fichier
pom.xml:
<sonar.projectKey>val7304_flashcards</sonar.projectKey>
<sonar.organization>val7304</sonar.organization>
<sonar.projectName>flashcards</sonar.projectName>
Nous n'avons alors besoin que des informations de connexion dans la CI
Résultat des logs
Ce log montre l'exécution de SonarScanner (SonarQube/SonarCloud) intégré au pipeline GitLab CI. C'est une étape cruciale pour auditer la qualité et la sécurité du code avant ou après sa fusion sur main. Voici les 5 éléments importants à retenir de ce log:
1. La détection et liaison automatique
[INFO] --------------------< com.example:flashcards >--------------------
[INFO] Building flashcards 1.1.0
[INFO] from pom.xml
[INFO] -----------------------------[ jar ]------------------------------
[INFO] Starting SonarScanner Engine...
[INFO] Java 21.0.12.1 Eclipse Adoptium (64-bit)
[INFO] Load global settings
[INFO] Found an active CI vendor: 'Gitlab CI'
[INFO] Detected project binding: BOUND
Le scanner extrait du fichier pom.xml les coordonnées du projet (com.example:flashcards) ainsi que sa version exacte (1.1.0). Cela permet à SonarQube de lier l'historique de l'analyse à une version précise de l'application en production.
Le moteur s'exécute avec Java 21. La version du JDK qui fait tourner le scanner doit être parfaitement compatible avec la syntaxe du code source du projet pour éviter les erreurs d'analyse syntaxique.
SonarScanner détecte nativement la plateforme ou il tourne (GitLab-Github).
Le statut BOUND confirme que le projet est correctement lié.
Cela permet à SonarQube de renvoyer les résultats de l'analyse dans l'interface des "Merge Requests"
2. Le ciblage de la branche principale (main)
[INFO] Auto-configuring branch main
[INFO] Branch name: main, type: long
Le scanner identifie la branche sur laquelle il effectue son analyse.
Et c'est sur cette branche qu'il établira la "référence" de la qualité du code.
3. L'exclusion stratégique des fichiers pour la couverture
[INFO] Excluded sources for coverage: **/FlashcardsApplication.java,
**/*Configuration.java, **/*Config.java, **/dto/**, **/entity/**
Ces exclusions sont définies dans le fichier pom.xml au niveau du plugin jacoco
Le pipeline exclut du calcul de couverture de code les fichiers de configuration, les DTOs et les entités, qui sont des fichiers purement déclaratifs sans logique métier.
Les inclure fausserait artificiellement à la baisse du score de couverture globale.
4. L'importation du rapport JaCoCo
[INFO] Sensor JavaSensor [java] (done) | time=6723ms
[INFO] Sensor JaCoCo XML Report Importer [jacoco]
[INFO] Importing 1 report(s).
SonarQube s'appuie sur le rapport généré en amont par JaCoCo (lors du lancement de la commande "verify")
Le log confirme que Sonar a trouvé et importé ce rapport avec succès.
5. L'audit de sécurité avancé (Taint Analysis)
[INFO] Sensor JavaSecuritySensor [security]
[INFO] 29 taint analysis rules enabled.
[INFO] Analyzing 82 UCFGs to detect vulnerabilities.
Sonar exécute un capteur de sécurité pour chercher des vulnérabilités critiques (comme les injections SQL, failles XSS, etc.) via une
taint analysis (analyse de la propagation des données non fiables): qui est
un bouclier de sécurité indispensable avant de pousser le code vers la production.
Le concept de la "Quality Gate" (Porte de Qualité)
SonarCloud a besoin de l'historique complet pour attribuer correctement la paternité des lignes de code (le "Git Blame") et déterminer précisément quelles nouvelles lignes de code ont introduit des bugs ou des vulnérabilités.
Intégrer SonarCloud avec l'option "sonar.qualitygate.wait=true" transforme donc un simple outil de mesure en un véritable douanier automatisé
C'est l'outil ultime pour stopper la prolifération de la dette technique AVANT qu'elle n'atteigne les utilisateurs finaux
Overview de l'application dans SonarCloud

Quality Gates de l'application

Mise à jour de Sonar
UPDATE En août 2026, une mise à jour majeure de la pile technique de qualité a été opérée, impactant directement le comportement de la CI/CD sur main. Voici les enseignements et corrections appliquées:
Montée de version des composants clés
sonar.plugin.version: 4.0.0.4121 ➔ 5.5.0.6356
Lombok (1.18.30 ➔ 1.18.36)
JaCoCo (0.8.11 ➔ 0.8.12)
La montée de version a nécessité l'ajout d'un fichier lombok.config à la racine du projet avec la configuration suivante:
lombok.addLombokGeneratedAnnotation = true
Cette ligne force JaCoCo à ignorer le code généré par lombock et
évite de polluer les rapports avec du code non testé mais sans logique métier.
Résolution du Warning
Suite au passage à la version 5 du scanner, une alerte est apparue dans les logs :
[WARNING] Unresolved imports/types have been detected during analysis.
Enable DEBUG mode to see them.
:→ Pendant l'analyse, le scanner n'a pas réussi à résoudre certains types Java.
Le scanner ne parvenant plus à localiser les classes compilées du projet,
Les causes les plus fréquentes sont :
- Les classes compiléeset/ou les classes de test ne sont pas trouvées
- Certaines dépendances ne sont pas disponibles
Pour résoudre ce problème, deux actions conjointes ont été nécessaires:
Ajouter des propriétés pour indiquer explicitement au scanner où trouver les binaires:
<sonar.java.binaries>${project.build.outputDirectory}</sonar.java.binaries>
<sonar.java.test.binaries>${project.build.testOutputDirectory}</sonar.java.test.binaries>
Modification de la section "artifacts" du job de build afin de persister et transmettre les dossiers compilés:
- target/classes
- target/test-classes/
→ De cette manière, le job "prod-build" va explicitement fournir les artefacts de ces classes et les transmettre au job courant de manière à ce que l'analyse les trouvent.
Grâce à cette transmission explicite des binaires d'un job à l'autre et à l'exclusion du code Lombok, le projet a retrouvé une stricte parité des résultats d'analyse entre GitLab et GitHub.
Le passage de la version 4 à la version 5 de SonarScanner introduit des changements structurels majeurs en arrière-plan. Au-delà du problème d'importation des binaires Java que j'ai corrigé, voici les trois grandes différences fondamentales introduites par la version 5:
1. Le nouveau mécanisme de "Bootstrapping" automatique: Sonar a découplé son moteur d'analyse. On continue de compiler l'application avec notre JDK (ici Java 17/21), mais le scanner est capable de provisionner ou d'isoler son propre runtime d'analyse de manière transparente. À l'avenir, si l'éditeur pousse son moteur vers Java 25, cela n'impactera plus le pipeline.
2. Le mode "Server-side Caching" (Exclusion intelligente) Lors de l'analyse d'une Merge Request, Sonar identifie désormais de manière chirurgicale les fichiers qui n'ont pas subi de modifications. Sur ces fichiers inchangés, il va sauter la quasi-totalité des règles locales pour ne rejouer que les règles globales (inter-fichiers). Le gain de temps sur les gros projets est massif.
3. Durcissement de l'analyse de propagation de sécurité (Taint Analysis) La version 5 intègre nativement des moteurs d'analyse de sécurité beaucoup plus profonds.
[main] Build de l'image Docker
Ce job constitue une étape importante du pipeline CI/CD: il transforme l'application compilée et les contrôles précédents en une image Docker destinée à la suite du pipeline.
prod-docker-build:
stage: docker
needs:
- job: prod-build
artifacts: true
- job: prod-sonarqube
optional: true
- prod-trivy-fs
image: docker:29.6.1
services:
- name: docker:29.6.1-dind
alias: docker
variables:
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_TLS_VERIFY: "1"
DOCKER_CERT_PATH: "/certs/client"
script:
- docker build \
-f "${DOCKERFILE}" \
-t "${IMAGE_NAME}:${IMAGE_TAG}" flashcards prod
- docker save flashcards:prod -o flashcards-prod.tar
artifacts:
paths:
- flashcards-prod.tar
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
Résumé des étapes
Grâce à needs, le job récupère les artefacts de compilation produits par "prod-build" et définit ses dépendances envers les contrôles de qualité et de sécurité. La construction de l'image peut ainsi s'appuyer sur les résultats des jobs précédents.
Le mot-clé "optional: true" permet de déclarer une dépendance facultative envers un job qui peut être absent du pipeline. Sans cela, la dépendance vers un job absent peut empêcher la création du pipeline.
Avec l'utilisation du plan gratuit de Sonar:
Lorsque le pipeline est déclenché par un tag de version, ce job n'est pas créé.
Le job utilise Docker-in-Docker (DinD) pour construire l'image dans l'environnement du Runner. Le service dind fournit le démon Docker auquel le client Docker se connecte. Les variables TLS configurent l'utilisation de TLS pour sécuriser cette communication (certificats, service configuré).
Le script récupère le fichier .jar déjà produit et le Dockerfile se contente de le copier dans une image d'exécution légère (Runtime Java), réduisant ainsi la surface d'attaque et le poids de l'image.
L'image construite est exportée en archive .tar. Et, comme en staging, cette image sera lue, telle quelle par les jobs de déploiement et de scan d'images. Elle n'est construite qu'une seule fois, garantissant l'immuabilité du livrable de production.
L'importance du couplage Artifacts/Docker
Dans un pipeline de production moderne, l'image Docker doit être la plus légère et la plus sécurisée possible.
En associant le mot-clé "needs" à la construction Docker, on évite le piège classique des images "obèses" contenant des outils de build non-nécessaires.
Avec cette pratique, le conteneur généré ne contient que le strict nécessaire pour exécuter l'application, réduisant ainsi drastiquement sa surface d'attaque
[main] Trivy Scan (Docker Image)
Après le scan du code source (prod-trivy-fs), cette étape de sécurité avancée audite directement l'artefact de production.
Ce contrôle garantit qu'aucune image potentiellement infectée n'est poussée sur le registre.
prod-trivy-image:
stage: security-image
needs:
- job: prod-docker-build
artifacts: true
image:
name: aquasec/trivy:0.72.0
entrypoint: [""]
cache:
- key: maven
paths:
- .m2/repository
policy: pull
- key: trivy-db
paths:
- .trivycache
policy: pull-push
variables:
TRIVY_CACHE_DIR: ".trivycache"
script:
trivy image \
--input "flashcards-prod.tar" \
--severity HIGH,CRITICAL \
--exit-code 1 \
--ignore-unfixed \
--ignorefile .trivyignore.yaml \
--timeout 10m \
--cache-dir ~/.cache/trivy \
--format table \
--no-progress
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
Résumé des étapes
Le job exploite le mot-clé "needs" pour récupérer l'archive "flashcards-prod.tar" produite par le job précédent. Trivy inspecte ce fichier en local, assurant que l'image scannée correspond exactement à celle qui sera déployée.
La commande cible les sévérités HIGH,CRITICAL. Si une faille critique corrigible est détectée, la commande retourne un code d'erreur 1, ce qui stoppe immédiatement le pipeline GitLab CI et empêche la mise en ligne.
En production, certaines failles de sécurité n'ont pas encore de correctif éditeur. Bloquer un déploiement pour une faille impossible à corriger paralyserait le déploiement. Le script résout ce problème grâce à deux options:
1. --ignore-unfixed: Ignore les vulnérabilités pour lesquelles les éditeurs n'ont pas encore sorti de correctif (patch).
2. --ignorefile .trivyignore.yaml: Permet de déclarer dans le dépôt des exceptions documentées et validées (faux positifs ou des risques acceptés), évitant ainsi les alertes bloquantes injustifiées.
En ajoutant --timeout 10m, on sécurise le Runner: Si le scan s'interrompt (ou freeze), le job s'arrête après 10 minutes au lieu de consommer indéfiniment les éventuels crédits de calcul CI/CD
Tout comme pour le premier scan, l'utilisation de "trivy-db" via le cache GitLab évite de télécharger à chaque fois la base de données des vulnérabilités, divisant le temps d'exécution du job par 10
Résultat des logs
...Created fresh repository.
... Restoring cache
Downloading artifacts
Executing "step_script" stage of the job script
Using effective pull policy of [always] for container aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969
...
$ chmod +x ci-scripts/trivy-image.sh
$ ./ci-scripts/trivy-image.sh flashcards-prod.tar 1 .trivyignore.yaml
2026-09-03T21:50:33Z INFO [vuln] Vulnerability scanning is enabled
2026-09-03T21:50:33Z INFO [secret] Secret scanning is enabled
2026-09-03T21:50:34Z INFO Detected OS family="debian" version="13.6"
2026-09-03T21:50:34Z INFO [debian] Detecting vulnerabilities... os_version="13" pkg_num=25
2026-09-03T21:50:34Z INFO Number of language-specific files num=1
2026-09-03T21:50:34Z INFO [jar] Detecting vulnerabilities...
2026-09-03T21:50:34Z WARN Using severities from other vendors for some vulnerabilities.
Read https://trivy.dev/docs/v0.74/guide/scanner/vulnerability#severity-selection for details.
Report Summary
┌───────────────────────────────────┬────────┬─────────────────┬─────────┐
│ Target │ Type │ Vulnerabilities │ Secrets │
├───────────────────────────────────┼────────┼─────────────────┼─────────┤
│ flashcards-prod.tar (debian 13.6) │ debian │ 0 │ - │
├───────────────────────────────────┼────────┼─────────────────┼─────────┤
│ app/app.jar │ jar │ 0 │ - │
└───────────────────────────────────┴────────┴─────────────────┴─────────┘
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)
Saving cache for successful job
Creating cache trivy-db-4-protected...
.trivycache/: found 751 matching artifact files and directories
Using presigned URL for cache upload
Uploading cache.zip to https://storage.googleapis.com/gitlab-com-runners-cache/project/40456472/a1/XXXXXXXXXXXXXX
Created cache
Cleaning up project directory and file based variables
Job succeeded
En cas de vulnérabilités trouvées avec Trivy, le fichier
pom.xml peut être modifié:
Si la vulnérabilité concerne l'image Docker, il faudra sans doute mettre à jour la version de l'image distroless via le
Dockerfile, disponible dans
Github.
Retrouvez
l'
historique de Trivy | ainsi qu'un cas traité (avec résolution):
Trivy: Installation failed et vulnérabilité sur l'image Docker
Comparaison du log entre l'utilisation du Runner local et du GitLab Runner (et quota de minutes ok), vue depuis les logs:
Avec le runner local et Docker Desktop, nous avions ce message:
No URL provided, cache will not be uploaded to shared cache server. Cache will be stored only locally.
Created cache
Le runner compresse et stocke les fichiers dans un dossier local de la machine (souvent lié aux volumes). C'est rapide, car il n'y a pas de réseau à traverser.
Le cache n'est disponible que sur la machine qui l'a exécuté.
Si un autre développeur lance le pipeline, il repartira de zéro.
Avec le Runner Cloud de GitLab (SaaS):
Using presigned URL for cache upload
Uploading cache.zip to https://storage.googleapis.com/gitlab-com-runners-cache/project/40456472/a1/XXXXXXXXXXXXXX
Les runners partagés de GitLab sont connectés à un serveur de cache centralisé (un bucket Google Cloud Storage sécurisé). GitLab génère une URL temporaire présignée (une autorisation d'accès).
À la fin du job, le runner envoie le fichier cache.zip vers le cloud de GitLab.
Au prochain job, un autre runner (qui pourrait être une machine totalement différente) téléchargera ce zip pour restaurer l'état.
Conséquence: cela consomme un peu de bande passante réseau à la fin du job, mais le cache est partagé de manière universelle pour tout le projet.
[main] Le Smoke Test de production
Ce test dynamique ne cherche qu'à répondre à une question critique : "L'image finale est-elle capable de démarrer correctement?"
En simulant un micro-déploiement dans le Runner, il élimine définitivement le risque du conteneur qui s'éteint en boucle une fois déployé (CrashLoopBackOff)
prod-smoke:
stage: smoke
needs:
- job: prod-docker-build
artifacts: true
image: docker:29.6.1
services:
- name: docker:29.6.1-dind
alias: docker
variables:
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_TLS_VERIFY: "1"
DOCKER_CERT_PATH: "/certs/client"
script:
- docker load -i flashcards-prod.tar
- docker network create flashcards-net
- |
docker run -d \
--name postgres \
--network flashcards-net \
-e POSTGRES_DB=flashcardsdb \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=$POSTGRES_PASSWORD \
postgres:16.14-alpine3.23
- |
until docker exec postgres pg_isready -U postgres; do
sleep 2
done
- |
docker run -d \
--name flashcards-prod \
--network flashcards-net \
-e SPRING_PROFILES_ACTIVE=prod \
-e SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/flashcardsdb \
-e SPRING_DATASOURCE_USERNAME=postgres \
-e SPRING_DATASOURCE_PASSWORD=$POSTGRES_PASSWORD \
flashcards:prod
- |
started=false
for i in $(seq 1 60); do
if docker logs flashcards-prod 2>&1 | grep -q "Started FlashcardsApplication"; then
started=true
break
fi
sleep 2
done
if [ "$started" != "true" ]; then
echo "Application did not start"
docker logs flashcards-prod
exit 1
fi
- |
for i in $(seq 1 30); do
if docker run --rm \
--network flashcards-net \
curlimages/curl:8.13.0 \
curl -fs http://flashcards-prod:8080/actuator/health; then
echo "Health endpoint OK"
break
fi
sleep 2
if [ "$i" -eq 30 ]; then
echo "Health endpoint never became available"
docker logs flashcards-prod
exit 1
fi
done
after_script:
- docker logs flashcards-prod > container-prod.log 2>&1 || true
- docker rm -f flashcards-prod || true
- docker rm -f postgres || true
artifacts:
when: always
paths:
- container-prod.log
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
L'intérêt du Smoke Testing avant le déploiement
Combien de fois une application a-t-elle échoué à démarrer en production à cause d'une variable d'environnement manquante ou d'un profil mal configuré ?
Le Smoke Test résout ce problème en simulant un micro-déploiement automatisé dans le Runner. Si l'application ne passe pas le test de santé (/actuator/health), le pipeline s'arrête. On sécurise ainsi la mise en production en s'assurant que le conteneur est viable.
Ce test, très léger, valide le démarrage réel de l'application.
On s'assure que le conteneur Docker final est:
capable de démarrer correctement
de se connecter à sa base de données
et de répondre à des requêtes réseau (sans planter)
Résumé des étapes
On réutilise l'archive "flashcards-prod.tar" via le mot-clé "needs" pour garantir qu'on teste exactement le binaire validé précédemment par le scanner Trivy
On reproduit l'environnement de production au sein du Runner:
La création d'un réseau privé, virtuel et isolé, pour permettre aux conteneurs de communiquer entre eux par leur nom d'hôte
On interroge intelligemment PostgreSQL, en boucle, jusqu'à ce qu'il soit prêt à accepter des connexions
L'attente active de la base avec "pg_isready" est une bonne pratique à souligner
Au démarrage de l'application, on injecte le profil Spring Boot de production (SPRING_PROFILES_ACTIVE=prod) et des variables de connexion
Une fois connecté, on utilise une double validation pour déclarer le succès du test:
1. Le Log Scraper
Comme en staging; une boucle inspecte les logs de l'application pendant maximum 2 minutes (60 essais x 2 secondes) à la recherche de la chaine "Started FlashcardsApplication"
Si Spring Boot échoue à démarrer,
le job s'arrête immédiatement et affiche les logs pour faciliter le débogage
2. Le test de l'endpoint (/actuator/health)
Un conteneur éphémère Curl est lancé sur le même réseau pour interroger le point de terminaison de santé de l'application
Si l'application répond avec un code
HTTP 200,
Le "
Smoke Test est officiellement réussi # output
{"status":"UP","groups":["liveness","readiness"]}Health endpoint OK
Si l'application échoue au endpoint de santé d'Actuator,
Le message "Health endpoint never became available" est affiché, les logs du container sont sauvegardé et le pipeline s'arrête automatiquement avec un status "Failed"
Qu'il y ait un succès ou un échec, le bloc after_script supprime les conteneurs créés pour ne pas saturer le disque du GitLab Runner
Génération du rapport de crash
Les logs de l'application sont extraits vers un fichier container-prod.log exporté en artefact
En cas d'échec du pipeline, on peut ouvrir ce fichier depuis l'interface GitLab pour comprendre le bug
[main] La publication officielle sur le registre Gitlab
Le stage publish est l'étape finale du pipeline de production. L'image Docker, après avoir passé les contrôles qualité, est publiée dans le GitLab Container Registry du projet, où elle devient disponible pour les déploiements.
Pour cette série, je ne conserve pas Docker Hub pour l'image provenant de Gitlab. En effet les deux projets ne disposent pas des mêmes commits et s'ils utilisaient le même script, on ne saurait plus d'ou vient les changements. il est donc plus propre et plus prudent de publier l'image Docker dans un registre différent.
prod-publish:
stage: publish
needs:
- job: prod-docker-build
artifacts: true
- job: prod-smoke
- job: prod-trivy-image
image: docker:29.6.1
services:
- name: docker:29.6.1-dind
alias: docker
variables:
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_TLS_VERIFY: "1"
DOCKER_CERT_PATH: "/certs/client"
script:
- docker load -i flashcards-prod.tar
- echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin
- docker tag "${IMAGE_NAME}:${SOURCE_TAG}" "${CI_REGISTRY_IMAGE}:sha-${CI_COMMIT_SHORT_SHA}"
- docker tag "${IMAGE_NAME}:${SOURCE_TAG}" "${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG}"
- docker tag "${IMAGE_NAME}:${SOURCE_TAG}" "${CI_REGISTRY_IMAGE}:latest"
- docker push "${CI_REGISTRY_IMAGE}:sha-${CI_COMMIT_SHORT_SHA}"
- docker push "${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG}"
- docker push "${CI_REGISTRY_IMAGE}:latest"
- echo "Docker image successfully pushed to GitLab Container Registry"
rules:
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
Les tags facilitent la traçabilité, l'identification des versions et le déploiement.
Ce registre constitue la référence des images produites par le pipeline GitLab
Résumé des étapes
Ce job ne démarre que si l'image a été construite (prod-docker-build), que l'analyse de vulnérabilités de l'image a réussi (prod-trivy-image) et que le test de démarrage du conteneur est concluant (prod-smoke).
La publication est également limitée aux tags de version au format vX.Y.Z (par exemple v1.2.0), qui matérialisent une décision de livraison.
Authentification sécurisée au registre GitLabL'authentification utilise les
variables prédéfinies par GitLab CI/CD:
CI_REGISTRY est adresse du
registre GitlabCI_REGISTRY_USER est l'utilisateur utilisé pour l'authentification
CI_REGISTRY_PASSWORD est le mot de passe temporaire associé au job.
La commande docker login utilise l'option --password-stdin afin de transmettre le mot de passe via l'entrée standard plutôt que de l'inscrire directement dans la ligne de commande.
L'image validée est publiée sous trois références:
sha-commit: identifie le commit source associé à l'image
v1.2.0: identifie la version livrée
latest: la dernière image publiée par ce pipeline.
En production, il est fortement déconseillé d'utiliser uniquement le tag latest, car il est changeant et imprévisible.
Associer un tag immuable (comme le numéro de version v1.0.0 ou le SHA du commit Git) garantit la reproductibilité des déploiements.
Si un problème survient, l'équipe Ops peut effectuer un retour arrière (rollback) instantané vers une version précédente parfaitement identifiée.
Publication dans le GitLab Container Registry
Chaque projet GitLab dispose de son propre espace de stockage pour les images Docker/OCI.
La connexion est automatique dans les pipelines en utilisant les variables d'environnement CI_REGISTRY_USER et CI_REGISTRY_PASSWORD
1. Processus de publication de la release (push via un tag de version)
1. Récupèrer les dernières modifications de main
git checkout main
git pull origin main
2. Vérifier le commit courant
git log -1 --oneline
2.a Le résultat attendu est: fa9763d (HEAD -> main, tag: v1.2.0, origin/main, origin/HEAD) Merge branch 'staging' into 'main'2.b: vous pouvez aussi le voir dans le dernier pipeline exécuté (
# 2954920700):
3. Créer le tag sur le commit courant
git tag -a v1.2.0 -m "Release v1.2.0"
git push origin v1.2.0
Une fois cette dernière commande envoyée, le pipeline # 2955191337, est lancé ↑
Il ne reste plus qu'à sécurisé le tag ↓
2. Protéger le tag
Depuis le
projet Settings → Repository:
Le tag doit contenir le SHORT commit du push de la branche main
3. Vérification dans le registre Gitlab
Pour chaque tag, il affiche les détails:
registry.gitlab.com/val7304/flashcards:v1.2.0
107.71 MiB
Published 20 hours ago
Digest: 168ee0d
Published to the val7304/flashcards image repository on 16 October 2026 at 14:53:39 CEST
Manifest digest: sha256:168ee0d098037709d684b4dac5caa645864a49bd8ef73b1398c537de776922cc
Manifest media type: application/vnd.docker.distribution.manifest.v2+json
Configuration digest: sha256:136f30bf656a68a4acfd24e1489e121748e846418ea1ba728725a31e0110a8c1
Vue complète des jobs du pipeline de production
1. Le pipeline de production sans publication de la Docker Image:
Le job de publication n'apparait pas dans le flux du pipeline si on ne l'exécute pas. Au moment de l'exécution, il rejouera tout le pipeline.
2. Le pipeline de production exécuté avec un push contenant le tag de version: Le job Sonar à été ignoré et le job de publication de l'image Docker à bien été exécutée:

Conclusion
Ce pipeline valide à présent:
Compilation
Tests unitaires
Tests d'intégration
Génération du rapport JaCoCo
Analyse & Publication SonarCloud
Semgrep
Trivy FileSystem
Construction de l'image Docker
Scan de l'image Docker avec Trivy
Smoke Test
Publication Docker Hub
L'image Docker n'est donc publiée que si tous ces contrôles ont réussi.
Dans l'article suivant, nous poursuivrons la structuration du projet en mettant en place la gouvernance GitLab: architecture, labels et Issue Templates.
Ces éléments serviront ensuite de fondation pour automatiser la création des Merge Requests et son template.