Chapitre 526 minIntermédiaire
GitLab CI : le fichier .gitlab-ci.yml
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- écrire un .gitlab-ci.yml avec des stages et des jobs ;
- contrôler la création des jobs avec rules et workflow ;
- utiliser needs, artifacts, cache, services et environment ;
- traduire un workflow GitHub Actions en GitLab CI.
4 exercices corrigés1 schéma
GitLab CI/CD est le système d'intégration continue intégré à GitLab : un fichier .gitlab-ci.yml à la racine du dépôt décrit un pipeline fait de stages (étapes) qui s'enchaînent et de jobs qui s'exécutent en parallèle à l'intérieur de chaque stage, sur des machines appelées runners. Les notions sont celles de GitHub Actions, la syntaxe change. Ce chapitre construit le pipeline de sorbier-frais pour GitLab, explique les mots-clés essentiels (rules, needs, artifacts, cache, services, environment) et donne la table d'équivalence avec GitHub Actions.
Le fichier et les runners
GitLab cherche un seul fichier, .gitlab-ci.yml, à la racine du dépôt. Chaque push, chaque merge request (le nom des pull requests chez GitLab) ou chaque tag peut créer un pipeline, visible dans le menu Build, Pipelines du projet.
Les jobs s'exécutent sur des runners :
- sur GitLab.com, des runners d'instance partagés, avec un quota mensuel de minutes de calcul selon l'offre ;
- dans une entreprise qui héberge son propre GitLab, des runners installés par l'équipe d'exploitation avec le programme GitLab Runner. Chaque runner a un exécuteur :
docker(chaque job tourne dans un conteneur neuf, le cas le plus courant),shell(directement sur la machine),kubernetes…
Avec l'exécuteur docker, chaque job démarre dans l'image indiquée par image:. C'est une grande différence avec GitHub Actions : au lieu d'installer Node.js avec une action, on choisit une image qui le contient déjà, comme node:24.
Stages et jobs
stages:
- test
- build
- deploy
lint:
stage: test
image: node:24
script:
- npm ci
- npm run lint
tests-unitaires:
stage: test
image: node:24
script:
- npm ci
- npm test -- --ci
image-docker:
stage: build
image: docker:29-cli
services:
- docker:29-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
script:
- docker build -t sorbier-frais:$CI_COMMIT_SHORT_SHA .
deploiement-recette:
stage: deploy
image: alpine:3.24
script:
- echo "Déploiement de la version $CI_COMMIT_SHORT_SHA en recette"
Les règles d'exécution :
- Les stages s'exécutent dans l'ordre de la liste
stages. - Les jobs d'un même stage tournent en parallèle.
- Un stage ne démarre que si tous les jobs du stage précédent ont réussi.
- Un job est un bloc de premier niveau avec un
script; son nom est libre, sauf les mots réservés (stages,variables,default,workflow,include…). Un nom qui commence par un point (.node) désigne un job caché, qui ne s'exécute pas et sert de modèle.
Sans liste stages, GitLab utilise build, test et deploy, dans cet ordre (plus .pre au tout début et .post à la toute fin).
Les mots-clés essentiels
| Mot-clé | Rôle | Exemple |
|---|---|---|
stage | Le stage du job | stage: test |
image | L'image Docker dans laquelle le job s'exécute | image: node:24 |
script | Les commandes, obligatoires | script: [npm ci, npm test] |
before_script, after_script | Des commandes avant le script, et après (même en cas d'échec) | installation, nettoyage |
variables | Des variables d'environnement | variables: { NODE_ENV: test } |
rules | Les conditions pour créer le job | voir plus bas |
needs | Les jobs à attendre, sans respecter l'ordre des stages | needs: [tests-unitaires] |
artifacts | Les fichiers à conserver et à transmettre | artifacts: { paths: [dist/] } |
cache | Les fichiers à garder entre les pipelines | dépendances téléchargées |
services | Des conteneurs annexes (base de données) | services: [mysql:8.4] |
environment | L'environnement de déploiement | environment: production |
when | Quand lancer le job : on_success (par défaut), manual, always, never | when: manual |
allow_failure | Un échec n'arrête pas le pipeline | allow_failure: true |
tags | Choisir un runner qui porte ces étiquettes | tags: [docker] |
extends, include | Réutiliser une configuration | voir plus bas |
Choisir quand un job tourne : rules
rules est une liste de conditions évaluées dans l'ordre : la première qui correspond décide. Si aucune ne correspond, le job n'est pas créé.
tests-unitaires:
stage: test
image: node:24
script:
- npm ci
- npm test -- --ci
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event" # dans une merge request
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # sur la branche principale
deploiement-production:
stage: deploy
image: alpine:3.24
script:
- ./scripts/deployer.sh production
rules:
- if: $CI_COMMIT_TAG # seulement pour un tag
when: manual # et après un clic
Les conditions lisent les variables prédéfinies de GitLab :
| Variable | Contenu |
|---|---|
CI_PIPELINE_SOURCE | Ce qui a créé le pipeline : push, merge_request_event, schedule, web (bouton), api… |
CI_COMMIT_BRANCH | La branche (absente pour un tag et dans un pipeline de merge request) |
CI_COMMIT_TAG | Le tag (absent sinon) |
CI_DEFAULT_BRANCH | La branche par défaut, souvent main |
CI_COMMIT_SHA, CI_COMMIT_SHORT_SHA | L'empreinte du commit, complète ou sur 8 caractères |
CI_REGISTRY_IMAGE | L'adresse de l'image du projet dans le registre de conteneurs de GitLab |
CI_JOB_TOKEN | Un jeton temporaire du job, pour accéder au registre et à l'API |
Éviter les pipelines en double. Comme avec GitHub, un push sur une branche qui a une merge request ouverte peut créer deux pipelines. Le bloc workflow décide, au niveau du pipeline entier, lequel garder :
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI_COMMIT_BRANCH
- if: $CI_COMMIT_TAG
Lecture : un pipeline de merge request est toujours créé ; un push sur une branche qui a déjà une merge request ouverte n'en crée pas un second ; les autres push et les tags créent un pipeline.
needs : ne pas attendre tout le stage
Par défaut, un job attend la fin de tout le stage précédent. needs le fait démarrer dès que les jobs qu'il cite ont réussi : le pipeline devient un graphe (DAG, directed acyclic graph) au lieu d'une suite de colonnes.
audit-dependances:
stage: deploy
image: node:24
needs: [tests-unitaires] # démarre dès la fin de tests-unitaires
script:
- npm audit --audit-level=high
needs: [] (liste vide) fait démarrer le job immédiatement, au tout début du pipeline. needs transmet aussi les artefacts : un job ne télécharge que les artefacts des jobs cités.
Artefacts, rapports et cache
tests-unitaires:
stage: test
image: node:24
variables:
npm_config_cache: "$CI_PROJECT_DIR/.npm"
cache:
key:
files:
- package-lock.json # la clé change quand les dépendances changent
paths:
- .npm/
script:
- npm ci --prefer-offline
- npm test -- --ci --coverage
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
artifacts:
when: always # même si les tests échouent
expire_in: 1 week
paths:
- coverage/
reports:
junit: reports/junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
cachegarde le dossier.npm/entre les pipelines ; la variablenpm_config_cachedit à npm de l'utiliser. GitLab ne met en cache que des dossiers situés dans le projet.artifacts:pathsconservecoverage/pendant une semaine ;when: alwaysl'envoie même en cas d'échec.artifacts:reports:junitfait afficher le détail des tests (réussis, échoués, durée) dans la merge request ;coverage_reportcolore les lignes couvertes dans le diff.coverageest une expression régulière que GitLab applique au journal du job pour extraire le pourcentage de couverture et l'afficher dans la merge request ; celle-ci correspond au résumé affiché par Jest.
Pour produire reports/junit.xml et coverage/cobertura-coverage.xml, Jest a besoin du rapporteur jest-junit et du format de couverture cobertura (chapitre 6).
Une base de données pour les tests : services
tests-integration:
stage: test
image: node:24
services:
- name: mysql:8.4
alias: mysql
variables:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: frais_test
DB_HOST: mysql
DB_USER: root
DB_PASSWORD: root
DB_NAME: frais_test
script:
- npm ci
- npm run test:integration
GitLab démarre un conteneur mysql:8.4 à côté du job ; il est joignable sous le nom de son alias, ici mysql. Les variables du job sont aussi transmises au service, ce qui initialise la base frais_test. Le mot de passe root n'est pas un secret : la base est vide, jetable, et inaccessible hors du job.
Environnements et déploiement manuel
deploiement-recette:
stage: deploy
image: alpine:3.24
environment:
name: recette
url: https://recette.sorbier.example
script:
- ./scripts/deployer.sh recette
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
deploiement-production:
stage: deploy
image: alpine:3.24
needs: [deploiement-recette]
environment:
name: production
url: https://frais.sorbier.example
script:
- ./scripts/deployer.sh production
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
environment enregistre chaque déploiement dans le menu Operate, Environments : on voit quelle version tourne où, et un bouton permet de redéployer une version précédente. when: manual affiche un bouton de lecture sur le job : le pipeline s'arrête sur ce job tant que personne ne clique, ce qui est une livraison continue. Les offres payantes de GitLab ajoutent les environnements protégés, où seules certaines personnes peuvent lancer le déploiement.
Réutiliser la configuration : extends, default et include
Les jobs Node.js répètent la même image et la même installation. Un job caché sert de modèle :
.node:
image: node:24
before_script:
- npm ci
lint:
extends: .node
stage: test
script:
- npm run lint
tests-unitaires:
extends: .node
stage: test
script:
- npm test -- --ci
extendsfusionne le modèle dans le job ; le job peut redéfinir n'importe quelle clé.default:donne des valeurs à tous les jobs (default: { image: node:24 }).include:importe d'autres fichiers : un fichier du projet (include: { local: ci/tests.yml }), d'un autre projet, ou un modèle fourni par GitLab, comme l'analyse de sécurité du code (include: [{ template: Jobs/SAST.gitlab-ci.yml }], chapitre 7).
Équivalences entre GitHub Actions et GitLab CI
| Notion | GitHub Actions | GitLab CI |
|---|---|---|
| Fichier | .github/workflows/*.yml (plusieurs fichiers) | .gitlab-ci.yml (un fichier, plus des include) |
| Regroupement ordonné | Jobs reliés par needs | stages, ou needs |
| Unité d'exécution | Job | Job |
| Commandes | steps avec run | script |
| Environnement d'exécution | runs-on et des actions setup-* | image Docker |
| Composant réutilisable | Action (uses:) | include, extends, composants du catalogue CI/CD |
| Conditions | on (événements) et if | rules et workflow:rules |
| Matrice | strategy.matrix | parallel:matrix |
| Fichiers entre jobs | Artefacts (upload-artifact, download-artifact) | artifacts, transmis automatiquement aux stages suivants |
| Cache | actions/cache, cache: des actions setup-* | cache: avec key et paths |
| Base de données de test | services | services |
| Secrets | Secrets and variables, ${{ secrets.X }} | Settings, CI/CD, Variables : $X |
| Jeton automatique | GITHUB_TOKEN | CI_JOB_TOKEN |
| Déploiement manuel | Environnement avec relecteurs obligatoires | when: manual |
| Registre d'images | ghcr.io | $CI_REGISTRY |
Et Jenkins ?
Jenkins est le plus ancien des serveurs d'intégration continue encore très utilisés. On l'installe soi-même ; le pipeline est décrit dans un Jenkinsfile écrit dans un langage dérivé de Groovy :
pipeline {
agent { docker { image 'node:24' } } // nécessite le plugin Docker Pipeline
stages {
stage('Installer') { steps { sh 'npm ci' } }
stage('Lint') { steps { sh 'npm run lint' } }
stage('Tests') { steps { sh 'npm test -- --ci' } }
}
post {
always { junit 'reports/junit.xml' } // publie le rapport de tests
}
}
On y retrouve les mêmes idées : des étapes ordonnées, des commandes, un rapport publié même en cas d'échec. Jenkins est très souple grâce à ses milliers d'extensions, mais il faut maintenir le serveur, ses mises à jour et la sécurité de ses extensions.
Exercices
Exercice 1 sur 4 Ordre d'exécution Réussi À revoir
stages: [test, build, deploy]
lint:
stage: test
script: [npm run lint]
tests:
stage: test
script: [npm test]
construction:
stage: build
script: [docker build -t app .]
audit:
stage: deploy
needs: [tests]
script: [npm audit --audit-level=high]
recette:
stage: deploy
script: [./deployer.sh recette]
- Le lint dure 1 minute, les tests 4 minutes, la construction de l'image (job
construction) 3 minutes, l'audit 1 minute, le déploiement en recette 2 minutes. À quelle minute chaque job démarre-t-il et se termine-t-il, si les runners sont disponibles ? - Que se passe-t-il si le lint échoue ?
Voir la réponseMasquer la réponse de l'exercice 1
lint: 0 → 1.tests: 0 → 4.auditn'attend quetestsgrâce àneeds: 4 → 5.constructionattend tout le stagetest(fini à la minute 4) : 4 → 7.recetten'a pas deneeds, donc il attend tout le stagebuild: 7 → 9. Le pipeline dure 9 minutes.- Le stage
testéchoue :constructionetrecette, qui attendent la fin réussie du stage, ne démarrent pas.audit, lui, ne dépend que detests: il démarre quand même à la minute 4, et s'exécute sitestsréussit.
Tu avais trouvé ?
Exercice 2 sur 4 Traduire un workflow GitHub en GitLab Réussi À revoir
Traduis ce workflow GitHub Actions en .gitlab-ci.yml. Les deux jobs doivent tourner dans les merge requests et sur la branche principale.
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- run: npm ci
- run: npm run lint
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- run: npm ci
- run: npm test
Voir la réponseMasquer la réponse de l'exercice 2
stages:
- test
.node:
image: node:24
before_script:
- npm ci
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
lint:
extends: .node
stage: test
script:
- npm run lint
tests:
extends: .node
stage: test
script:
- npm test
Pas d'équivalent à actions/checkout : GitLab récupère le code automatiquement au début de chaque job. setup-node est remplacé par l'image node:24. Les deux jobs, dans le même stage, tournent en parallèle comme les deux jobs GitHub. Une version sans job caché, qui répète image, before_script et rules dans chaque job, est aussi correcte.
Tu avais trouvé ?
Exercice 3 sur 4 Pourquoi ce job ne tourne-t-il jamais ? Réussi À revoir
Hugo a écrit ce job pour déployer en production à chaque tag. Il pousse le tag v2.1.0, mais le job n'apparaît pas dans le pipeline.
deploiement-production:
stage: deploy
script:
- ./deployer.sh production
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG
when: never
Voir la réponseMasquer la réponse de l'exercice 3
Dans un pipeline de tag, CI_COMMIT_BRANCH n'est pas défini : la première règle ne correspond pas. La deuxième correspond (CI_COMMIT_TAG vaut v2.1.0) et dit when: never : le job n'est pas créé. Hugo a inversé la logique. Pour déployer à chaque tag, après un clic :
rules:
- if: $CI_COMMIT_TAG
when: manual
Avec cette seule règle, le job n'est créé que pour les tags.
Tu avais trouvé ?
Exercice 4 sur 4 Les variables prédéfinies Réussi À revoir
Dans un job, Lina veut construire une image Docker étiquetée avec l'empreinte courte du commit et l'envoyer dans le registre du projet. Complète la commande avec les bonnes variables prédéfinies : docker build -t ________:________ .
Voir la réponseMasquer la réponse de l'exercice 4
docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
CI_REGISTRY_IMAGE donne l'adresse de l'image du projet dans le registre de GitLab (par exemple registry.gitlab.com/sorbier/sorbier-frais), CI_COMMIT_SHORT_SHA les huit premiers caractères du commit. Avant le docker push, il faut se connecter au registre : echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" (chapitre 9).
Tu avais trouvé ?