Cache GitHub Actions : accélérer vos workflows
Mise à jour :
Vous venez de corriger une faute de frappe dans votre README. Vous poussez le commit. GitHub Actions démarre… et vous attendez 4 minutes pendant que npm télécharge 847 paquets pour la 50ᵉ fois cette semaine.
Frustrant, non ?
Ce guide vous explique comment diviser ce temps par 10 grâce au cache GitHub Actions. Pas besoin d’être expert : si vous savez écrire un workflow basique, vous saurez utiliser le cache en 10 minutes.
Le problème : pourquoi vos workflows sont lents
Observons ce qui se passe quand vous lancez un workflow sans cache :
Commit #1 (lundi 9h00)├── Checkout du code .............. 2s├── npm ci (téléchargement) ....... 47s ← Internet├── npm run build ................. 35s└── npm test ...................... 12s Total: 96 secondes
Commit #2 (lundi 9h15)├── Checkout du code .............. 2s├── npm ci (téléchargement) ....... 47s ← Encore Internet, mêmes paquets !├── npm run build ................. 35s└── npm test ...................... 12s Total: 96 secondesEntre ces deux commits, rien n’a changé dans les dépendances. Pourtant, GitHub télécharge les mêmes 847 paquets, depuis les mêmes serveurs npm, pour la deuxième fois en 15 minutes.
Sur une semaine avec 50 commits, c’est 39 minutes perdues à télécharger les mêmes fichiers.
La solution : garder les fichiers en mémoire
L’analogie du frigo
Imaginez que vous prépariez un gâteau chaque matin. Deux approches possibles :
Chaque matin, vous allez au supermarché acheter :
- 500g de farine
- 6 œufs
- 250g de beurre
- 200g de sucre
Temps passé : 45 minutes de courses + 30 minutes de préparation = 75 minutes
Le lendemain ? Rebelote : 45 minutes de courses pour les mêmes ingrédients.
Le premier jour, vous achetez les ingrédients et vous les rangez au frigo.
Les jours suivants, vous ouvrez le frigo et tout est là.
Temps passé : 2 minutes pour sortir les ingrédients + 30 minutes de préparation = 32 minutes
Économie : 43 minutes par jour (soit 57% de gain).
Le cache GitHub Actions, c’est votre frigo numérique. Au lieu de télécharger vos dépendances depuis Internet à chaque fois, vous les stockez une première fois, puis vous les récupérez instantanément.
Ce que ça donne en pratique
Commit #1 (premier run, cache vide)├── Checkout du code .............. 2s├── Restauration du cache ......... 0s ← Pas de cache encore├── npm ci (téléchargement) ....... 47s ← Internet├── Sauvegarde du cache ........... 5s ← On remplit le frigo├── npm run build ................. 35s└── npm test ...................... 12s Total: 101 secondes
Commit #2 (cache disponible)├── Checkout du code .............. 2s├── Restauration du cache ......... 3s ← Le frigo est plein !├── npm ci ........................ 2s ← Rien à télécharger├── npm run build ................. 35s└── npm test ...................... 12s Total: 54 secondes ✨Résultat : 47 secondes économisées à chaque commit. Sur 50 commits par semaine, c’est 39 minutes récupérées.
Activer le cache : la méthode la plus simple
Bonne nouvelle : activer le cache prend une seule ligne.
Avant (sans cache)
name: CIon: [push]
jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- uses: actions/setup-node@v4 with: node-version: '20'
- run: npm ci # ← 47 secondes à chaque fois - run: npm run build - run: npm testAprès (avec cache)
name: CIon: [push]
jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' # ← C'est tout ! Une ligne.
- run: npm ci # ← 2 secondes si le cache existe - run: npm run build - run: npm testLa ligne cache: 'npm' fait tout le travail :
- Elle cherche un cache existant
- Si trouvé, elle restaure les fichiers
- À la fin du job, elle sauvegarde le cache pour la prochaine fois
Vérifier que ça fonctionne
Après avoir ajouté cache: 'npm', lancez deux workflows consécutifs. Dans les
logs du second, vous devriez voir :
Run actions/setup-node@v4 Cache hit occurred on the primary key: node-cache-Linux-npm-d5ea0750...Le message Cache hit confirme que le cache a été utilisé. Si vous voyez Cache miss, c’est normal pour le premier run : le cache n’existe pas encore.
Comment le cache sait quoi garder ?
Vous vous demandez peut-être : « Comment GitHub sait que mes dépendances n’ont pas changé ? »
La réponse tient en un mot : le hash.
Le hash, c’est quoi ?
Un hash est une empreinte digitale d’un fichier. Si le fichier change, même d’une virgule, le hash change complètement.
package-lock.json (version 1) → Hash: abc123package-lock.json (version 2) → Hash: xyz789 (complètement différent !)Quand vous utilisez cache: 'npm', GitHub calcule le hash de votre fichier
package-lock.json. Ce hash devient l’étiquette du cache.
Pourquoi c’est malin
- Lundi : vous poussez un commit, hash =
abc123, cache créé - Mardi : nouveau commit, hash =
abc123(identique !) - GitHub dit : « J’ai déjà un cache pour
abc123, je le réutilise » - Résultat : cache hit, restauration en 3 secondes
- Lundi : hash =
abc123, cache créé - Mardi : vous ajoutez
lodash, hash =xyz789(différent !) - GitHub dit : « Je n’ai pas de cache pour
xyz789, je télécharge » - À la fin : nouveau cache créé avec l’étiquette
xyz789 - Mercredi : hash =
xyz789, cache hit !
Le système est automatique : si vos dépendances changent, le cache se renouvelle. Si elles ne changent pas, le cache est réutilisé.
Pourquoi package-lock.json et pas package.json ?
C’est une question fréquente. La réponse est simple :
| Fichier | Contenu | Exemple |
|---|---|---|
package.json | Plages de versions | "lodash": "^4.17.0" (peut être 4.17.0, 4.17.1, 4.18.0…) |
package-lock.json | Versions exactes | "lodash": "4.17.21" (toujours cette version précise) |
Si vous utilisez package.json, le hash peut rester identique alors que les
versions réelles ont changé. Vous risquez de restaurer un cache incompatible
avec vos dépendances actuelles.
Le fichier lock garantit que le cache correspond exactement à ce qui sera installé.
La clé de cache : l’étiquette de votre frigo
Quand vous utilisez cache: 'npm', GitHub crée automatiquement une clé de
cache. Cette clé, c’est comme l’étiquette sur une boîte au frigo : elle dit ce
qu’il y a dedans.
Anatomie d’une clé
Une clé typique ressemble à ça :
npm-Linux-d5ea0750abc123def456│ │ └── Hash du package-lock.json│ └── Système d'exploitation└── Type de cache (npm, pip, maven...)Pourquoi ces trois parties ?
| Partie | Rôle | Exemple |
|---|---|---|
| Préfixe | Évite les collisions entre types de cache | npm- vs pip- |
| OS | Les dépendances compilées diffèrent selon l’OS | Linux vs Windows |
| Hash | Identifie la version exacte des dépendances | Change si le lockfile change |
Que se passe-t-il quand la clé ne correspond pas ?
Imaginons que vous ajoutiez une nouvelle dépendance. Le hash change. GitHub cherche un cache avec la nouvelle clé… et ne trouve rien.
C’est là qu’interviennent les restore-keys (clés de secours).
key: npm-Linux-xyz789 # Clé exacte (nouvelle)restore-keys: | npm-Linux- # Fallback : n'importe quel cache npm sur Linux npm- # Fallback ultime : n'importe quel cache npm-
Recherche exacte
GitHub cherche
npm-Linux-xyz789. Pas trouvé ? On passe à l’étape suivante. -
Recherche par préfixe
GitHub cherche un cache commençant par
npm-Linux-. Il trouvenpm-Linux-abc123(l’ancien cache). Il le restaure. -
Installation partielle
npm cis’exécute. 95% des paquets sont déjà là (depuis l’ancien cache). Seule la nouvelle dépendance est téléchargée. -
Nouveau cache
À la fin, GitHub sauvegarde le nouveau cache avec la clé
npm-Linux-xyz789.
Résultat : au lieu de tout télécharger (47 secondes), vous ne téléchargez que la différence (5 secondes).
Configuration avancée : prendre le contrôle
La méthode cache: 'npm' est magique, mais parfois vous avez besoin de plus de
contrôle. C’est là qu’intervient l’action actions/cache.
Quand utiliser actions/cache ?
| Situation | Solution |
|---|---|
| Cache basique des dépendances | cache: 'npm' (simple) |
| Cache du build (Next.js, Webpack…) | actions/cache (avancé) |
| Plusieurs chemins à cacher | actions/cache (avancé) |
| Clé personnalisée | actions/cache (avancé) |
Exemple : cacher le build Next.js
Next.js stocke son cache de build dans .next/cache. Sans ce cache, le build
peut prendre 2 minutes. Avec, il prend 30 secondes.
name: CI avec cache de build
on: [push]
jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
# Cache des dépendances (méthode simple) - uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm'
# Cache du build Next.js (méthode avancée) - name: Cache du build Next.js uses: actions/cache@v4 with: path: .next/cache key: nextjs-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('src/**') }} restore-keys: | nextjs-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}- nextjs-${{ runner.os }}-
- run: npm ci - run: npm run build # ← 30s au lieu de 2min ! - run: npm testDécortiquons cette clé :
nextjs-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('src/**') }}│ │ │ ││ │ │ └── Change si le code source change│ │ └── Change si les dépendances changent│ └── Linux, macOS ou Windows└── Préfixe pour identifier ce cacheLe cache est invalidé si :
- Vous changez de système d’exploitation
- Vous modifiez les dépendances
- Vous modifiez le code source
C’est logique : le build dépend de ces trois éléments.
Exemple : cacher plusieurs chemins
Vous pouvez cacher plusieurs dossiers dans une seule entrée :
- uses: actions/cache@v4 with: path: | ~/.npm .next/cache node_modules/.cache key: all-caches-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}Ce qu’il faut cacher (et ne pas cacher)
À cacher : les dépendances
| Langage | Quoi cacher | Pourquoi |
|---|---|---|
| Node.js | ~/.npm | Cache global npm, fonctionne partout |
| Python | ~/.cache/pip | Paquets téléchargés par pip |
| Java | ~/.m2/repository | Artefacts Maven |
| Go | ~/go/pkg/mod | Modules Go |
| Rust | ~/.cargo | Crates Cargo |
À cacher : les résultats de build
| Outil | Quoi cacher | Gain typique |
|---|---|---|
| Next.js | .next/cache | 50-80% |
| Webpack | node_modules/.cache | 40-60% |
| Gradle | ~/.gradle/caches | 50-70% |
| Rust | target/ | 60-80% |
À NE PAS cacher
| Ne pas cacher | Pourquoi |
|---|---|
node_modules directement | Fragile, dépend de l’OS exact |
| Secrets, tokens | Le cache est lisible par les PRs |
| Fichiers volumineux non réutilisés | Gaspille l’espace (limite 10 GB) |
| Code exécutable | Risque de sécurité (cache poisoning) |
Sécurité : qui peut accéder à mon cache ?
Le cache n’est pas partagé n’importe comment. GitHub applique des règles d’isolation strictes.
Les règles d’accès
| Depuis | Peut accéder au cache de | Autorisé ? |
|---|---|---|
Branche feature | main (branche par défaut) | ✅ Oui |
Branche feature-a | feature-b (branche sœur) | ❌ Non |
| Fork externe | Repo parent | ❌ Non |
| PR | Branche cible | ✅ Oui |
Pourquoi ces restrictions ?
Imaginez qu’un attaquant crée un fork de votre projet. S’il pouvait écrire dans
votre cache, il pourrait y injecter du code malveillant. Lors du prochain build
sur main, ce code s’exécuterait avec accès à vos secrets.
Les restrictions empêchent ce scénario : les forks ne peuvent pas toucher au cache du repo parent.
Pour renforcer la sécurité de vos workflows, pensez aussi à épingler vos actions par SHA.
Le risque de cache poisoning
Limites et quotas
Ce que vous devez savoir
| Limite | Valeur | Ce que ça signifie |
|---|---|---|
| Taille par entrée | 10 GB | Un seul cache ne peut pas dépasser 10 GB |
| Taille totale | 10 GB | Tous les caches du repo combinés |
| Rétention | 7 jours | Cache supprimé s’il n’est pas utilisé pendant 7 jours |
| Longueur de clé | 512 caractères | Attention aux clés trop complexes |
Quand le cache est plein
Si vous dépassez 10 GB, GitHub supprime les caches les plus anciens (ceux non utilisés depuis longtemps) jusqu’à repasser sous la limite.
Problème potentiel : si vos caches sont tous volumineux et utilisés régulièrement, vous pouvez entrer dans un cycle de création/suppression appelé cache thrashing. Chaque run crée un cache qui chasse l’ancien.
Solutions :
- Réduisez ce que vous cachez (seulement le nécessaire)
- Demandez une augmentation de quota (entreprises)
- Surveillez avec
gh cache list
Débugger quand ça ne marche pas
Le cache n’est jamais utilisé
Symptôme : vous voyez toujours « Cache miss » dans les logs.
Causes possibles :
-
La clé change à chaque run
# ❌ Mauvais : github.sha change à chaque commitkey: cache-${{ github.sha }}# ✅ Bon : change seulement si les dépendances changentkey: cache-${{ hashFiles('**/package-lock.json') }} -
Le fichier hashé n’existe pas
# ❌ Le fichier n'existe pas → hash vide → clé toujours différentekey: cache-${{ hashFiles('package-lock.json') }}# ✅ Glob pattern qui trouve le fichierkey: cache-${{ hashFiles('**/package-lock.json') }} -
Pas de restore-keys
Sans restore-keys, le moindre changement = cache miss complet.
Comment voir ce qui se passe
Ajoutez une étape de debug :
- name: Debug cache run: | echo "Clé calculée: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}" echo "Le fichier existe ?" ls -la package-lock.json || echo "Fichier non trouvé !"Lister et supprimer les caches
La CLI GitHub permet de gérer vos caches :
# Voir tous les caches du repogh cache list
# Supprimer un cache spécifiquegh cache delete "npm-Linux-abc123"
# Tout supprimer (utile pour repartir de zéro)gh cache delete --allWorkflow complet commenté
Voici un workflow production-ready avec toutes les bonnes pratiques :
name: CI Production
on: push: branches: [main] pull_request: branches: [main]
# Permissions minimales (sécurité)permissions: contents: read
jobs: build: runs-on: ubuntu-latest
steps: # 1. Récupérer le code - name: Checkout uses: actions/checkout@v4
# 2. Configurer Node.js avec cache des dépendances - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' # Équivalent à : # - Chercher un cache avec clé npm-Linux-<hash de package-lock.json> # - Restaurer ~/.npm si trouvé # - Sauvegarder ~/.npm à la fin
# 3. Cache du build Next.js (optionnel mais recommandé) - name: Cache build Next.js uses: actions/cache@v4 with: path: .next/cache key: nextjs-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('src/**', 'app/**') }} restore-keys: | nextjs-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}- nextjs-${{ runner.os }}-
# 4. Installer les dépendances # - Avec cache hit : ~2 secondes # - Sans cache : ~45 secondes - name: Install dependencies run: npm ci
# 5. Build # - Avec cache build : ~30 secondes # - Sans cache : ~2 minutes - name: Build run: npm run build
# 6. Tests - name: Test run: npm testÀ retenir
-
Une ligne suffit pour commencer
Ajoutez
cache: 'npm'(oupip,maven…) dans votre actionsetup-*. C’est tout. Vous venez de gagner 30-60 secondes par run. -
Le hash du lockfile est la clé
Le cache est identifié par le hash de votre fichier lock (
package-lock.json,poetry.lock…). Si le fichier ne change pas, le cache est réutilisé. -
Les restore-keys sont votre filet de sécurité
Même si la clé exacte n’existe pas, les restore-keys permettent de récupérer un cache partiel. Toujours les configurer.
-
Cachez le global, pas le local
Préférez
~/.npmànode_modules. C’est plus robuste et plus sécurisé. -
Le cache est isolé par branche
Les branches feature peuvent lire le cache de
main, mais les forks ne peuvent pas accéder au cache du repo parent. -
10 GB max, 7 jours de rétention
Surveillez votre utilisation avec
gh cache list. Ne cachez que le nécessaire. -
Vérifiez dans les logs
Cherchez « Cache hit » ou « Cache miss » pour confirmer que le cache fonctionne.
Liens utiles
- Artifacts vs Cache — Quand utiliser le cache vs les artifacts
- Debug des workflows — Diagnostiquer les problèmes de cache et autres
- Épingler les actions par SHA — Sécuriser vos workflows
- Optimiser les workflows — Vue d’ensemble des techniques d’optimisation
- Documentation officielle GitHub ↗ — Référence complète
- actions/cache sur GitHub ↗ — Exemples par langage