Aller au contenu
CI/CD & Automatisation medium

Écrire un fichier .gitlab-ci.yml

18 min de lecture

logo gitlab

Vous connaissez maintenant les concepts fondamentaux : pipeline, stages, jobs, runners. Il est temps de passer à la pratique ! Le fichier .gitlab-ci.yml décrit ce que GitLab doit automatiser. Ce guide vous apprend à le structurer méthodiquement : comment découper votre workflow en stages, définir des jobs, et écrire les scripts qui seront exécutés.

À la fin de ce module, vous saurez :

  • Structurer un fichier .gitlab-ci.yml : la hiérarchie stages → jobs → script
  • Définir des stages : organiser les grandes étapes de votre pipeline
  • Créer des jobs : configurer les tâches avec les bonnes propriétés
  • Écrire des scripts : les commandes exécutées par chaque job
  • Spécifier une image Docker : choisir l'environnement d'exécution
  • Utiliser les variables : configurer vos jobs dynamiquement
  • Valider votre fichier : éviter les erreurs de syntaxe

Avant de continuer, assurez-vous de maîtriser :

Avant d'écrire du YAML, posez-vous ces questions :

  1. Quelles sont les grandes étapes de mon workflow ? → Ce seront vos stages
  2. Quelles tâches dois-je accomplir à chaque étape ? → Ce seront vos jobs
  3. Quelles commandes chaque tâche doit-elle exécuter ? → Ce sera le script

Structure d'un fichier .gitlab-ci.yml : stages contiennent des jobs qui contiennent des scripts

Exemple de raisonnement pour un projet web :

QuestionRéponseÉlément YAML
Grandes étapes ?Compiler, tester, déployerstages: [build, test, deploy]
Tâches du test ?Tests unitaires, linting2 jobs dans le stage test
Commandes du lint ?npm run lintscript: [npm run lint]

Un pipeline GitLab tient en très peu de lignes. Le seul élément réellement indispensable est un job doté d'un script : le bloc stages est facultatif, GitLab en fournit un par défaut. Déclarez-le quand même, car c'est lui qui rend l'ordre d'exécution lisible pour la personne qui reprendra le fichier après vous.

Un fichier .gitlab-ci.yml valide contient au minimum :

# 1. Déclarer les stages (optionnel mais recommandé)
stages:
- build
- test
# 2. Définir au moins un job avec un script
mon-job:
stage: build
script:
- echo "Hello GitLab CI!"

Le fichier doit être :

  • Nommé exactement .gitlab-ci.yml (avec le point)
  • Placé à la racine du repository
  • Encodé en UTF-8

Les stages représentent les grandes étapes de votre pipeline. Ils s'exécutent dans l'ordre déclaré.

stages:
- build # Étape 1 : compiler le code
- test # Étape 2 : exécuter les tests
- deploy # Étape 3 : déployer l'application

Règles des stages :

RègleExplication
Ordre séquentielbuild se termine avant que test commence
Échec bloquantSi build échoue, test et deploy ne s'exécutent pas
Stages par défautSans déclaration, GitLab utilise : .pre, build, test, deploy, .post

Créez un stage quand vous avez une dépendance logique :

  • Oui : test dépend de build, donc 2 stages séparés
  • Oui : deploy-staging avant deploy-prod, donc 2 stages séparés
  • Non : tests unitaires et tests d'intégration, même stage car aucune dépendance entre eux

Un job est une tâche qui appartient à un stage. C'est ici que vous décrivez ce qui doit être fait.

# Syntaxe d'un job
nom-du-job: # Nom libre, visible dans l'interface
stage: test # À quel stage appartient ce job
script: # Commandes à exécuter (OBLIGATOIRE)
- commande1
- commande2

Une seule ligne de ce tableau est obligatoire : script. Toutes les autres ont une valeur par défaut, et deux de ces valeurs par défaut surprennent régulièrement. Un job sans stage atterrit dans test, même si vous pensiez le placer en début de pipeline. Un job sans image hérite de l'image configurée sur le runner, pas d'une image liée à votre langage. Les trois dernières lignes (cache, rules, needs) relèvent d'un usage plus avancé ; laissez-les de côté pour un premier pipeline.

