Aller au contenu
CI/CD & Automatisation medium

GitHub Actions Artifacts : partager des fichiers entre jobs

24 min de lecture

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.

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.

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

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

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

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

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

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

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-24.04, 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-24.04
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é
- 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é.
# 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
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"
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

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

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 ?

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

  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 :