Aller au contenu
CI/CD & Automatisation medium

Matrix strategy GitHub Actions

24 min de lecture

Imaginez que vous devez tester votre application sur 3 systèmes d’exploitation et 3 versions de Node.js. Cela fait 9 combinaisons à tester. Sans outil adapté, vous devriez copier-coller le même code 9 fois. La matrix strategy résout ce problème élégamment.

Pensez à un tableau à double entrée comme ceux qu’on utilisait à l’école :

Tableau matrix : combinaisons OS × versions Node générant 9 jobs

Chaque cellule du tableau représente un job GitHub Actions. La matrix génère automatiquement toutes ces combinaisons à partir de deux listes :

  • Liste des OS : [ubuntu, windows, macos]
  • Liste des versions : [18, 20, 22]

3 × 3 = 9 jobs, sans écrire 9 fois le même code !

❌ Sans matrix : duplication massive

jobs:
test-ubuntu-18:
runs-on: ubuntu-24.04
steps:
- uses: actions/setup-node@v4
with:
node-version: 18
- run: npm test
test-ubuntu-20:
runs-on: ubuntu-24.04
steps:
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm test
test-ubuntu-22:
# ... encore et encore
test-windows-18:
# ... 6 autres jobs identiques

✅ Avec matrix : un seul bloc

jobs:
test:
strategy:
matrix:
os: [ubuntu-24.04, windows-latest, macos-latest]
node: [18, 20, 22]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm test
  1. Définir les axes : quelles variables voulez-vous combiner ?

    strategy:
    matrix:
    os: [ubuntu-24.04, windows-latest]
    python: ['3.10', '3.11', '3.12']
  2. Utiliser les variables : accédez aux valeurs avec ${{ matrix.xxx }}

    runs-on: ${{ matrix.os }}
    steps:
    - uses: actions/setup-python@v5
    with:
    python-version: ${{ matrix.python }}
  3. Lancer le workflow : GitHub génère automatiquement 2 × 3 = 6 jobs

name: Tests multi-configurations
on: [push, pull_request]
jobs:
test:
# Le nom du job affiche les valeurs de la matrix
name: Test Python ${{ matrix.python }} sur ${{ matrix.os }}
strategy:
matrix:
# Axe 1 : les systèmes d'exploitation
os: [ubuntu-24.04, windows-latest]
# Axe 2 : les versions de Python
python: ['3.10', '3.11', '3.12']
# Cette ligne UTILISE la valeur de matrix.os
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# Cette action UTILISE la valeur de matrix.python
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ matrix.python }}
- run: pip install -e ".[test]"
- run: pytest

Résultat dans l’interface GitHub :

✓ Test Python 3.10 sur ubuntu-24.04 (2m 15s)
✓ Test Python 3.11 sur ubuntu-24.04 (2m 08s)
✓ Test Python 3.12 sur ubuntu-24.04 (2m 12s)
✓ Test Python 3.10 sur windows-latest (3m 45s)
✓ Test Python 3.11 sur windows-latest (3m 38s)
✓ Test Python 3.12 sur windows-latest (3m 42s)

GitHub calcule le produit cartésien de tous les axes. Ne vous laissez pas intimider par ce terme mathématique : c’est simplement toutes les combinaisons possibles entre vos listes de valeurs.

Imaginez un menu de restaurant : si vous avez 2 entrées et 3 plats, vous pouvez composer 2 × 3 = 6 menus différents. Le produit cartésien, c’est exactement ça !

Avec une matrix à deux axes :

Axe 1: [A, B] ┐
├──→ Combinaisons: [A,X], [A,Y], [B,X], [B,Y]
Axe 2: [X, Y] ┘
2 × 2 = 4 jobs

Comment lire ce schéma ? GitHub prend chaque valeur du premier axe (A, puis B) et l’associe à chaque valeur du second axe (X, puis Y). Résultat : 4 combinaisons, donc 4 jobs parallèles.

Avec trois axes, le principe reste le même — on multiplie simplement les possibilités :

