Changer le thème

Matrix GitHub Actions : guide pratique des tests parallèles multi-plateformes et multi-versions

Easton editorial illustration: single workflow definition card, OS-by-version matrix hinge, parallel test lanes, completion collector

L’année dernière, un projet open source m’a contacté : leur fichier de configuration CI avait gonflé à plus de 800 lignes. Je l’ai ouvert — des définitions de job répétées partout : Node 16 sur Ubuntu, Node 16 sur Windows, Node 16 sur macOS… puis Node 18, puis Node 20. Modifier une commande de test ? Douze endroits à toucher. Ajouter une version ? Quinze minutes de copier-coller.

À ce moment-là, j’ai réalisé que beaucoup de monde maintient encore à la main des tests multi-versions et multi-plateformes.

Le Matrix de GitHub Actions, en résumé, déploie automatiquement ces configurations répétitives. Vous définissez quelques systèmes d’exploitation et quelques versions de runtime, et il exécute toutes les combinaisons. Ça a l’air simple, mais en pratique les pièges sont nombreux — explosion du nombre de combinaisons et facture qui explose, un échec qui fait tout tomber, exclusion d’une combinaison spécifique mal comprise… Je suis passé par là.

Cet article part de la syntaxe Matrix la plus basique pour vous faire maîtriser exclude/include, le choix de fail-fast, la limitation de concurrence avec max-parallel, et la génération dynamique de Matrix. À la fin, 5 modèles de workflow prêts à copier dans votre projet, du projet perso à l’entreprise.

2. Concepts clés du Matrix : déployer plusieurs tâches en un clic

La logique du Matrix est simple : vous définissez des dimensions, GitHub Actions calcule le produit cartésien.

Par exemple, votre projet doit être testé sur Ubuntu, Windows et macOS, avec Node.js 18, 20 et 22. En écriture classique, 9 jobs à la main, chacun répétant environnement, installation et tests. Avec Matrix :

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest, macos-latest]
    node: [18, 20, 22]

runs-on: ${{ matrix.os }}

steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: ${{ matrix.node }}
  - run: npm test

Ces 10 lignes deviennent 3 x 3 = 9 tâches parallèles. Chaque tâche reçoit des valeurs différentes pour matrix.os et matrix.node.

Le projet à 800 lignes de config ? Refactoré avec Matrix, environ 120 lignes — plus de 60 % de code en moins. Ajouter une version : un chiffre dans le tableau, plus de copier-coller de jobs.

Ce que Matrix vous apporte :

  • Générer d’un coup les combinaisons multi-plateformes et multi-versions
  • Déployer toute la config sans duplication manuelle
  • Exclure les combinaisons connues comme problématiques via exclude
  • Ajouter des cas avec configuration spéciale via include
  • Contrôler la concurrence pour équilibrer vitesse et coût

Ce qu’il ne résout pas :

  • Des tests mal écrits — Matrix ne les sauvera pas
  • Trop de combinaisons et facture qui explose — à vous de maîtriser les dimensions
  • Installation lente des dépendances — à combiner avec la mise en cache

Franchement, Matrix n’est pas difficile à comprendre ; le dur, c’est l’appliquer aux vrais projets. On commence par la syntaxe de base.

3. Syntaxe de base : principe os x version

Les combinaisons Matrix, c’est le produit cartésien : chaque dimension se combine avec les autres.

Une dimension, N valeurs -> N tâches

Deux dimensions, M x N valeurs -> M x N tâches

Trois dimensions, A x B x C valeurs -> A x B x C tâches

Exemple concret : projet Python à tester sur Linux et Windows, Python 3.9 à 3.12, avec PostgreSQL et MySQL :

jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
        python-version: ['3.9', '3.10', '3.11', '3.12']
        database: [postgresql, mysql]
    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - name: Setup ${{ matrix.database }}
        run: |
          # Démarrer le service de base de données correspondant
          if [ "${{ matrix.database }}" = "postgresql" ]; then
            docker run -d -p 5432:5432 postgres
          else
            docker run -d -p 3306:3306 mysql
          fi
        shell: bash
      - run: pip install -r requirements.txt
      - run: pytest

Cette config génère 2 x 4 x 2 = 16 tâches, chacune dans un environnement isolé.

Accéder aux variables Matrix :

  • ${{ matrix.os }} — système d’exploitation de la tâche courante
  • ${{ matrix.python-version }} — version Python
  • ${{ matrix.database }} — type de base de données

