Changer le thème

Matrix GitHub Actions : tests parallèles multi-versions en pratique

Easton editorial illustration: hardened server operations console

La semaine dernière, le projet venait tout juste de passer en production. Avant même la fin de l’après-midi, un utilisateur nous a signalé que la page affichait un écran blanc sous Node 16. J’ai eu un choc.

Deux heures de débogage. J’ai parcouru les logs, comparé le code en trois passes, et j’ai fini par comprendre : une API traitait JSON.stringify() différemment sous Node 16 et Node 20 — l’ancienne version levait une erreur sur les références circulaires, la nouvelle les gérait silencieusement. Notre pipeline CI ne testait que Node 20, donc ce problème de compatibilité n’avait jamais été intercepté.

Lors du post-mortem, je me suis dit : avec des tests parallèles multi-versions, on aurait détecté ça avant la mise en ligne. C’est à partir de ce moment que j’ai creusé le Matrix de GitHub Actions — le terme impressionne, mais en réalité il s’agit de découper automatiquement une tâche en plusieurs instances qui tournent en parallèle sur différentes versions et plateformes.

Dans cet article, je couvre le Matrix de A à Z : syntaxe de base, combinaisons de filtres exclude/include, choix de la stratégie fail-fast, contrôle des ressources avec max-parallel. À la fin, un modèle complet de tests Node.js multi-versions prêt à copier. Environ 10 minutes de lecture, et vous pourrez basculer votre CI en exécution parallèle multi-versions.

Bases du Matrix — prise en main en 5 minutes

Le Matrix, en une phrase : vous écrivez un job, GitHub Actions le déploie automatiquement en plusieurs tâches parallèles.

Exemple concret. Vous définissez trois versions Node.js [18, 20, 22], et le Matrix crée trois tâches de test indépendantes, chacune sous Node 18, Node 20 ou Node 22. Ces trois tâches démarrent en même temps, s’exécutent en parallèle, sans se gêner.

La configuration minimale ressemble à ceci :

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci && npm test

Regardez surtout strategy.matrix. node-version est le nom de variable que vous choisissez ; le tableau [18, 20, 22] en liste les valeurs possibles. GitHub Actions parcourt ce tableau, assigne chaque valeur à matrix.node-version, puis crée une instance de job correspondante.

La syntaxe ${{ matrix.node-version }} référence la valeur courante. Au premier passage c’est 18, puis 20, puis 22.

Ma première question en l’utilisant : ces trois tâches sont-elles séquentielles ou parallèles ? Réponse : par défaut, parallèles. À chaque push, GitHub lance trois Runners en même temps. En pratique, tester trois versions en série prenait 15 minutes ; avec le Matrix, 5 minutes — les tâches tournent en parallèle, le temps total se réduit à celle qui est la plus lente.

Attention toutefois : le parallélisme consomme plus de minutes Runner. Trois tâches, c’est trois fois le temps facturé. Avec le quota gratuit (2 000 minutes par mois), un gros matrix peut vite l’épuiser. On reviendra sur max-parallel plus loin.

Filtres exclude/include — affiner la matrice de tests

Quand vous combinez plusieurs dimensions, le nombre de combinaisons du Matrix explose.

Trois versions Node [16, 18, 20], trois OS [ubuntu, windows, macos], ça fait 3 × 3 = 9 tâches. Ajoutez des suites de tests [unit, integration, e2e], et vous atteignez 27. Beaucoup pour une petite équipe, où les minutes Runner ont un coût.

Certaines combinaisons n’ont aucun sens. Node 16 est EOL (End of Life) : le tester sous Windows et macOS, c’est du temps perdu. C’est là qu’intervient exclude.

strategy:
  matrix:
    node-version: [16, 18, 20]
    os: [ubuntu-latest, windows-latest, macos-latest]
    exclude:
      - node-version: 16
        os: windows-latest
      - node-version: 16
        os: macos-latest

