GitLab CI/CD Cheatsheet GitLab 17.x

Qu'est-ce que CI/CD ?

CI (Intégration Continue) automatise la vérification du code à chaque push : compilation, tests, linting. CD (Livraison/Déploiement Continu) automatise ensuite la mise en production. GitLab CI/CD lit le fichier .gitlab-ci.yml à la racine de votre repo et exécute automatiquement un pipeline.

Flux CI/CD complet 💻 Developer écrit du code git push 🦊 GitLab lit .gitlab-ci.yml déclenche PIPELINE BUILD install build-app ✓ succès TEST unit-tests lint ✓ succès DEPLOY deploy-staging deploy-prod 🖱 ⏸ attente manuelle feedback immédiat si ❌ échec
TermeSignificationExemple concret
CI — Intégration ContinueVérifier automatiquement chaque commitTests lancés à chaque push
CD — Livraison ContinueCode toujours prêt à être déployéBuild versionné disponible après chaque merge
CD — Déploiement ContinuDéploiement automatique en productionChaque merge sur main → prod mise à jour
PipelineEnsemble des stages et jobs déclenchésbuild → test → deploy
StageGroupe de jobs qui s'exécutent en parallèleTous les jobs de "test" tournent en même temps
JobUnité d'exécution avec un scriptunit-tests, lint, docker-build
RunnerMachine qui exécute les jobsServeur Linux avec gitlab-runner installé
ArtifactFichier produit par un job, partagé dans le pipelinedist/, rapport de tests
💡 Prérequis — Vous avez juste besoin d'un repo GitLab et d'un Runner disponible (GitLab.com fournit des runners gratuits). Aucune installation locale requise.

Démarrage rapide — Premier pipeline en 5 min

1
Créer le fichier à la racine du repo
Dans votre projet GitLab, créez .gitlab-ci.yml à la racine (même niveau que README.md).
2
Coller ce pipeline minimal
.gitlab-ci.yml — Le plus simple possible
# Ce pipeline a 1 stage et 1 job
stages:
  - hello

dire-bonjour:
  stage: hello
  script:
    - echo "Bonjour depuis GitLab CI !"
    - echo "Je tourne sur un runner"
3
Pusher et observer
Après le push → GitLab déclenche le pipeline. Allez dans CI/CD → Pipelines dans la barre latérale gauche de votre projet. Vous verrez le pipeline apparaître en quelques secondes.
4
Pipeline réaliste — Node.js
Voici un vrai pipeline de 3 stages pour une app Node.js :
.gitlab-ci.yml — Pipeline Node.js réel
stages:
  - install
  - test
  - build

default:
  image: node:20-alpine      # image Docker utilisée par tous les jobs
  cache:
    key: "$CI_COMMIT_REF_SLUG"
    paths: [node_modules/]   # mise en cache pour accélérer

installer-deps:
  stage: install
  script:
    - npm ci                   # install propre depuis package-lock.json

lancer-tests:
  stage: test
  script:
    - npm test
  artifacts:
    paths: [coverage/]        # conserve le rapport de couverture
    expire_in: 1 week

compiler-app:
  stage: build
  script:
    - npm run build
  artifacts:
    paths: [dist/]            # fichiers compilés disponibles pour téléchargement
5
Valider votre YAML avant de pusher
Dans GitLab → votre projet → CI/CD → Pipelines → CI Lint (bouton en haut à droite). Collez votre YAML, cliquez "Validate". GitLab vous dit immédiatement s'il y a des erreurs de syntaxe.
⚠️ Indentation YAML — Le YAML est sensible à l'indentation. Utilisez toujours des espaces (jamais des tabulations). 2 espaces par niveau est la convention GitLab CI.
🔍 Où voir les logs — CI/CD → Pipelines → cliquez sur le pipeline → cliquez sur un job. Vous verrez la sortie complète du script en temps réel.
1

Structure .gitlab-ci.yml

Fichier YAML placé à la racine du repo. GitLab le lit à chaque push, MR, tag ou déclenchement manuel.

YAML — Structure complète annotée
# ── 1. ORDRE DES STAGES ───────────────────────────────────────
stages:
  - build        # les jobs de ce stage tournent avant "test"
  - test
  - deploy

# ── 2. VARIABLES GLOBALES ────────────────────────────────────
variables:
  APP_NAME: "my-app"
  DOCKER_IMAGE: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"

# ── 3. DEFAULTS (appliqués à tous les jobs) ──────────────────
default:
  image: node:20-alpine
  before_script:
    - npm ci --cache .npm
  retry: 2              # retry automatique en cas d'échec runner
  timeout: 10 minutes

# ── 4. WORKFLOW — contrôle quand un pipeline est créé ────────
workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_TAG
    - when: never

# ── 5. UN JOB ────────────────────────────────────────────────
build-app:
  stage: build        # dans quel stage s'exécute ce job
  image: node:20      # surcharge l'image default
  script:             # commandes exécutées dans le conteneur
    - npm run build
  artifacts:          # fichiers à conserver
    paths: [dist/]