Ces variables servent dans runs-on, steps, env, etc.

Piège fréquent : beaucoup pensent que Matrix gère automatiquement les dépendances. Chaque tâche est un environnement neuf — l’installation se répète. Si l’install prend 2 minutes, 16 tâches = 32 minutes d’attente (en séquentiel).

Deux pistes :

  1. Cache — mettre en cache pip ou npm pour éviter les retéléchargements
  2. Moins de combinaisons — exclude pour retirer les tests superflus

J’ai détaillé la stratégie de cache dans [Stratégie de cache GitHub Actions : accélérer le pipeline CI/CD par 5]. Ici, on se concentre sur exclude/include.

4. exclude/include : contrôler finement les combinaisons

Par défaut, Matrix fait le produit cartésien complet. En pratique, certaines combinaisons sont inutiles ou demandent un traitement spécial.

4.1 exclude : écarter les combinaisons invalides

Sur un projet Python : Windows + Python 3.9 échouait toujours — une dépendance incompatible sur Windows en 3.9. Le projet cible surtout Linux ; Windows est secondaire, pas la peine de corriger ce bug précis.

exclude entre en jeu :

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest, macos-latest]
    python-version: ['3.9', '3.10', '3.11', '3.12']
    exclude:
      - os: windows-latest
        python-version: '3.9'
      - os: macos-latest
        python-version: '3.9'

On retire Python 3.9 sur Windows et macOS. 12 tâches au départ, 10 après exclusion.

Cas typiques pour exclude :

  1. Problème de compatibilité connu — une version ne tourne pas sur un OS
  2. Limite de ressources — peu de runners self-hosted, réduire les combinaisons
  3. Cas marginaux — combinaisons rarement utilisées, pas worth le temps CI

4.2 include : ajouter une configuration spéciale

include fait l’inverse — combinaisons supplémentaires ou variables en plus.

Exemple : activer le rapport de couverture uniquement sur Python 3.12 :

strategy:
  matrix:
    python-version: ['3.10', '3.11', '3.12']
    include:
      - python-version: '3.12'
        coverage: true

steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-python@v5
    with:
      python-version: ${{ matrix.python-version }}
  - run: pip install -r requirements.txt
  - name: Run tests
    run: |
      if [ "${{ matrix.coverage }}" = "true" ]; then
        pytest --cov=src --cov-report=xml
      else
        pytest
      fi
    shell: bash

include ici :

  1. Ajoute une combinaison — test Python 3.12
  2. Ajoute une variablecoverage: true

Cas typiques pour include :

  1. Version expérimentale — ex. Python 3.13 preview sur un seul OS
  2. Config spéciale — variables ou paramètres supplémentaires
  3. Couverture des cas rares — combinaisons peu fréquentes ajoutées à la main

4.3 exclude et include ensemble

Souvent les deux : exclure tout Python 3.9, mais garder un test minimal Ubuntu + Python 3.9 :

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest]
    python-version: ['3.9', '3.10', '3.11', '3.12']
    exclude:
      - python-version: '3.9'
    include:
      - os: ubuntu-latest
        python-version: '3.9'
        minimal: true

Ordre d’exécution : toutes les combinaisons -> exclude -> include. Résultat : 4 versions sur Ubuntu, 3 sur Windows (sans 3.9).

5. Stratégie fail-fast : échec rapide vs débogage complet

Par défaut, si une tâche Matrix échoue, les autres en cours sont annulées. C’est fail-fast, activé par défaut.

strategy:
  fail-fast: true  # valeur par défaut, peut être omis
  matrix:
    os: [ubuntu-latest, windows-latest]
    node: [18, 20, 22]

5.1 Quand utiliser fail-fast: true (défaut)

Tests PR — vous voulez savoir vite si ça casse. Un échec, les autres suivront souvent (même bug de code), inutile de continuer.

Budget serré — quota GitHub Actions limité, runners self-hosted limités. fail-fast économise.

Mon habitude : fail-fast: true en PR, fail-fast: false sur les tests complets de main.

5.2 Quand utiliser fail-fast: false

Phase de débogage — vous voulez voir quelles combinaisons échouent et pourquoi. Avec fail-fast: true, seule la première échec visible.

Tests de compatibilité — chaque combinaison compte, même si une version pose problème.

Rapport complet — un bilan de toutes les combinaisons après le CI.