Avec exclude, vous listez les combinaisons à retirer. La config ci-dessus supprime Node 16 + Windows et Node 16 + macOS. De 9 tâches, on passe à 7 — 22 % de minutes Runner en moins.

include fait l’inverse : ajouter des combinaisons ou des variables supplémentaires. Par exemple, tester Node 23 en version expérimentale, uniquement sur Ubuntu :

strategy:
  matrix:
    node-version: [16, 18, 20]
    os: [ubuntu-latest, windows-latest]
    include:
      - node-version: 23
        os: ubuntu-latest
        experimental: true

Détail important : include n’ajoute pas seulement des combinaisons, il peut aussi ajouter des variables. Ici, experimental: true n’existe que pour la tâche Node 23. Vous pouvez l’exploiter dans les étapes suivantes, par exemple pour ne pas bloquer tout le workflow si la version expérimentale échoue :

- name: Run tests
  run: npm test
  continue-on-error: ${{ matrix.experimental == true }}

Piège que j’ai rencontré : la priorité entre exclude et include. GitHub Actions exécute d’abord include pour ajouter des combinaisons, puis exclude pour en supprimer. Si vous incluez une combinaison puis l’excluez, elle n’apparaîtra pas. Inverser l’ordre dans votre tête peut donner un résultat inattendu.

En résumé :

  • exclude : supprimer les combinaisons inutiles, économiser temps et budget
  • include : ajouter des cas particuliers, avec des variables pour un traitement différencié

fail-fast et max-parallel — optimiser la stratégie parallèle

Le Matrix a un comportement par défaut facile à oublier : fail-fast: true.

Concrètement : dès qu’une tâche du matrix échoue, GitHub Actions annule les autres encore en cours. Dix tâches en parallèle, la 3ᵉ tombe après une minute — les sept restantes sont arrêtées immédiatement.

Est-ce une bonne chose ? Ça dépend du contexte.

Pour une vérification de PR, fail-fast est pertinent. Quelqu’un pousse du code, le test Node 18 casse — inutile d’attendre les autres versions. Retour rapide à l’auteur, gain de temps et de ressources.

En revanche, pour des tests Nightly ou une régression périodique, fail-fast peut gêner. Vous voulez un rapport complet : quelles versions posent problème, lesquelles passent. Si Node 18 échoue et stoppe tout, vous ignorez si Node 20 a le même bug. Là, configurez fail-fast: false.

strategy:
  fail-fast: false
  matrix:
    node-version: [16, 18, 20]

max-parallel limite le nombre de tâches lancées simultanément. Par défaut, pas de limite : GitHub démarre autant de tâches que possible. Sur un gros matrix — disons 30 combinaisons — vous ne voulez peut-être pas tout consommer d’un coup.

strategy:
  fail-fast: true
  max-parallel: 6
  matrix:
    node-version: [16, 18, 20, 22]
    test-suite: [unit, integration, e2e]

Ici, au maximum 6 tâches en parallèle. Les 30 combinaisons s’exécutent par lots de 6. Avantage : ressources Runner maîtrisées, quota préservé. Inconvénient : durée totale plus longue.

Tableau de décision simple selon le scénario :

Scénariofail-fastmax-parallelRaison
Vérification PRtrueillimitéRetour rapide, arrêt dès le premier échec
Tests Nightlyfalse4-6Rapport complet, repérer tous les bugs
Gros matrix (>20 combinaisons)true4Limiter la consommation, éviter d’épuiser le quota
Versions expérimentalesfalseillimitéL’échec expérimental n’influence pas le jugement global

En pratique, fail-fast: true par défaut suffit la plupart du temps. Passez à false quand vous avez besoin d’un diagnostic complet. max-parallel impacte peu les petits matrix (moins de 10 combinaisons) ; c’est sur les grands qu’il faut y réfléchir.

