Aller au contenu

Matrix strategy GitHub Actions

Mise à jour :

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.

C’est quoi une matrix, concrètement ?

L’analogie du tableau de bord

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 !

Avant/après : la puissance de la matrix

❌ Sans matrix : duplication massive

jobs:
test-ubuntu-18:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
with:
node-version: 18
- run: npm test
test-ubuntu-20:
runs-on: ubuntu-latest
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-latest, 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

Votre première matrix en 3 étapes

  1. Définir les axes : quelles variables voulez-vous combiner ?

    strategy:
    matrix:
    os: [ubuntu-latest, 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

Exemple complet commenté

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-latest, 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-latest (2m 15s)
✓ Test Python 3.11 sur ubuntu-latest (2m 08s)
✓ Test Python 3.12 sur ubuntu-latest (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)

Comment ça marche techniquement ?

Le produit cartésien

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.

Les variables de la matrix

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"

Syntaxe détaillée

Définir une matrix

jobs:
build:
strategy:
matrix:
# Chaque clé devient une variable ${{ matrix.xxx }}
os: [ubuntu-latest, 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.

Utiliser les valeurs de la matrix

Les valeurs sont accessibles via le context matrix :

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

Include : ajouter ou enrichir des combinaisons

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

Cas 1 : Ajouter une combinaison spéciale

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

strategy:
matrix:
os: [ubuntu-latest, 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-latest
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

Cas 2 : Enrichir une combinaison existante

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-latest, 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-latest
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é

Cas 3 : Matrix basée uniquement sur include

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 : retirer des combinaisons indésirables

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

Pourquoi exclure des combinaisons ?

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

Exemple concret

strategy:
matrix:
os: [ubuntu-latest, 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

fail-fast : tout arrêter ou continuer ?

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]

max-parallel : limiter les jobs simultanés

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

Techniques avancées

Matrix dynamique avec fromJSON

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-latest
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-latest
steps:
- run: echo "Building version ${{ matrix.version }}"

Matrix depuis un fichier de configuration

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-latest
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-latest
steps:
- run: echo "Testing ${{ matrix.name }} at ${{ matrix.path }}"

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

Exemples prêts à l’emploi

Test multi-versions Python

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-latest, 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

Build multi-architecture Docker

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-latest
- 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éploiement multi-environnements

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-latest
steps:
- run: |
echo "🚀 Deploying to ${{ matrix.env }}"
echo "URL: ${{ matrix.url }}"
echo "Replicas: ${{ matrix.replicas }}"

Les 5 règles d’or des matrix

1. Nommez vos jobs explicitement

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-latest, windows-latest]
node: [18, 20]

Résultat dans l’interface :

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

2. Utilisez fail-fast: false pour débugger

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]

3. Limitez la taille de votre matrix

# ❌ É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-latest
node: 20
database: postgres
# Validation Windows
- os: windows-latest
node: 20
database: postgres
# Test rétro-compatibilité
- os: ubuntu-latest
node: 18
database: mysql

4. Gérez les spécificités de chaque OS

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

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

5. Documentez vos exclusions

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

À retenir

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

Liens utiles