Variables prédéfinies clés (toujours disponibles)
VariableExemple de valeurUsage
CI_COMMIT_SHAa1b2c3d4e5f6...Hash complet du commit
CI_COMMIT_SHORT_SHAa1b2c3d4Tag d'image Docker, nommage de build
CI_COMMIT_BRANCHmain, feature/loginConditionner selon la branche
CI_COMMIT_TAGv1.2.3Non défini si ce n'est pas un tag
CI_COMMIT_MESSAGE"Fix bug #42"Message du commit
CI_PIPELINE_ID12345Identifiant unique du pipeline
CI_PIPELINE_SOURCEpush, merge_request_event, scheduleOrigine du pipeline
CI_JOB_NAMEbuild-appNom du job en cours
CI_PROJECT_DIR/builds/group/projectRépertoire de travail
CI_PROJECT_NAMEmy-appNom du projet GitLab
CI_PROJECT_PATHmy-group/my-appChemin complet namespace/projet
CI_REGISTRYregistry.gitlab.comURL du registry Docker GitLab
CI_REGISTRY_IMAGEregistry.gitlab.com/group/projectImage de base du projet
CI_REGISTRY_USER / PASSWORD(auto-générés)Login registry CI
CI_ENVIRONMENT_NAMEstaging, productionNom de l'environnement actif
CI_DEFAULT_BRANCHmainBranche principale du projet
GITLAB_USER_EMAILdev@example.comEmail de l'auteur du push
📋 Toutes les variables — La liste complète est disponible dans votre job lui-même : ajoutez - export dans le script pour les voir toutes.
2

Stages & Jobs

Les stages définissent l'ordre d'exécution. Tous les jobs d'un même stage tournent en parallèle. Le stage suivant ne démarre que si tous les jobs du stage précédent réussissent.

LES JOBS D'UN MÊME STAGE TOURNENT EN PARALLÈLE BUILD install-deps npm ci build-frontend npm run build build-backend gradle build ✓ tous OK TEST unit-tests npm test lint eslint src/ security-scan npm audit ✓ tous OK DEPLOY deploy-staging automatique deploy-prod when: manual 🖱 LÉGENDE Stage build Stage test Stage deploy séquence
YAML — Job avec toutes les options
unit-tests:
  stage: test
  image: node:20-alpine       # surcharge l'image default
  tags:                        # cibler un runner spécifique
    - docker
    - linux
  allow_failure: false         # true = job rouge mais pipeline continue
  timeout: 10 minutes
  retry:
    max: 2
    when:
      - runner_system_failure
      - stuck_or_timeout_failure
  before_script:
    - npm ci
  script:                      # ← OBLIGATOIRE
    - npm test -- --coverage
  after_script:               # toujours exécuté, même si script échoue
    - echo "Tests terminés"
  coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'  # regex extraction % coverage

# Job déclenché manuellement
deploy-prod:
  stage: deploy
  script:
    - ./deploy.sh
  when: manual           # on_success | on_failure | always | manual | delayed
  start_in: "1 hour"    # uniquement pour when: delayed
Valeurs de `when`
ValeurComportementCas d'usage
on_successDéfaut — s'exécute si tous les jobs précédents réussissentJob normal
on_failureS'exécute uniquement si un job précédent échoueNotification d'erreur, rollback
alwaysS'exécute quoi qu'il arriveNettoyage de ressources
manualNécessite un clic humain dans l'UI GitLabDeploy en production
delayedAttend un délai configuré via start_inCanary deploy progressif
neverNe s'exécute jamais (utile avec rules)Désactiver un job selon condition
3

Variables

Les variables sont des paires clé=valeur disponibles dans les scripts via $NOM_VARIABLE. On les définit dans le YAML ou dans l'interface GitLab pour les secrets.

YAML — Variables : définition et usage
# Variables globales (tous les jobs)
variables:
  ENV: "production"
  PORT: "3000"                 # toujours une string en YAML
  URL: "https://$CI_PROJECT_NAME.example.com"  # interpolation OK

# Variable locale à un job (surcharge la globale)
my-job:
  variables:
    ENV: "staging"
    GIT_STRATEGY: none      # fetch | clone | none — "none" = pas de checkout
    GIT_DEPTH: "10"          # shallow clone, accélère le checkout
  script:
    - echo $ENV              # → staging
    - echo ${ENV:-default}   # valeur par défaut bash si vide
    - echo ${APP_NAME:?Variable obligatoire}  # erreur si non définie

# Secrets — définis dans GitLab UI (Settings › CI/CD › Variables)
# ⚠ Ne jamais mettre de mot de passe dans le YAML !
deploy:
  script:
    - echo $DB_PASSWORD      # variable masquée définie dans l'UI → *** dans les logs
    - curl -H "Token: $API_KEY" https://api.example.com
Variables CI/CD dans l'UI GitLab
📍 Où les créer : Votre projet GitLab → Settings → CI/CD → Variables → Add variable
OptionEffetQuand l'utiliser
MaskedValeur remplacée par *** dans les logsMots de passe, tokens API
ProtectedDisponible uniquement sur branches/tags protégésCredentials de production
Expand variable referencesInterpole $OTHER_VAR dans la valeurVariables composées
FileCrée un fichier temporaire avec le contenuClés SSH, certificates .pem
Priorité des variables (haute → basse)
#SourceScope
1Trigger variables (API)Variables passées au déclenchement
2Variables UI projet (protected)Branches/tags protégés uniquement
3Variables UI projetTous les pipelines du projet
4Variables UI groupeTous les projets du groupe
5Variables .gitlab-ci.yml (job)Job courant seulement
6Variables .gitlab-ci.yml (global)Tous les jobs du fichier
7Variables prédéfinies CICI_*, GITLAB_*, etc.
4

Rules & conditions

rules remplace only/except (déprécié). Il permet de conditionner finement quand un job s'exécute, avec quel comportement (when) et quelles variables.

🔄 Comment rules fonctionne — GitLab évalue chaque règle dans l'ordre. Dès qu'une règle correspond, elle s'applique et les suivantes sont ignorées. Si aucune règle ne correspond, le job est exclu du pipeline.
YAML — Rules : cas d'usage courants
deploy-staging:
  script: ./deploy.sh staging
  rules:
    # Si branche develop → exécute automatiquement
    - if: '$CI_COMMIT_BRANCH == "develop"'
      when: on_success
    # Si MR vers main → exécute manuellement
    - if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'
      when: manual
    # Si un fichier spécifique a été modifié
    - changes:
        - src/**/*
        - package.json
    # Fallback : ne jamais exécuter autrement
    - when: never