os: [ubuntu, windows] 2 valeurs
node: [18, 20, 22] 3 valeurs
database: [postgres, mysql] 2 valeurs
─────────
2 × 3 × 2 = 12 jobs

Traduction concrète : chaque combinaison OS + version Node + base de données sera testée. Ubuntu avec Node 18 et Postgres, Ubuntu avec Node 18 et MySQL, Ubuntu avec Node 20 et Postgres… et ainsi de suite jusqu’aux 12 combinaisons.

Chaque valeur définie dans la matrix devient accessible via le context matrix :

strategy:
matrix:
fruit: [pomme, banane]
couleur: [rouge, jaune]
# Dans les steps, vous pouvez utiliser :
# ${{ matrix.fruit }} → "pomme" ou "banane"
# ${{ matrix.couleur }} → "rouge" ou "jaune"
jobs:
build:
strategy:
matrix:
# Chaque clé devient une variable ${{ matrix.xxx }}
os: [ubuntu-24.04, windows-latest]
version: [1.0, 2.0, 3.0]
arch: [x64, arm64]
runs-on: ${{ matrix.os }}
steps:
- run: |
echo "OS: ${{ matrix.os }}"
echo "Version: ${{ matrix.version }}"
echo "Arch: ${{ matrix.arch }}"

Cet exemple génère 2 × 3 × 2 = 12 combinaisons.

Les valeurs sont accessibles via le context matrix :

jobs:
test:
strategy:
matrix:
python: ['3.9', '3.10', '3.11', '3.12']
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ matrix.python }}
- run: python --version

Le mot-clé include est comme un bonus pour votre matrix. Il permet deux choses :

  1. Ajouter des combinaisons qui n’existent pas dans le produit cartésien
  2. Enrichir des combinaisons existantes avec des variables supplémentaires

Imaginons que vous voulez tester Node 22, mais uniquement sur Ubuntu (car c’est expérimental sur les autres OS) :

strategy:
matrix:
os: [ubuntu-24.04, windows-latest]
node: [18, 20]
include:
# Cette combinaison N'EXISTE PAS dans le produit cartésien
# (node 22 n'est pas dans la liste originale)
- os: ubuntu-24.04
node: 22
experimental: true # Variable bonus pour cette combinaison

Sans include : 2 × 2 = 4 combinaisons Avec include : 4 + 1 = 5 combinaisons

Include - Ajout d'une combinaison supplémentaire avec variable bonus

Parfois, vous avez besoin de variables différentes selon la combinaison. Par exemple, le shell par défaut varie selon l’OS :

strategy:
matrix:
os: [ubuntu-24.04, windows-latest, macos-latest]
include:
# Ces lignes ENRICHISSENT les combinaisons existantes
# en ajoutant une variable "shell"
- os: windows-latest
shell: pwsh # PowerShell pour Windows
- os: ubuntu-24.04
shell: bash # Bash pour Ubuntu
- os: macos-latest
shell: bash # Bash pour macOS
runs-on: ${{ matrix.os }}
defaults:
run:
shell: ${{ matrix.shell }} # Utilise le shell approprié

Vous pouvez créer une matrix sans axes, uniquement avec include. Utile pour des configurations très différentes :

strategy:
matrix:
include:
- name: 'Production EU'
region: 'eu-west-1'
env: 'prod'
replicas: 3
- name: 'Production US'
region: 'us-east-1'
env: 'prod'
replicas: 3
- name: 'Staging'
region: 'eu-west-1'
env: 'staging'
replicas: 1
# 3 jobs avec des configurations complètement personnalisées

exclude fait l’inverse de include : il retire des combinaisons du produit cartésien. C’est comme dire “je veux tout, sauf ça”.

Quelques raisons courantes :

  • Une version n’est pas supportée sur un OS particulier
  • Une combinaison est redondante ou inutile
  • Économiser des minutes de CI sur des tests non pertinents
strategy:
matrix:
os: [ubuntu-24.04, windows-latest, macos-latest]
node: [18, 20, 22]
exclude:
# Node 22 a des problèmes connus sur Windows
- os: windows-latest
node: 22
# On ne teste pas toutes les versions sur macOS (coûteux)
- os: macos-latest
node: 18