Rappel : max-parallel limite les tâches que GitHub Actions lance en même temps, pas le nombre de Runners physiques. Avec un self-hosted runner, une valeur trop basse crée des files d’attente et ralentit l’ensemble. Sur les Runners publics, ce réglage a du sens.

Modèle complet — pipeline de tests Node.js multi-versions

Jusqu’ici, des notions isolées. Cette section propose une configuration complète, prête à copier.

Ce modèle inclut :

  • Trois versions Node (16, 18, 20)
  • Deux suites de tests (unit et integration)
  • Deux systèmes d’exploitation (Ubuntu et Windows)
  • Mise en cache automatique pour accélérer l’installation des dépendances
  • Exclusion des tests Windows sur Node 16 (version EOL)
name: Multi-Version Test Matrix

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      max-parallel: 6
      matrix:
        node-version: [16, 18, 20]
        test-suite: [unit, integration]
        os: [ubuntu-latest, windows-latest]
        exclude:
          - node-version: 16
            os: windows-latest

    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'

      - name: Install dependencies
        run: npm ci

      - name: Run ${{ matrix.test-suite }} tests
        run: npm run test:${{ matrix.test-suite }}

Quelques points clés :

runs-on: ${{ matrix.os }} : l’OS est dynamique ; chaque tâche choisit le Runner correspondant à sa combinaison matrix.

cache: 'npm' : fonction de cache intégrée à setup-node. Elle met en cache les dépendances npm selon le hash de package-lock.json ; au second passage, réutilisation directe sans retéléchargement. En pratique, plus de 50 % de temps gagné sur l’installation des dépendances.

fail-fast: false : volontairement à false ici, car l’objectif des tests multi-versions est de révéler tous les problèmes. Une version en échec ne doit pas arrêter les autres.

npm run test:${{ matrix.test-suite }} : en supposant que package.json définit test:unit et test:integration, le Matrix appelle chacune selon la combinaison.

Combien de tâches au total ?

3 versions × 2 tests × 2 OS = 12 tâches, moins Node 16 + Windows (2 suites de tests), soit 10 tâches restantes.

Mesures : sur plusieurs projets avec ce modèle et le cache, le CI est passé d’environ 25 minutes en série à 8 minutes. Le gain vient surtout du parallélisme et du cache des dépendances.

Pour des projets plus larges :

  • Augmenter max-parallel (8 ou 10)
  • Isoler les tests e2e dans un job dédié pour ne pas ralentir le reste
  • Utiliser continue-on-error pour les versions expérimentales

Copiez ce modèle dans .github/workflows/test.yml, ajustez versions et noms de suites, et vous devriez être opérationnel.

Conclusion

En synthèse :

Le Matrix déploie automatiquement un job en plusieurs tâches parallèles. Configuration simple, effet direct — un push, trois versions en parallèle, le temps CI se réduit à la tâche la plus lente.

exclude/include permettent un contrôle fin. Quand les combinaisons explosent, exclude supprime l’inutile et économise plus de 20 % de minutes Runner. include complète avec des cas spéciaux et des variables pour un traitement différencié.

fail-fast vaut true par défaut : un échec stoppe les autres. Pour les PR, gardez la valeur par défaut ; pour les tests Nightly, passez à false pour un rapport complet. max-parallel plafonne la concurrence — utile surtout sur les grands matrix.

Le cache est incontournable. Une ligne cache: 'npm' dans setup-node peut diviser par deux le temps d’installation des dépendances.

Prochaine étape : copiez le modèle complet dans .github/workflows/ de votre projet, lancez d’abord les tests sur trois versions Node.js. Une fois validé, étendez progressivement aux multi-plateformes et multi-suites. Pour approfondir le cache, consultez l’article de la série « Stratégies de cache GitHub Actions : accélérer votre pipeline CI/CD par 5 ».

Configurez les tests multi-versions tôt. N’attendez pas un bug en production — cet écran blanc, j’y repense encore avec la migraine.

Configurer les tests multi-versions avec GitHub Actions Matrix

