Aller au contenu
CI/CD & Automatisation medium

Matrices de jobs GitLab CI/CD (parallel:matrix)

28 min de lecture

logo gitlab

Vous devez tester sur Node 18, 20 et 22 sans copier-coller trois fois le même job ? Avec parallel:matrix, un seul job définit la logique, et GitLab génère automatiquement une instance par combinaison de variables. Plus de duplication, plus de risque d’oubli.

À la fin de ce module, vous saurez :

  • Configurer parallel:matrix : syntaxe et options
  • Tester multi-versions : Node.js, Python, Go en parallèle
  • Tester multi-OS : Linux, macOS, Windows avec des tags
  • Créer des matrices multidimensionnelles : version × OS × database
  • Combiner avec needs : dépendances entre jobs matriciels
  • Choisir matrix vs dynamique : quand utiliser chaque approche

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

Sans parallel:matrix, pour tester trois versions de Node.js, vous devriez écrire :

# ❌ Sans matrix : 3 jobs quasi-identiques
test_node18:
image: node:18
script: npm test
test_node20:
image: node:20
script: npm test
test_node22:
image: node:22
script: npm test

Trois jobs avec le même script, seule l’image change. Si vous modifiez le script, vous devez penser à le faire trois fois. Si vous ajoutez Node 24, vous créez un quatrième job. C’est fragile et fastidieux.

parallel:matrix résout ce problème en créant plusieurs instances d’un même job avec des valeurs de variables différentes. C’est l’équivalent d’une boucle for sur vos jobs : “pour chaque version dans [18, 20, 22], exécute ce job”.

Principe de parallel:matrix : un job template génère plusieurs instances avec des variables différentes

Concrètement, GitLab prend votre job “template” et le duplique autant de fois qu’il y a de valeurs dans votre matrice, en injectant la variable correspondante dans chaque copie.

Voici la structure d’un job avec parallel:matrix. Chaque ligne a un rôle précis :

stages:
- test
test:
stage: test
image: node:$NODE_VERSION # 1️⃣ La variable sera remplacée
parallel: # 2️⃣ Active le mode parallèle
matrix: # 3️⃣ Définit une matrice de valeurs
- NODE_VERSION: ["18", "20", "22"] # 4️⃣ Liste des valeurs à tester
script:
- node --version # Affiche la version (utile pour debug)
- npm ci # Installe les dépendances
- npm test # Lance les tests

Décortiquons chaque élément :

  1. image: node:$NODE_VERSION — Le $ indique une variable. GitLab la remplacera par 18, 20 ou 22 selon l’instance du job.

  2. parallel: — Ce mot-clé active l’exécution parallèle. Sans lui, vous n’avez qu’un seul job.

  3. matrix: — Indique que vous voulez générer des jobs à partir d’une combinaison de variables (et non juste N copies identiques).

  4. NODE_VERSION: ["18", "20", "22"] — La liste des valeurs. Chaque élément génère un job distinct.

À partir de ce seul job, GitLab crée 3 jobs distincts qui s’exécutent en parallèle :

Job crééVariable injectéeImage Docker utilisée
test: [18]NODE_VERSION=18node:18
test: [20]NODE_VERSION=20node:20
test: [22]NODE_VERSION=22node:22

Les trois jobs démarrent en même temps (si des runners sont disponibles), ce qui divise le temps d’exécution par trois.

GitLab injecte automatiquement des variables dans chaque job matriciel. Ces variables sont utiles pour le debug, les logs, ou pour adapter le comportement selon la position dans la matrice.

VariableÀ quoi ça sertValeur pour le job test: [20]
CI_NODE_INDEXSavoir quel job on est (1er, 2ème, 3ème…)2
CI_NODE_TOTALConnaître le nombre total de jobs générés3
NODE_VERSIONVotre variable de matrice20

Exemple concret : afficher la progression dans les logs.

test:
parallel:
matrix:
- NODE_VERSION: ["18", "20", "22"]
script:
- echo "🔄 Job $CI_NODE_INDEX / $CI_NODE_TOTAL" # Affiche "Job 2 / 3"
- echo "📦 Testing Node.js $NODE_VERSION" # Affiche "Testing Node.js 20"
- npm test

Le cas d’usage le plus courant de parallel:matrix est la validation de compatibilité : vous voulez garantir que votre code fonctionne sur plusieurs versions d’un runtime (Node.js, Python, Go…) sans maintenir un job par version.

Pour une application Node.js, testez les versions LTS actives. L’image Docker est paramétrée dynamiquement avec $NODE_VERSION, et GitLab génère un rapport JUnit par version pour visualiser les résultats dans l’interface.

test_nodejs:
stage: test
image: node:$NODE_VERSION-alpine
parallel:
matrix:
- NODE_VERSION: ["18", "20", "22"]
script:
- npm ci
- npm test -- --reporter=junit --outputFile=junit-$NODE_VERSION.xml
artifacts:
reports:
junit: junit-$NODE_VERSION.xml # Fichier unique par version

Avec cette configuration, si Node 22 introduit une régression dans une dépendance, vous le détectez immédiatement sans impacter les autres versions.

Pour Python, la matrice teste les versions mineures supportées. Notez l’utilisation de before_script pour l’installation des dépendances — ce bloc s’exécute avant chaque instance de la matrice.

test_python:
stage: test
image: python:$PYTHON_VERSION
parallel:
matrix:
- PYTHON_VERSION: ["3.10", "3.11", "3.12"]
before_script:
- pip install -r requirements.txt
script:
- pytest --junitxml=report.xml
artifacts:
reports:
junit: report.xml

Chaque version Python peut avoir des comportements différents (f-strings, match/case, typing). La matrice garantit une compatibilité réelle, pas supposée.

Go suit un cycle de release prévisible avec deux versions supportées. La matrice couvre la version actuelle, la précédente, et éventuellement la prochaine en RC.

test_go:
stage: test
image: golang:$GO_VERSION
parallel:
matrix:
- GO_VERSION: ["1.21", "1.22", "1.23"]
script:
- go test -v ./...

Le compilateur Go étant strict, cette matrice détecte rapidement les incompatibilités liées aux génériques ou aux nouvelles API de la bibliothèque standard.

Certaines applications doivent fonctionner sur plusieurs systèmes d’exploitation : une CLI en Go, un outil de build, une application Electron… Plutôt que de tester manuellement sur chaque OS, la matrice automatise cette validation.

Le défi : chaque OS nécessite un runner différent

Section intitulée « Le défi : chaque OS nécessite un runner différent »

Contrairement aux versions de Node (où seule l’image Docker change), tester sur macOS ou Windows nécessite un runner physique ou virtuel tournant sur cet OS. On utilise les tags pour diriger chaque job vers le bon runner.

test_multiplatform:
stage: test
parallel:
matrix:
# Chaque entrée définit l'OS ET le tag du runner correspondant
- PLATFORM: linux
RUNNER_TAG: linux
- PLATFORM: macos
RUNNER_TAG: macos
- PLATFORM: windows
RUNNER_TAG: windows
tags:
- $RUNNER_TAG # Le job sera assigné au runner avec ce tag
script:
- echo "Testing on $PLATFORM"
- ./run-tests.sh # Le même script, exécuté sur chaque OS

Comment ça marche :

  1. GitLab génère 3 jobs : test_multiplatform: [linux], test_multiplatform: [macos], test_multiplatform: [windows]
  2. Chaque job a un tag différent (linux, macos, windows)
  3. GitLab assigne chaque job au runner qui porte le tag correspondant
  4. Les trois tests s’exécutent en parallèle, chacun sur son OS natif

Jusqu’ici, nos matrices n’avaient qu’une seule variable (la version de Node). Mais que faire si vous voulez tester plusieurs dimensions en même temps ? Par exemple : Node 18 ET 20, avec PostgreSQL ET MySQL ?

C’est là que les matrices deviennent puissantes : GitLab génère automatiquement toutes les combinaisons possibles.

test:
stage: test
image: node:$NODE_VERSION
services:
- name: $DATABASE
alias: db
parallel:
matrix:
- NODE_VERSION: ["18", "20"]
DATABASE: ["postgres:15", "postgres:16", "mysql:8"]
script:
- npm ci
- npm test

Cet exemple génère 6 jobs (2 versions × 3 bases de données) :

  • test: [18, postgres:15]
  • test: [18, postgres:16]
  • test: [18, mysql:8]
  • test: [20, postgres:15]
  • test: [20, postgres:16]
  • test: [20, mysql:8]
