Votre suite de tests passe sur votre machine, en Node 24. Mais le serveur de la coopérative tourne peut-être encore en Node 22, et un contributeur teste déjà la toute nouvelle version 26. Un code qui marche chez vous et casse ailleurs, c’est le scénario classique du « pourtant ça marchait ». La matrice de GitHub Actions répond exactement à ça : lancer la même suite sur plusieurs versions d’un coup. Et comme multiplier les jobs multiplie le temps d’attente, on apprend dans la foulée à les accélérer avec le cache.
Ce tutoriel fait partie de la série « CI/CD avec GitHub Actions ». Pour la vue d’ensemble, lisez d’abord le guide principal.
🎯 Ce que vous allez apprendre
- Lancer une même suite de tests sur plusieurs versions de Node grâce à
strategy.matrix. - Comprendre
fail-fastet décider si un échec doit ou non arrêter les autres combinaisons. - Ajouter ou retirer des combinaisons ciblées avec
includeetexclude. - Mettre en cache les dépendances pour diviser le temps d’installation par plusieurs.
- Lire le verdict de cache (Cache restored / Cache saved) et invalider le cache au bon moment.
🛠️ Ce que vous allez construire
On reprend le pipeline de tests de l’API Récolte et on le rend robuste : au lieu d’un seul job en Node 24, il s’exécutera en parallèle sur Node 22, 24 et 26. Puis on branche le cache npm pour que la seconde exécution — et toutes les suivantes — s’installe en quelques secondes au lieu de retélécharger l’arbre des dépendances à chaque fois.
Prérequis
- Avoir suivi Automatiser ses tests : un job
testsqui lancenpm test. - Un
package-lock.jsoncommité (le cache s’appuie dessus). - Test express : si vous savez lire un fichier
ci.ymlavecjobsetsteps, vous êtes prêt. - ⏱️ Temps estimé : ~35 minutes.
Étape 1 — Comprendre la matrice avant de l’écrire
Une matrice, conceptuellement, c’est une liste de valeurs que GitHub démultiplie en autant de copies du même job. Vous décrivez une recette et une dimension de variation ; GitHub génère un job par valeur, les lance en parallèle, et chacun reçoit sa valeur dans une variable. Tester sur trois versions de Node ne demande donc pas trois jobs copiés-collés, mais un seul job paramétré.
Le choix des versions n’est pas arbitraire. En juin 2026, deux lignes sont en support long terme — Node 22 et Node 24 — et la version 26 vient d’arriver en ligne « Current ». Node 20, lui, a atteint sa fin de vie : on cesse de garantir qu’on le supporte. Tester sur 22, 24 et 26 couvre donc l’éventail réel de ce sur quoi votre API risque de tourner aujourd’hui et demain. C’est une décision de couverture, pas une coquetterie technique : chaque version a ses petites différences de comportement, et la matrice les révèle avant vos utilisateurs.
Étape 2 — Écrire la matrice (quick win)
On déclare la dimension sous strategy.matrix, puis on s’y réfère avec ${{ matrix.xxx }}. La clé que l’on choisit (node-version) devient le nom de la variable. On la branche directement sur setup-node.
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22, 24, 26]
name: Tests (Node ${{ matrix.node-version }})
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
GitHub lit la liste [22, 24, 26] et crée trois jobs : un par version. Le name dynamique les rend lisibles dans l’interface — « Tests (Node 22) », « Tests (Node 24) », « Tests (Node 26) » — au lieu de trois lignes identiques impossibles à distinguer. Chaque job reçoit sa valeur via ${{ matrix.node-version }}, que l’on passe à setup-node. Poussez : l’onglet Actions affiche désormais trois exécutions en parallèle sous le même workflow.
Un point essentiel pour la suite : ces trois jobs sont totalement isolés les uns des autres. Chacun tourne sur son propre runner vierge, ne partage aucun fichier ni aucune variable avec ses voisins, et ignore s’ils ont réussi ou échoué. Cette indépendance est une force — un job lent ne bloque pas les autres — mais elle a une conséquence : si une étape ultérieure (un déploiement, par exemple) ne doit se lancer qu’une fois toutes les versions validées, il faudra un job séparé qui dépend de l’ensemble de la matrice via needs: tests. On verra ce chaînage dans les tutoriels sur Docker et le déploiement ; retenez pour l’instant que la matrice produit des jobs frères, jamais une séquence.
✅ Point d’étape — Vous devez voir trois jobs distincts, nommés par version, qui tournent en même temps. Si l’un échoue et pas les autres, vous tenez une incompatibilité réelle à corriger.
Étape 3 — Maîtriser fail-fast
Par défaut, GitHub applique fail-fast: true : dès qu’une combinaison échoue, il annule les autres encore en cours. C’est économe — on ne gaspille pas de minutes sur des jobs dont on sait déjà que la PR ne passera pas. Mais c’est gênant pour diagnostiquer : si Node 22 échoue, vous ne saurez pas si 24 et 26 passaient. Quand vous cherchez à savoir quelles versions sont touchées, désactivez ce comportement.
strategy:
fail-fast: false
max-parallel: 2
matrix:
node-version: [22, 24, 26]
Avec fail-fast: false, toutes les combinaisons vont jusqu’au bout, et vous obtenez une photo complète : « casse en 22, passe en 24 et 26 ». max-parallel: 2 limite le nombre de jobs simultanés — utile pour ne pas saturer un quota de minutes ou pour ménager un service externe que vos tests solliciteraient. À l’usage, on garde fail-fast: true au quotidien (rapide, économe) et on bascule sur false le temps d’enquêter sur une régression.
Étape 4 — Ajouter et retirer des combinaisons ciblées
Une matrice à une seule dimension suffit souvent, mais on veut parfois croiser deux axes — par exemple tester aussi sur un autre système d’exploitation — sans pour autant lancer toutes les combinaisons possibles. C’est le rôle d’include (ajouter un cas précis) et d’exclude (en retirer un).
strategy:
matrix:
node-version: [22, 24, 26]
os: [ubuntu-latest, windows-latest]
exclude:
# la version 26, récente, n'est pas testée sur Windows
- node-version: 26
os: windows-latest
include:
# on ajoute un seul test ciblé sur macOS, pour la LTS 24
- node-version: 24
os: macos-latest
runs-on: ${{ matrix.os }}
exclude retire une combinaison du produit cartésien : ici, on ne teste pas la toute récente Node 26 sur Windows. include fait l’inverse — il greffe une combinaison qui ne découle pas du produit des listes : un seul job Node 24 sur macOS, sans dupliquer tout le reste sur macOS. Cette finesse évite l’explosion combinatoire : trois versions × deux systèmes donnent déjà six jobs, et l’on ajoute ou retire à la marge plutôt que de tout multiplier. On cible ce qui compte vraiment et on consomme juste les minutes nécessaires. Notez le runs-on: ${{ matrix.os }} : le type de runner devient lui aussi une variable de matrice.
Étape 5 — Mettre les dépendances en cache
Multiplier les jobs multiplie les npm ci, donc les téléchargements. Sans cache, chaque job repart de zéro et rapatrie tout le contenu du node_modules à reconstruire. La parade la plus simple est intégrée à setup-node : l’option cache mémorise le cache npm entre les exécutions, indexé sur votre package-lock.json.
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
À la première exécution, GitHub télécharge tout puis enregistre le cache. Aux suivantes, tant que le package-lock.json n’a pas changé, il restaure ce cache et npm ci s’exécute en quelques secondes. Dans le journal du step setup-node, vous lirez Cache restored from key… — le signe que ça fonctionne. Quand vous ajoutez une dépendance, le verrou change, sa clé de cache change, et un cache neuf est créé automatiquement : pas de cache périmé.
Pour des cas plus exotiques — un cache de build, un dossier de données — on utilise l’action générique actions/cache, qui donne la main sur la clé. Le principe est le même, exprimé explicitement :
- uses: actions/cache@v5
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
La key est l’empreinte exacte : elle inclut hashFiles('**/package-lock.json'), un condensé du verrou. Si une seule dépendance bouge, l’empreinte change et le cache est reconstruit. Les restore-keys sont des préfixes de repli : si la clé exacte n’existe pas, GitHub réutilise le cache le plus proche commençant par os-npm-, ce qui évite de tout retélécharger pour une dépendance ajoutée. C’est la mécanique que setup-node applique pour vous en une ligne ; la connaître vous permet de la reproduire pour n’importe quel dossier.
Deux limites à garder en tête pour ne pas être surpris. D’abord, le cache est immuable : une fois écrit pour une clé donnée, il ne se met pas à jour — c’est le changement de clé (donc du verrou) qui en crée un nouveau. Ensuite, l’espace de cache d’un dépôt est plafonné (de l’ordre de 10 Go) ; au-delà, GitHub évince les caches les moins récemment utilisés. Concrètement, vous n’avez presque jamais à intervenir : une clé bien construite autour de hashFiles fait que les vieux caches inutiles disparaissent d’eux-mêmes. Le piège inverse est plus courant — une clé trop générique, sans empreinte du verrou, qui restaure éternellement un cache périmé et masque une mise à jour de dépendance.
✅ Point d’étape — Lancez deux fois le workflow. À la seconde, le step d’installation doit être nettement plus court et le journal afficher Cache restored. Si chaque exécution reste lente, vérifiez que le
package-lock.jsonest bien commité et que son chemin correspond au motifhashFiles.
🐞 Pièges fréquents
| Symptôme / erreur | Cause probable | Correctif |
|---|---|---|
| Le cache ne se restaure jamais | Pas de package-lock.json, ou chemin hashFiles erroné |
Committer le verrou ; ajuster le motif **/package-lock.json |
The workflow is not valid … matrix |
Liste mal formée ou indentation sous strategy |
Vérifier l’indentation ; la liste va sous matrix: |
| Un échec annule mes autres jobs avant la fin | fail-fast: true (comportement par défaut) |
Passer fail-fast: false le temps du diagnostic |
| Trop de jobs lancés d’un coup | Produit cartésien de plusieurs dimensions | Réduire avec exclude ou poser max-parallel |
| Le cache grossit sans jamais se renouveler | Clé trop générique, sans empreinte du verrou | Inclure hashFiles dans la key |
🌍 Adaptation au contexte ouest-africain
Le cache n’est pas qu’un confort de vitesse : c’est une économie directe de bande passante et de minutes. Sur un dépôt privé au quota gratuit de 2 000 minutes par mois, trois jobs qui réinstallent tout à chaque push épuisent vite l’enveloppe ; les mêmes jobs avec cache consomment une fraction du temps. Pour une équipe qui pousse plusieurs fois par jour depuis Ouagadougou ou Conakry, la différence se chiffre en jours de quota gagnés sur le mois.
La matrice, elle, répond à une réalité de terrain : les serveurs hébergés localement ne sont pas toujours sur la dernière version de Node. Tester explicitement la version qui tourne en production — souvent une LTS comme la 22 — vous évite la mauvaise surprise du « ça marchait sur mon poste en 24 mais le VPS est en 22 ». La couverture multi-versions transforme une hypothèse en certitude vérifiée à chaque commit.
✅ Récapitulatif
Votre pipeline de tests est passé d’une seule version à une véritable grille de couverture. Vous savez déclarer une matrice à une ou plusieurs dimensions, nommer les jobs lisiblement, et arbitrer entre rapidité (fail-fast) et diagnostic complet. Vous ciblez les bonnes combinaisons avec include et exclude au lieu de subir l’explosion combinatoire. Et vous avez branché le cache npm — par setup-node pour la simplicité, par actions/cache pour le contrôle — en comprenant la mécanique des clés et des restore-keys. Résultat : une CI à la fois plus rigoureuse et plus rapide.
🧾 Aide-mémoire
| Élément | Rôle |
|---|---|
strategy.matrix |
Démultiplier un job sur une liste de valeurs |
${{ matrix.node-version }} |
Lire la valeur de la combinaison courante |
fail-fast: false |
Laisser toutes les combinaisons aller au bout |
max-parallel |
Limiter le nombre de jobs simultanés |
include / exclude |
Ajouter / retirer une combinaison ciblée |
cache: npm (setup-node) |
Cache des dépendances en une ligne |
actions/cache@v5 + hashFiles |
Cache générique piloté par clé |
💪 À vous de jouer
1. Ajoutez une variable de matrice booléenne experimental et autorisez l’échec uniquement pour Node 26 (la version la plus récente), sans bloquer la PR.
2. Mesurez le gain : notez la durée du step d’installation au premier run, puis au second avec cache.
Voir une piste pour le défi 1
strategy:
fail-fast: false
matrix:
node-version: [22, 24]
include:
- node-version: 26
experimental: true
continue-on-error: ${{ matrix.experimental == true }}
continue-on-error marque le job comme tolérant à l’échec : il s’exécute, son résultat est visible, mais un échec ne fait pas échouer l’ensemble. On l’active seulement pour la combinaison expérimentale grâce à la variable experimental ajoutée par include.
Tutoriels frères
- Automatiser ses tests avec GitHub Actions — la suite de tests que la matrice démultiplie.
- Secrets et environnements — protéger les valeurs sensibles que vos jobs manipulent.
Pour aller plus loin
- 🔝 Retour au guide principal : GitHub Actions : le guide CI/CD pour bien démarrer
- Documentation officielle : GitHub Docs — matrices et actions/cache.
- Étape suivante conseillée : les secrets et environnements.
FAQ
La matrice consomme-t-elle plus de minutes ?
Oui : trois versions = trois fois le temps de calcul. Le cache compense largement en réduisant l’installation, et sur un dépôt public les minutes sont gratuites de toute façon.
Quelles versions de Node mettre dans la matrice ?
Celles que vous supportez réellement : au minimum la version de production, idéalement les lignes LTS actives (22 et 24 en 2026) plus la version courante si vous voulez anticiper.
Mon cache est-il partagé entre branches ?
Une branche peut restaurer le cache de la branche par défaut, mais pas l’inverse : un cache créé sur une branche de fonctionnalité n’est pas visible ailleurs. C’est une isolation voulue, pour la sécurité.
Faut-il vider le cache manuellement ?
Rarement. La clé basée sur hashFiles renouvelle le cache dès que le verrou change. GitHub supprime aussi automatiquement les caches inutilisés depuis 7 jours et au-delà d’un quota de taille par dépôt.
Cache via setup-node ou via actions/cache ?
Pour les dépendances npm, cache: npm de setup-node suffit et tient en une ligne. Réservez actions/cache aux caches sur mesure (build, données, autres gestionnaires).
Mots-clés : matrice GitHub Actions, strategy matrix, fail-fast, include exclude, cache npm GitHub Actions, actions/cache, hashFiles, tests multi-versions Node.