Chapitre 422 minIntermédiaire
Matrice, cache et artefacts dans GitHub Actions
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- tester plusieurs versions en parallèle avec une matrice ;
- accélérer un pipeline avec un cache et une bonne clé ;
- transmettre des fichiers entre jobs avec des artefacts ;
- factoriser un workflow avec une action composite ou un workflow réutilisable.
5 exercices corrigés1 schéma
Trois mécanismes de GitHub Actions rendent un pipeline plus complet et plus rapide : la matrice lance le même job sur plusieurs versions ou systèmes en parallèle, le cache garde des fichiers coûteux à télécharger d'une exécution à l'autre, et les artefacts conservent les fichiers produits par un job pour les transmettre à un autre job ou les télécharger. Ce chapitre explique chacun d'eux, quand choisir l'un ou l'autre, et comment factoriser un workflow qui se répète.
La matrice : tester plusieurs versions en parallèle
sorbier-frais tourne en production sur Node.js 24. L'équipe veut vérifier qu'il fonctionne aussi sur Node.js 22, encore installé sur un ancien serveur, et sur Node.js 26, la nouvelle version LTS, pour préparer la migration. Au lieu d'écrire trois jobs identiques, on déclare une matrice :
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
node: [22, 24, 26]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm test -- --ci
GitHub crée trois jobs, tests (22), tests (24) et tests (26), qui tournent en parallèle. Dans chacun, ${{ matrix.node }} vaut la version correspondante.
| Version de Node.js | Statut en novembre 2026 | Fin de maintenance |
|---|---|---|
| 22 | LTS en maintenance (correctifs de sécurité seulement) | avril 2027 |
| 24 | LTS en maintenance depuis octobre 2026 | avril 2028 |
| 26 | LTS active depuis le 28 octobre 2026 | avril 2029 |
Une matrice peut combiner plusieurs dimensions : GitHub crée un job par combinaison.
strategy:
fail-fast: false
max-parallel: 4
matrix:
node: [22, 24, 26]
os: [ubuntu-latest, windows-latest]
exclude:
- node: 22
os: windows-latest
include:
- node: 26
os: ubuntu-latest
experimental: true
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental == true }}
| Clé | Effet |
|---|---|
Les listes (node, os) | Produit cartésien : 3 × 2 = 6 combinaisons |
exclude | Retire les combinaisons qui correspondent : il en reste 5 |
include | Ajoute des variables à une combinaison existante si c'est possible sans en écraser une valeur, sinon crée une nouvelle combinaison ; ici, experimental: true s'ajoute à la combinaison (26, ubuntu-latest) : toujours 5 jobs |
fail-fast | true par défaut : au premier job de la matrice en échec, les autres sont annulés. false les laisse finir, pour voir toutes les versions qui cassent |
max-parallel | Nombre maximal de jobs simultanés de la matrice |
Une matrice génère au maximum 256 jobs par exécution. En pratique, garde-la petite : trois versions de Node.js sur Linux suffisent pour une API ; ajoute Windows seulement si l'application y tourne vraiment.
Le cache : ne pas tout retélécharger
À chaque exécution, le runner est neuf : npm ci retélécharge toutes les dépendances depuis le registre npm, ce qui peut prendre une minute ou plus. Le cache sauvegarde un dossier à la fin d'un job et le restaure au début des suivants.
La méthode simple : le cache intégré aux actions setup-*. actions/setup-node avec cache: npm met en cache le dossier de téléchargement de npm (~/.npm, pas node_modules), avec une clé calculée à partir de package-lock.json. actions/setup-python fait de même avec cache: pip, actions/setup-java avec cache: maven.
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
La méthode générale : actions/cache. Pour tout autre dossier, on décrit soi-même le chemin et la clé. Exemple avec le cache de Composer pour une application PHP :
- name: Trouver le dossier de cache de Composer
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v6
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: composer-${{ runner.os }}-${{ hashFiles('composer.lock') }}
restore-keys: |
composer-${{ runner.os }}-
- run: composer install --no-interaction --no-progress
Le fonctionnement de la clé :
- Au début du job, GitHub cherche un cache dont la clé est exactement
composer-Linux-<empreinte de composer.lock>. S'il le trouve (cache hit), il le restaure. - Sinon (cache miss), il essaie les préfixes de
restore-keyset restaure le cache le plus récent qui commence parcomposer-Linux-: une version un peu ancienne, que Composer complète. - À la fin du job, s'il n'y a pas eu de correspondance exacte, il enregistre le dossier sous la clé exacte.
Comme la clé contient l'empreinte de composer.lock, toute modification des dépendances produit une nouvelle clé : le cache ne peut pas servir des versions périmées.
| Règle | Détail |
|---|---|
| Quota | 10 Go de cache par dépôt ; au-delà, les plus anciens sont supprimés |
| Expiration | Un cache inutilisé pendant 7 jours est supprimé |
| Portée | Un job peut lire les caches de sa branche et de la branche par défaut (et, pour une pull request, ceux de la branche cible), pas ceux d'une autre branche de travail |
| Contenu | Uniquement des fichiers reconstructibles : dépendances téléchargées, outils, résultats de compilation intermédiaires |
Les artefacts : conserver et transmettre des fichiers
Un artefact est un fichier ou un dossier produit par un job et conservé après la fin de l'exécution : le dossier dist/ construit, un rapport de couverture des tests, un journal d'erreur, une archive à livrer. On l'envoie avec actions/upload-artifact et on le récupère avec actions/download-artifact, dans un autre job ou depuis la page de l'exécution.
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v7
with:
name: site-statique
path: dist/
retention-days: 14
if-no-files-found: error
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v8
with:
name: site-statique
path: dist/
- run: ls -R dist/
| Paramètre | Rôle |
|---|---|
name | Le nom de l'artefact ; il doit être unique dans une exécution |
path | Le fichier, le dossier ou le motif à envoyer (dist/, coverage/**/*.xml) |
retention-days | Durée de conservation ; 90 jours par défaut, à réduire pour économiser le stockage |
if-no-files-found | warn par défaut ; error fait échouer l'étape si rien n'a été produit, ce qui évite de livrer un dossier vide |
Le job deploy ne refait ni checkout, ni installation, ni construction : il récupère exactement les fichiers construits et testés par build. C'est la règle « construire une fois, déployer partout » du chapitre 2.
Pour un rapport de tests, envoie l'artefact même en cas d'échec, puisque c'est justement là qu'il sert :
- run: npm test -- --ci --coverage
- uses: actions/upload-artifact@v7
if: always()
with:
name: couverture-node-${{ matrix.node }}
path: coverage/
Les artefacts se téléchargent en bas de la page de l'exécution, ou en ligne de commande avec gh run download. Ils comptent dans le quota de stockage des dépôts privés (500 Mo sur l'offre gratuite).
Cache ou artefact ?
| Cache | Artefact | |
|---|---|---|
| Sert à | Gagner du temps | Transmettre ou conserver un résultat |
| Entre | Des exécutions différentes | Les jobs d'une même exécution, ou vers une personne |
| Contenu typique | Dépendances téléchargées, outils | dist/, image exportée, rapport de tests, journaux |
| S'il disparaît | Le job est plus lent, mais il marche | Le job suivant échoue |
| Durée de vie | 7 jours sans utilisation | 90 jours par défaut, réglable |
Ne pas se répéter : workflows réutilisables et actions composites
Les jobs lint, tests et build répètent les mêmes trois étapes : checkout, setup-node, npm ci. Deux mécanismes évitent ces copies.
L'action composite regroupe des étapes dans une action locale. On crée .github/actions/preparer-node/action.yml :
name: Préparer Node.js
description: Installe Node.js et les dépendances du projet
inputs:
node-version:
description: Version de Node.js à installer
default: '24'
runs:
using: composite
steps:
- uses: actions/setup-node@v7
with:
node-version: ${{ inputs.node-version }}
cache: npm
- run: npm ci
shell: bash
Dans une action composite, chaque étape run doit préciser son shell. Les jobs l'utilisent après le checkout (l'action est un fichier du dépôt, il faut d'abord le récupérer) :
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/preparer-node
with:
node-version: 26
- run: npm test -- --ci
Le workflow réutilisable factorise des jobs entiers. Le fichier .github/workflows/tests-node.yml se déclare avec workflow_call :
name: Tests Node.js
on:
workflow_call:
inputs:
node-version:
type: string
default: '24'
permissions:
contents: read
jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: ${{ inputs.node-version }}
cache: npm
- run: npm ci
- run: npm test -- --ci
Un autre workflow l'appelle au niveau d'un job, avec uses à la place de runs-on et steps :
jobs:
tests-26:
uses: ./.github/workflows/tests-node.yml
with:
node-version: '26'
S'il a besoin de secrets, l'appelant les transmet un par un sous secrets:, ou tous avec secrets: inherit. Une organisation peut ainsi écrire une fois son workflow de tests et l'appeler depuis tous ses dépôts, avec uses: organisation/depot/.github/workflows/tests-node.yml@v1.
| Action composite | Workflow réutilisable | |
|---|---|---|
| Regroupe | Des étapes | Des jobs entiers |
| S'utilise dans | La liste steps d'un job | Un job, à la place de runs-on |
| Choisit la machine | Non, elle tourne dans le job appelant | Oui, chacun de ses jobs a son runs-on |
| Idéal pour | « Préparer l'environnement » | « Le pipeline de tests standard de l'équipe » |
Exercices
Exercice 1 sur 5 Compter les jobs d'une matrice Réussi À revoir
strategy:
matrix:
node: [22, 24, 26]
os: [ubuntu-latest, windows-latest]
exclude:
- node: 22
os: windows-latest
include:
- node: 24
os: macos-latest
- Combien de jobs cette matrice crée-t-elle ? Liste-les.
- Combien en créerait-elle si l'
includeétait- node: 26avecos: ubuntu-latestetexperimental: true?
Voir la réponseMasquer la réponse de l'exercice 1
- Le produit cartésien donne 3 × 2 = 6 combinaisons ;
excluderetire (22, windows-latest), il en reste 5. L'include(24, macos-latest) ne peut s'ajouter à aucune combinaison sans écraser la valeur deos: il crée une nouvelle combinaison. Total : 6 jobs : (22, ubuntu-latest), (24, ubuntu-latest), (24, windows-latest), (26, ubuntu-latest), (26, windows-latest), (24, macos-latest). - (26, ubuntu-latest) existe déjà et
experimentalest une nouvelle variable : elle s'ajoute à cette combinaison sans en créer. Total : 5 jobs.
Tu avais trouvé ?
Exercice 2 sur 5 Écrire une matrice Réussi À revoir
Le script Python inventaire.py de l'équipe d'exploitation doit être testé avec Python 3.13 et 3.14, sous Linux et sous Windows. Toutes les combinaisons doivent aller au bout, même si l'une échoue. Écris le job tests : installation des dépendances listées dans requirements.txt, avec le cache de pip, puis lancement de pytest.
Voir la réponseMasquer la réponse de l'exercice 2
jobs:
tests:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
python: ['3.13', '3.14']
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python }}
cache: pip
- run: pip install -r requirements.txt
- run: pytest
La matrice crée 2 × 2 = 4 jobs. runs-on lit le système dans la matrice. Les versions sont entre guillemets : ce n'est pas indispensable pour 3.13 et 3.14, mais c'est une bonne habitude, car 3.10 sans guillemets deviendrait le nombre 3.1. fail-fast: false laisse les trois autres jobs se terminer si l'un échoue, pour voir d'un coup toutes les combinaisons qui cassent. Les commandes pip et pytest sont les mêmes sous Windows : le shell par défaut y est PowerShell, qui les exécute sans changement.
Tu avais trouvé ?
Exercice 3 sur 5 Cache ou artefact ? Réussi À revoir
Pour chaque besoin, choisis entre un cache et un artefact, et justifie.
- Garder le dossier de téléchargement de pip d'une exécution à l'autre.
- Récupérer, dans le job
deploy, l'archive.zipconstruite par le jobbuild. - Permettre à la professeure de télécharger le rapport de couverture des tests.
- Éviter de retélécharger à chaque exécution le navigateur utilisé par les tests de bout en bout.
Voir la réponseMasquer la réponse de l'exercice 3
- Cache (
cache: pipdansactions/setup-python) : on gagne du temps, et s'il disparaît, pip retélécharge. - Artefact : c'est un résultat à transmettre entre deux jobs de la même exécution ; s'il manquait, le déploiement serait impossible.
- Artefact : il reste téléchargeable depuis la page de l'exécution pendant sa durée de conservation.
- Cache : le navigateur est un outil reconstructible ; la clé peut contenir la version de l'outil de test, pour être renouvelée quand il change.
Tu avais trouvé ?
Exercice 4 sur 5 Un artefact en conflit Réussi À revoir
Le job tests tourne en matrice sur Node.js 22, 24 et 26. Il se termine par :
- uses: actions/upload-artifact@v7
with:
name: rapport-tests
path: reports/junit.xml
Le job tests (22) réussit, mais tests (24) et tests (26) échouent sur cette étape. Explique pourquoi et corrige.
Voir la réponseMasquer la réponse de l'exercice 4
Les trois jobs envoient un artefact nommé rapport-tests. Le premier arrivé le crée ; les suivants échouent, car un artefact est immuable et son nom unique dans l'exécution (Conflict: an artifact with this name already exists on the workflow run). On ajoute la variable de matrice au nom :
- uses: actions/upload-artifact@v7
if: always()
with:
name: rapport-tests-node-${{ matrix.node }}
path: reports/junit.xml
Le if: always() n'est pas demandé, mais il est utile : le rapport est surtout précieux quand des tests échouent.
Tu avais trouvé ?
Exercice 5 sur 5 Lire une clé de cache Réussi À revoir
Un job utilise key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }} et restore-keys: npm-${{ runner.os }}-. Que se passe-t-il au début et à la fin du job dans chacun de ces cas ?
- Aucun fichier n'a changé depuis l'exécution d'hier.
- Lina vient d'ajouter une dépendance (le
package-lock.jsona changé). - Le dépôt n'a pas été utilisé depuis dix jours.
Voir la réponseMasquer la réponse de l'exercice 5
- Correspondance exacte (cache hit) : le cache est restauré, et rien n'est enregistré à la fin puisque la clé existe déjà.
- L'empreinte change, la clé exacte n'existe pas (cache miss). Le préfixe
npm-Linux-restaure le cache le plus récent ;npm cine télécharge que la nouvelle dépendance. À la fin, le dossier est enregistré sous la nouvelle clé. - Les caches inutilisés pendant 7 jours ont été supprimés : ni la clé exacte ni le préfixe ne trouvent rien. Tout est téléchargé, puis enregistré à la fin. Le job est plus lent, mais il réussit.
Tu avais trouvé ?