Calcul des jobs :

  • Produit cartésien : 3 × 3 = 9 combinaisons
  • Exclusions : -2 combinaisons
  • Total : 7 jobs

Contrôler l’exécution : fail-fast et max-parallel

Section intitulée « Contrôler l’exécution : fail-fast et max-parallel »

Par défaut, fail-fast est à true. Cela signifie que si un seul job de la matrix échoue, GitHub annule tous les autres immédiatement.

Comparaison fail-fast: true vs false - comportement en cas d'échec

Quand garder fail-fast: true (défaut) :

  • Vous voulez un feedback rapide
  • Un échec rend les autres résultats inutiles
  • Vous économisez des minutes de CI

Quand utiliser fail-fast: false :

  • Vous voulez voir tous les résultats
  • Vous debuggez et cherchez quelles combinaisons échouent
  • Les jobs sont indépendants
strategy:
fail-fast: false # Continue même si un job échoue
matrix:
node: [18, 20, 22]

Par défaut, GitHub lance tous les jobs en parallèle. Avec max-parallel, vous pouvez limiter ce nombre :

strategy:
max-parallel: 2 # Maximum 2 jobs en même temps
matrix:
node: [18, 20, 22] # 3 jobs au total

Pourquoi limiter le parallélisme ?

SituationRaison
Tests avec base de données partagéeÉviter les conflits de données
API externe avec rate limitingNe pas dépasser les quotas
Runners self-hosted limitésÉviter la saturation
Économiser les minutesRéduire les coûts (repos privés)

max-parallel: 2 - Exécution par lots de 2 jobs

Parfois, vous ne connaissez pas les valeurs de la matrix à l’avance. Par exemple, vous voulez tester uniquement les modules modifiés. La solution : générer la matrix dynamiquement dans un premier job.

jobs:
# Job 1 : Déterminer quoi tester
setup:
runs-on: ubuntu-24.04
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- id: set-matrix
run: |
# Génère un JSON qui sera la matrix
echo 'matrix={"version":["1.0","2.0","3.0"]}' >> $GITHUB_OUTPUT
# Job 2 : Utilise la matrix générée
build:
needs: setup
strategy:
# fromJSON convertit la string en objet
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
runs-on: ubuntu-24.04
steps:
- run: echo "Building version ${{ matrix.version }}"

Pour une meilleure maintenabilité, stockez la matrix dans un fichier :

Fichier .github/matrix.json :

{
"include": [
{ "name": "app-frontend", "path": "./apps/frontend" },
{ "name": "app-backend", "path": "./apps/backend" },
{ "name": "app-api", "path": "./apps/api" }
]
}

Workflow :

jobs:
setup:
runs-on: ubuntu-24.04
outputs:
matrix: ${{ steps.read.outputs.matrix }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- id: read
run: |
# jq -c compacte le JSON sur une ligne
echo "matrix=$(cat .github/matrix.json | jq -c .)" >> $GITHUB_OUTPUT
test:
needs: setup
strategy:
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
runs-on: ubuntu-24.04
steps:
- run: echo "Testing ${{ matrix.name }} at ${{ matrix.path }}"

Voir le guide jq pour maîtriser le traitement JSON en ligne de commande.

Un classique : tester sur plusieurs versions de Python et plusieurs OS.

name: Tests Python
on: [push, pull_request]
permissions:
contents: read
jobs:
test:
name: Python ${{ matrix.python }} / ${{ matrix.os }}
strategy:
fail-fast: false # Voir tous les résultats
matrix:
os: [ubuntu-24.04, windows-latest, macos-latest]
python: ['3.10', '3.11', '3.12']
exclude:
# macOS coûte cher, on limite les versions testées
- os: macos-latest
python: '3.10'
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ matrix.python }}
- run: pip install -e ".[test]"
- run: pytest --verbose

Pour créer des images Docker ARM64 et AMD64 :