build:
stage: build
parallel:
matrix:
- GOOS: [linux, darwin, windows]
GOARCH: [amd64, arm64]
GO_VERSION: ["1.22"]
image: golang:$GO_VERSION
script:
- GOOS=$GOOS GOARCH=$GOARCH go build -o app-$GOOS-$GOARCH ./...
artifacts:
paths:
- app-*

Résultat : 6 binaires compilés en parallèle.

Parfois, vous ne voulez pas toutes les combinaisons. Vous avez des configurations spécifiques qui vont ensemble, et mélanger les variables n’aurait pas de sens.

Exemple concret : vous déployez sur trois environnements, chacun avec son cluster Kubernetes et son nombre de réplicas. Ce n’est pas “staging × prod × 3 clusters” (9 combinaisons), c’est “staging-eu, prod-eu, prod-us” (3 configurations distinctes).

Pour cela, définissez chaque configuration sur sa propre ligne dans la matrice :

deploy:
stage: deploy
parallel:
matrix:
# Chaque ligne = une configuration complète
- ENVIRONMENT: staging
CLUSTER: staging-eu
REPLICAS: "2"
- ENVIRONMENT: production
CLUSTER: prod-eu
REPLICAS: "5"
- ENVIRONMENT: production
CLUSTER: prod-us
REPLICAS: "3"
script:
- kubectl config use-context $CLUSTER
- kubectl scale deployment app --replicas=$REPLICAS
environment:
name: $ENVIRONMENT

Ici, 3 jobs (pas 9), car chaque entrée définit une configuration complète.

Le mot-clé needs (que vous connaissez peut-être du guide DAG) permet de définir des dépendances entre jobs. Avec les matrices, une question se pose : si test génère 3 jobs, le job deploy doit-il attendre les 3, ou un seul ?

Par défaut, needs: ["test"] attend toutes les instances générées par la matrice. C’est le comportement le plus courant : on ne déploie que si tous les tests passent.

test:
stage: test
parallel:
matrix:
- NODE_VERSION: ["18", "20", "22"]
script:
- npm test
deploy:
stage: deploy
needs: ["test"] # ⏳ Attend les 3 jobs : test:[18], test:[20], test:[22]
script:
- ./deploy.sh

Comportement : deploy ne démarre que lorsque test: [18], test: [20] ET test: [22] sont tous au vert.

Parfois, vous ne voulez pas attendre toutes les combinaisons. Par exemple, vous buildez pour 6 plateformes (3 OS × 2 architectures), mais le déploiement sur un serveur Linux n’a besoin que du binaire linux/amd64.

C’est là qu’intervient needs:parallel:matrix : il permet de cibler une combinaison précise au lieu de toute la matrice.

build:
stage: build
parallel:
matrix:
- PLATFORM: [linux, macos, windows]
ARCH: [amd64, arm64]
script:
- ./build.sh # Compile pour $PLATFORM / $ARCH
# 🎯 Déployer DÈS QUE le build linux/amd64 est prêt
# (sans attendre macos, windows, arm64...)
deploy_linux:
stage: deploy
needs:
- job: build # Le nom du job source
parallel:
matrix:
- PLATFORM: linux # Cible cette combinaison précise
ARCH: amd64
script:
- ./deploy.sh linux-amd64

Avantage : deploy_linux démarre dès que build: [linux, amd64] est terminé, même si les builds macOS ou arm64 sont encore en cours.

GitLab supporte des expressions pour cibler plusieurs combinaisons d’une matrice dans needs:parallel:matrix :

build:
parallel:
matrix:
- OS: [linux, macos, windows]
ARCH: [amd64, arm64]
# Attendre tous les builds Linux (amd64 et arm64)
package_linux:
needs:
- job: build
parallel:
matrix:
- OS: linux
ARCH: [amd64, arm64] # Correspond aux deux combinaisons Linux

Cette syntaxe évite de lister chaque combinaison individuellement.

Vous voulez peut-être adapter le comportement de la matrice selon le contexte. Par exemple :

  • Sur une merge request : tester uniquement la version LTS (rapide, feedback immédiat)
  • Sur la branche main : tester toutes les versions (validation complète avant release)

Méthode 1 : exclure des combinaisons avec when: never

