🔝 Retour au Sommaire
Le fichier .gitlab-ci.yml est le cœur de tout pipeline GitLab CI. Placé à la racine du dépôt, il décrit de manière déclarative l'intégralité du processus d'intégration continue : quels stages exécuter, dans quel ordre, avec quels outils, dans quel environnement, et sous quelles conditions. Pour un projet C++, ce fichier devient rapidement substantiel car il doit orchestrer la compilation (souvent multi-compilateur), les tests, les sanitizers, l'analyse statique et le packaging.
Cette section détaille la syntaxe et les directives YAML essentielles pour construire un .gitlab-ci.yml adapté à un projet C++ moderne, puis présente un fichier complet et commenté que vous pourrez utiliser comme point de départ.
Un fichier .gitlab-ci.yml pour un projet C++ suit une structure récurrente composée de plusieurs blocs de premier niveau :
# ── Variables globales ──────────────────────────────────────
variables:
# ...
# ── Image Docker par défaut ─────────────────────────────────
default:
image: registry.exemple.com/cpp-build:latest
# ── Définition des stages ──────────────────────────────────
stages:
- lint
- build
- test
- package
- deploy
# ── Templates réutilisables (ancres YAML) ──────────────────
.build_template: &build_template
# ...
# ── Jobs ────────────────────────────────────────────────────
format-check:
stage: lint
# ...
build-gcc:
stage: build
# ...
unit-tests:
stage: test
# ...Chaque bloc remplit un rôle précis. Examinons-les un par un.
Le bloc variables définit des variables d'environnement accessibles par tous les jobs du pipeline. Pour un projet C++, ce bloc contient typiquement les chemins de build, les options CMake et la configuration de ccache :
variables:
# ── Répertoire de build CMake ──
BUILD_DIR: "build"
BUILD_TYPE: "Release"
# ── Standard C++ par défaut ──
CPP_STANDARD: "20"
# ── Configuration CMake ──
CMAKE_GENERATOR: "Ninja"
CMAKE_EXPORT_COMPILE_COMMANDS: "ON"
# ── Configuration ccache ──
CCACHE_DIR: "${CI_PROJECT_DIR}/.ccache"
CCACHE_MAXSIZE: "2G"
CCACHE_COMPILERCHECK: "content"
# ── Comportement Git ──
GIT_SUBMODULE_STRATEGY: recursive
GIT_DEPTH: 1Quelques points importants sur ces variables :
CCACHE_DIR est défini relativement à ${CI_PROJECT_DIR} (la racine du checkout). C'est une exigence pour que le cache GitLab CI puisse persister ce répertoire entre les exécutions — GitLab ne peut cacher que des chemins situés sous le répertoire de travail du projet.
CCACHE_COMPILERCHECK: "content" indique à ccache de vérifier le contenu du compilateur plutôt que son timestamp pour valider les entrées du cache. C'est nécessaire en environnement Docker où le compilateur peut avoir le même chemin mais un contenu différent entre deux images.
GIT_DEPTH: 1 effectue un clone superficiel (shallow clone) qui ne récupère que le dernier commit. Pour un projet C++ où le dépôt peut contenir un historique volumineux, cela réduit significativement le temps de checkout. Si votre build a besoin de l'historique complet (par exemple pour générer un numéro de version basé sur git describe), vous pouvez surcharger cette variable dans les jobs concernés.
GIT_SUBMODULE_STRATEGY: recursive est utile si votre projet intègre des dépendances comme sous-modules Git (une pratique courante en C++ pour inclure des librairies header-only ou des dépendances légères).
Les variables définies au niveau global peuvent être surchargées dans n'importe quel job. Par exemple, un job de build Debug redéfinira BUILD_TYPE: "Debug" localement.
Le bloc default définit des valeurs par défaut appliquées à tous les jobs du pipeline, sauf si un job les surcharge explicitement :
default:
image: registry.exemple.com/cpp-build:latest
before_script:
- g++ --version || true
- cmake --version
- ninja --version
- ccache --zero-stats
after_script:
- ccache --show-statsimage spécifie l'image Docker utilisée par défaut. Plutôt que de répéter cette directive dans chaque job, la définir dans default centralise le choix et facilite la mise à jour.
before_script est exécuté avant le script principal de chaque job. Afficher les versions des outils en début de job est une bonne pratique : en cas de problème, les logs montrent immédiatement quel compilateur et quelles versions d'outils ont été utilisés. L'appel à ccache --zero-stats remet à zéro les compteurs pour que les statistiques affichées dans after_script reflètent uniquement le job en cours.
after_script est exécuté après le script principal, même si celui-ci a échoué. Afficher les statistiques ccache en fin de job permet de surveiller le taux de cache hit et de détecter une dégradation de la performance du cache.
Le bloc stages définit l'ordre séquentiel des étapes du pipeline :
stages:
- lint
- build
- test
- package
- deployLes stages s'exécutent dans l'ordre déclaré. Tous les jobs d'un même stage s'exécutent en parallèle (si des runners sont disponibles), et le pipeline ne passe au stage suivant que lorsque tous les jobs du stage courant ont réussi. Si un job du stage build échoue, aucun job du stage test ne sera lancé.
Ce comportement séquentiel entre stages correspond exactement à la logique d'un pipeline C++ : il est inutile de lancer les tests si la compilation a échoué, et il est inutile de packager si les tests ne passent pas.
Le parallélisme intra-stage est en revanche très précieux : les jobs build-gcc et build-clang peuvent compiler simultanément, exploitant deux runners en parallèle et divisant le temps d'attente.
Un job est l'unité de travail fondamentale. Voici la structure complète d'un job de build C++ avec toutes les directives pertinentes :
build-gcc-release:
stage: build
image: registry.exemple.com/cpp-build:latest
variables:
CXX: "g++-15"
CC: "gcc-15"
BUILD_TYPE: "Release"
cache:
key: "gcc-15-${CI_COMMIT_REF_SLUG}"
paths:
- .ccache/
policy: pull-push
before_script:
- ccache --zero-stats
script:
- cmake -B ${BUILD_DIR}
-G ${CMAKE_GENERATOR}
-DCMAKE_BUILD_TYPE=${BUILD_TYPE}
-DCMAKE_CXX_STANDARD=${CPP_STANDARD}
-DCMAKE_CXX_COMPILER=${CXX}
-DCMAKE_C_COMPILER=${CC}
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache
-DCMAKE_EXPORT_COMPILE_COMMANDS=${CMAKE_EXPORT_COMPILE_COMMANDS}
- cmake --build ${BUILD_DIR} --parallel $(nproc)
after_script:
- ccache --show-stats
artifacts:
paths:
- ${BUILD_DIR}/
expire_in: 1 hour
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'Décortiquons chaque directive.
Rattache le job à un stage déclaré dans le bloc stages. Le job build-gcc-release appartient au stage build et s'exécutera donc après le stage lint et avant le stage test.
L'image Docker dans laquelle le job s'exécute. Si elle est identique à celle définie dans default, cette ligne peut être omise. Elle devient nécessaire pour surcharger l'image — par exemple, un job de build Clang pourrait utiliser silkeh/clang:20 tandis que le job GCC utilise gcc:15.
Variables d'environnement locales au job, qui surchargent les variables globales. Ici, CXX et CC sont définis pour que CMake utilise GCC 15. Le BUILD_TYPE est surchargé en Release.
La directive cache contrôle la persistance de répertoires entre les exécutions successives du même job :
cache:
key: "gcc-15-${CI_COMMIT_REF_SLUG}"
paths:
- .ccache/
policy: pull-pushkey identifie le cache. Un cache est partagé entre les exécutions qui ont la même clé. Inclure le nom du compilateur dans la clé est indispensable : le cache ccache d'un build GCC n'est pas réutilisable par un build Clang (les fichiers objets sont incompatibles). ${CI_COMMIT_REF_SLUG} est une variable prédéfinie GitLab qui contient le nom de la branche nettoyé — cela permet à chaque branche d'avoir son propre cache, tout en bénéficiant du cache de la branche parente si aucun cache local n'existe encore.
paths liste les répertoires à cacher. Le chemin est relatif à ${CI_PROJECT_DIR}. C'est pourquoi la variable globale CCACHE_DIR est définie comme ${CI_PROJECT_DIR}/.ccache — pour que ccache écrive dans un répertoire que GitLab CI sait persister.
policy contrôle le comportement du cache. pull-push (valeur par défaut) signifie que le job télécharge le cache en début d'exécution et l'uploade en fin d'exécution. Pour les jobs qui ne modifient pas le cache (comme les jobs de test), vous pouvez utiliser pull pour éviter un upload inutile.
Le bloc script contient les commandes exécutées séquentiellement. C'est le cœur du job. Pour un job de build C++, cela se résume généralement à deux commandes : la configuration CMake (cmake -B) et la compilation (cmake --build).
L'option --parallel $(nproc) indique à CMake (et à Ninja derrière) d'utiliser tous les cœurs disponibles sur le runner. Sur un runner dédié avec 16 cœurs, cela peut diviser le temps de compilation par un facteur significatif par rapport à un build mono-thread.
Les artifacts sont des fichiers ou répertoires produits par un job et rendus disponibles pour les jobs des stages suivants :
artifacts:
paths:
- ${BUILD_DIR}/
expire_in: 1 hourIci, tout le répertoire de build est déclaré comme artifact. Les jobs du stage test pourront ainsi accéder aux binaires compilés sans avoir à recompiler. expire_in définit la durée de rétention — une heure est suffisante pour un artifact interne au pipeline, mais les artifacts de release (paquets DEB, binaires) devraient avoir une durée plus longue ou être stockés dans un registry externe.
⚠️ Attention à la taille des artifacts. Déclarer tout le répertoirebuild/comme artifact peut représenter plusieurs centaines de mégaoctets (fichiers objets, librairies intermédiaires, compilation database). Pour optimiser, vous pouvez ne déclarer que les binaires finaux nécessaires aux tests :artifacts: paths: - ${BUILD_DIR}/bin/ - ${BUILD_DIR}/tests/ - ${BUILD_DIR}/compile_commands.json expire_in: 1 hour
La directive rules détermine quand le job doit s'exécuter :
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'Cette configuration signifie : exécuter le job sur les merge requests et sur les commits poussés directement sur main. Les commits sur les branches de feature qui n'ont pas de merge request ouverte ne déclencheront pas ce job.
rules remplace l'ancienne directive only/except qui est aujourd'hui considérée comme dépréciée. La syntaxe rules est plus explicite et plus puissante, supportant des conditions combinées avec when, changes et allow_failure :
rules:
# Exécuter sur les MR qui touchent du code source
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- src/**/*
- include/**/*
- CMakeLists.txt
- tests/**/*
# Toujours exécuter sur main
- if: '$CI_COMMIT_BRANCH == "main"'
# Toujours exécuter sur les tags de version
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'La directive changes est particulièrement utile pour un projet C++ : si un commit ne modifie que la documentation ou le README, il n'y a aucune raison de déclencher une compilation complète.
À mesure que le nombre de jobs augmente (GCC Debug, GCC Release, Clang Debug, Clang Release, builds avec sanitizers…), la duplication de configuration devient un problème. Les ancres YAML et la directive extends permettent de factoriser les parties communes.
Les ancres sont une fonctionnalité native de YAML, pas de GitLab CI. Elles permettent de définir un bloc réutilisable :
# Définition d'un template (le point au début du nom empêche
# GitLab de traiter ce bloc comme un job)
.cmake_build: &cmake_build
stage: build
cache:
paths:
- .ccache/
policy: pull-push
before_script:
- ccache --zero-stats
script:
- cmake -B ${BUILD_DIR}
-G Ninja
-DCMAKE_BUILD_TYPE=${BUILD_TYPE}
-DCMAKE_CXX_STANDARD=${CPP_STANDARD}
-DCMAKE_CXX_COMPILER=${CXX}
-DCMAKE_C_COMPILER=${CC}
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
- cmake --build ${BUILD_DIR} --parallel $(nproc)
after_script:
- ccache --show-stats
artifacts:
paths:
- ${BUILD_DIR}/
expire_in: 1 hour
# Utilisation du template avec surcharge
build-gcc-release:
<<: *cmake_build
variables:
CXX: "g++-15"
CC: "gcc-15"
BUILD_TYPE: "Release"
cache:
key: "gcc15-release-${CI_COMMIT_REF_SLUG}"
paths:
- .ccache/
build-clang-release:
<<: *cmake_build
variables:
CXX: "clang++-20"
CC: "clang-20"
BUILD_TYPE: "Release"
cache:
key: "clang20-release-${CI_COMMIT_REF_SLUG}"
paths:
- .ccache/L'opérateur <<: *cmake_build injecte tout le contenu de l'ancre dans le job, et les directives locales (comme variables et cache) surchargent celles du template.
⚠️ Limitation des ancres YAML : l'opérateur<<:effectue un merge superficiel. Si le template définit un blocvariableset que le job en définit un autre, le bloc du job remplace entièrement celui du template — il ne fusionne pas les deux. C'est pourquoi les variables véritablement globales sont mieux placées dans le blocvariablesde premier niveau.
GitLab CI propose extends comme alternative aux ancres YAML. Le comportement est similaire mais extends effectue un merge profond (deep merge), ce qui résout la limitation des ancres sur les blocs imbriqués :
.cmake_build:
stage: build
variables:
BUILD_TYPE: "Release"
cache:
paths:
- .ccache/
policy: pull-push
script:
- cmake -B ${BUILD_DIR}
-G Ninja
-DCMAKE_BUILD_TYPE=${BUILD_TYPE}
-DCMAKE_CXX_STANDARD=${CPP_STANDARD}
-DCMAKE_CXX_COMPILER=${CXX}
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
- cmake --build ${BUILD_DIR} --parallel $(nproc)
artifacts:
paths:
- ${BUILD_DIR}/
expire_in: 1 hour
build-gcc-release:
extends: .cmake_build
variables:
CXX: "g++-15"
CC: "gcc-15"
cache:
key: "gcc15-release-${CI_COMMIT_REF_SLUG}"
build-clang-release:
extends: .cmake_build
variables:
CXX: "clang++-20"
CC: "clang-20"
cache:
key: "clang20-release-${CI_COMMIT_REF_SLUG}"Avec extends, le bloc cache du job est fusionné avec celui du template : la clé key du job est ajoutée, et les clés paths et policy du template sont conservées. Avec les ancres YAML, le bloc cache du job aurait entièrement remplacé celui du template, perdant paths et policy.
Recommandation : privilégiez extends pour les pipelines C++ complexes. La fusion profonde évite des bugs subtils lorsque les jobs surchargent partiellement des blocs imbriqués.
Par défaut, GitLab CI impose un ordre strictement séquentiel entre les stages. Tous les jobs d'un stage doivent terminer avant que le stage suivant ne commence. La directive needs permet de court-circuiter cette contrainte en déclarant des dépendances explicites entre jobs :
unit-tests-gcc:
stage: test
needs:
- job: build-gcc-release
artifacts: true
script:
- cd ${BUILD_DIR} && ctest --output-on-failure --parallel $(nproc)Avec needs, le job unit-tests-gcc démarre dès que build-gcc-release a terminé, sans attendre que build-clang-release ou les autres jobs du stage build soient terminés. Sur un pipeline avec de nombreux jobs de build, cela peut réduire significativement le temps total du pipeline.
La directive artifacts: true indique que ce job a besoin des artifacts produits par le job référencé. C'est le mécanisme qui permet au job de test d'accéder aux binaires compilés par le job de build.
GitLab CI expose de nombreuses variables d'environnement prédéfinies. Voici celles qui sont les plus utiles dans un contexte C++ :
| Variable | Description | Usage typique en C++ |
|---|---|---|
CI_PROJECT_DIR |
Chemin absolu du checkout | Base pour CCACHE_DIR |
CI_COMMIT_REF_SLUG |
Nom de branche nettoyé | Clé de cache par branche |
CI_COMMIT_TAG |
Nom du tag (si applicable) | Déclencher le packaging |
CI_COMMIT_SHORT_SHA |
Hash court du commit | Versioning des binaires |
CI_PIPELINE_SOURCE |
Origine du pipeline | Filtrer MR vs push |
CI_DEFAULT_BRANCH |
Branche par défaut | Fallback du cache |
CI_JOB_NAME |
Nom du job courant | Nommage des artifacts |
CI_REGISTRY_IMAGE |
URL du registry Docker | Push d'images de build |
La variable CI_COMMIT_TAG est particulièrement utile pour conditionner le stage package : la création de paquets DEB ou d'une release n'a de sens que lorsqu'un tag de version est poussé.
Voici un .gitlab-ci.yml complet et fonctionnel pour un projet C++ moderne, intégrant tous les concepts abordés dans cette section. Ce fichier sera étoffé dans les sections suivantes (38.1.2 pour les jobs détaillés, 38.3 pour l'accélération) :
# ============================================================
# .gitlab-ci.yml — Pipeline CI/CD pour un projet C++ / CMake
# ============================================================
# ── Variables globales ──────────────────────────────────────
variables:
BUILD_DIR: "build"
CPP_STANDARD: "20"
CCACHE_DIR: "${CI_PROJECT_DIR}/.ccache"
CCACHE_MAXSIZE: "2G"
CCACHE_COMPILERCHECK: "content"
GIT_SUBMODULE_STRATEGY: recursive
GIT_DEPTH: 1
# ── Image et comportement par défaut ────────────────────────
default:
image: registry.exemple.com/cpp-build:latest
before_script:
- ccache --zero-stats
# ── Stages (ordre séquentiel) ──────────────────────────────
stages:
- lint
- build
- test
- package
- deploy
# ── Template de build CMake ─────────────────────────────────
.cmake_build:
stage: build
script:
- cmake -B ${BUILD_DIR}
-G Ninja
-DCMAKE_BUILD_TYPE=${BUILD_TYPE}
-DCMAKE_CXX_STANDARD=${CPP_STANDARD}
-DCMAKE_CXX_COMPILER=${CXX}
-DCMAKE_C_COMPILER=${CC}
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
- cmake --build ${BUILD_DIR} --parallel $(nproc)
after_script:
- ccache --show-stats
cache:
paths:
- .ccache/
policy: pull-push
artifacts:
paths:
- ${BUILD_DIR}/
expire_in: 2 hours
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- if: '$CI_COMMIT_TAG'
# ── Template de test ────────────────────────────────────────
.cmake_test:
stage: test
script:
- cd ${BUILD_DIR}
- ctest --output-on-failure --parallel $(nproc)
cache:
paths:
- .ccache/
policy: pull # Lecture seule — le job de test ne compile pas
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- if: '$CI_COMMIT_TAG'
# ── Jobs de lint ────────────────────────────────────────────
format-check:
stage: lint
script:
- find src include -name '*.cpp' -o -name '*.hpp'
| xargs clang-format-20 --dry-run --Werror
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
# ── Jobs de build ───────────────────────────────────────────
build-gcc-release:
extends: .cmake_build
variables:
CXX: "g++-15"
CC: "gcc-15"
BUILD_TYPE: "Release"
cache:
key: "gcc15-release-${CI_COMMIT_REF_SLUG}"
build-clang-release:
extends: .cmake_build
variables:
CXX: "clang++-20"
CC: "clang-20"
BUILD_TYPE: "Release"
cache:
key: "clang20-release-${CI_COMMIT_REF_SLUG}"
# ── Jobs de test ────────────────────────────────────────────
test-gcc:
extends: .cmake_test
needs:
- job: build-gcc-release
artifacts: true
test-clang:
extends: .cmake_test
needs:
- job: build-clang-release
artifacts: true
# ── Job de packaging (tags uniquement) ──────────────────────
package-deb:
stage: package
needs:
- job: build-gcc-release
artifacts: true
script:
- echo "Packaging DEB..." # Détaillé en section 38.5
artifacts:
paths:
- "*.deb"
expire_in: 30 days
rules:
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'Ce fichier contient environ 100 lignes et couvre un pipeline fonctionnel avec lint, build multi-compilateur, tests, et packaging conditionnel. Les sections suivantes enrichiront chaque partie avec des configurations plus avancées.
Avant de passer à la définition détaillée des jobs (section 38.1.2), retenez ces principes pour un .gitlab-ci.yml maintenable :
Factorisez avec extends. Dès que deux jobs partagent plus de trois lignes de configuration, extrayez un template préfixé par un point (.cmake_build, .cmake_test). Le pipeline devient plus lisible et les modifications se font en un seul endroit.
Nommez vos jobs de manière descriptive. build-gcc-release est bien plus lisible dans l'interface GitLab que job1 ou build_1. Le nom du job apparaît dans l'UI du pipeline, dans les notifications et dans les logs — il doit être auto-explicatif.
Séparez les clés de cache par compilateur. Un cache ccache GCC est inutilisable par Clang et vice versa. Utiliser une clé de cache unique pour tous les jobs de build invalide tout l'intérêt du cache à chaque alternance de compilateur.
Utilisez needs pour le parallélisme. Sans needs, le stage test attend que tous les jobs de build soient terminés. Avec needs, chaque job de test démarre dès que son job de build est terminé, ce qui peut réduire le temps total du pipeline de plusieurs minutes.
Limitez la taille des artifacts. Ne déclarez comme artifacts que les fichiers strictement nécessaires aux jobs suivants. Le répertoire build/ complet peut peser des centaines de mégaoctets — les binaires de test et la compilation database suffisent généralement.
Versionnez la durée de vie des artifacts. Les artifacts intra-pipeline (binaires pour les tests) peuvent expirer en 1-2 heures. Les artifacts de release (paquets DEB, binaires de distribution) devraient avoir une durée plus longue ou être poussés vers un stockage externe.
Section suivante : 38.1.2 Jobs et stages — Définition détaillée de chaque job : lint, build multi-compilateur, tests avec sanitizers, packaging et déploiement.