strategy:
  fail-fast: false  # laisser toutes les tâches se terminer
  matrix:
    os: [ubuntu-latest, windows-latest, macos-latest]
    node: [18, 20, 22]

5.3 Cas réel

L’année dernière, CI qui échouait sur Ubuntu + Node 18 alors que le reste passait. Avec fail-fast par défaut, à chaque run on ne voyait que cet échec puis annulation. Passage à fail-fast: false : Windows + Node 18 OK, seul Ubuntu posait problème — casse de casse dans les chemins de fichiers.

Conseil : débogage avec fail-fast: false ; en production stable, fail-fast: true pour économiser temps et argent.

6. max-parallel : concurrence et optimisation des coûts

Les tâches Matrix tournent en parallèle par défaut. Runners GitHub hébergés : limite 20 en public ; privé gratuit : 2.

Parfois il faut limiter manuellement — max-parallel.

6.1 Quand limiter la concurrence

Runners self-hosted limités — serveur 4 cœurs / 8 Go, 8 tâches simultanées le tuent.

Rate limiting API tierce — tests qui appellent une API avec quota QPS.

Pool de connexions DB — 10 connexions max, trop de tâches = épuisement.

strategy:
  max-parallel: 4  # maximum 4 tâches en parallèle
  matrix:
    os: [ubuntu-latest, windows-latest]
    node: [18, 20, 22]

6 tâches générées, 4 max en parallèle. Une finit, une autre démarre.

6.2 Exemple de calcul de coût

Projet : 3 OS x 4 versions Node = 12 tâches, ~10 minutes chacune.

Sans limite (runners suffisants) :

  • 12 tâches en parallèle
  • Durée totale ~10 minutes
  • Temps de calcul = 12 x 10 = 120 minutes

Avec max-parallel: 4 :

  • 12 tâches en 3 vagues
  • Durée totale ~30 minutes
  • Temps de calcul = 120 minutes (inchangé)

max-parallel ne réduit pas le temps de calcul total, seulement la durée du pipeline. Pourquoi l’utiliser ?

Coût de pic et limites de ressources.

GitHub facture les minutes ; en self-hosted ou cloud au pic, la concurrence compte. 12 tâches = 12 connexions DB ; par vagues de 4, il en faut 4.

Mon expérience :

  • Dépôt public, runners GitHub : pas besoin de max-parallel
  • Privé, quota gratuit : max-parallel: 2
  • Self-hosted : selon la machine, 4 cœurs → 2–4 en parallèle

7. Matrix dynamique : fromJSON en profondeur

Jusqu’ici, Matrix statique dans le YAML. Parfois il faut générer les combinaisons selon les changements de code.

Monorepo avec plusieurs services : ne tester que ceux touchés par le commit.

7.1 Workflow en deux étapes

Pas de syntaxe « matrix dynamique » native — un job produit la config, un autre l’utilise via fromJSON().

jobs:
  # Étape 1 : détecter les services modifiés et générer la config matrix
  detect:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2  # récupérer le commit précédent

      - name: Detect changed services
        id: set-matrix
        run: |
          # Fichiers modifiés dans ce commit
          CHANGED_FILES=$(git diff --name-only HEAD^ HEAD)

          # Déterminer quels services ont changé
          SERVICES="[]"
          if echo "$CHANGED_FILES" | grep -q "services/auth/"; then
            SERVICES=$(echo $SERVICES | jq '. + ["auth"]')
          fi
          if echo "$CHANGED_FILES" | grep -q "services/api/"; then
            SERVICES=$(echo $SERVICES | jq '. + ["api"]')
          fi
          if echo "$CHANGED_FILES" | grep -q "services/web/"; then
            SERVICES=$(echo $SERVICES | jq '. + ["web"]')
          fi

          # Si aucun service modifié, tester tous par défaut
          if [ "$SERVICES" = "[]" ]; then
            SERVICES='["auth", "api", "web"]'
          fi

          echo "matrix={\"service\":$(echo $SERVICES)}" >> $GITHUB_OUTPUT

  # Étape 2 : utiliser le matrix généré dynamiquement
  test:
    needs: detect
    runs-on: ubuntu-latest
    strategy:
      matrix: ${{ fromJSON(needs.detect.outputs.matrix) }}

    steps:
      - uses: actions/checkout@v4
      - name: Test ${{ matrix.service }}
        run: |
          cd services/${{ matrix.service }}
          npm install
          npm test

