Skip to content

3.02 Uploading and Downloading Artifacts

Overview

Storing a job's output takes one step; consuming it in a later job takes one more. actions/upload-artifact saves files from the runner, actions/download-artifact fetches them back.

Abstract

upload-artifact needs two inputs under with: — a name identifying the artifact and a path pointing at the files. Multiple paths go on separate lines after a |, and exclusions are written with !. Once the run finishes, the artifact appears at the bottom of the run summary as a downloadable ZIP. In a later job — which must have needs on the producing job — download-artifact with the same name fetches and unpacks it into the workspace. The unpacking is where people get caught: the uploaded folder itself is not recreated, so files inside dist/ land directly in the working directory.

Why It Matters in Production

This pair is the handoff that lets a pipeline build once and deploy that exact output. The build job validates and produces; the deploy job consumes. Nothing is rebuilt, so nothing can differ.

The directory-flattening behavior matters practically. A deploy step written as aws s3 sync dist/ s3://bucket works in the build job and fails in the deploy job, because after download the files are at the root, not under dist/. Knowing that before writing the upload command saves a debugging cycle on every new pipeline.

Key Concepts

Input Action Purpose
name both Identifier for the artifact; must match between upload and download
path upload-artifact File(s) or folder(s) to store — one per line after |
path download-artifact Destination directory on the runner (defaults to the workspace)
retention-days upload-artifact Days to keep it before deletion
if-no-files-found upload-artifact warn (default), error, or ignore

The path structure rule

The archive preserves paths relative to the least common ancestor of everything you uploaded. This single rule explains all the confusing cases:

Uploaded path Least common ancestor Archive contains After download
dist dist/ index.html, assets/… files at the workspace root
dist and package.json workspace root dist/, package.json dist/ folder plus package.json
dist/** dist/ contents of dist files at the root
build/out build/out/ contents of out files at the root

The uploaded folder is not recreated

Uploading path: dist on its own stores the folder's contents, so after download the files sit directly in the job's working directory — there is no dist/ to navigate into. Add a second path (or use download-artifact's path: input) if you need the folder back.

needs is mandatory, not stylistic

Without needs: build, the deploy job starts in parallel with the build job and the artifact does not exist yet. The download step fails or finds nothing.

Common Use Cases

  • Upload dist/ from a build job and deploy it from a separate job
  • Upload a binary or installer for manual download and signing
  • Bundle several paths (build output plus a manifest) under one artifact name
  • Exclude source maps or test fixtures from an otherwise useful artifact
  • Download into an explicit directory so deploy commands can keep using familiar paths

Example Configuration or Commands

The full three-job pipeline — test, build with upload, deploy with download:

name: Deploy website
on:
  push:
    branches:
      - main

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Get code
        uses: actions/checkout@v4
      - name: Install dependencies
        run: npm ci
      - name: Lint code
        run: npm run lint
      - name: Test code
        run: npm run test

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - name: Get code
        uses: actions/checkout@v4
      - name: Install dependencies
        run: npm ci
      - name: Build website
        run: npm run build
      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: dist-files
          path: dist

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Get build artifacts
        uses: actions/download-artifact@v4
        with:
          name: dist-files
      - name: Output contents
        run: ls
      - name: Deploy
        run: echo "Deploying..."

The deploy job needs no checkout and no npm ci — it only consumes what the build job produced.

- uses: actions/upload-artifact@v4
  with:
    name: dist-files
    path: dist
Archive holds the folder's contents; after download they are at the workspace root.

- uses: actions/upload-artifact@v4
  with:
    name: dist-files
    path: |
      dist
      package.json
The common ancestor is now the workspace, so the archive keeps dist/ as a folder alongside package.json.

- uses: actions/upload-artifact@v4
  with:
    name: dist-files
    path: |
      dist
      !dist/**/*.map
      !dist/**/*.test.js
# Keeps deploy commands that expect ./dist working unchanged
- uses: actions/download-artifact@v4
  with:
    name: dist-files
    path: dist
- run: ls -R dist
# Each artifact lands in its own subdirectory under the given path
- uses: actions/download-artifact@v4
  with:
    path: artifacts
- run: ls -R artifacts

Confirming what actually arrived

ls after the download step is the cheapest diagnostic in the whole pipeline. The run log also reports how many files were downloaded, so a count that looks wrong is visible before any deploy command runs:

- name: Get build artifacts
  uses: actions/download-artifact@v4
  with:
    name: dist-files
- name: Output contents
  run: ls -R

Retrieving the same artifact outside the workflow:

# The run summary page lists artifacts at the bottom; the CLI equivalent:
gh run download <run-id> --name dist-files --dir ./downloaded
ls -R ./downloaded

# Artifact metadata via the REST API
gh api repos/OWNER/REPO/actions/runs/<run-id>/artifacts \
  --jq '.artifacts[] | {name, size_in_bytes, expired}'

Version note

The lecture uses @v3. Prefer @v4 for new pipelines: it uploads faster and makes artifacts immutable. Two consequences — a repeated name within a run now fails instead of merging, and v3 and v4 cannot be mixed in the same workflow because their storage backends differ.

Best Practices

Best Practices

  • Upload only what the next job needs. The redundant extra path is useful for learning, not for a real pipeline.
  • Give the download step an explicit path: matching the folder your deploy commands expect — it removes the flattening surprise entirely.
  • Always ls (or ls -R) once after adding a download step, before trusting any path in a later command.
  • Keep the artifact name in one place conceptually; a mismatch between upload and download is the most common failure and produces a confusing "not found".
  • Skip checkout and dependency installs in a consuming job unless it genuinely needs the source.
  • Exclude source maps and test files from deployment artifacts.
  • Set retention-days on large or frequent uploads; the default is far longer than useful.
  • Use if-no-files-found: error so a build that produced nothing fails loudly instead of uploading an empty artifact.