PropriétéObligatoireDescription
scriptOuiListe des commandes à exécuter
stageNonStage d'appartenance (défaut: test)
imageNonImage Docker pour l'environnement
variablesNonVariables d'environnement du job
artifactsNonFichiers à conserver après le job
cacheNonFichiers à réutiliser entre pipelines
rulesNonConditions d'exécution
needsNonDépendances explicites (ignore l'ordre des stages)

Les jobs d'un même stage s'exécutent en parallèle :

stages:
- test
test-unitaires:
stage: test
script:
- npm run test:unit
test-integration:
stage: test
script:
- npm run test:integration
lint:
stage: test
script:
- npm run lint

Ces 3 jobs démarrent simultanément car ils sont tous dans le stage test.

Le script contient les commandes shell exécutées par le job, dans l'ordre de la liste. Chaque item est une commande distincte, exécutée à la racine du dépôt fraîchement cloné par le runner. Les lignes echo de l'exemple ne servent pas de décoration : elles apparaissent dans les logs du job et servent de repères quand vous cherchez à quelle étape une construction s'est arrêtée.

build:
stage: build
script:
- echo "Début du build"
- npm ci
- npm run build
- echo "Build terminé"

La ligne à retenir est la troisième : GitLab ne juge pas la sortie de vos commandes, il regarde uniquement leur code de retour. Une commande qui affiche « ERROR » mais sort avec 0 laisse le job en vert, et une commande qui sort avec 1 stoppe le job même si tout semblait fonctionner. La dernière ligne explique pourquoi tant de jobs échouent sur une image Alpine : le shell y est /bin/sh, pas Bash, donc la syntaxe [[ ... ]] et les tableaux Bash provoquent une erreur de syntaxe.

RègleExemple
Chaque ligne = une commande- npm install
Exécution séquentielleLigne 1, puis ligne 2, puis ligne 3...
Échec = arrêtSi une commande retourne un code ≠ 0, le job échoue
Shell par défaut/bin/sh (ou celui de l'image Docker)

Le bloc littéral YAML | permet d'écrire plusieurs commandes qui partagent le même shell. C'est ce qui rend possible une condition if ou une boucle : découpées en items séparés, chaque ligne s'exécuterait dans son propre appel et la structure serait cassée. Attention toutefois, ce bloc reste une seule entrée de script pour GitLab : l'interface affiche tout d'un bloc et un échec en milieu de bloc est plus difficile à localiser dans les logs.

Pour des commandes longues ou des structures de contrôle, utilisez le bloc littéral | :

script:
- |
echo "Première ligne"
echo "Deuxième ligne"
if [ -f "config.json" ]; then
echo "Config trouvée"
fi

Ces deux sections évitent de répéter la même préparation dans chaque job. La différence qui compte est leur comportement en cas d'échec : before_script est concaténé au script, donc s'il échoue, le job échoue immédiatement et le script n'est jamais lancé. À l'inverse, after_script s'exécute dans un shell séparé, y compris quand le job a échoué ou a été annulé, ce qui en fait le bon endroit pour un nettoyage ou la récupération d'un log de diagnostic. Corollaire à connaître : les variables définies dans le script ne sont pas visibles depuis after_script.

Déclarez-les sous default pour les appliquer à tous les jobs :

# S'exécute AVANT le script de chaque job
default:
before_script:
- echo "Préparation..."
- npm ci
test:
script:
- npm test
after_script:
- echo "Nettoyage..." # S'exécute même si le job échoue

Un job sans image n'échoue pas : il hérite de l'image configurée sur le runner. Sur les runners hébergés par GitLab.com, cette valeur par défaut est ruby:3.1 ; sur un runner que vous administrez, c'est la clé image de la section [runners.docker] du fichier config.toml. Autrement dit, le même pipeline peut fonctionner chez vous et échouer chez un collègue sur command not found. Déclarer explicitement l'image est donc ce qui rend un pipeline reproductible.

build:
image: node:24-alpine # Node.js 24 LTS sur Alpine Linux
script:
- node --version
- npm ci
- npm run build

Le tableau ci-dessous donne des points de départ, pas des obligations. La colonne image légère repose sur Alpine ou sur une variante slim : elle télécharge beaucoup plus vite, ce qui compte sur un pipeline lancé cinquante fois par jour, mais elle utilise la bibliothèque C musl au lieu de glibc. Un module natif compilé pour glibc peut donc refuser de se charger. En cas de doute au premier essai, prenez la colonne de droite.

LangageImage légèreImage complète
Node.jsnode:24-alpinenode:24
Pythonpython:3.13-slimpython:3.13
Gogolang:1.26-alpinegolang:1.26
Javaeclipse-temurin:21-alpineeclipse-temurin:21
Génériquealpine:3.24ubuntu:24.04

Les variables permettent de configurer vos jobs sans modifier le script.

Un bloc variables: écrit à la racine du fichier s'applique à tous les jobs du pipeline. Ces valeurs sont exportées comme variables d'environnement dans le conteneur du job, et s'utilisent avec la syntaxe shell $NOM. Réservez cet emplacement à ce qui est vraiment commun, comme une version d'outil ou un nom d'application ; une valeur qui ne concerne qu'un job appartient au job. Et gardez en tête que ce fichier est versionné dans Git : aucun secret n'a sa place ici, les jetons et mots de passe se déclarent dans les variables CI/CD du projet, masquées et protégées.

variables:
NODE_ENV: production
DEPLOY_TARGET: staging
build:
script:
- echo "Environment: $NODE_ENV"
- echo "Target: $DEPLOY_TARGET"

Un bloc variables: placé dans un job ne vaut que pour ce job, et il écrase la valeur globale portant le même nom. C'est le mécanisme qui permet à un même pipeline de compiler en mode développement puis de déployer en mode production sans dupliquer le script. Le piège à connaître : ce bloc remplace intégralement la valeur globale pour la clé concernée, il ne la complète pas. Dans l'exemple ci-dessous, le NODE_ENV: production global n'a plus aucun effet sur le job build.

build:
variables:
NODE_ENV: development
script:
- npm run build
deploy:
variables:
NODE_ENV: production
script:
- npm run deploy

GitLab injecte automatiquement des variables utiles :

VariableContenu
$CI_COMMIT_BRANCHNom de la branche
$CI_COMMIT_SHORT_SHAHash court du commit
$CI_PROJECT_NAMENom du projet
$CI_PIPELINE_SOURCEDéclencheur (push, merge_request, etc.)
$CI_JOB_NAMENom du job en cours
build:
script:
- echo "Building $CI_PROJECT_NAME on branch $CI_COMMIT_BRANCH"
- echo "Commit: $CI_COMMIT_SHORT_SHA"

Ce pipeline rassemble tout ce qui précède sur un projet Node.js. Trois détails méritent votre attention à la lecture. Le job build publie dist/ en artifact, ce qui est la seule façon de transmettre des fichiers d'un stage au suivant : le répertoire de travail, lui, repart de zéro à chaque job. Les jobs test-unit et test-lint refont chacun un npm ci, parce qu'ils démarrent dans des conteneurs distincts et ne partagent aucun node_modules. Enfin, le job deploy combine rules et when: manual : il n'apparaît que sur la branche main, et il attend un clic dans l'interface avant de démarrer.

Voici un pipeline réaliste pour un projet Node.js :

# Déclaration des stages dans l'ordre d'exécution
stages:
- build
- test
- deploy
# Variables globales disponibles dans tous les jobs
variables:
NODE_ENV: production
# --- STAGE BUILD ---
build:
stage: build
image: node:24-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd
script:
- echo "Installation des dependances..."
- npm ci
- echo "Compilation..."
- npm run build
artifacts:
paths:
- dist/ # Conserve le dossier dist/ pour les jobs suivants
expire_in: 1 hour # Supprimé après 1 heure
# --- STAGE TEST ---
# Ces 2 jobs s'exécutent EN PARALLÈLE
test-unit:
stage: test
image: node:24-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd
script:
- npm ci
- npm run test:unit
test-lint:
stage: test
image: node:24-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd
script:
- npm ci
- npm run lint
# --- STAGE DEPLOY ---
deploy:
stage: deploy
image: alpine:3.24@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b
script:
- echo "Deploiement vers $DEPLOY_TARGET..."
- echo "Fichiers a deployer:" && ls dist/
rules:
- if: $CI_COMMIT_BRANCH == "main" # Seulement sur main
when: manual # Clic requis pour démarrer

Une erreur de syntaxe ne se voit qu'après le push, et elle coûte un commit de correction dans l'historique. Les deux méthodes ci-dessous vérifient le fichier avant qu'il ne parte. Elles font toutes les deux appel au validateur de GitLab lui-même, pas à un analyseur YAML générique : elles détectent donc aussi un stage inconnu ou une propriété mal orthographiée, ce qu'un simple yamllint laisserait passer.

  1. Dans GitLab : Build > Pipeline editor > onglet Validate
  2. En local avec glab, la CLI officielle GitLab :
    Fenêtre de terminal
    glab ci lint .gitlab-ci.yml

La validation mérite un traitement à part entière, notamment pour l'automatiser en pre-commit : le guide Valider un fichier .gitlab-ci.yml détaille l'API CI Lint et ses limites.

NiveauÉlémentRôle
1stagesDéfinit l'ordre des grandes étapes
2JobsTâches à accomplir (nommées librement)
3scriptCommandes shell à exécuter

Construction logique :

  1. Listez vos étapes → stages
  2. Pour chaque étape, listez les tâches → jobs
  3. Pour chaque tâche, listez les commandes → script

Vous avez la méthode pour structurer un .gitlab-ci.yml. Passez maintenant sur un vrai cas guidé pour l'appliquer étape par étape dans GitLab.

Passez au Lab 01, Mon premier pipeline pour construire votre premier pipeline complet.

Testez vos connaissances sur la structure des fichiers .gitlab-ci.yml.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

10 questions
5 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn