GitHub Actions Artifacts : partager des fichiers entre jobs
Mise à jour :
Vous avez passé 3 heures à configurer un pipeline CI/CD parfait. Build, tests,
déploiement. Vous lancez le workflow, confiant. Le job build compile votre
application en 2 minutes. Le job deploy démarre… et échoue :
Error: dist/ folder not foundVous vérifiez. Le build a bien généré dist/. Mais le job deploy ne le trouve
pas. Vos fichiers ont disparu dans le néant.
Ce guide vous explique pourquoi ça arrive, et comment y remédier avec les artifacts. En 15 minutes, vous comprendrez non seulement comment les utiliser, mais surtout pourquoi ils fonctionnent ainsi.
Comprendre le problème
Chaque job vit dans sa bulle
Quand vous écrivez un workflow avec plusieurs jobs, GitHub Actions ne les exécute pas “à la suite” sur la même machine. Chaque job démarre sur un runner frais, une machine virtuelle toute neuve, vierge de tout contexte.
Visualisons ce qui se passe réellement :
Votre workflow "Build & Deploy"││ ┌─────────────────────────────────────────┐│ │ Runner Ubuntu #1 (machine virtuelle) │├──│ ││ │ Job: build ││ │ ├── Checkout du code source ││ │ ├── npm ci (installation) ││ │ ├── npm run build ││ │ └── Génère le dossier dist/ ││ │ ││ │ ⚠️ FIN DU JOB → MACHINE DÉTRUITE ││ │ Tout ce qui était dessus disparaît ││ └─────────────────────────────────────────┘││ ┌─────────────────────────────────────────┐│ │ Runner Ubuntu #2 (AUTRE machine) │└──│ │ │ Job: deploy │ │ ├── Checkout du code source │ │ └── Cherche dist/ → 💥 N'EXISTE PAS │ │ │ │ Cette machine n'a jamais vu │ │ le dossier dist/ du runner #1 │ └─────────────────────────────────────────┘Le runner #1 et le runner #2 sont deux machines complètement séparées. Elles ne partagent rien. Pas de disque commun, pas de réseau local, rien.
L’analogie des cuisiniers
Imaginez un restaurant avec deux cuisiniers qui travaillent dans des cuisines séparées, sans aucune communication :
Cuisinier A (cuisine n°1) :
- Reçoit la commande : “Préparer un gâteau”
- Prépare la pâte parfaitement
- La pose sur son plan de travail
- Rentre chez lui (fin de son service)
- Sa cuisine est nettoyée de fond en comble
Cuisinier B (cuisine n°2) :
- Arrive pour son service
- Consigne : “Décorer le gâteau”
- Cherche le gâteau… rien sur le plan de travail
- Sa cuisine est vide, comme toujours au début du service
- Échec de la commande
Le gâteau du cuisinier A a été jeté lors du nettoyage. Le cuisinier B n’a aucun moyen de savoir qu’il existait.
Entre les deux cuisines, on installe un passe-plat : une étagère partagée où les cuisiniers peuvent déposer et récupérer des préparations.
Cuisinier A (cuisine n°1) :
- Prépare la pâte
- Dépose le gâteau dans le passe-plat avec une étiquette “gâteau-cmd-42”
- Rentre chez lui
- Sa cuisine est nettoyée, mais le passe-plat reste intact
Cuisinier B (cuisine n°2) :
- Arrive pour son service
- Récupère le gâteau depuis le passe-plat (étiquette “gâteau-cmd-42”)
- Le décore
- Commande servie avec succès
Le passe-plat survit au nettoyage des cuisines. C’est un espace de stockage externe aux machines de travail.
Les artifacts GitHub Actions, c’est exactement ce passe-plat. Un espace de stockage sur les serveurs GitHub, externe aux runners, où vous pouvez déposer des fichiers pour que d’autres jobs les récupèrent.
La solution en pratique
Anatomie d’un artifact
Un artifact est un paquet de fichiers avec trois caractéristiques :
| Propriété | Description | Exemple |
|---|---|---|
| Nom | Identifiant unique dans le workflow | build-output |
| Contenu | Fichiers ou dossiers (compressés automatiquement) | dist/, coverage/*.html |
| Rétention | Durée de conservation (1 à 90 jours) | retention-days: 5 |
Pensez-y comme une boîte étiquetée que vous déposez dans un casier. L’étiquette (le nom) permet aux autres de la retrouver. Le contenu peut être n’importe quoi. Et le casier est vidé automatiquement après un certain temps.
Votre premier workflow avec artifacts
Reprenons l’exemple du début et corrigeons-le. On va procéder étape par étape pour bien comprendre chaque ligne.
-
Le job
buildgénère et uploade les fichiers.github/workflows/build-deploy.yml name: Build & Deployon:push:branches: [main]jobs:build:runs-on: ubuntu-lateststeps:- name: Checkout du codeuses: actions/checkout@v4- name: Build de l'applicationrun: npm ci && npm run build# À ce stade, dist/ existe sur le runner- name: Sauvegarder les fichiers builduses: actions/upload-artifact@v4with:name: dist-production # L'étiquette de notre boîtepath: dist/ # Ce qu'on met dedansretention-days: 1 # Garde 1 jour (suffisant pour CI)# dist/ est maintenant copié sur les serveurs GitHubCe qui se passe : l’action
upload-artifactprend le dossierdist/, le compresse en ZIP, et l’envoie sur le stockage GitHub avec l’étiquettedist-production. Même quand le runner sera détruit, ce fichier restera accessible. -
Le job
deployattend et téléchargedeploy:runs-on: ubuntu-latestneeds: build # ⬅️ CRUCIAL : attend que build soit terminésteps:- name: Checkout du codeuses: actions/checkout@v4- name: Récupérer les fichiers builduses: actions/download-artifact@v4with:name: dist-production # Même étiquette que l'uploadpath: dist/ # Où extraire les fichiers- name: Vérifier que les fichiers sont làrun: ls -la dist/# Vous devriez voir tous vos fichiers !- name: Déployerrun: echo "Déploiement de dist/ en production..."Deux points cruciaux :
-
needs: build: sans cette ligne, les jobs s’exécutent en parallèle. Le jobdeploydémarrerait avant quebuildait fini, et l’artifact n’existerait pas encore. Erreur garantie. -
name: dist-production: doit être identique dans upload et download. C’est l’étiquette qui permet de retrouver la bonne boîte.
-
-
Vérifier que ça fonctionne
Après avoir poussé votre workflow, allez dans l’onglet Actions de votre repo GitHub. Vous devriez voir :
✓ Build & Deploy├── ✓ build (32s)│ └── Artifact: dist-production (2.4 MB)└── ✓ deploy (18s)└── Downloaded: dist-productionEn bas de la page du workflow run, vous verrez aussi une section Artifacts avec un lien pour télécharger manuellement le ZIP.
Comprendre le flux complet
Récapitulons ce qui se passe chronologiquement :
Temps Événement───── ──────────────────────────────────────────────────────────0:00 Push sur main → GitHub Actions démarre le workflow0:01 Runner #1 alloué pour le job "build"0:03 Checkout du code sur runner #10:05 npm ci + npm run build → dist/ créé sur runner #10:35 upload-artifact : dist/ compressé et envoyé sur GitHub Storage0:37 Job "build" terminé → runner #1 DÉTRUIT (dist/ local supprimé) Mais l'artifact "dist-production" reste sur GitHub Storage0:38 Runner #2 alloué pour le job "deploy" (car needs: build satisfait)0:40 download-artifact : récupère l'artifact depuis GitHub Storage0:41 dist/ restauré sur runner #2 → déploiement possible0:55 Job "deploy" terminé → runner #2 DÉTRUIT L'artifact reste disponible 1 jour pour debug/téléchargementL’artifact survit à la destruction des runners. C’est la clé.
Cas d’usage courants
Partager un build entre tests parallèles
Scénario classique : vous compilez une fois, puis lancez plusieurs types de tests en parallèle sur le même build. Sans artifacts, chaque job de test devrait recompiler l’application.
name: CI
on: [push, pull_request]
jobs: # ════════════════════════════════════════════════════════════════ # ÉTAPE 1 : Build unique # ════════════════════════════════════════════════════════════════ build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20
- name: Install & Build run: npm ci && npm run build
- name: Upload build uses: actions/upload-artifact@v4 with: name: app-build path: dist/ retention-days: 1
# ════════════════════════════════════════════════════════════════ # ÉTAPE 2 : Tests en parallèle (tous dépendent de build) # ════════════════════════════════════════════════════════════════ test-unit: needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Download build uses: actions/download-artifact@v4 with: name: app-build path: dist/
- name: Run unit tests run: npm ci && npm run test:unit
test-integration: needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Download build uses: actions/download-artifact@v4 with: name: app-build path: dist/
- name: Run integration tests run: npm ci && npm run test:integration
test-e2e: needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Download build uses: actions/download-artifact@v4 with: name: app-build path: dist/
- name: Run E2E tests run: npm ci && npx playwright test
# ════════════════════════════════════════════════════════════════ # ÉTAPE 3 : Déploiement (attend TOUS les tests) # ════════════════════════════════════════════════════════════════ deploy: needs: [test-unit, test-integration, test-e2e] if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - name: Download build uses: actions/download-artifact@v4 with: name: app-build path: dist/
- name: Deploy to production run: echo "Deploying..."Ce que ça donne :
build (2 min) │ ├─── test-unit (30s) ─┐ ├─── test-integration (1m) ├─ En parallèle ! └─── test-e2e (2m) ─┘ │ deploy (20s)Sans artifacts, chaque job de test devrait rebuilder (2 min × 3 = 6 min). Avec un seul build partagé, vous économisez 4 minutes par pipeline.
Conserver des rapports de tests
Les rapports de tests (couverture, résultats) sont précieux pour le debug. Uploadez-les comme artifacts pour pouvoir les consulter après le run.
test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Run tests with coverage run: npm ci && npm run test:coverage
- name: Upload test results uses: actions/upload-artifact@v4 if: always() # ⬅️ Upload même si les tests échouent with: name: test-results path: | coverage/ test-results/ retention-days: 7 # Garde 7 jours pour analyseLe if: always() est important : sans lui, si les tests échouent, l’upload est
sauté et vous perdez les rapports qui auraient pu vous aider à debug.
Récupérer des artifacts de tests matriciels
Quand vous utilisez une matrice (tests sur plusieurs versions Node, plusieurs OS…), chaque combinaison peut produire ses propres artifacts.
test: strategy: matrix: node: [18, 20, 22] os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4
- name: Setup Node ${{ matrix.node }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }}
- name: Run tests run: npm ci && npm test
- name: Upload coverage uses: actions/upload-artifact@v4 with: # Nom unique pour chaque combinaison name: coverage-node${{ matrix.node }}-${{ matrix.os }} path: coverage/
# Job qui récupère TOUS les rapports pour les fusionner merge-coverage: needs: test runs-on: ubuntu-latest steps: - name: Download all coverage reports uses: actions/download-artifact@v4 with: pattern: coverage-* # Tous les artifacts coverage-* path: all-coverage/ merge-multiple: true # Fusionne dans un seul dossier
- name: Merge and publish run: | ls -la all-coverage/ # Ici, fusionner les rapports avec votre outil préféréOptions avancées
Upload : contrôler ce qui part
- uses: actions/upload-artifact@v4 with: name: release-build path: | dist/ # Tout le dossier dist !dist/**/*.map # SAUF les source maps !dist/**/*.d.ts # SAUF les déclarations TypeScript retention-days: 30 # Garde 30 jours (release) compression-level: 9 # Compression maximale (plus lent mais plus petit) if-no-files-found: error # Échoue si dist/ est vide| Option | Valeurs | Description |
|---|---|---|
retention-days | 1-90 | Durée de conservation. Par défaut : 90 jours. |
compression-level | 0-9 | 0 = pas de compression, 9 = max. Par défaut : 6. |
if-no-files-found | warn, error, ignore | Comportement si aucun fichier trouvé. |
Download : contrôler ce qui arrive
# Télécharger UN artifact spécifique- uses: actions/download-artifact@v4 with: name: build-output path: ./mon-dossier/ # Destination personnalisée
# Télécharger TOUS les artifacts du run- uses: actions/download-artifact@v4 with: path: ./tous-les-artifacts/ # Chaque artifact dans un sous-dossier
# Télécharger par pattern- uses: actions/download-artifact@v4 with: pattern: coverage-* # Tous ceux qui matchent merge-multiple: true # Fusionner dans un seul dossierDépanner les problèmes
”Artifact not found”
Error: Unable to find any artifacts for the associated workflow runChecklist de diagnostic :
-
Le job producteur a-t-il terminé ?
- Vérifiez que
needs: <job-producteur>est présent - Sans
needs, les jobs tournent en parallèle → l’artifact n’existe pas encore
- Vérifiez que
-
Le nom est-il correct ?
- Comparez caractère par caractère :
dist-build≠dist_build≠distbuild - Copier-coller le nom pour éviter les typos
- Comparez caractère par caractère :
-
L’artifact a-t-il expiré ?
- Vérifiez
retention-daysdans l’upload - Les artifacts expirent silencieusement
- Vérifiez
-
L’upload a-t-il réussi ?
- Consultez les logs du job producteur
- Cherchez “Artifact … has been successfully uploaded"
"No files found”
Warning: No files were found with the provided path: dist/Le chemin n’existe pas au moment de l’upload. Ajoutez un step de debug :
- name: Debug - vérifier les fichiers avant upload run: | echo "=== Répertoire courant ===" pwd echo "=== Contenu de dist/ ===" ls -la dist/ || echo "dist/ n'existe pas !" echo "=== Arborescence complète ===" find . -name "*.js" -o -name "*.html" | head -20
- name: Upload artifact uses: actions/upload-artifact@v4 with: name: mon-build path: dist/ if-no-files-found: error # ⬅️ Échoue explicitement si videLes fichiers ne sont pas au bon endroit
Après download, vos commandes ne trouvent pas les fichiers.
Cause probable : download-artifact crée un sous-dossier par défaut.
# Ce que vous écrivez :- uses: actions/download-artifact@v4 with: name: my-build
# Ce que vous obtenez :# ./my-build/ ← Dossier créé automatiquement# └── index.html# └── main.js
# Ce que vous vouliez probablement :- uses: actions/download-artifact@v4 with: name: my-build path: dist/ # ⬅️ Extraction directe dans dist/
# Résultat :# ./dist/# └── index.html# └── main.jsBonnes pratiques
Nommer clairement
Les noms d’artifacts doivent être explicites et uniques :
# ✅ Bons nomsname: frontend-build-prodname: api-build-${{ github.sha }}name: test-coverage-node20-ubuntuname: release-v${{ github.ref_name }}
# ❌ Mauvais nomsname: build # Trop génériquename: output # Ça dit quoi ?name: files # Quels fichiers ?Optimiser la taille
Les artifacts sont stockés et transférés. Plus c’est gros, plus c’est lent et coûteux.
- uses: actions/upload-artifact@v4 with: name: production-build path: | dist/ !dist/**/*.map # Exclure source maps (souvent 50% du poids) !dist/**/*.d.ts # Exclure déclarations TypeScript !dist/**/*.test.js # Exclure fichiers de test compression-level: 9 # Compression max pour releasesAdapter la rétention
| Type de build | Rétention suggérée |
|---|---|
| CI quotidienne / PRs | 1-3 jours |
| Branches feature | 7 jours |
| Releases candidates | 30 jours |
| Releases officielles | 90 jours (max) |
# CI rapide : on ne garde pas longtempsretention-days: 1
# Release : on garde pour pouvoir rollback ou analyserretention-days: 90Sécurité
Limites et coûts
| Limite | Valeur |
|---|---|
| Taille max d’un artifact | 10 Go |
| Stockage inclus (Free) | 500 Mo |
| Stockage inclus (Pro) | 2 Go |
| Stockage inclus (Team) | 2 Go |
| Stockage inclus (Enterprise) | 50 Go |
| Rétention max | 90 jours (public) / 400 jours (privé) |
Au-delà du quota gratuit, chaque Go supplémentaire est facturé. C’est pourquoi
retention-days: 1 est recommandé pour la CI quotidienne.
Points clés à retenir
-
Chaque job = un runner isolé — Les fichiers ne se téléportent pas d’un job à l’autre
-
L’artifact = le passe-plat — Un espace de stockage externe aux runners pour faire transiter des fichiers
-
needs:est obligatoire — Sans lui, le job consommateur démarre avant que l’artifact existe -
Même nom partout — Le nom dans
upload-artifactdoit être identique à celui dansdownload-artifact -
retention-days: 1pour la CI — Économise du stockage, suffit pour les builds quotidiens -
Jamais de secrets — Les artifacts sont accessibles à tous les collaborateurs (voire au public)
Aller plus loin :
- Artifacts vs Cache : bien choisir — Comprendre quand utiliser l’un ou l’autre
- Cache GitHub Actions — Accélérer vos workflows en réutilisant les dépendances