Construire de zéro un pipeline de tests parallèles multi-versions, couvrant Node.js 16/18/20 et les plateformes Ubuntu/Windows

⏱️ Estimated time: 15 min

  1. 1

    Step 1: Créer le fichier workflow

    Créez `.github/workflows/test.yml` à la racine du projet :

    • Vérifiez la structure : `.github/workflows/`
    • Nom de fichier libre ; `test.yml` ou `ci.yml` sont courants
  2. 2

    Step 2: Configurer les déclencheurs

    Définissez quand lancer les tests :

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

    • Push sur main déclenche les tests
    • Création ou mise à jour d'une PR également
  3. 3

    Step 3: Définir la matrice Matrix

    Configurez versions, plateformes et suites de tests :

    ```yaml
    strategy:
    fail-fast: false
    max-parallel: 6
    matrix:
    node-version: [16, 18, 20]
    test-suite: [unit, integration]
    os: [ubuntu-latest, windows-latest]
    exclude:
    - node-version: 16
    os: windows-latest
    ```

    • fail-fast: false pour un rapport complet
    • max-parallel: 6 pour plafonner la concurrence
    • exclude pour écarter les combinaisons invalides
  4. 4

    Step 4: Configurer les étapes de test

    Définissez l'exécution concrète :

    ```yaml
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
    with:
    node-version: ${{ matrix.node-version }}
    cache: 'npm'
    - run: npm ci
    - run: npm run test:${{ matrix.test-suite }}
    ```

    • cache: 'npm' active le cache des dépendances
    • Les variables matrix configurent dynamiquement la version
  5. 5

    Step 5: Pousser et vérifier

    Déclenchez les tests par un push :

    • Commit sur main ou ouverture d'une PR
    • Consultez l'exécution parallèle dans l'onglet GitHub Actions
    • Vérifiez que chaque version affiche un résultat normal

FAQ

Le Matrix consomme-t-il plus de minutes Runner ?
Oui. Chaque tâche déployée par le matrix est facturée séparément. Par exemple 3 versions × 2 plateformes = 6 tâches ; si chacune dure 5 minutes, le total est 30 minutes (et non 5). En revanche, le temps d'attente global diminue fortement grâce au parallélisme.
fail-fast : true ou false ?
Selon le scénario :

• Vérification PR : true recommandé (échec rapide, retour immédiat)
• Tests Nightly : false recommandé (rapport complet)
• Versions expérimentales : false (ne pas fausser le jugement global)

La valeur par défaut est true ; suffisant pour la plupart des PR.
exclude et include : lequel s'exécute en premier ?
include s'exécute d'abord pour ajouter des combinaisons, puis exclude pour en supprimer. Si une combinaison est à la fois incluse et exclue, elle n'apparaît pas. Listez d'abord toutes les combinaisons sur papier, puis décidez lesquelles retirer.
Quelle valeur pour max-parallel ?
Repères :

• Petit matrix (<10 combinaisons) : pas besoin de le définir
• Matrix moyen (10-20 combinaisons) : 6-8
• Gros matrix (>20 combinaisons) : 4-6

Trop bas allonge l'attente totale ; trop haut peut épuiser d'un coup les ressources Runner.
Comment accélérer avec le cache dans un Matrix ?
Ajoutez `cache: 'npm'` dans `actions/setup-node`. Le cache se base sur le hash de package-lock.json. En pratique, plus de 50 % de temps gagné sur l'installation. Pour pnpm ou yarn, utilisez `cache: 'pnpm'` ou `cache: 'yarn'`.
Quels types de variables le Matrix accepte-t-il ?
Trois types :

• Tableaux : `[18, 20, 22]`
• Tableaux d'objets : `[{name: 'a', value: 1}, {name: 'b', value: 2}]`
• Chaînes : à ajouter via include

Privilégiez tableaux et tableaux d'objets pour la lisibilité.

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

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog