Aller au contenu

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 found

Vous 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.

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.

Schéma du principe des
artifacts

La solution en pratique

Anatomie d’un artifact

Un artifact est un paquet de fichiers avec trois caractéristiques :

PropriétéDescriptionExemple
NomIdentifiant unique dans le workflowbuild-output
ContenuFichiers ou dossiers (compressés automatiquement)dist/, coverage/*.html
RétentionDuré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.

Cycle de vie d'un
artifact

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.

  1. Le job build génère et uploade les fichiers

    .github/workflows/build-deploy.yml
    name: Build & Deploy
    on:
    push:
    branches: [main]
    jobs:
    build:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout du code
    uses: actions/checkout@v4
    - name: Build de l'application
    run: npm ci && npm run build
    # À ce stade, dist/ existe sur le runner
    - name: Sauvegarder les fichiers build
    uses: actions/upload-artifact@v4
    with:
    name: dist-production # L'étiquette de notre boîte
    path: dist/ # Ce qu'on met dedans
    retention-days: 1 # Garde 1 jour (suffisant pour CI)
    # dist/ est maintenant copié sur les serveurs GitHub

    Ce qui se passe : l’action upload-artifact prend le dossier dist/, le compresse en ZIP, et l’envoie sur le stockage GitHub avec l’étiquette dist-production. Même quand le runner sera détruit, ce fichier restera accessible.

  2. Le job deploy attend et télécharge

    deploy:
    runs-on: ubuntu-latest
    needs: build # ⬅️ CRUCIAL : attend que build soit terminé
    steps:
    - name: Checkout du code
    uses: actions/checkout@v4
    - name: Récupérer les fichiers build
    uses: actions/download-artifact@v4
    with:
    name: dist-production # Même étiquette que l'upload
    path: 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éployer
    run: echo "Déploiement de dist/ en production..."

    Deux points cruciaux :

    • needs: build : sans cette ligne, les jobs s’exécutent en parallèle. Le job deploy démarrerait avant que build ait 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.

  3. 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-production

    En 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 workflow
0:01 Runner #1 alloué pour le job "build"
0:03 Checkout du code sur runner #1
0:05 npm ci + npm run build → dist/ créé sur runner #1
0:35 upload-artifact : dist/ compressé et envoyé sur GitHub Storage
0:37 Job "build" terminé → runner #1 DÉTRUIT (dist/ local supprimé)
Mais l'artifact "dist-production" reste sur GitHub Storage
0:38 Runner #2 alloué pour le job "deploy" (car needs: build satisfait)
0:40 download-artifact : récupère l'artifact depuis GitHub Storage
0:41 dist/ restauré sur runner #2 → déploiement possible
0:55 Job "deploy" terminé → runner #2 DÉTRUIT
L'artifact reste disponible 1 jour pour debug/téléchargement

L’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.

Pipeline avec tests
parallèles

.github/workflows/ci.yml
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 analyse

Le 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
OptionValeursDescription
retention-days1-90Durée de conservation. Par défaut : 90 jours.
compression-level0-90 = pas de compression, 9 = max. Par défaut : 6.
if-no-files-foundwarn, error, ignoreComportement 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 dossier

Dépanner les problèmes

”Artifact not found”

Error: Unable to find any artifacts for the associated workflow run

Checklist de diagnostic :

  1. 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
  2. Le nom est-il correct ?

    • Comparez caractère par caractère : dist-builddist_builddistbuild
    • Copier-coller le nom pour éviter les typos
  3. L’artifact a-t-il expiré ?

    • Vérifiez retention-days dans l’upload
    • Les artifacts expirent silencieusement
  4. 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 vide

Les 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.js

Bonnes pratiques

Nommer clairement

Les noms d’artifacts doivent être explicites et uniques :

# ✅ Bons noms
name: frontend-build-prod
name: api-build-${{ github.sha }}
name: test-coverage-node20-ubuntu
name: release-v${{ github.ref_name }}
# ❌ Mauvais noms
name: build # Trop générique
name: 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 releases

Adapter la rétention

Type de buildRétention suggérée
CI quotidienne / PRs1-3 jours
Branches feature7 jours
Releases candidates30 jours
Releases officielles90 jours (max)
# CI rapide : on ne garde pas longtemps
retention-days: 1
# Release : on garde pour pouvoir rollback ou analyser
retention-days: 90

Sécurité

Limites et coûts

LimiteValeur
Taille max d’un artifact10 Go
Stockage inclus (Free)500 Mo
Stockage inclus (Pro)2 Go
Stockage inclus (Team)2 Go
Stockage inclus (Enterprise)50 Go
Rétention max90 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

  1. Chaque job = un runner isolé — Les fichiers ne se téléportent pas d’un job à l’autre

  2. L’artifact = le passe-plat — Un espace de stockage externe aux runners pour faire transiter des fichiers

  3. needs: est obligatoire — Sans lui, le job consommateur démarre avant que l’artifact existe

  4. Même nom partout — Le nom dans upload-artifact doit être identique à celui dans download-artifact

  5. retention-days: 1 pour la CI — Économise du stockage, suffit pour les builds quotidiens

  6. Jamais de secrets — Les artifacts sont accessibles à tous les collaborateurs (voire au public)


Aller plus loin :