name: Build Multi-Arch
on:
push:
branches: [main]
jobs:
build:
name: Build ${{ matrix.platform }}
strategy:
matrix:
include:
- platform: linux/amd64
runner: ubuntu-24.04
- platform: linux/arm64
runner: ubuntu-24.04-arm # Runner ARM natif
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Build image
run: |
docker build \
--platform ${{ matrix.platform }} \
-t myapp:${{ github.sha }}-${{ matrix.platform }} .

Déployer sur staging ou production selon le choix de l’utilisateur :

name: Deploy
on:
workflow_dispatch:
inputs:
target:
description: 'Où déployer ?'
type: choice
options: [staging, production]
default: staging
jobs:
deploy:
name: Deploy to ${{ matrix.env }}
strategy:
matrix:
include:
- env: staging
url: https://staging.example.com
replicas: 1
- env: production
url: https://example.com
replicas: 3
exclude:
# Astuce : exclure l'environnement non choisi
- env: ${{ github.event.inputs.target == 'staging' && 'production' || 'staging' }}
environment: ${{ matrix.env }}
runs-on: ubuntu-24.04
steps:
- run: |
echo "🚀 Deploying to ${{ matrix.env }}"
echo "URL: ${{ matrix.url }}"
echo "Replicas: ${{ matrix.replicas }}"

Sans nom personnalisé, GitHub affiche “test (1)”, “test (2)”… Peu utile !

jobs:
test:
# ✅ Nom explicite avec les valeurs de la matrix
name: Test ${{ matrix.os }} / Node ${{ matrix.node }}
strategy:
matrix:
os: [ubuntu-24.04, windows-latest]
node: [18, 20]

Résultat dans l’interface :

✓ Test ubuntu-24.04 / Node 18 (2m 15s)
✓ Test ubuntu-24.04 / Node 20 (2m 08s)
✓ Test windows-latest / Node 18 (3m 45s)
✓ Test windows-latest / Node 20 (3m 38s)

Quand vous cherchez quelles combinaisons échouent, vous voulez tous les résultats :

strategy:
fail-fast: false # Ne pas annuler les autres jobs en cas d'échec
matrix:
node: [18, 20, 22]
# ❌ ÉVITER : 3 × 4 × 3 = 36 combinaisons !
matrix:
os: [ubuntu, windows, macos]
node: [16, 18, 20, 22]
database: [postgres, mysql, sqlite]
# ✅ PRÉFÉRER : combinaisons ciblées
matrix:
include:
# Test complet sur Ubuntu (référence)
- os: ubuntu-24.04
node: 20
database: postgres
# Validation Windows
- os: windows-latest
node: 20
database: postgres
# Test rétro-compatibilité
- os: ubuntu-24.04
node: 18
database: mysql

Windows et Linux n’utilisent pas les mêmes commandes. Utilisez include pour personnaliser :

strategy:
matrix:
os: [ubuntu-24.04, windows-latest]
include:
- os: ubuntu-24.04
script: ./scripts/test.sh
shell: bash
- os: windows-latest
script: .\scripts\test.ps1
shell: pwsh
steps:
- run: ${{ matrix.script }}
shell: ${{ matrix.shell }}

Les exclusions peuvent être mystérieuses pour les contributeurs. Ajoutez toujours un commentaire :

exclude:
# Python 3.9 est en fin de vie (EOL octobre 2025)
# On ne le supporte plus que sur Ubuntu pour les systèmes legacy
- python: '3.9'
os: macos-latest
- python: '3.9'
os: windows-latest
# Node 22 a un bug connu avec Windows Server 2022
# Voir https://github.com/nodejs/node/issues/XXXXX
- node: 22
os: windows-latest
ConceptSyntaxeUsage
Axes de la matrixmatrix: { os: [...], node: [...] }Définir les combinaisons
Accès aux valeurs${{ matrix.os }}Utiliser dans les steps
Ajouter/enrichirinclude: [...]Cas spéciaux
Exclureexclude: [...]Retirer des combinaisons
Continuer sur échecfail-fast: falseDebug
Limiter le parallélismemax-parallel: NRessources limitées
Matrix dynamiquefromJSON(...)Valeurs calculées