Security Best Practices

Security

  • Artifacts on a public repository are downloadable by anyone with the link. The upload step is effectively a publish step.
  • path: . or a bare workspace path sweeps up .git/, node_modules/, and any file a previous step wrote — including credentials materialized at runtime. Name specific paths.
  • Exclude source maps (!dist/**/*.map) unless you intend to ship them; they reconstruct your source.
  • A deploy job that downloads an artifact executes or publishes files it did not build. Keep deploy jobs on trusted triggers, never on fork pull requests.
  • Do not use artifacts to pass secrets between jobs. They are stored, downloadable, and retained. Use secrets or job outputs instead.
  • Check what the build wrote before uploading. A build that bakes an environment variable into its output puts that value in a downloadable ZIP.
  • Keep retention-days short for anything containing internal detail — storage you no longer need is exposure you no longer need.

Do and Don't

✅ Do ❌ Don't
Keep needs between producer and consumer Let the deploy job race the build job
Use | for multi-line path values Put several paths on one line
Run ls after downloading Assume the folder structure survived
Set path: on download when you want a folder Hardcode dist/ in a deploy command without checking
Name specific upload paths Upload the whole workspace
Match upload and download names exactly Rename one and forget the other
Use @v4 consistently Mix @v3 and @v4 in one workflow

Common Mistakes

Common Mistakes

  • Expecting dist/ to exist after download. Uploading path: dist alone stores its contents; the folder is not recreated.
  • Missing the pipe. Multiple paths require path: | with one entry per indented line.
  • Name mismatch. A typo between upload and download produces "unable to find an artifact", not a helpful diff.
  • Forgetting needs. Parallel execution means the artifact isn't uploaded when the download runs.
  • Deploy commands with stale paths. npm run deploy ./dist fails after a flattened download.
  • Checking out code in the deploy job out of habit. Harmless but wasteful, and it can overwrite or mask downloaded files.
  • Uploading an empty folder. The build step failed but the upload step only warns by default.
  • Reusing an artifact name across matrix jobs. v4 rejects the duplicate.
  • Expecting cross-run downloads. By default a download only sees artifacts from the current run.

Breaks in production

A deploy step that silently uploads the wrong directory — the workspace root instead of the flattened build output — publishes package.json and node_modules/ to the web host and no index.html. The pipeline is green; the site is broken.

Troubleshooting

# What did the build actually produce?  Add before the upload step.
ls -R dist

# What arrived in the consuming job?  Add after the download step.
ls -R

# Artifact name, size, and expiry for a run
gh api repos/OWNER/REPO/actions/runs/<run-id>/artifacts \
  --jq '.artifacts[] | "\(.name) \(.size_in_bytes) expired=\(.expired)"'

# Pull it locally and inspect the real structure
gh run download <run-id> --name dist-files --dir /tmp/check && find /tmp/check
Symptom Likely cause
"No files were found with the provided path" Build output is elsewhere, or the build step failed
"Unable to find any artifacts for the associated workflow run" Name mismatch, missing needs, or wrong run
Files at the root instead of in dist/ Expected flattening — set path: on download
Nested dist/dist/ after download Uploaded multiple paths, then downloaded into dist as well
Downloaded file count looks wrong Upload path too broad or too narrow — check the build job's ls
Conflict on upload Duplicate artifact name within the run (v4)
Deploy publishes the wrong files Deploy command assumes a folder the download flattened away

Tip

The artifact section at the bottom of the run summary page shows the size. A dist-files artifact measured in hundreds of megabytes almost always means node_modules/ got swept in by an over-broad path.

Quick Recap

  • actions/upload-artifact takes name and path under with:.
  • Multiple paths need path: | with one entry per line; ! excludes.
  • The finished run lists artifacts at the bottom of the summary page as downloadable ZIPs.
  • actions/download-artifact with the matching name fetches and unpacks the artifact.
  • The consuming job must declare needs on the producing job.
  • Archive paths are relative to the least common ancestor of the uploaded paths.
  • Uploading one folder stores its contents, so the folder is not recreated on download.
  • Use download-artifact's path: input to place files where later commands expect them.
  • ls after downloading is the fastest way to confirm the structure.

Interview / Revision Notes

Quick revision

Q: Which two inputs does upload-artifact require? name and path.

Q: How do you upload several paths? path: | followed by one path per indented line.

Q: How do you exclude files? Prefix a pattern with ! in the path list.

Q: Where do you find uploaded artifacts manually? At the bottom of the workflow run summary page, as a ZIP download (or via the REST API).

Q: What does download-artifact need? The same name used on upload, and the job must needs the producing job.

Q: What happens to the folder structure on download? Paths are relative to the least common ancestor, so uploading just dist puts its contents at the destination root.

Q: How do you get the folder back? Set path: dist on the download step, or upload a second path so the common ancestor moves up.

Q: Why does the deploy job not need checkout? It consumes build output, not source.

Q: Why does needs matter here? Parallel jobs would try to download an artifact that hasn't been uploaded yet.

This topic can be expanded into sub-pages: artifact retention and storage billing, job outputs for passing values, caching dependencies, multi-platform matrix artifacts, and publishing release assets.