release:
  script: ./release.sh
  rules:
    # Uniquement sur les tags semver (ex: v1.2.3)
    - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'

# Injecter des variables différentes selon la règle
build:
  script: npm run build
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      variables:
        BUILD_ENV: production
        MINIFY: "true"
    - if: '$CI_COMMIT_BRANCH'
      variables:
        BUILD_ENV: staging
        MINIFY: "false"

# Combiner if ET changes (les deux doivent être vrais)
test-api:
  script: npm run test:api
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      changes:
        - api/**/*
        - tests/api/**/*
Conditions `if` les plus utiles
ConditionCas d'usage
$CI_COMMIT_BRANCH == "main"Uniquement sur main
$CI_COMMIT_BRANCH =~ /^feature/Toutes les branches feature/*
$CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCHTout sauf la branche principale
$CI_COMMIT_TAGSi un tag est poussé (non nul)
$CI_COMMIT_TAG =~ /^v\d+/Tags de type v1.x.x
$CI_PIPELINE_SOURCE == "merge_request_event"Pipeline MR uniquement
$CI_PIPELINE_SOURCE == "schedule"Pipeline planifié (cron GitLab)
$CI_PIPELINE_SOURCE == "web"Déclenché manuellement depuis l'UI
$CI_PIPELINE_SOURCE == "api"Déclenché via l'API GitLab
$MY_VAR == nullVariable non définie
$MY_VAR != nullVariable définie (quelle que soit la valeur)
$MY_VAR == "true" && $ENV == "prod"Combinaison avec ET logique
⚠️ Guillemets dans les conditions — Les conditions if doivent être entre guillemets simples : if: '$VAR == "valeur"'. Les guillemets internes utilisent des doubles guillemets.
5

Needs & DAG

needs transforme votre pipeline en graphe orienté acyclique (DAG). Un job peut démarrer dès que ses dépendances directes sont terminées, sans attendre tous les jobs du stage précédent.

SANS needs: — Séquentiel chaque stage attend le précédent t=0 t=12min build-A (4min) build-B (4min) stage: build test-A (4min) test-B (4min) stage: test deploy (4min) stage: deploy ⏱ Durée totale : ~12 minutes AVEC needs: — DAG chaque job démarre dès que ses dépendances sont OK build-A (4min) build-B (2m) test-A (4min) test-B (2m) deploy (2m) needs: aucun needs: [build-A] needs: [build-B] needs: [test-A, test-B] ⚡ Durée totale : ~10 minutes — gain : 2 min
YAML — Needs & DAG
stages:
  - build
  - test
  - deploy

build-frontend:
  stage: build
  script: npm run build:frontend

build-backend:
  stage: build
  script: npm run build:backend

# Ne attend que build-frontend, pas build-backend
test-frontend:
  stage: test
  needs:
    - job: build-frontend
      artifacts: true       # télécharge les artifacts du job parent
  script: npm run test:frontend

test-backend:
  stage: test
  needs: [build-backend]   # syntaxe courte
  script: npm run test:backend

# Attend les deux tests
deploy-staging:
  stage: deploy
  needs: [test-frontend, test-backend]
  script: ./deploy.sh

# needs: [] = démarre immédiatement (ignore les stages précédents)
notify-start:
  stage: test
  needs: []
  script: curl -X POST $SLACK_WEBHOOK -d '{"text":"Pipeline démarré"}'
6

Artifacts & Cache

Deux mécanismes pour partager des fichiers, souvent confondus. Comprendre la différence est essentiel.

ARTIFACTS — entre jobs du MÊME pipeline PIPELINE #1 build → dist/ 📦 garanti test lit dist/ ✅ deploy lit dist/ ✅ 📦 Artifacts uploadés sur GitLab, téléchargeables dans l'UI ✅ Garanti disponible • ❌ Pas persisté entre pipelines CACHE — entre pipelines DIFFÉRENTS PIPELINE #1 npm ci → node_modules/ → cache 📤 🗄 Cache Store sur le runner PIPELINE #2 npm ci 📥 (rapide!) ⚡ Accélère les pipelines • ⚠️ Best-effort (peut être absent)
Artifacts — partager des fichiers entre jobs
YAML — Artifacts
build:
  script: npm run build
  artifacts:
    paths:
      - dist/
      - coverage/
    exclude:
      - dist/**/*.map              # exclure les sourcemaps
    reports:
      junit: test-results.xml     # rapport visible dans l'UI MR
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura.xml
    expire_in: 1 week            # 30 mins | 1 day | 1 week | never
    when: always                  # conserver même si le job échoue
    untracked: false             # inclure les fichiers non-git
Cache — accélérer les jobs (node_modules, pip…)
YAML — Cache
install:
  script: npm ci
  cache:
    key:
      files:                       # clé basée sur le hash du fichier lock
        - package-lock.json
    paths:
      - node_modules/
      - .npm/
    policy: pull-push             # pull | push | pull-push (défaut)

# Job qui ne fait que lire le cache (pas d'écriture)
test:
  cache:
    key: "$CI_COMMIT_REF_SLUG-node"
    paths: [node_modules/]
    policy: pull                  # plus rapide : ne re-pousse pas

# Cache Python pip
test-python:
  image: python:3.12
  cache:
    key:
      files: [requirements.txt]
    paths: [.pip-cache/]
  variables:
    PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
  script:
    - pip install -r requirements.txt
    - pytest
CritèreArtifactsCache
PortéeJobs du même pipelineEntre pipelines (même branche/runner)
FiabilitéGaranti disponibleBest-effort (peut ne pas exister)
StockageServeur GitLabRunner local (ou S3 configurable)
Visible dans UIOui, téléchargeableNon
Usage typiquedist/, rapports de tests, binairesnode_modules/, .pip-cache/, .m2/
7

Runners

Un Runner est un agent (programme) installé sur une machine qui reçoit les jobs de GitLab et les exécute. GitLab.com propose des runners partagés gratuits. Vous pouvez aussi enregistrer vos propres runners.

ARCHITECTURE RUNNER 🦊 GitLab Server gitlab.com ou self-hosted register (1x) job assigné ⚙️ GitLab Runner process qui poll GitLab tags: docker, linux… exécute via EXECUTOR (type d'exécution) 🐳 Docker conteneur isolé ☸ K8s pod éphémère 💻 Shell machine hôte ⭐ Docker recommandé : isolation + reproductibilité
Types d'exécuteur (executor)
ExecutorDescriptionRecommandé pour
dockerChaque job dans un conteneur Docker isolé (image: dans le YAML)La plupart des projets ✅
shellExécute directement sur le serveur hôte (pas de conteneur)Legacy, accès hardware spécifique
kubernetesCrée un Pod K8s éphémère par jobInfra cloud native
docker+machineAuto-scale via Docker MachinePipelines à fort volume
virtualbox / parallelsVMs isoléesTests iOS / cross-platform
BASH — Installer et enregistrer un runner
# ── Installation sur Ubuntu/Debian ────────────────────────────
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash
sudo apt install gitlab-runner

# ── Enregistrement interactif (pose des questions) ────────────
sudo gitlab-runner register

# ── Enregistrement non-interactif (scripts CI) ────────────────
sudo gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.com/" \
  --registration-token "$RUNNER_TOKEN" \
  --executor "docker" \
  --docker-image "alpine:latest" \
  --description "mon-runner-docker" \
  --tag-list "docker,linux" \
  --docker-privileged    # nécessaire pour Docker-in-Docker

# ── Vérifier que le runner tourne ────────────────────────────
sudo gitlab-runner status
sudo gitlab-runner list
YAML — Cibler un runner par tags
# Job sur runner avec tag "docker"
build-docker:
  tags:
    - docker
    - linux

# Job sur runner K8s
deploy-k8s:
  tags:
    - kubernetes

# Job sur runner Windows
build-windows:
  tags:
    - windows
  script:
    - msbuild solution.sln

# ⚠ Si aucun runner ne correspond aux tags → job bloqué "stuck"
🆓 Runners GitLab.com — Sur GitLab.com, des runners partagés sont disponibles gratuitement (400 min/mois sur le plan Free, illimité sur Premium). Pas besoin d'installer quoi que ce soit pour commencer.
8

Services (Docker, BDD)

Les services lancent des conteneurs annexes accessibles depuis votre job via leur alias hostname. Parfait pour les tests d'intégration avec une vraie base de données.

🌐 Comment ça fonctionne — GitLab démarre le conteneur du service en même temps que le job. Le service est accessible via son alias (ou le nom de l'image par défaut). Les deux partagent le même réseau Docker.
YAML — Services BDD pour tests d'intégration
tests-integration:
  image: node:20-alpine
  services:
    - name: postgres:16-alpine
      alias: db                  # accès via hostname "db"
    - name: redis:7-alpine
      alias: redis
    - name: elasticsearch:8.11.0
      alias: elastic
      variables:
        discovery.type: single-node
  variables:
    POSTGRES_DB: testdb
    POSTGRES_USER: ci
    POSTGRES_PASSWORD: ci
    DATABASE_URL: "postgresql://ci:ci@db:5432/testdb"
    REDIS_URL: "redis://redis:6379"
  before_script:
    - npm ci
    - npx wait-on tcp:db:5432  # attendre que postgres soit prêt
  script:
    - npm run db:migrate
    - npm run test:integration
YAML — Docker-in-Docker (build d'image)
# Docker-in-Docker : exécuter Docker depuis un job Docker
build-image:
  image: docker:26
  services:
    - docker:26-dind              # Docker daemon dans un conteneur
  variables:
    DOCKER_TLS_CERTDIR: "/certs"  # TLS activé (recommandé)
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
    # Taguer aussi "latest" sur main
    - |
      if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then
        docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA $CI_REGISTRY_IMAGE:latest
        docker push $CI_REGISTRY_IMAGE:latest
      fi
ServiceImageAlias conseilléVariable clé
PostgreSQLpostgres:16-alpinedbPOSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD
MySQLmysql:8.0mysqlMYSQL_DATABASE, MYSQL_ROOT_PASSWORD
MongoDBmongo:7mongoMONGO_INITDB_DATABASE
Redisredis:7-alpineredis
Elasticsearchelasticsearch:8.11.0elasticdiscovery.type=single-node
RabbitMQrabbitmq:3-managementrabbitmqRABBITMQ_DEFAULT_USER/PASS
9

Environments & Deploy

Les environments tracent vos déploiements dans GitLab (Operate → Environments). Chaque deploy est enregistré avec son SHA, sa date et son auteur. Vous pouvez déclencher un rollback depuis l'UI.

PROGRESSION DES ENVIRONNEMENTS 💻 Code git push 🖥 Development when: on_success ✅ automatique dev.example.com 👁 review 🧪 Staging when: on_success ✅ automatique staging.example.com 🔐 approbation 🚀 Production when: manual 🖱 clic humain requis example.com ↩ rollback possible depuis GitLab UI (Operate → Environments)
YAML — Environments complet
deploy-staging:
  stage: deploy
  script:
    - ./deploy.sh $CI_ENVIRONMENT_URL
  environment:
    name: staging
    url: https://staging.example.com
    on_stop: stop-staging         # job à appeler pour supprimer l'env
    deployment_tier: staging

stop-staging:
  stage: deploy
  script:
    - ./teardown.sh staging
  environment:
    name: staging
    action: stop               # stop | start | prepare | verify | access
  when: manual

# ── Review Apps : environnement dynamique par MR ─────────────
deploy-review:
  stage: deploy
  script:
    - ./deploy-review.sh $CI_COMMIT_REF_SLUG
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    url: https://$CI_COMMIT_REF_SLUG.review.example.com
    on_stop: stop-review
    auto_stop_in: 1 week        # suppression automatique
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

stop-review:
  stage: deploy
  script: ./teardown.sh $CI_COMMIT_REF_SLUG
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    action: stop
  when: manual
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: manual

# ── Production : déclenchement sur tag uniquement ─────────────
deploy-prod:
  stage: deploy
  script:
    - ./deploy.sh production
  environment:
    name: production
    url: https://example.com
    deployment_tier: production
  rules:
    - if: '$CI_COMMIT_TAG =~ /^v\d+/'
      when: manual
10

Extends & Anchors YAML

Pour éviter la duplication dans le YAML. extends est la méthode GitLab recommandée, les YAML anchors (&, *) sont une alternative native YAML.

extends — héritage de jobs (recommandé)
YAML — extends
# Job de base — préfixe . = caché (non exécuté directement)
.base-node:
  image: node:20-alpine
  before_script:
    - npm ci --cache .npm
  cache:
    key: "$CI_COMMIT_REF_SLUG"
    paths: [.npm/]

# .rules-mr-only — conditions réutilisables
.rules-mr-only:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

build:
  extends: .base-node        # hérite image, before_script, cache
  stage: build
  script: npm run build      # seule différence

test:
  extends: .base-node
  stage: test
  script: npm test

# extends multiple (héritage de plusieurs sources)
lint:
  extends:
    - .base-node
    - .rules-mr-only           # applique aussi les rules
  script: npm run lint
Anchors YAML — alternative native
YAML — Anchors & alias
# Définir un ancre &
.before_npm: &before_npm
  - npm ci
  - echo "Dépendances prêtes"

# Réutiliser avec *
build:
  before_script: *before_npm
  script: npm run build

# Merge de maps avec <<:
.common: &common
  image: node:20
  tags: [docker]
  retry: 2

my-job:
  <<: *common               # fusionne les clés de .common
  script: echo "hello"
💡 extends vs anchors — Préférez extends : il fusionne récursivement les clés imbriquées, les anchors YAML non. extends est aussi plus lisible dans l'interface GitLab (onglet "Full config").
11

Include & Templates

include permet de découper votre CI en plusieurs fichiers et de réutiliser des templates officiels GitLab ou d'équipe.

YAML — Include : toutes les sources
include:
  # Fichier local dans ce repo
  - local: ".gitlab/ci/build.yml"
  - local: ".gitlab/ci/deploy.yml"

  # Template officiel GitLab (hébergé sur gitlab.com)
  - template: Jobs/SAST.gitlab-ci.yml
  - template: Workflows/MergeRequest-Pipelines.gitlab-ci.yml

  # Depuis un autre projet GitLab (templates d'équipe)
  - project: my-group/ci-templates
    ref: main
    file:
      - templates/docker.yml
      - templates/notify.yml

  # URL externe (HTTPS)
  - remote: https://example.com/ci/shared-template.yml

# Surcharger un job venant d'un template inclus
SAST:
  variables:
    SAST_EXCLUDED_PATHS: "spec,test,tests,tmp,node_modules"
  rules:
    - if: $CI_PIPELINE_SOURCE != "schedule"
Templates officiels les plus utiles
TemplateContenu
Jobs/SAST.gitlab-ci.ymlAnalyse statique de sécurité du code source
Jobs/Secret-Detection.gitlab-ci.ymlDétection de secrets (tokens, clés) dans le code
Jobs/Container-Scanning.gitlab-ci.ymlScan des vulnérabilités dans les images Docker
Jobs/Dependency-Scanning.gitlab-ci.ymlVulnérabilités dans les dépendances (npm, pip…)
Jobs/DAST.gitlab-ci.ymlScan dynamique de l'application déployée
Workflows/MergeRequest-Pipelines.ymlConfigure les pipelines pour les MR
Auto-DevOps.gitlab-ci.ymlPipeline complet clés en main (build, test, deploy K8s)
📁 Organiser son CI — Pour les gros projets, créez un dossier .gitlab/ci/ avec un fichier par domaine : build.yml, test.yml, deploy.yml, security.yml. Puis include-les depuis .gitlab-ci.yml.
12

Parallel matrix

parallel duplique un job. parallel: matrix crée une combinatoire de variables — idéal pour tester sur plusieurs versions ou déployer en multi-régions.

YAML — Parallel & Matrix
# N copies identiques d'un job (utile pour fragmenter une suite de tests)
rspec:
  script: bundle exec rspec
  parallel: 5                   # 5 jobs identiques → $CI_NODE_INDEX (1..5)

# Matrix — combinatoire de variables
test-matrix:
  image: "$RUNTIME:$VERSION"
  script: npm test
  parallel:
    matrix:
      - RUNTIME: node
        VERSION: ["18", "20", "22"]
      - RUNTIME: bun
        VERSION: ["1.0", "1.1"]
  # → 5 jobs : node:18, node:20, node:22, bun:1.0, bun:1.1

# Matrix pour déploiement multi-régions × multi-envs
deploy:
  script:
    - ./deploy.sh --region $REGION --env $ENV
  environment:
    name: "$ENV/$REGION"
  parallel:
    matrix:
      - REGION: [eu-west-1, us-east-1, ap-southeast-1]
        ENV: [staging, production]
  # → 6 jobs (3 régions × 2 envs)
13

Pipelines parent / enfant

Pour les monorepos ou projets complexes : le pipeline parent orchestre des sous-pipelines enfants, déclenchés selon les fichiers modifiés.

YAML — Pipeline parent
# .gitlab-ci.yml (pipeline parent)
stages:
  - triggers

trigger-frontend:
  stage: triggers
  trigger:
    include: frontend/.gitlab-ci.yml  # pipeline enfant local
    strategy: depend                  # parent attend la fin de l'enfant
  rules:
    - changes: [frontend/**/*]        # déclenché seulement si frontend modifié

trigger-api:
  trigger:
    project: my-group/api-service   # pipeline dans un AUTRE repo
    branch: main
  rules:
    - changes: [api/**/*]
YAML — Pipeline dynamique (généré à la volée)
# Job qui génère un fichier YAML dynamiquement (Python, bash, etc.)
generate-pipeline:
  script:
    - python3 scripts/gen_pipeline.py > generated-pipeline.yml
  artifacts:
    paths: [generated-pipeline.yml]

run-generated:
  trigger:
    include:
      - artifact: generated-pipeline.yml
        job: generate-pipeline
    strategy: depend
  needs: [generate-pipeline]
14

Security scans (SAST, DAST…)

GitLab embarque des scanners de sécurité accessibles via des templates officiels. Les résultats s'affichent directement dans les MR.

YAML — Activer les scanners de sécurité
# Inclure les scanners souhaités
include:
  - template: Jobs/SAST.gitlab-ci.yml
  - template: Jobs/Secret-Detection.gitlab-ci.yml
  - template: Jobs/Dependency-Scanning.gitlab-ci.yml
  - template: Jobs/Container-Scanning.gitlab-ci.yml

# Personnaliser SAST (exclure des dossiers)
semgrep-sast:
  variables:
    SAST_EXCLUDED_PATHS: "spec,test,tests,tmp,node_modules"
    SAST_EXCLUDED_ANALYZERS: "bandit,flawfinder"

# DAST — scanner l'appli déployée (nécessite un env accessible)
include:
  - template: DAST.gitlab-ci.yml

dast:
  variables:
    DAST_WEBSITE: https://staging.example.com
    DAST_FULL_SCAN_ENABLED: "true"
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# Container scanning — analyser une image Docker
container_scanning:
  variables:
    CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
ScannerCe qu'il cherchePlan requis
SASTVulnérabilités dans le code source (injection, XSS…)Free (résultats basiques)
Secret DetectionClés API, tokens, mots de passe dans le codeFree
Dependency ScanningCVE dans les dépendances npm, pip, maven…Ultimate complet
Container ScanningCVE dans les layers de l'image DockerUltimate complet
DASTVulnérabilités sur l'appli en cours d'exécutionUltimate complet
License ScanningLicences des dépendances (GPL, MIT…)Ultimate
📊 Résultats — Visibles dans votre MR (onglet Security) et dans Secure → Vulnerability Report du projet. GitLab bloque la MR si des vulnérabilités critiques sont détectées (configurable).
15

Bonnes pratiques

YAML — Workflow recommandé (évite doublons push+MR)
workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_TAG
    - if: $CI_PIPELINE_SOURCE == "schedule"
    - when: never                  # tous les autres cas ignorés

# Optimisations de performance
.slim-defaults:
  image: alpine:3.19           # préférer :alpine ou :slim
  variables:
    GIT_DEPTH: "1"             # shallow clone
    FF_USE_FASTZIP: "true"     # compression artifacts plus rapide
    TRANSFER_METER_FREQUENCY: "5s"
Check-list CI/CD
✅ À FAIRE
// Utiliser rules: plutôt que only:/except: (déprécié)
// Versionner les images Docker (node:20.11 > node:latest)
// Séparer build / test / deploy en stages distincts
// Utiliser needs: pour paralléliser et accélérer le pipeline
// Stocker les secrets dans CI/CD Variables (masked + protected)
// Factoriser avec .hidden-jobs + extends:
// Activer Secret Detection au minimum (gratuit)
// Fixer expire_in sur les artifacts non essentiels
// Utiliser GIT_DEPTH: "1" si historique git non nécessaire
// Valider le YAML avec CI Lint avant de pusher
// Utiliser workflow: rules pour éviter les pipelines en double

❌ À ÉVITER
// Stocker des secrets en clair dans le YAML ou le code
// image: latest (non reproductible, peut casser)
// allow_failure: true sur des jobs critiques (sécurité, tests)
// Scripts inline trop longs — extraire dans des fichiers .sh
// Un seul job monolithique qui fait tout (illisible, lent)
// Cache avec policy: pull-push dans des jobs parallèles (conflits)
// Ignorer les rapports Security dans les MR
// Laisser des tokens/credentials dans les variables non-masked
16

Exemple complet — Application Node.js + Docker

Un pipeline réel et complet pour une application Node.js/Express déployée avec Docker. Copiez-collez et adaptez les variables à votre projet.

Flux du pipeline
install quality build docker deploy-staging deploy-prod 🖱
.gitlab-ci.yml — Pipeline complet Node.js + Docker
# ═══════════════════════════════════════════════════════════════
# Pipeline complet : install → quality → build → docker → deploy
# Adapté pour : Node.js 20, Docker, déploiement SSH/Kubernetes
# ═══════════════════════════════════════════════════════════════

# Évite les pipelines en double (push + MR)
workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_TAG
    - when: never

stages:
  - install
  - quality
  - build
  - docker
  - deploy-staging
  - deploy-prod

variables:
  NODE_VERSION: "20"
  IMAGE_TAG: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
  IMAGE_LATEST: "$CI_REGISTRY_IMAGE:latest"

# ── Template de base partagé ──────────────────────────────────
.node-base:
  image: node:$NODE_VERSION-alpine
  cache:
    key:
      files: [package-lock.json]
    paths: [node_modules/, .npm/]
    policy: pull

# ── STAGE : install ───────────────────────────────────────────
install-dependencies:
  extends: .node-base
  stage: install
  cache:
    key:
      files: [package-lock.json]
    paths: [node_modules/, .npm/]
    policy: pull-push            # ce job écrit le cache
  script:
    - npm ci --cache .npm --prefer-offline

# ── STAGE : quality (parallèle) ───────────────────────────────
lint:
  extends: .node-base
  stage: quality
  needs: [install-dependencies]
  script:
    - npm run lint
  allow_failure: false

unit-tests:
  extends: .node-base
  stage: quality
  needs: [install-dependencies]
  script:
    - npm test -- --coverage --ci
  coverage: '/Statements\s*:\s*(\d+\.?\d*)%/'
  artifacts:
    when: always
    reports:
      junit: junit.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura.xml
    paths: [coverage/]
    expire_in: 30 days

security-audit:
  extends: .node-base
  stage: quality
  needs: [install-dependencies]
  script:
    - npm audit --audit-level=high
  allow_failure: true            # ne bloque pas si vulnerabilité modérée

# ── STAGE : build ─────────────────────────────────────────────
build-app:
  extends: .node-base
  stage: build
  needs: [lint, unit-tests]
  script:
    - npm run build
  artifacts:
    paths: [dist/]
    expire_in: 1 week

# ── STAGE : docker ────────────────────────────────────────────
build-image:
  stage: docker
  image: docker:26
  services:
    - docker:26-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  needs:
    - job: build-app
      artifacts: true
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker build --build-arg NODE_ENV=production -t $IMAGE_TAG .
    - docker push $IMAGE_TAG
    - |
      if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then
        docker tag $IMAGE_TAG $IMAGE_LATEST
        docker push $IMAGE_LATEST
      fi
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_TAG

# ── STAGE : deploy-staging ────────────────────────────────────
deploy-to-staging:
  stage: deploy-staging
  image: alpine:3.19
  needs: [build-image]
  before_script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | ssh-add -      # clé SSH définie dans CI/CD Variables
    - mkdir -p ~/.ssh && chmod 700 ~/.ssh
    - ssh-keyscan $STAGING_HOST >> ~/.ssh/known_hosts
  script:
    - ssh deploy@$STAGING_HOST "docker pull $IMAGE_TAG && docker-compose up -d"
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# ── STAGE : deploy-prod (manuel, sur tag uniquement) ──────────
deploy-to-production:
  stage: deploy-prod
  image: alpine:3.19
  needs: [deploy-to-staging]
  before_script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | ssh-add -
    - mkdir -p ~/.ssh && chmod 700 ~/.ssh
    - ssh-keyscan $PROD_HOST >> ~/.ssh/known_hosts
  script:
    - ssh deploy@$PROD_HOST "docker pull $IMAGE_TAG && docker-compose up -d"
  environment:
    name: production
    url: https://example.com
    deployment_tier: production
  rules:
    - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
      when: manual
Dockerfile correspondant
Dockerfile — Multi-stage optimisé
# Étape 1 : build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist/ ./dist/

# Étape 2 : image finale (légère)
FROM node:20-alpine
WORKDIR /app
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=builder /app .
USER appuser
EXPOSE 3000
CMD ["node", "dist/index.js"]
Variables CI/CD à configurer dans GitLab UI
VariableTypeValeur
SSH_PRIVATE_KEYFile / MaskedClé SSH privée du compte deploy
STAGING_HOSTVariableIP ou hostname du serveur staging
PROD_HOSTVariable / ProtectedIP ou hostname du serveur prod
17

Troubleshooting — Résoudre les problèmes courants

🟡 Job bloqué en "pending" ou "stuck" indéfiniment
CauseAucun runner disponible ne correspond aux tags du job. Ou tous les runners sont occupés / hors ligne.
SolutionVérifiez Settings → CI/CD → Runners. Assurez-vous qu'un runner actif a les tags correspondants. Si aucun tag dans le job, n'importe quel runner shared peut l'exécuter. Vérifiez que le runner n'est pas "paused".
🔴 ERROR: Job failed — exit code 1
CauseUne commande dans le script a retourné un code non-zéro. En bash, toute commande qui échoue arrête le job.
SolutionCliquez sur le job → regardez les logs. La commande en rouge indique le problème. Ajoutez set -x en début de script pour voir chaque commande exécutée. Temporairement : command || true pour ignorer une erreur.
🔴 Could not find artifact from job
CauseLe job qui produit l'artifact a échoué (ou n'a pas été exécuté), ou le chemin dans artifacts.paths est incorrect.
SolutionVérifiez que le job source a bien réussi. Vérifiez le chemin : il doit être relatif au répertoire de travail. Ajoutez when: always pour conserver les artifacts même en cas d'échec.
🟡 Cache non restauré / ignoré
CauseLa clé de cache a changé (ex: nouveau package-lock.json). Ou le runner ne dispose pas de cache préexistant (premier run, runner différent).
SolutionLe cache est best-effort, pas une garantie. Si la clé change, le cache repart de zéro — c'est normal. Pour diagnostiquer : activez CI_DEBUG_TRACE: "true" (attention : affiche tous les secrets).
🔴 Rules : le job n'est jamais dans le pipeline
CauseAucune règle ne correspond. Ou la syntaxe de la condition est incorrecte (guillemets manquants, espace dans l'opérateur).
SolutionTestez dans CI Lint (CI/CD → Pipelines → CI Lint). Ajoutez temporairement - when: on_success à la fin des rules pour voir si le job apparaît. Vérifiez la valeur réelle de la variable avec - echo "$CI_PIPELINE_SOURCE".
🔴 Docker: Cannot connect to the Docker daemon
CauseDocker-in-Docker n'est pas correctement configuré. Le service docker:dind n'est pas démarré ou l'executor n'est pas en mode privileged.
SolutionVérifiez : (1) service docker:XX-dind bien déclaré, (2) variable DOCKER_TLS_CERTDIR: "/certs", (3) runner enregistré avec --docker-privileged. Alternative : utiliser kaniko (sans DinD).
🔴 Permission denied (publickey) — déploiement SSH
CauseLa clé SSH n'est pas chargée correctement, ou la clé publique n'est pas dans ~/.ssh/authorized_keys du serveur cible.
SolutionVérifiez : (1) ssh-add -l dans le job liste la clé, (2) la variable CI contient bien la clé privée complète (avec -----BEGIN/END-----), (3) ssh-keyscan est fait avant le premier ssh, (4) permissions : chmod 700 ~/.ssh.
🟡 YAML invalide — pipeline ne se déclenche pas
CauseErreur d'indentation, tabulations au lieu d'espaces, guillemets non fermés, ou clé inconnue.
SolutionUtilisez CI/CD → Pipelines → CI Lint dans GitLab avant chaque push. Ou validez localement avec yamllint .gitlab-ci.yml. Les tabulations sont interdites en YAML — utilisez toujours des espaces.
BASH — Outils de diagnostic
# Activer le mode debug (affiche toutes les commandes + variables)
# ⚠️ Expose les secrets masqués dans les logs !
my-job:
  variables:
    CI_DEBUG_TRACE: "true"      # ou: CI_DEBUG_SERVICES pour les services
  script:
    - set -x                   # trace bash (moins intrusif)
    - env | sort               # affiche toutes les variables d'env
    - whoami && pwd            # vérifier l'utilisateur et le répertoire

# Valider le YAML localement (installer gitlab-ci-local)
npm install -g gitlab-ci-local
gitlab-ci-local --job mon-job  # exécute un job en local

# Valider via l'API GitLab (sans push)
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
     --form "content=@.gitlab-ci.yml" \
     https://gitlab.com/api/v4/ci/lint
18

Glossaire CI/CD

Artifact
Fichier(s) produit(s) par un job et conservé sur GitLab. Partageable entre jobs du même pipeline. Téléchargeable depuis l'UI.
Cache
Répertoire conservé entre les exécutions de pipelines pour accélérer les jobs (node_modules, pip cache…). Best-effort, pas garanti.
CI (Intégration Continue)
Pratique consistant à intégrer, compiler et tester automatiquement le code à chaque commit pour détecter les régressions rapidement.
CD (Livraison Continue)
Le code est toujours dans un état deployable. Chaque merge en branche principale produit un artefact prêt à être mis en production.
CD (Déploiement Continu)
Variante plus avancée : chaque commit validé est déployé automatiquement en production, sans intervention humaine.
DAG (Directed Acyclic Graph)
Pipeline en graphe : avec needs:, un job démarre dès que ses dépendances directes sont terminées, sans attendre tout le stage.
Environment
Représentation d'un environnement de déploiement (staging, production). GitLab trace l'historique des déploiements et permet le rollback.
Executor
Mode d'exécution du runner : docker (conteneur isolé), shell (machine hôte), kubernetes (pod éphémère). Docker est recommandé.
extends
Mot-clé GitLab CI pour hériter les propriétés d'un job de base (commençant par .). Fusion récursive des clés.
Hidden job (.nom)
Job dont le nom commence par .. Non exécuté directement, utilisé comme template pour extends.
include
Directive pour importer d'autres fichiers YAML (locaux, templates officiels, projets externes) dans votre pipeline.
Job
Unité d'exécution atomique du pipeline. Contient un script, un stage, et éventuellement des rules, artifacts, cache.
MR Pipeline
Pipeline déclenché par une Merge Request. Source : merge_request_event. Permet de valider le code avant de merger.
needs
Déclare les dépendances directes d'un job, permettant l'exécution en DAG plutôt qu'en séquence stricte par stages.
Pipeline
Ensemble des stages et jobs déclenchés par un événement (push, MR, tag, schedule). Visible dans CI/CD → Pipelines.
Registry
Dépôt d'images Docker intégré à GitLab (registry.gitlab.com). Accessible avec CI_REGISTRY_USER/PASSWORD.
Review App
Environnement dynamique créé automatiquement pour chaque MR. Permet de tester visuellement les changements avant merger.
rules
Remplace only/except. Conditions évaluées dans l'ordre pour décider si un job est inclus dans le pipeline et avec quel comportement.
Runner
Agent installé sur une machine qui récupère et exécute les jobs GitLab. Peut être partagé (GitLab.com) ou dédié (self-hosted).
SAST
Static Application Security Testing. Analyse le code source à la recherche de vulnérabilités connues sans l'exécuter.
Schedule
Pipeline planifié (type cron). Configuré dans CI/CD → Schedules. Source : schedule. Utile pour backups, rapports nocturnes.
Service
Conteneur Docker lancé en parallèle du job (postgres, redis…). Accessible via son alias hostname depuis le script du job.
Stage
Groupe logique de jobs. Les jobs d'un même stage s'exécutent en parallèle. Les stages s'enchaînent séquentiellement.
trigger
Déclenche un pipeline enfant (local ou dans un autre projet). Avec strategy: depend, le parent attend la fin de l'enfant.
variables (prédéfinies)
Variables injectées automatiquement par GitLab : CI_COMMIT_SHA, CI_COMMIT_BRANCH, CI_REGISTRY, etc. Toujours disponibles.
when
Condition d'exécution d'un job : on_success, on_failure, always, manual, delayed, never.
workflow
Contrôle global de création du pipeline (pas d'un job individuel). Évite les pipelines en double push+MR.

GitLab CI/CD Cheatsheet — GitLab 17.x — 18 sections — 2026