Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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

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

  1. Les stages s'exécutent dans l'ordre de la liste stages.
  2. Les jobs d'un même stage tournent en parallèle.
  3. Un stage ne démarre que si tous les jobs du stage précédent ont réussi.
  4. 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).

stage teststage buildstage deploylinttests-unitairesimage-dockerdeploiement-recetteaudit-dependancesneeds : sans attendre buildSans needs, un stage démarre quand tout le stage précédent a réussi.
Les stages s'enchaînent, les jobs d'un stage tournent en parallèle ; needs permet de démarrer un job dès que ses dépendances ont réussi

Les mots-clés essentiels

Mot-cléRôleExemple
stageLe stage du jobstage: test
imageL'image Docker dans laquelle le job s'exécuteimage: node:24
scriptLes commandes, obligatoiresscript: [npm ci, npm test]
before_script, after_scriptDes commandes avant le script, et après (même en cas d'échec)installation, nettoyage
variablesDes variables d'environnementvariables: { NODE_ENV: test }
rulesLes conditions pour créer le jobvoir plus bas
needsLes jobs à attendre, sans respecter l'ordre des stagesneeds: [tests-unitaires]
artifactsLes fichiers à conserver et à transmettreartifacts: { paths: [dist/] }
cacheLes fichiers à garder entre les pipelinesdépendances téléchargées
servicesDes conteneurs annexes (base de données)services: [mysql:8.4]
environmentL'environnement de déploiementenvironment: production
whenQuand lancer le job : on_success (par défaut), manual, always, neverwhen: manual
allow_failureUn échec n'arrête pas le pipelineallow_failure: true
tagsChoisir un runner qui porte ces étiquettestags: [docker]
extends, includeRéutiliser une configurationvoir 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éé.

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

VariableContenu
CI_PIPELINE_SOURCECe qui a créé le pipeline : push, merge_request_event, schedule, web (bouton), api…
CI_COMMIT_BRANCHLa branche (absente pour un tag et dans un pipeline de merge request)
CI_COMMIT_TAGLe tag (absent sinon)
CI_DEFAULT_BRANCHLa branche par défaut, souvent main
CI_COMMIT_SHA, CI_COMMIT_SHORT_SHAL'empreinte du commit, complète ou sur 8 caractères
CI_REGISTRY_IMAGEL'adresse de l'image du projet dans le registre de conteneurs de GitLab
CI_JOB_TOKENUn 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 :

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

yaml
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

yaml
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
  • cache garde le dossier .npm/ entre les pipelines ; la variable npm_config_cache dit à npm de l'utiliser. GitLab ne met en cache que des dossiers situés dans le projet.
  • artifacts:paths conserve coverage/ pendant une semaine ; when: always l'envoie même en cas d'échec.
  • artifacts:reports:junit fait afficher le détail des tests (réussis, échoués, durée) dans la merge request ; coverage_report colore les lignes couvertes dans le diff.
  • coverage est 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

yaml
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

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

yaml
.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
  • extends fusionne 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

NotionGitHub ActionsGitLab CI
Fichier.github/workflows/*.yml (plusieurs fichiers).gitlab-ci.yml (un fichier, plus des include)
Regroupement ordonnéJobs reliés par needsstages, ou needs
Unité d'exécutionJobJob
Commandessteps avec runscript
Environnement d'exécutionruns-on et des actions setup-*image Docker
Composant réutilisableAction (uses:)include, extends, composants du catalogue CI/CD
Conditionson (événements) et ifrules et workflow:rules
Matricestrategy.matrixparallel:matrix
Fichiers entre jobsArtefacts (upload-artifact, download-artifact)artifacts, transmis automatiquement aux stages suivants
Cacheactions/cache, cache: des actions setup-*cache: avec key et paths
Base de données de testservicesservices
SecretsSecrets and variables, ${{ secrets.X }}Settings, CI/CD, Variables : $X
Jeton automatiqueGITHUB_TOKENCI_JOB_TOKEN
Déploiement manuelEnvironnement avec relecteurs obligatoireswhen: manual
Registre d'imagesghcr.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 :

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

yaml
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]
  1. 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 ?
  2. Que se passe-t-il si le lint échoue ?
Voir la réponseMasquer la réponse de l'exercice 1
  1. lint : 0 → 1. tests : 0 → 4. audit n'attend que tests grâce à needs : 4 → 5. construction attend tout le stage test (fini à la minute 4) : 4 → 7. recette n'a pas de needs, donc il attend tout le stage build : 7 → 9. Le pipeline dure 9 minutes.
  2. Le stage test échoue : construction et recette, qui attendent la fin réussie du stage, ne démarrent pas. audit, lui, ne dépend que de tests : il démarre quand même à la minute 4, et s'exécute si tests ré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.

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

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

yaml
  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é ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Tests automatisés dans la CI et protection de brancheRevoir le sommaire du cours