Section intitulée « Méthode 1 : exclure des combinaisons avec when: never »

La matrice est dépliée d’abord, puis chaque job passe dans rules. Utilisez when: never pour exclure certaines combinaisons :

test:
stage: test
image: node:$NODE_VERSION
parallel:
matrix:
- NODE_VERSION: ["18", "20", "22"]
rules:
# 📝 En MR : exclure tout sauf Node 20
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $NODE_VERSION != "20"'
when: never
# 🚀 Sinon : exécuter normalement
- when: on_success
script:
- npm test

Résultat :

  • MR → 1 job (test: [20]) → feedback rapide
  • main → 3 jobs parallèles → validation complète

GitLab propose deux façons de générer plusieurs jobs à partir d’un seul fichier .gitlab-ci.yml. Comment choisir ?

Critèreparallel:matrixPipeline dynamique
ComplexitéSimple, déclaratifPlus complexe (génération YAML)
FlexibilitéMême job, variables différentesJobs totalement différents
DebugFacile (tout est visible dans le YAML)Plus difficile (YAML généré à la volée)
CombinaisonsConnues à l’avance (statiques)Détectées au runtime (dynamiques)
Cas d’usage typiqueTests multi-versions, build multi-OSMonorepo, jobs selon fichiers modifiés
Limite200 jobs max par matrice150 jobs par child pipeline

Utilisez parallel:matrix quand vos combinaisons sont prévisibles et stables. Vous connaissez à l’avance les versions à tester, et le script reste identique — seules les variables changent.

Cas typiques :

  • Tests multi-versions : Node 18/20/22, Python 3.10/3.11/3.12. Les versions évoluent lentement (tous les 6-12 mois).
  • Build multi-plateforme : Linux/macOS/Windows × amd64/arm64. L’ensemble des cibles est fixe.
  • Validation de compatibilité : tester avec PostgreSQL ET MySQL, pas l’un ou l’autre.

Le point commun : même logique, contextes différents.

Préférez les pipelines dynamiques quand les jobs à exécuter dépendent du contenu du commit ou nécessitent une logique de génération.

Cas typiques :

  • Monorepo : détecter quels packages ont changé et ne tester que ceux-là.
  • Jobs conditionnels complexes : générer un job de déploiement uniquement si certains fichiers sont modifiés.
  • Scripts différents par contexte : le job “build” d’un service Go n’a rien à voir avec celui d’un frontend React.

Le point commun : logique variable, décision au runtime.

Chaque variable multiplie le nombre de jobs. Avec 3 dimensions de 3 valeurs chacune, vous obtenez 27 jobs — et vous approchez vite de la limite de 200. La solution : identifier les combinaisons réellement critiques plutôt que tester exhaustivement.

Dans l’exemple ci-dessous, on teste toutes les bases de données avec la version LTS (Node 20), mais on ne valide qu’une seule DB pour les versions secondaires. Résultat : 5 jobs au lieu de 27, avec une couverture suffisante.

# ❌ 3 × 3 × 3 = 27 jobs !
test:
parallel:
matrix:
- NODE: [18, 20, 22]
DB: [postgres, mysql, sqlite]
CACHE: [redis, memcached, none]
# ✅ Tester les combinaisons critiques seulement
test:
parallel:
matrix:
- NODE: "20"
DB: [postgres, mysql, sqlite]
- NODE: [18, 22]
DB: postgres # Un seul DB pour les autres versions

Quand un job matriciel produit des artefacts, nommez-les avec les variables de la matrice. Sans cela, les fichiers se chevauchent ou deviennent impossibles à identifier dans l’interface GitLab.

L’attribut name définit le nom du ZIP téléchargeable, et les variables sont interpolées automatiquement.

build:
parallel:
matrix:
- OS: [linux, windows]
ARCH: [amd64, arm64]
script:
- ./build.sh
artifacts:
paths:
- build/app-$OS-$ARCH*
name: "build-$OS-$ARCH"

Par défaut, si un job de la matrice échoue, tout le pipeline échoue. C’est souvent le comportement souhaité, mais parfois vous voulez continuer malgré un échec sur une version expérimentale (ex : Node 23 en RC).

Utilisez allow_failure avec des codes de sortie spécifiques pour distinguer les vrais échecs des avertissements.