Fonctionnement :

  1. Job detect : répertoires modifiés
  2. Génération JSON du matrix
  3. Job test : fromJSON() et tâches correspondantes

7.2 Cas d’usage du matrix dynamique

Monorepo — seulement les services modifiés

Déploiement à la demande — Dockerfile changé → build des images concernées

Optimisation — matrix complet seulement si package.json change

Pièges que j’ai vus :

  1. fromJSON() uniquement sur la valeur de strategy.matrix
  2. JSON valide, ex. {"service": ["auth", "api"]}
  3. Matrix vide = erreur workflow — prévoir une valeur par défaut

8. Bibliothèque de modèles : 5 workflows de production

Cinq modèles copiables, du projet perso à l’entreprise.

8.1 Modèle 1 : tests Node.js multi-versions (base)

Scénario : lib ou app Node.js, plusieurs versions Node

name: Node.js CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        node-version: [18, 20, 22, 23]

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'

      - run: npm ci
      - run: npm run build --if-present
      - run: npm test

      - name: Upload coverage
        if: matrix.node-version == 22
        uses: codecov/codecov-action@v4

Points clés :

  • npm ci plutôt que npm install pour verrouiller les versions
  • Couverture uploadée seulement sur Node 22
  • cache: 'npm' pour accélérer l’install

8.2 Modèle 2 : Python multi-plateformes et multi-versions (intermédiaire)

Scénario : projet Python, tests cross-OS et cross-version

name: Python CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        python-version: ['3.10', '3.11', '3.12']
        exclude:
          - os: windows-latest
            python-version: '3.10'  # problème de compatibilité connu

    steps:
      - uses: actions/checkout@v4

      - name: Setup Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: 'pip'

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt

      - name: Run tests
        run: pytest -v

      - name: Lint check
        run: |
          pip install ruff
          ruff check .

Points clés :

  • exclude pour combinaisons connues comme problématiques
  • cache: 'pip'
  • Lint avec ruff

8.3 Modèle 3 : exclude/include fin (avancé)

Scénario : contrôle fin, exclusions et tests spéciaux

name: Advanced Matrix

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest]
        python-version: ['3.10', '3.11', '3.12']
        exclude:
          # Exclure Windows + Python 3.10 (problème connu)
          - os: windows-latest
            python-version: '3.10'
        include:
          # Test expérimental : Ubuntu + Python 3.13 preview
          - os: ubuntu-latest
            python-version: '3.13-dev'
            experimental: true
          # Rapport de couverture pour Python 3.12
          - python-version: '3.12'
            coverage: true

    continue-on-error: ${{ matrix.experimental == true }}

    steps:
      - uses: actions/checkout@v4

      - name: Setup Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: 'pip'

      - run: pip install -r requirements.txt

      - name: Run tests
        run: |
          if [ "${{ matrix.coverage }}" = "true" ]; then
            pytest --cov=src --cov-report=xml
          else
            pytest
          fi
        shell: bash

Points clés :

  • continue-on-error pour tests expérimentaux
  • include pour nouvelles combinaisons et variables
  • shell: bash pour cohérence Windows/Linux

8.4 Modèle 4 : matrix dynamique + cache (avancé)

Scénario : monorepo, combinaisons selon les fichiers modifiés

name: Dynamic Matrix CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  detect:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2

      - name: Detect changed packages
        id: set-matrix
        run: |
          CHANGED_FILES=$(git diff --name-only HEAD^ HEAD)

          PACKAGES="[]"
          for dir in packages/*/; do
            pkg=$(basename $dir)
            if echo "$CHANGED_FILES" | grep -q "^packages/$pkg/"; then
              PACKAGES=$(echo $PACKAGES | jq ". + [\"$pkg\"]")
            fi
          done

          # Sans changement, tester tous les packages
          if [ "$PACKAGES" = "[]" ]; then
            PACKAGES='["core", "utils", "cli"]'
          fi

          echo "matrix={\"package\":$(echo $PACKAGES)}" >> $GITHUB_OUTPUT

  test:
    needs: detect
    runs-on: ubuntu-latest
    strategy:
      matrix: ${{ fromJSON(needs.detect.outputs.matrix) }}

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build --if-present

      - name: Test ${{ matrix.package }}
        run: |
          cd packages/${{ matrix.package }}
          npm test

Points clés :

  • fetch-depth: 2 pour comparer au commit précédent
  • jq pour manipuler le JSON
  • Valeur par défaut si aucun changement

8.5 Modèle 5 : self-hosted + max-parallel (entreprise)

Scénario : runners self-hosted, concurrence et ressources strictes

name: Enterprise CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: [self-hosted, linux, x64]
    strategy:
      fail-fast: true
      max-parallel: 4
      matrix:
        java-version: [11, 17, 21]
        database: [postgresql, mysql]

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: postgres
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

      mysql:
        image: mysql:8
        env:
          MYSQL_ROOT_PASSWORD: root
        ports:
          - 3306:3306
        options: >-
          --health-cmd "mysqladmin ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4

      - name: Setup Java ${{ matrix.java-version }}
        uses: actions/setup-java@v4
        with:
          java-version: ${{ matrix.java-version }}
          distribution: 'temurin'
          cache: 'maven'

      - name: Run tests with ${{ matrix.database }}
        env:
          DB_TYPE: ${{ matrix.database }}
          DB_HOST: localhost
          DB_PORT: ${{ matrix.database == 'postgresql' && 5432 || 3306 }}
        run: mvn test -Dspring.profiles.active=${{ matrix.database }}

      - name: Archive test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results-${{ matrix.java-version }}-${{ matrix.database }}
          path: target/surefire-reports

Points clés :

  • runs-on: [self-hosted, linux, x64]
  • max-parallel: 4 pour protéger le serveur
  • services pour les bases de test
  • if: always() pour archiver même en échec

9. Pièges courants et bonnes pratiques

Quelques pièges après des années de Matrix.

9.1 Piège 1 : explosion des combinaisons

Le pire vu : 4 OS x 5 runtimes x 3 bases x 2 caches = 120 tâches. 45 minutes par CI, facture qui explose.

Solutions :

  • Matrix complet sur main seulement ; PR avec combinaisons clés
  • exclude pour les cas marginaux
  • Chaque dimension est-elle vraiment nécessaire — 4 OS ?
# PR : combinaisons clés uniquement
on:
  pull_request:
    branches: [main]

jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest]  # PR : Ubuntu seulement
        node: [20]            # PR : Node 20 seulement

9.2 Piège 2 : fail-fast bloque le débogage

fail-fast: true par défaut : un échec, tout s’arrête — pas de rapport complet.

Solution : fail-fast: false en débogage, puis remettre true.

Ou via variable :

strategy:
  fail-fast: ${{ github.event_name == 'pull_request' }}

9.3 Piège 3 : pas de cache

Matrix répète le même job : réinstaller à chaque fois coûte cher. 12 combinaisons x 2 min d’install = 24 minutes rien que pour l’install.

Solution : cache GitHub Actions ou action dédiée.

- uses: actions/setup-node@v4
  with:
    node-version: ${{ matrix.node-version }}
    cache: 'npm'  # activer le cache npm

9.4 Synthèse des bonnes pratiques

Pratique 1 : Matrix réduit en PR + Matrix complet sur main

jobs:
  test:
    strategy:
      matrix:
        # PR : combinaisons clés uniquement
        ${{ github.event_name == 'pull_request' && fromJSON('{"os":["ubuntu-latest"],"node":[20]}') || fromJSON('{"os":["ubuntu-latest","windows-latest","macos-latest"],"node":[18,20,22]}') }}

Pratique 2 : exclude pour les combinaisons connues

Problème sur une combinaison : exclude d’abord, note pour corriger plus tard.

Pratique 3 : cache pour réduire le temps d’install

L’install des dépendances domine souvent — le cache passe de minutes à secondes.

Pratique 4 : nommer les combinaisons

Par défaut test (ubuntu-latest, 20) — personnalisez avec name :

jobs:
  test:
    name: Test (${{ matrix.os }}, Node ${{ matrix.node }})
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
        node: [18, 20, 22]

Plus lisible dans l’interface GitHub Actions.

10. Conclusion

Le Matrix GitHub Actions est l’outil idéal pour les tests multi-plateformes et multi-versions. En bref : définir les dimensions, contrôler les combinaisons, gérer la concurrence, générer dynamiquement.

Trop de projets maintiennent encore des configs CI dupliquées — une commande à changer à dix endroits. Matrix compresse des centaines de lignes en quelques dizaines.

Rappel :

  1. Syntaxe : produit cartésien avec matrix.os et matrix.node
  2. Contrôle : exclude et include
  3. Stratégie : fail-fast false en débogage, true en prod
  4. Concurrence : max-parallel pour self-hosted et coûts
  5. Dynamique : fromJSON() pour tester à la demande

Les 5 modèles sont copiables tels quels, du Node.js simple au self-hosted entreprise.

Si vous débutez, partez du modèle 1, ajoutez exclude/include, puis le dynamique. Pas tout d’un coup.

Questions en commentaire, ou doc officielle GitHub Actions. Partagez vos astuces Matrix si vous en avez.

Configurer le Matrix GitHub Actions pour tests multi-plateformes

Configurer une stratégie Matrix pour des tests automatisés cross-OS et cross-version

⏱️ Estimated time: 30 min

  1. 1

    Step 1: Définir les dimensions Matrix

    Ajoutez strategy.matrix au job du workflow :

    ```yaml
    strategy:
    matrix:
    os: [ubuntu-latest, windows-latest]
    node: [18, 20, 22]
    ```

    Cela génère 2 x 3 = 6 tâches parallèles.
  2. 2

    Step 2: Utiliser les variables Matrix

    Référencez les variables dans runs-on et steps :

    ```yaml
    runs-on: ${{ matrix.os }}
    steps:
    - uses: actions/setup-node@v4
    with:
    node-version: ${{ matrix.node }}
    ```

    Chaque tâche reçoit automatiquement os et node correspondants.
  3. 3

    Step 3: Exclure des combinaisons (optionnel)

    Utilisez exclude pour les combinaisons problématiques :

    ```yaml
    strategy:
    matrix:
    os: [ubuntu-latest, windows-latest]
    node: [18, 20, 22]
    exclude:
    - os: windows-latest
    node: 18
    ```

    Exclut Windows + Node 18, il reste 5 tâches.
  4. 4

    Step 4: Configurer la stratégie d'échec

    Choisissez fail-fast selon le contexte :

    - Tests PR : fail-fast: true (échec rapide, économie)
    - Débogage : fail-fast: false (voir tous les échecs)
    - Branche main : fail-fast: false (rapport complet)

    ```yaml
    strategy:
    fail-fast: false
    matrix:
    # ...
    ```
  5. 5

    Step 5: Limiter la concurrence (optionnel)

    Avec runners self-hosted ou ressources limitées :

    ```yaml
    strategy:
    max-parallel: 4
    matrix:
    # ...
    ```

    Au maximum 4 tâches simultanées, évite la surcharge du runner.

FAQ

Y a-t-il une limite au nombre de combinaisons Matrix ?
GitHub impose une limite souple. Dépôts publics : jusqu'à 256 tâches ; privés selon l'offre. En pratique, restez sous ~20 combinaisons pour éviter des CI trop longs et des factures élevées. 4 OS x 5 versions x 3 bases = 60 tâches, c'est déjà beaucoup.
Quelle est la valeur par défaut de fail-fast ?
fail-fast vaut true par défaut. Un échec annule les autres tâches en cours. En débogage, mettez false pour voir toutes les causes ; en production, gardez le défaut pour économiser.
Quel est l'ordre d'exécution de exclude et include ?
Ordre : toutes les combinaisons -> exclude -> include. Vous pouvez exclure tout Python 3.9 puis include un test minimal Ubuntu + Python 3.9.
Où peut-on utiliser fromJSON() pour un matrix dynamique ?
fromJSON() uniquement sur la valeur de strategy.matrix, pas sur d'autres champs YAML. Le JSON doit être un matrix valide, ex. {"os": ["ubuntu", "windows"]}. Matrix vide = erreur — prévoyez une valeur par défaut.
max-parallel réduit-il le temps de calcul total ?
Non. max-parallel limite seulement le parallélisme, pas le total de minutes. 12 tâches x 10 min = 120 min quel que soit le parallélisme. Mais limiter la concurrence : 1) réduit le pic de ressources ; 2) évite d'épuiser le pool DB ; 3) protège les runners self-hosted.
Les tâches Matrix partagent-elles le cache ?
Oui. Le cache GitHub Actions est au niveau du dépôt, accessible à tous les jobs. Activez cache dans setup-node ou setup-python. La première tâche crée le cache, les suivantes le réutilisent.
Comment nommer personnaliser les tâches Matrix ?
Utilisez l'attribut name du job avec les variables matrix :

• name: Test (${{ matrix.os }}, Node ${{ matrix.node }})

L'interface affichera par ex. "Test (ubuntu-latest, Node 20)" pour identifier chaque tâche.

13 min de lecture · Publié le: 28 avr. 2026 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog