Skip to content

Latest commit

 

History

History
567 lines (446 loc) · 24.2 KB

File metadata and controls

567 lines (446 loc) · 24.2 KB

🔝 Retour au Sommaire

38.1.1 Structure .gitlab-ci.yml

Introduction

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.

Anatomie générale du fichier

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.

Les variables globales

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: 1

Quelques 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

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-stats

image 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.

La déclaration des stages

Le bloc stages définit l'ordre séquentiel des étapes du pipeline :

stages:
  - lint
  - build
  - test
  - package
  - deploy

Les 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.

Structure d'un job

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.

stage

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.

image

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

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.

cache

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-push

key 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.

script

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.

artifacts

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 hour

Ici, 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épertoire build/ 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  

rules

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.

Les ancres YAML et les templates

À 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.

Ancres YAML (& et *)

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 bloc variables et 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 bloc variables de premier niveau.

La directive extends

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.

Le bloc needs : optimiser le parallélisme

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.

Variables prédéfinies GitLab CI utiles pour le C++

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é.

Fichier complet commenté

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.

Bonnes pratiques de structuration

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.

⏭️ Jobs et stages