Maximize Page
Tech & DevOps HubEspace Tech & DevOps: Explorez le monde du Dev, du Cloud et des outils DevOps à travers nos articles et discussions Explore the world of development, the cloud and DevOps tools

Intégration continue: la branche staging

Date de l'article:16-06-2026
CI/CD JaCoCo Newman Gitlab
Passage à l'étape supérieure avec la configuration de la branche staging. Nous détaillons ici la mise en place de jobs majeurs intégrant toute la série de tests ainsi que les scans de sécurité et les tests de performance. L'objectif est de valider la stabilité de l'application avant sa mise en production.
Mise en place du pipeline staging
Contrairement à la branche develop (qui s'appuie sur une base de données H2 en mémoire), la branche staging valide l'application dans des conditions rigoureusement identiques à celles de la production.
Nous commencerons par l'analyse et la feuille de route et, suivant cela, nous ferons l'adaptation des stages dans la CI et passerons directement à l'écriture des jobs. Nous ferons une parenthèse pour représenter graphiquement la relation entre les 3 premiers jobs.
Nous terminerons avec la représentation du flux du pipeline contenant l'enchaînement des étapes de celui-ci, le cycle de vie et la réutilisation de l'image.
Voici les jobs de la branche [staging] et leurs liens directs:

Analyse et feuille de route
name: CI - Staging (GitHub)
jobs:
├── build-test-static-analysis
│   ├── Démarrage de PostgreSQL 16
│   ├── Spotless, Checkstyle et SpotBugs
│   ├── Tests unitaires
│   ├── Tests d'intégration
│   ├── Rapport JaCoCo
│   ├── Construction du fichier JAR
│   ├── Analyse CodeQL
│   └── Scan Trivy du système de fichiers
├── functional-tests
│   ├── Téléchargement du fichier JAR
│   ├── Scan de l'image Docker
│   ├── Démarrage du conteneur Flashcards
│   ├── Vérification de l'endpoint Actuator
│   ├── Installation de Node.js et Newman
│   └── Exécution des tests d'API
└── load-tests
    ├── Téléchargement du fichier JAR
    ├── Démarrage de l'application Spring Boot
    ├── Installation de k6
    └── Exécution des tests de charge
Le pipeline GitHub centralise la logique autour de trois blocs majeurs, utilisant tous un conteneur postgres:16
Pour migrer ce comportement vers GitLab, nous devons reproduire l'accès à PostgreSQL ainsi que l'isolation des phases de tests Règle d'or pour la base de données sous GitLab CI
Pour interconnecter l'application et la base de données, on déclare le conteneur PostgreSQL directement dans le job:
services:
  - name: postgres:16
    alias: postgres
L'application Spring Boot doit interroger le service via son alias réseau et non via localhost (déclaré dans les fichiers de l'application)
De même que l'on utilisera
jdbc:postgresql://postgres:5432/flashcardsdb
au lieu de
jdbc:postgresql://localhost:5432/flashcardsdb
[staging] GitLab
staging-build
     │
     ▼
staging-it
     │
     ├──────────────┐
     ▼              ▼
staging-trivy      semgrep-sast
     │              │
     └──────┬───────┘
            ▼
   staging-docker-build
            │
            ▼
   staging-trivy-image
            │
            ▼
   staging-functional
            │
            ▼
   staging-load-test
Nous diviserons les étapes en une série de jobs ordonnés séquentiellement et parallèlement

Adaptation des stages
Dans la branche develop, nous avions deux stages:
test
security
À ce stade, le fichier ".gitlab-ci.yml" ressemble à:
stages:
  - test             
  - security     
  - docker            
  - security-image    
  - functional        
  - performance       

Pour cette branche, nous déclarons l'ensemble des étapes pour couvrir tout le cycle de vie de l'image
Ajoutons les stages suivant:

test: pour les jobs de tests (+ spotles,checkStyle,Spotbug) | security: englobe Trivy(FS), Semgrep
docker: pour les jobs exécutant des commandes Docker (build)
security-image: sera utilisé pour le scan de l'image Docker (Trivy)
functional: exécutera les tests API (Postman/Newman)
performance: sera dedié au test de charge (Grafana K6)

[staging] Build des tests unitaires
staging-build:
  stage: test
  image: maven:3.9.11-eclipse-temurin-17
  cache:
    key: maven
    paths:
      - .m2/repository
    policy: pull-push
  script:
    - chmod +x mvnw
    - ./mvnw spotless:check
    - ./mvnw -B clean test
  artifacts:
    when: always
    paths:
      - target/site/jacoco/
  rules:
    - if: '$CI_COMMIT_BRANCH == "staging"'
Résumé des étapes
On l'appelle staging-build pour l'exécution des tests unitaire
Alors que [develop] utilise une base h2 en mémoire, ici on n'embarque pas de base de données. La commande ./mvnw -B clean test compile le code et exécute les tests unitaires via le plugin Maven Surefire.
On ajoute les commandes à exécuter dans la partie script: Tous les tests seront lancés et le rapport JaCoCo sera généré
Cet environnement suivra le flow suivant notre stratégie: la branche sera poussée via un "merge" venant de la branche [develop]

Résultat: les tests unitaires passent, mais aucun test d'intégration n'est lancé et aucun package (.jar) n'est créé.