test:
parallel:
matrix:
- NODE_VERSION: ["18", "20", "22"]
script:
- npm test
allow_failure:
exit_codes: [42] # Code spécifique = warning, pas blocage

Dans l’interface GitLab, les jobs matriciels apparaissent comme test: [18], test: [20]… C’est suffisant pour des matrices simples, mais avec plusieurs dimensions, ça devient illisible (test: [18, postgres, redis]).

Ajoutez une variable descriptive et affichez-la en début de script pour faciliter le diagnostic.

test:
parallel:
matrix:
- NODE_VERSION: ["18", "20", "22"]
variables:
JOB_DESCRIPTION: "Node.js $NODE_VERSION tests"
script:
- echo "$JOB_DESCRIPTION"
- npm test

Voici les problèmes les plus courants et comment les résoudre :

Symptôme : Le pipeline échoue immédiatement avec ce message.

Cause : Votre matrice génère plus de 200 jobs (la limite GitLab).

Exemple : 4 versions × 5 DB × 3 OS × 4 navigateurs = 240 jobs → échec.

Solution : Réduisez les combinaisons (voir section Limiter l’explosion combinatoire).

Symptôme : “Job not found” ou dépendance cassée.

Cause : L’ordre des variables dans needs:parallel:matrix ne correspond pas à celui du job source.

Solution : Vérifiez que vous utilisez exactement le même ordre :

# Si le job source est :
build:
parallel:
matrix:
- OS: [linux, windows] # OS en premier
ARCH: [amd64, arm64] # ARCH en second
# Le needs doit respecter cet ordre :
needs:
- job: build
parallel:
matrix:
- OS: linux # OS en premier
ARCH: amd64 # ARCH en second

Symptôme : GitLab utilise littéralement $NODE_VERSION au lieu de 18.

Cause : Vous avez utilisé des guillemets simples au lieu de doubles.

Solution : Les variables s’interpolèrent correctement avec $VAR directement :

# ✅ Correct
image: node:$NODE_VERSION
# ✅ Correct aussi
image: "node:$NODE_VERSION"
# ❌ Problème potentiel avec guillemets simples
image: 'node:$NODE_VERSION' # $NODE_VERSION n'est pas remplacé

Symptôme : Aucun job n’est généré.

Cause : Vous avez un tableau vide [] ou aucune valeur.

Solution : Chaque variable doit avoir au moins une valeur :

# ❌ Tableau vide = aucun job
parallel:
matrix:
- NODE_VERSION: [] # Erreur !
# ✅ Au moins une valeur
parallel:
matrix:
- NODE_VERSION: ["20"] # OK, 1 job

GitLab nomme automatiquement les jobs matriciels selon le pattern job: [val1, val2]. Ce nommage apparaît dans l’interface et les logs — autant qu’il soit lisible.

À éviterPréférerPourquoi
node:18-alpine18Valeurs courtes = lisibilité
postgres:15-alpinepg15Alias courts pour les services
us-east-1us-e1Abréviations reconnues

Exemple concret :

# ❌ Illisible dans l'UI : test: [node:18-alpine, postgres:15-alpine]
parallel:
matrix:
- IMAGE: ["node:18-alpine", "node:20-alpine"]
DB: ["postgres:15-alpine", "mysql:8"]
# ✅ Lisible : test: [18, pg15]
parallel:
matrix:
- NODE: ["18", "20"]
DB: ["pg15", "mysql8"]
variables:
NODE_IMAGE: "node:$NODE-alpine"
DB_IMAGE: "$DB" # Mapping dans le script ou variables

Utilisez des alias courts dans la matrice, et construisez les vraies valeurs (images Docker, URLs) dans variables: ou le script.

  1. parallel:matrix multiplie un job avec différentes valeurs de variables
  2. Nommage automatique : job: [val1, val2]
  3. Variables disponibles : CI_NODE_INDEX, CI_NODE_TOTAL, vos variables
  4. Combinatoire : toutes les permutations sont générées
  5. needs: ["job"] attend toutes les instances matricielles
  6. needs:parallel:matrix pour cibler une combinaison spécifique
  7. Limite : 200 jobs maximum par matrice
  8. Alternative : pipelines dynamiques pour des cas plus complexes

Ce site vous est utile ?

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

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.