[staging] Les tests d'intégrations
Le job staging-it est chargé d'exécuter les tests d'intégration (Integration Tests) et de générer le rapport de couverture de code pour l'environnement de staging
staging-it:
  stage: test
  needs:
    - job: staging-build
      artifacts: true
  cache:
    key: maven
    paths:
      - .m2/repository
    policy: pull-push
  image: maven:3.9.11-eclipse-temurin-17
  services:
    - name: postgres:16.14-alpine3.23
      alias: postgres
  variables:
    SPRING_PROFILES_ACTIVE: it
    POSTGRES_DB: flashcardsdb
    POSTGRES_USER: postgres
    POSTGRES_PASSWORD: $POSTGRES_PASSWORD
    DB_HOST: postgres
    DB_PORT: "5432"
    DB_USER: postgres
    DB_PASSWORD: $POSTGRES_PASSWORD
  script:
    - chmod +x mvnw
    - sleep 10
    - ./mvnw -B verify -DskipUnitTests=true -Pcoverage-staging
  artifacts:
    when: always
    paths:
      - target/site/jacoco/
      - target/*.jar
      - target/failsafe-reports/
      - .m2/repository/
    expire_in: 30 mins
  rules:
    - if: '$CI_COMMIT_BRANCH == "staging"'
À ce stade, notre pipeline valide désormais :
Les tests unitaires
La communication avec PostgreSQL et les tests d'intégration
Résumé des étapes
Le job ne réexécute pas la compilation complète du code: Il utilise "needs" pour récupérer les classes compilées et les livrables générés par le job "staging-build", accélérant ainsi le pipeline.
Il utilise un cache sur le dossier .m2/repository pour éviter de retélécharger les dépendances à chaque exécution
Il s'exécute dans un conteneur Linux préinstallé avec Java 17 et Maven pour pouvoir exécuter les tests
GitLab provisionne un conteneur PostgreSQL dédié. Grâce à son alias réseau, l'application peut communiquer avec la base de données en utilisant l'hôte postgres
Le job injecte les variables d'environnement nécessaires pour configurer l'application Spring Boot (activation du profil it) et l'accès à la base de données (nom, ports, identifiants)
Le script attend 10 secondes pour laisser le temps au conteneur PostgreSQL de démarrer complètement et d'être prêt à recevoir des connexions avant que l'application ne tente de s'y connecter.

Séquence d'exécution:
Une fois le délai passé, la commande Maven lance uniquement les tests d'intégration (phase verify). Les tests unitaires sont ignorés (-DskipUnitTests=true) car déjà validés en amont.
Le profil "coverage-staging" déclenche JaCoCo pour vérifier que le seuil de couverture minimale (fixé à 70%) est bien atteint pour cette branche. Si ce n'est pas le cas, le job s'arrête avec un statut "failed"
Qu'il y ait un succès ou un échec, GitLab sauvegarde et rend téléchargeables pendant 30 minutes les rapports de couverture, les livrables .jar générés ainsi que les rapports détaillés des tests d'intégration (failsafe-reports).

[staging] Trivy Scan (FileSystem)
Le script réalise une analyse de sécurité automatisé pour détecter les vulnérabilités et les erreurs de configuration dans le projet.
staging-trivy:
  stage: security
  needs:
    - job: staging-it
      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 fs \
        --severity HIGH,CRITICAL \
        --format table \
        --scanners vuln,misconfig \
        --skip-dirs ".trivycache" \
        --skip-dirs "target" \
        --cache-dir "$TRIVY_CACHE_DIR" \
        --offline-scan \
        --exit-code 0 \
        --no-progress \
      .
  rules:
    - if: '$CI_COMMIT_BRANCH == "staging"'
Résumé des étapes

Avec "needs", on attend la validation des tests d'intégration et on récupère ensuite l'ensemble de ses artefacts (comme le code testé et les dépendances consolidées)
On utilise l'image officielle de Trivy pour exécuter l'analyse. Avec entrypoint: [""], on réinitialise le point d'entrée par défaut qui permet à GitLab d'exécuter ses scripts de pipeline sans blocage

Double système de cache (Spécificité Staging vs [develop]):
1. Le cache Maven permet à Trivy d'accéder directement aux dépendances Java pré-téléchargées dans le dossier .m2/repository afin d'auditer leur sécurité
2. Le cache trivy-db stocke la base de données des vulnérabilités de Trivy localement dans le dossier .trivycache. La variable "TRIVY_CACHE_DIR" pointe vers ce dossier situé à la racine du projet, évitant au runner de devoir retélécharger cette base lourde à chaque exécution.

Détail des options de scan :
--severity HIGH,CRITICAL: filtre les résultats pour remonter les failles "hautes" et "critiques"
--skip-dirs: exclut certains répertoires pour ne pas analyser inutilement du code compilé
--scanners vuln,misconfig: recherche les vulnérabilités et les mauvaises configurations
--offline-scan: force Trivy à utiliser la base de données locale du cache

Comme dans [develop], on fait une analyse pûrement informative: les dépendances sont scannées avec un "exit-code 0" qui ne bloquera pas le pipeline.

Chaîne de validation des 3 premiers jobs : du build à la sécurité
Maintenant que nous avons analysé en détail la configuration de ces trois jobs, vous avez sans doute constaté à quel point ils sont étroitement interconnectés.
Rien n'est laissé au hasard: le succès de l'un conditionne le démarrage de l'autre, et chacun réutilise intelligemment le travail effectué par ses prédécesseurs.
Afin de mieux visualiser cette mécanique, voici une représentation graphique de l'enchaînement des jobs, de la circulation de leurs données et de la façon dont ils exploitent les caches pour optimiser le temps d'exécution
  +-----------------------+
  |     staging-build     |
  +-----------------------+
              |
              | (1) Télécharge & remplit le cache
              ▼
    ===================================================
    ||  CACHE GLOBAL : maven (.m2/repository)        || <----+
    ===================================================      |
             |                                               |
             | (2) Réutilise (Évite les retéléchargements)   | (5) Analyse les 
             ▼                                               |   .jar en local 
  +-----------------------+                                  |       (Offline)
  |      staging-it       | [Service: PostgreSQL]            |
  +-----------------------+                                  |
              |                                              |
              | (3) Génère les fichiers compilés             |
              ▼                                              |
     ====================================================    |
     ||  ARTEFACTS: Dossier target/ (.jar & rapports)  ||    |
     ====================================================    |
              |                                              |
              | (4) Transmet instantanément via "needs"      |
              ▼                                              |
  +-----------------------+                                  |
  |     staging-trivy     | ---------------------------------+
  +-----------------------+
              ▲
              | (6) Persiste les signatures de failles
              ▼
    =================================================
    ||  CACHE LOCAL : trivy-db (.trivycache)       ||
    =================================================
(1) "staging-build" démarre de zéro, télécharge toutes les dépendances Java et les stocke dans le cache commun maven.
(2) "staging-it" démarre en récupérant ce cache pour gagner du temps. Il lance ensuite les tests d'intégration face à la base de données PostgreSQL éphémère.
(3) "staging-it" génère ses fichiers exécutables dans le dossier target/ sous forme d'artefacts.
(4) "staging-trivy" récupère ces artefacts immédiatement grâce à la directive needs, évitant ainsi d'avoir à recompiler le projet une troisième fois.
(5) "staging-trivy" va piocher dans le dossier .m2/repository (récupéré en lecture seule) pour analyser la sécurité des bibliothèques logicielles.
(6) Grâce à l'argument --offline-scan, "staging-trivy" effectue sa recherche uniquement en comparant vos fichiers avec le cache trivy-db, éliminant le risque d'erreur HTTP 429 (voir develop)

[staging] Build de l'image Docker
Une fois l'application compilée, testée et auditée sur le plan de la sécurité, l'étape suivante consiste à empaqueter notre application Java dans une image de conteneur.
staging-docker-build:
  stage: docker
  needs:
    - staging-trivy
    - job: staging-it
      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:
    - chmod +x ci-scripts/docker-build.sh
    - ./ci-scripts/docker-build.sh flashcards staging
    - docker save flashcards:staging -o flashcards-staging.tar
  artifacts:
    paths:
      - flashcards-staging.tar
  rules:
    - if: '$CI_COMMIT_BRANCH == "staging"'
Grâce à la directive "needs", il démarre à la seconde précise où le job de sécurité "staging-trivy" se termine avec succès ET dès que le job "staging-it" a fini de générer ses artefacts (le fichier .jar de l'application).
Le job utilise la technique "Docker-in-Docker" pour pouvoir lancer des commandes Docker à l'intérieur d'un conteneur GitLab, suivi du service contenant le démon Docker qui s'exécute en arrière-plan.
Les variables "DOCKER_TSL" sécurisent l'échange de données entre le client Docker et le service Docker-in-Docker via des certificats chiffrés
L'hôte "tcp://docker:2376" indique au client Docker l'adresse réseau sécurisée pour communiquer avec le démon
Le script d'exécution déclenche les commandes pour construire l'image Docker avec le nom "flashcards" et le tag "staging"
La commande "docker save" exporte l'image Docker construite sous la forme d'un fichier archive .tar et permet aux jobs des étapes suivantes de récupérer cette image, sans avoir à la reconstruire

Le script externe
Le script ci-scripts/docker-build.sh auxquel on fait appel, existait déja lors du projet GitHub actions
# bash version
#!/bin/bash
set -euo pipefail

IMAGE_NAME=${1:-flashcards}
IMAGE_TAG=${2:-latest}

echo "== Building Docker image: ${IMAGE_NAME}:${IMAGE_TAG} =="
docker build -t ${IMAGE_NAME}:${IMAGE_TAG} .
Très peu de changements à faire dans ce script, puisqu'il utilise principalement des commandes Docker
Dans GitHub, la construction de l'image reposait sur un script écrit en Bash s'exécutant sur un exécuteur Ubuntu (embarque nativement Bash)
Dans GitLab CI, nous utilisons des conteneurs et des environnements légers basés sur "Alpine" Linux, où bash n'est pas installé par défaut.
Nous devons donc convertir le script, le rendant ainsi compatible avec les deux plateformes.
#shell version
#!/bin/sh
set -eu

IMAGE_NAME=${1:-flashcards}
IMAGE_TAG=${2:-latest}
DOCKERFILE=${3:-Dockerfile}

echo "====================================="
echo "Building Docker image"
echo "Image      : ${IMAGE_NAME}:${IMAGE_TAG}"
echo "Dockerfile : ${DOCKERFILE}"
echo "====================================="

docker build \
  -f "${DOCKERFILE}" \
  -t "${IMAGE_NAME}:${IMAGE_TAG}" \
  .

echo "Docker image successfully built"
À ce stade, nous produisons une image Docker testée et validée, qui servira à tous les tests suivants

[staging] Trivy Scan (Docker Image)
Après avoir validé la sécurité du code source, nous devons appliquer le même niveau d'exigence à notre conteneur finalisé.
Ce job effectue un scan de sécurité applicatif à la recherche de failles au niveau du système d'exploitation et des packages embarqués.
staging-trivy-image:
  stage: security-image
  needs:
    - job: staging-docker-build
      artifacts: true
  image:
    name: aquasec/trivy:0.72.0
    entrypoint: [""]
  cache:
    - key: trivy-db
      paths:
        - .trivycache
      policy: pull-push
  variables:
    TRIVY_CACHE_DIR: ".trivycache"
  script:
	  -  trivy image \
       --input flashcards-staging.tar \
       --severity HIGH,CRITICAL \
       --exit-code 0 \
       --ignore-unfixed \
       --timeout 10m \
       --cache-dir "$TRIVY_CACHE_DIR" \
       --format table \
       --no-progress
  rules:
    - if: '$CI_COMMIT_BRANCH == "staging"'
Résumé des étapes
Le job télécharge l'artefact "flashcards-staging.tar" généré précédemment pour pouvoir l'analyser.
La variable "TRIVY_CACHE_DIR" indique à Trivy où lire et écrire la base de données des vulnérabilités. En lui associant une politique de cache pull-push, le runner télécharge la base de données existante au démarrage et téléverse les dernières mises à jour à la fermeture du job. La base de données n'est ainsi jamais reconstruite à partir de zéro, préservant nos quotas de requêtes.
Le script d'image: analyse du conteneur finalisé
--input flashcards-staging.tar demande à Trivy d'analyser directement l'image Docker contenue dans l'archive (et créée via staging-docker-build)
--severity HIGH,CRITICAL se concentre uniquement sur les menaces hautes et critiques. Associé à --exit-code 0 on ne bloquera jamais le pipeline, même en cas de failles. C'est idéal pour un scan purement "informatif"
Avec --ignore-unfixed, on masque les vulnérabilités pour lesquelles aucun correctif (patch) n'existe encore
On interrompt le scan s'il prend plus de 10 minutes pour éviter de bloquer le runner en cas de bug

Résultat des logs
Voici les points essentiels des logs pour illustrer le succès du pipeline de sécurité.
En élaguant le "bruit" (les lignes d'attente répétitives) et en ciblant les étapes clés, ce log relève les points principaux:
Mise à jour des bases de données: Téléchargement réussi des bases de vulnérabilités globales (trivy-db:2) et Java (trivy-java-db:1).
Cette dernière est mise en cache pour une durée de 3 jours afin de réduire la consommation réseau.
L'activation des modules: de scanners pour les vulnérabilités logicielles (Vulnerability scanning) et de la détection des secrets enfouis (Secret scanning).
L'analyse de l'environnement: Détection automatique du système d'exploitation de base de l'image (système Debian 13.6)
et inventaire de 25 paquets OS, couplé à l'analyse du livrable Java (app.jar).
$ chmod +x ci-scripts/trivy-image.sh
$ ./ci-scripts/trivy-image.sh flashcards-staging.tar 0
2026-09-23T11:23:07Z	INFO	[vulndb] Need to update DB
2026-09-23T11:23:07Z	INFO	[vulndb] Downloading vulnerability DB...
2026-09-23T11:23:07Z	INFO	[vulndb] Downloading artifact...	repo="mirror.gcr.io/aquasec/trivy-db:2"
2026-09-23T11:23:32Z	INFO	[vulndb] Artifact successfully downloaded	repo="mirror.gcr.io/aquasec/trivy-db:2"
2026-09-23T11:23:32Z	INFO	[vuln] Vulnerability scanning is enabled
2026-09-23T11:23:32Z	INFO	[secret] Secret scanning is enabled

2026-09-23T11:23:45Z	INFO	[javadb] Downloading Java DB...
2026-09-23T11:23:45Z	INFO	[javadb] Downloading artifact...	repo="mirror.gcr.io/aquasec/trivy-java-db:1"
2026-09-23T11:25:34Z	INFO	[javadb] Artifact successfully downloaded	repo="mirror.gcr.io/aquasec/trivy-java-db:1"
2026-09-23T11:25:34Z	INFO	[javadb] Java DB is cached for 3 days. If you want to update the database more frequently, "trivy clean --java-db" command clears the DB cache.
2026-09-23T11:25:36Z	INFO	Detected OS	family="debian" version="13.6"
2026-09-23T11:25:36Z	INFO	[debian] Detecting vulnerabilities...	os_version="13" pkg_num=25
2026-09-23T11:25:36Z	INFO	Number of language-specific files	num=1
2026-09-23T11:25:36Z	INFO	[jar] Detecting vulnerabilities...
2026-09-23T11:25:37Z	WARN	Using severities from other vendors for some vulnerabilities. Read https://trivy.dev/docs/v0.74/guide/scanner/vulnerability#severity-selection for details.

Après analyse, le bilan de sécurité affiche les résultats:

0 vulnérabilité et 0 secret détectés sur l'OS Debian.
0 vulnérabilité détectée dans les dépendances du JAR applicatif
L'image de staging est certifiée 100 % saine (Clean)

Report Summary
┌──────────────────────────────────────┬────────┬─────────────────┬─────────┐
│                Target                │  Type  │ Vulnerabilities │ Secrets │
├──────────────────────────────────────┼────────┼─────────────────┼─────────┤
│ flashcards-staging.tar (debian 13.6) │ debian │        0        │    -    │
├──────────────────────────────────────┼────────┼─────────────────┼─────────┤
│ app/app.jar                          │  jar   │        0        │    -    │
└──────────────────────────────────────┴────────┴─────────────────┴─────────┘
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)
Post-exécution:
La sauvegarde et la mise à jour réussies du cache local .trivycache/ s'effectue pour optimiser la vitesse des futurs lancements, suivi de la validation finale du job par GitLab.
Saving cache for successful job
Creating cache trivy-db-4-protected...
.trivycache/: found 9 matching artifact files and directories 
Created cache
Cleaning up project directory and file based variables
Job succeeded

[staging] Tests de l'API
Ce job simule un environnement de production complet: Il déploie l'application, l'interconnecte à la base de données, puis exécute les collections Postman via Newman. Pour voir quelles test ont été exécutés, rendez-vous dans l'article sur la creation-des-scenarios-de-tests-en-local
staging-functional:
  stage: functional
  needs:
    - job: staging-docker-build
      artifacts: true
    - staging-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-staging.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:18.6-alpine3.23
    - |
      until docker exec postgres pg_isready -U postgres; do
        echo "Waiting for PostgreSQL..."
        sleep 2
      done
    - |
      docker run -d \
        --name flashcards-staging \
        --network flashcards-net \
        -e SPRING_PROFILES_ACTIVE=staging \
        -e SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/flashcardsdb \
        -e SPRING_DATASOURCE_USERNAME=postgres \
        -e SPRING_DATASOURCE_PASSWORD=$POSTGRES_PASSWORD \
        flashcards:staging
    - |
      started=false

      for i in $(seq 1 30); do
        if docker logs flashcards-staging 2>&1 | grep -q "Started FlashcardsApplication"; then
          echo "Application started"
          started=true
          break
        fi
        sleep 2
      done

      if [ "$started" != "true" ]; then
        echo "Application failed to start"
        docker logs flashcards-staging
        exit 1
      fi

    - |
      docker run --rm \
        --network flashcards-net \
        -v "$CI_PROJECT_DIR/postman:/etc/newman" \
        postman/newman:alpine \
        run /etc/newman/flashcards.postman_collection.json \
        -e /etc/newman/local.postman_environment.json \
        --env-var "base-url=http://flashcards-staging:8081" \
        --reporters cli,junit \
        --reporter-junit-export /etc/newman/newman-functional.xml

    - |
      docker run --rm \
        --network flashcards-net \
        -v "$CI_PROJECT_DIR/postman:/etc/newman" \
        postman/newman:alpine \
        run /etc/newman/flashcards_error_cases.postman_collection.json \
        -e /etc/newman/local.postman_environment.json \
        --env-var "base-url=http://flashcards-staging:8081" \
        --reporters cli,junit \
        --reporter-junit-export /etc/newman/newman-error.xml
  artifacts:
    when: always
    paths:
      - postman/newman-functional.xml
      - postman/newman-error.xml
      - container.log
    reports:
      junit:
        - postman/newman-functional.xml
        - postman/newman-error.xml
  after_script:
    - docker logs flashcards-staging > container.log 2>&1 || true
    - docker rm -f flashcards-staging || true
    - docker rm -f postgres || true
  rules:
    - if: '$CI_COMMIT_BRANCH == "staging"'
Résumé des étapes

Chargement de l'application et réseau dédié:
Avec "docker load -i flashcards-staging.tar", on restaure l'image générée en amont via docker load -i flashcards-staging.tar
Il crée ensuite un réseau isolé (docker network create flashcards-net) afin que l'ensemble des futurs conteneurs puissent communiquer de manière transparente via leurs noms DNS respectifs (postgres, flashcards-staging).

Démarrage et résilience de la base de données
Lancement d'une instance PostgreSQL isolée
La boucle "until pg_isready" garantit que la base de données accepte les connexions avant que l'application Spring Boot ne tente de s'y connecter, évitant ainsi un crash au démarrage

Lancement et validation de l'application
On démarre l'application en injectant les variables d'environnement (profil Spring pour staging, URL JDBC pointant vers le conteneur Postgres)
La boucle de "Healthcheck (seq 1 30) " analyse les logs en continu pendant 60 secondes maximum à la recherche de la chaîne "Started FlashcardsApplication".
Si l'application échoue à démarrer, le job extrait les logs et s'arrête immédiatement (exit 1)

Exécution des tests d'API avec Newman (Postman)
On utilise l'image "postman/newman:alpine" pour exécuter 2 collections de tests (cas nominaux et cas d'erreur)
Ces fichiers sont localisés dans l'application et accessibles depuis le dossier "postman"
On monte le dossier local ($CI_PROJECT_DIR/postman) dans le conteneur pour lire les fichiers JSON
Avec la variable d'environnement:
--env-var "base-url=http://flashcards-staging:8081",
On écrase dynamiquement l'URL de base pour cibler le conteneur de l'application, et générer des rapports au format JUnit XML

Collecte et Intégration GitLab des rapports
En déclarant les fichiers .xml (reports: junit: ...), GitLab va analyser les résultats de Newman et les afficher directement dans l'onglet "Tests" du Pipeline
Quoi qu'il arrive (when: always), on conserve les fichiers XML et les logs du conteneur en téléchargement

Nettoyage automatique (after_script)
Le bloc s'exécute même si le script principal échoue (par exemple, si un test Postman rate)
On exporte les logs finaux de l'application dans le fichier "container.log" pour le débogage
On supprime ensuite les conteneurs Postgres et flashcards-staging pour libérer les ressources du GitLab Runner

Et le check Actuator alors?
Présent dans GitHub, on ne l'a pas ajouté ici car cela aurait eu un effet redondant. Le job vérifie déjà plusieurs choses:
  1. PostgreSQL démarre
  2. Spring Boot démarre
  3. Spring Boot se connecte à PostgreSQL
  4. Les endpoints HTTP répondent via les requêtes POST et GET sur le endpoint "/api/categories"
  5. Les scénarios métier fonctionnent

Nous avons maintenant validé que l'image de pré-production, démarre correctement et répond aux scénarios fonctionnels.

Résultat des logs
Voici les points essentiels des logs pour illustrer le succès du pipeline de test API.
En élaguant le "bruit" et en ciblant les étapes clés, ce log relève les points principaux:
Voici les détails:
Le job démarre avec l'initialisation de l'environnement: Il télécharge l'artefact de build (flashcards:staging) et se connecte au réseau isolé flashcards-net.
Il vérifie la disponibilité des services en validant l'état de PostgreSQL (accepting connections) et valide également le démarrage de l'application (Application started).
..Downloading artifacts for staging-docker-build (16678713644)...
Downloading artifacts from coordinator... ok        correlation_id=a3f934366cb1aa7c-BRU id=16678713644 status=302 token=73_TZUsSx
Executing "step_script" stage of the job script
Using effective pull policy of [always] for container docker:29.8.1@sha256:3f3c01aaaebf7cce837356b688b7c059a4749f10bd7660dec7c58fc454a283f0
$ docker load -i flashcards-staging.tar
Loaded image: flashcards:staging
$ docker network create flashcards-net
7b66b06f79becf4a5a4bc8bc8dcb6a542303207d4dec495d15d3318c44d2c9a0
$ docker run -d \ 
docker.io/library/[MASKED]@sha256:885cf05d376c7cf27afef02073e6bdac3841252537f16e244fd1c1e6a7c99fb1: Pulling from library/[MASKED]
.. Status: Downloaded newer image for [MASKED]@sha256:885cf05d376c7cf27afef02073e6bdac3841252537f16e244fd1c1e6a7c99fb1

$ until docker exec [MASKED] pg_isready -U [MASKED]; do 
/var/run/[MASKED]ql:5432 - accepting connections
$ docker run -d \ 
18b2dad0c0f476ec28467bd65a727f7c9acaf847c3c4662dfb8b2bd4b1efb17f
$ started=false 
Application started
$ docker run --rm \ 
alpine: Pulling from postman/newman
Digest: sha256:d9b5e780ead0026bbb37f3759cd7b10fc8a88cd4030c8266a7c5e343d75a5198
Status: Downloaded newer image for postman/newman:alpine
Lancement du 1er test: Démarrage et validation des cas nominaux (Newman Functional):
0 échec sur 15 requêtes HTTP exécutées (opérations CRUD complètes sur les catégories et les flashcards).
30 assertions logiques validées à 100 % (vérification des structures JSON, présence des IDs et exactitude des données retournées).
Durée du run de 1,97 s avec un temps de réponse moyen de 84 ms.
extrait sur les catégories et leur process
→ add categories
  POST http://flashcards-staging:8081/api/categories [200 OK,356B,510ms]
  ✓  Category created
  ✓  Response is JSON
  ✓  Category ID is present

→ get category by id
  GET http://flashcards-staging:8081/api/categories/1 [200 OK,356B,66ms]
  ✓  Category retrieved
  ✓  Correct category returned

→ modify categories
  PUT http://flashcards-staging:8081/api/categories/1 [200 OK,373B,93ms]
  ✓  categoryId is defined and valid

→ get category by id after update
  GET http://flashcards-staging:8081/api/categories/1 [200 OK,373B,19ms]
  ✓  Category retrieved after an update
  ✓  Correct category returned

→ get category by name
  GET http://flashcards-staging:8081/api/categories/search?name=git
  ✓  Search categories returns 200                  [200 OK,336B,185ms]
  ✓  Response is JSON array
  ✓  All categories match search term
  ✓  Category object structure is valid

→ delete categorie
  DELETE http://flashcards-staging:8081/api/categories/1
  ✓  Category deleted                                [200 OK,293B,76ms]

→ get categories
  GET http://flashcards-staging:8081/api/categories  [200 OK,336B,23ms]
  ✓  Deleted category is not present anymore
→ Si je reprends le scénario Postman, on voit qu'il respecte bien l'ordre établi:
Flashcards API
├── Categories
│   ├── get categories (global)
│   ├── add category
│   ├── get category by id
│   ├── modify category
│   ├── get category by id (after update)
│   ├── get category by name (by word)
│   ├── delete category
│   └── get category (after delete)
├── Flashcards
│   ├── get flashcards (global)
│   └── etc...
Il exécute ensuite le scénario pour les flashcards
→ Il affiche ensuite le récapitulatif des résultats:
┌─────────────────────────┬────────────────────┬───────────────────┐
│                         │           executed │            failed │
├─────────────────────────┼────────────────────┼───────────────────┤
│              iterations │                  1 │                 0 │
├─────────────────────────┼────────────────────┼───────────────────┤
│                requests │                 15 │                 0 │
├─────────────────────────┼────────────────────┼───────────────────┤
│            test-scripts │                 30 │                 0 │
├─────────────────────────┼────────────────────┼───────────────────┤
│      prerequest-scripts │                 17 │                 0 │
├─────────────────────────┼────────────────────┼───────────────────┤
│              assertions │                 30 │                 0 │
├─────────────────────────┴────────────────────┴───────────────────┤
│ total run duration: 1977ms                                       │
├──────────────────────────────────────────────────────────────────┤
│ total data received: 441B (approx)                               │
├──────────────────────────────────────────────────────────────────┤
│ average response time: 84ms [min: 19ms, max: 510ms, s.d.: 120ms] │
└──────────────────────────────────────────────────────────────────┘
L'exécution des tests et la validation de ceux-ci se fait instantanément.
La ligne accompagnant la requête est un résumé du résultat de la requête réseau. exemple →
[200 OK, 399B, 19ms]
[statut HTTP, taille du message en Octets, temps de réponse en millisecondes]
Lancement du second test: Validation des cas d'erreur (Newman Error Cases):
0 échec sur 6 requêtes ciblées simulant des comportements incorrects.
Validation de la robustesse de l'API avec la bonne interception des codes erreurs attendus (404 Not Found et 400 Bad Request).
$ docker run --rm flashcards_error_cases
→ GET category by invalid id categorie
  GET http://flashcards-staging:8081/api/categories/189782 [404 Not Found]
  ✓  Can't Get: Category Not Found - error 404
  ✓  GET /categories/{categoryId} returns 404 when category does not exist
→ PUT category by invalid id categorie
  PUT http://flashcards-staging:8081/api/categories/189782 
  ✓  Can't Update: Category does not exist - Bad Request 400
→ POST category by invalid name
  POST http://flashcards-staging:8081/api/categories/ 
  ✓  Can't add: Category Not Found               [404 Not Found,534B,37ms]
  ✓  Returns client error (4xx)
→ GET flashcards by invalid id
  GET http://flashcards-staging:8081/api/flashcards/124578 
  ✓  Can't Get: Flashcard Not Found - error 404  [404 Not Found,300B,19ms]
→ POST flashcards by invalid id category
  POST http://flashcards-staging:8081/api/flashcards/ 
  ✓  Can't add: Flashcard Not Found - error 404 [404 Not Found,534B,18ms]
→ PUT flashcards by invalid id categorie
  PUT http://flashcards-staging:8081/api/flashcards/ [404 Not Found]
  ✓  Can't Update: Flashcard not found - error 404

Suivi du récapitalif du résultat:
┌─────────────────────────┬───────────────────┬───────────────────┐
│                         │          executed │            failed │
├─────────────────────────┼───────────────────┼───────────────────┤
│              iterations │                 1 │                 0 │
├─────────────────────────┼───────────────────┼───────────────────┤
│                requests │                 6 │                 0 │
├─────────────────────────┼───────────────────┼───────────────────┤
│            test-scripts │                 6 │                 0 │
├─────────────────────────┼───────────────────┼───────────────────┤
│      prerequest-scripts │                 0 │                 0 │
├─────────────────────────┼───────────────────┼───────────────────┤
│              assertions │                 8 │                 0 │
├─────────────────────────┴───────────────────┴───────────────────┤
│ total run duration: 511ms                                       │
├─────────────────────────────────────────────────────────────────┤
│ total data received: 424B (approx)                              │
├─────────────────────────────────────────────────────────────────┤
│ average response time: 45ms [min: 16ms, max: 116ms, s.d.: 36ms] │
└─────────────────────────────────────────────────────────────────┘
la durée du run de 511 ms avec un temps de réponse moyen de 45 ms
La dernière étape est l'arrêt et le nettoyage des conteneurs, suivis du téléversement réussi des rapports
→ JUnit (newman-functional.xml, newman-error.xml) et des logs applicatifs (container.log) vers GitLab.
Running after script...
..
Uploading artifacts for successful job
postman/newman-functional.xml: found 1 matching artifact files and directories 
postman/newman-error.xml: found 1 matching artifact files and directories 
container.log: found 1 matching artifact files and directories 
Uploading artifacts as "archive" to coordinator... 201 Created  correlation_id=a3f938f798d3b753-BRU id=16678713646 status=201 token=73_TZUsSx
Uploading artifacts...
postman/newman-functional.xml: found 1 matching artifact files and directories 
postman/newman-error.xml: found 1 matching artifact files and directories 
Uploading artifacts as "junit" to coordinator... 201 Created  correlation_id=a3f9390228e1e0ca-BRU id=16678713646 status=201 token=73_TZUsSx

Cleaning up project directory and file based variables
Job succeeded

[staging] Le test de charge
Ce job est placé dans une étape finale dédiée aux analyses de performance, en orchestrant un test de charge automatisé et éphémère avec Grafana k6.
Si vous désirez en savoir + sur le test de charge et son résultat, RV dans l'article sur la mise-en-place-du-test-de-charge-grafana-k6
staging-load-test:
  stage: performance
  when: manual
  allow_failure: true
  needs:
    - job: staging-docker-build
      artifacts: true
    - staging-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-staging.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-staging \
        --network flashcards-net \
        -p 8081:8081 \
        -e SPRING_PROFILES_ACTIVE=staging \
        -e SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/flashcardsdb \
        -e SPRING_DATASOURCE_USERNAME=postgres \
        -e SPRING_DATASOURCE_PASSWORD=$POSTGRES_PASSWORD \
        flashcards:staging
    - |
      for i in $(seq 1 30); do
        if docker logs flashcards-staging 2>&1 | grep -q "Started FlashcardsApplication"; then
          echo "Application ready after ${i} attempt(s)"
          break
        fi
        if [ "$i" -eq 30 ]; then
          echo "Application failed to start after 60 seconds"
          docker logs flashcards-staging
          exit 1
        fi
        sleep 2
      done
    - |
      docker run --rm \
        --network flashcards-net \
        -e BASE_URL=http://flashcards-staging:8081 \
        -v "$CI_PROJECT_DIR/load-test:/scripts" \
        grafana/k6:2.2.0\
        run \
          --vus 50 \
          --duration 2m \
          --out json=/scripts/k6-report.json \
          /scripts/flashcards.js
  artifacts:
    when: always
    paths:
      - load-test/k6-report.json
      - container-loadtest.log
  after_script:
    - docker logs flashcards-staging > container-loadtest.log 2>&1 || true
    - docker rm -f flashcards-staging || true
    - docker rm -f postgres || true
  rules:
    - if: '$CI_COMMIT_BRANCH == "staging"'
Rôle global
Le load test k6 est pertinent car il vérifie:
  • La performance
  • Le temps de réponses aux requêtes
  • Le comportement mis sous charge
  • La stabilité de la base PostgreSQL
  • La consommation mémoire/CPU ( Autant vous dire que docker-desktop à chauffé, atteignant le 348% de CPU durant la phase de stabilisation)
Résumé des étapes
Spécificités du déclenchement
Avec le déclencheur "when: manual", le pipeline s'arrête et attend une action humaine pour lancer ce job. Il ajoute le bouton "Play" dans l'interface GitLab
C'est une bonne pratique pour les tests de charge, car ils consomment beaucoup de ressources et ne doivent être lancés qu'à la demande.
Avec allow_failure: true, on permet que si les tests échouent (par exemple, si les seuils de performance ou de taux d'erreur de k6 sont dépassés), le pipeline global reste "vert". Cela évite de bloquer la suite des livraisons pour des raisons de métriques de performance pure.
L'orchestration de l'infrastructure Docker-in-Docker reste identique au job précédent (PostgreSQL + boucle d'attente + validation du démarrage de Spring Boot)
Le conteneur de l'application mappe son port
sur l'hôte Docker (-p 8081:8081). Et bien que k6 utilise le réseau virtuel Docker, cette configuration permet d'ouvrir des flux pour, par exemple monitorer le conteneur depuis l'extérieur
Exécution de Grafana k6
On simule 50 utilisateurs virtuels simultanément en exécutant des requêtes en boucle
On maintient la charge, constante pendant 2 minutes
On exporte ensuite l'intégralité des métriques brutes de performance (temps de réponse, requêtes/sec, CPU virtuel k6) dans un fichier JSON
On monte le dossier du projet pour que le conteneur puisse lire le script de test flashcards.js et y écrire le rapport
Collecte des métriques
le rapport load-test/k6-report.json est archivé en tant qu'artefact GitLab
Pour terminer, la commande docker rm -f supprime de force les conteneurs pour libérer les ressources du GitLab Runner.

Adaptation GitLab
On n'a plus besoin de lancer le JAR directement dans le container (comme dans GitHub Actions) nohup java -jar ...
Les commandes "docker load" et "docker run" permettent de réutiliser exactement la même image Docker

Résumé du résultat des logs
Voici les points essentiels des logs pour illustrer le succès du pipeline de performance.
En élaguant le "bruit" et en ciblant les étapes clés, ce log relève les points principaux, après la préparation et le lancement du gitlab-runner 19.1.1 ..
On y voit clairement:
→ La récupération de l'image de l'application et la création d'un réseau Docker isolé.
→ L'attente du démarrage de la base de données PostgreSQL, puis de l'application ( prête en 16 secondes, après 8 tentatives)
Downloading artifacts
  Downloading artifacts for staging-docker-build (15578506279)...
  Direct download from cdn.artifacts.gitlab-static.net  correlation_id=a22531ce3d2e90b8-BRU id=15578506279 status=302 token=71_ZAtRjf
  Downloading artifacts from coordinator... ok        correlation_id=a22531ce3d2e90b8-BRU id=15578506279 status=302 token=71_ZAtRjf
  Executing "step_script" stage of the job script
  Using effective pull policy of [always] for container docker:29.6.1@sha256:66d292e5c26bd33a6f6f61cacb880de2186339a524ecba1ce098dbbaceed6515
  Using docker image sha256:66d292e5c26bd33a6f6f61cacb880de2186339a524ecba1ce098dbbaceed6515 .. 
  $ docker load -i flashcards-staging.tar
  Loaded image: flashcards:staging
  $ docker network create flashcards-net
  0807c33c91fcbaf7fbbb1ec8d714a9341aeff0d7673b514ff21c7401a6ec3321
  $ docker run -d 
  ..
  Digest: sha256:42b8b8b29c8a4e933d88943e5b03001a78794905cf786e6e7634e9f2abd5a0d3
  Status: Downloaded newer image for [MASKED]@sha256:42b8b8b29c8a4e933d88943e5b03001a78794905cf786e6e7634e9f2abd5a0d3
  203df52b9949ff8f903223c40a3bf6957d3c462e09f4f500fcd2dd59893dcabb
  $ until docker exec [MASKED] pg_isready -U [MASKED]; do 
  ..
  /var/run/[MASKED]ql:5432 - accepting connections
  $ docker run -d 
  0c24d0c815b06fd5f6105ced0d03f123abca7759f0ff1086b982c67d1d669228
  $ for i in $(seq 1 30); do 
  Application ready after 8 attempt(s)
  $ docker run --rm 
  91627df7da4b: Download complete
  Digest: sha256:65c920dc067d5e2e00befbf982af6ad6ad0117034e8b1c65817c7975c52d4669
  Status: Downloaded newer image for grafana/k6@sha256:65c920dc067d5e2e00befbf982af6ad6ad0117034e8b1c65817c7975c52d4669  
→ Il lance ensuite le test de charge qui effectue une simulation de 50 utilisateurs pendant 2 minutes via Grafana k6 (17 680 requêtes au total)
→ Au niveau performances: Succès total (0 % d'échecs), débit de 146 req/s et temps de réponse de 242 ms (le seuil de 500 ms est validé).

/\ Grafana /‾‾/ /\ / \ |\ __ / / / \/ \ | |/ / / ‾‾\ / \ | ( | (‾) | / __________ \ |_|\_\ \_____/
execution: local script: /scripts/flashcards.js output: json (/scripts/k6-report.json) scenarios: (100.00%) 1 scenario, 50 max VUs, 2m30s max duration (incl. graceful stop): * default: 50 looping VUs for 2m0s (gracefulStop: 30s) running (0m01.0s), 50/50 VUs, 0 complete and 0 interrupted iterations default [ 1% ] 50 VUs 0m01.0s/2m0s running (0m02.0s), 50/50 VUs, 0 complete and 0 interrupted iterations default [ 2% ] 50 VUs 0m02.0s/2m0s running (0m03.0s), 50/50 VUs, 50 complete and 0 interrupted iterations ... running (2m01.0s), 03/50 VUs, 4417 complete and 0 interrupted iterations default ↓ [ 100% ] 50 VUs 2m0s █ THRESHOLDS http_req_duration ✓ 'p(95)<500' p(95)=242.19ms http_req_failed ✓ 'rate<0.01' rate=0.00% █ TOTAL RESULTS checks_total.......: 17680 146.039257 /s checks_succeeded...: 100.00% 17680 out of 17680 checks_failed......: 0.00% 0 out of 17680 ✓ create category OK ✓ create flashcard OK ✓ get categories OK ✓ get flashcards OK HTTP http_req_duration..............: avg=77.74ms min=4.16ms med=48.23ms max=1.16s p(90)=169.18ms p(95)=242.19ms { expected_response:true }...: avg=77.74ms min=4.16ms med=48.23ms max=1.16s p(90)=169.18ms p(95)=242.19ms http_req_failed................: 0.00% 0 out of 17680 http_reqs......................: 17680 146.039257/s EXECUTION iteration_duration.............: avg=1.36s min=1.11s med=1.27s max=2.97s p(90)=1.65s p(95)=1.84s iterations.....................: 4420 36.509814/s vus............................: 3 min=3 max=50 vus_max........................: 50 min=50 max=50 NETWORK data_received..................: 1.0 GB 8.4 MB/s data_sent......................: 2.6 MB 21 kB/s running (2m01.1s), 00/50 VUs, 4420 complete and 0 interrupted iterations default ✓ [ 100% ] 50 VUs 2m0s
→ Une fois le test terminé, on cloture le job avec le nettoyage des conteneurs
et la sauvegarde automatique des rapports d'analyse dans GitLab.
Running after script...
$ docker logs flashcards-staging > container-loadtest.log 2>&1 || true
$ docker rm -f flashcards-staging || true
flashcards-staging
$ docker rm -f [MASKED] || true
[MASKED]

Uploading artifacts for successful job
Uploading artifacts...
load-test/k6-report.json: found 1 matching artifact files and directories 
container-loadtest.log: found 1 matching artifact files and directories 
Uploading artifacts as "archive" to coordinator... 201 Created  correlation_id=a22538f76b80ba5e-BRU id=15580439447 status=201 token=71_ZAtRjf

Cleaning up project directory and file based variables
Job succeeded

Flux du pipeline
Bonne pratique CI/CD : L'immuabilité de l'artefact
Une image Docker ne doit être construite qu'une seule fois. Cette même image est ensuite testée à travers différentes étapes (sécurité, fonctionnel, performance) avant d'être promue vers la production. Cela garantit que le code validé en staging est rigoureusement identique à celui qui tournera en production.
1. Enchaînement des étapes du Pipeline
staging-build
      ↓ (Compilation & Tests Unitaires)
staging-package
      ↓ (Packaging de l'application)
staging-docker-build
      ↓ (Construction unique de l'image)
staging-functional
      ↓ (Tests d'intégration d'API)
staging-load-test
        (Tests de performance à la demande)
2. Cycle de vie et réutilisation de l'image
                docker build (Image: flashcards:staging)
                              ▼
                  docker save → flashcards-staging.tar
                              ▼
                GitLab Artifact (Persistance temporaire)
                              ▼
      ┌───────────────────────┴───────────────────────┐
      ▼ [staging-functional]                          ▼ [staging-load-test]
      download artefact                               download artefact
            ↓                                               ↓
      docker load                                     docker load
            ↓                                               ↓
      docker run                                      docker run
            ↓                                               ↓
      Newman (Postman)                                k6 (Grafana)

Vue complète des jobs du pipeline
Tous les jobs du pipelines
Le job manuel est marqué comme (skipped) et peux être lancé à tout moment via le bouton "Play" (Run manual or delayed jobs), accessible depuis la liste des pipelines manual job
Dans l'article suivant, nous verrons la branche de production (main) et y intégrerons les contrôles spécifiques de tests et de publications
Naviguez entre les articlesBrowse through the articles

Laissez-moi un commentaire

En postant un commentaire anonyme, vous adhérez automatiquement aux conditions d'utilisation du site.