Skip to content

3.01 Job Artifacts

Overview

A job that builds something produces files. Job artifacts are how those files leave the runner — either for a human to download, or for a later job to pick up.

Abstract

An artifact is the output of a job: website files, an app binary, an installer package, test reports, coverage data, log files. Because every job runs on a fresh runner whose filesystem is discarded when the job ends, anything worth keeping must be uploaded explicitly with actions/upload-artifact. Stored artifacts can then be downloaded manually through the GitHub UI or the REST API, or automatically inside another job with actions/download-artifact.

Why It Matters in Production

Artifacts are the seam between build and deploy. Building once and shipping that exact output is the difference between deploying what you tested and deploying something rebuilt from the same source and hoping it is identical.

They are also the only practical way to debug a failed run. Test logs, coverage reports, screenshots from a failed browser test, and crash dumps all vanish with the runner unless uploaded.

And they cost money. Artifact storage counts against the account's storage allowance, so retention and what you choose to upload are real budget decisions on an active repository.

Key Concepts

Concept Meaning
Job artifact Files or folders produced by a job and stored by GitHub
Ephemeral runner Each job's filesystem is destroyed when the job ends
actions/upload-artifact Stores files from the runner under a named artifact
actions/download-artifact Retrieves a stored artifact into a later job's workspace
Manual retrieval Download from the run summary page, or via the REST API
Retention How long GitHub keeps the artifact before deleting it

Two ways to consume an artifact

Route How Typical use
Manually GitHub UI (run summary → Artifacts) or REST API Inspect test logs, grab a binary to upload to an app store
In another job actions/download-artifact Deploy job consumes the build job's output

Artifacts vs caches vs outputs

An artifact is a deliverable you want to keep or hand to another job. A cache is a speed optimization GitHub may evict at any time. A job output is a small string value (a version number, an image tag) passed via needs, not a file. Don't use a cache where you mean an artifact — a cache miss is silent.

Common Use Cases

  • Build once in a build job, deploy the uploaded output in a deploy job
  • Keep website dist/ files for upload to a hosting provider
  • Store a mobile app package or desktop executable for manual distribution
  • Attach test reports, coverage output, or JUnit XML to the run for inspection
  • Capture log files and screenshots from a failing test so they survive the run
  • Produce a release binary a human downloads and signs before publishing

Example Configuration or Commands

Build in one job, deploy the stored output in another:

name: Deploy Project
on:
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Get code
        uses: actions/checkout@v4
      - name: Install NodeJS
        uses: actions/setup-node@v4
        with:
          node-version: 18
          cache: 'npm'
      - name: Install dependencies
        run: npm ci
      - name: Build project
        run: npm run build
      - name: Upload build output
        uses: actions/upload-artifact@v4
        with:
          name: dist-files
          path: dist

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download build output
        uses: actions/download-artifact@v4
        with:
          name: dist-files
          path: dist
      - name: Output contents
        run: ls -R dist
      - name: Deploy
        run: echo "Deploying..."
- uses: actions/upload-artifact@v4
  with:
    name: build-output
    path: |
      dist
      package.json
      !dist/**/*.map        # exclusion patterns are supported
- name: Run tests
  run: npm test
- name: Upload failure logs
  if: failure()             # the step would otherwise be skipped
  uses: actions/upload-artifact@v4
  with:
    name: test-logs
    path: |
      logs/
      coverage/
    retention-days: 7
- uses: actions/upload-artifact@v4
  with:
    name: reports
    path: reports/
    retention-days: 5          # shorter than the repository default
    if-no-files-found: error   # warn (default) | error | ignore
    compression-level: 9       # 0–9; 0 for already-compressed files
- name: Download all artifacts
  uses: actions/download-artifact@v4
  with:
    path: artifacts           # each artifact lands in its own subfolder
# v4 artifacts are immutable — two uploads with the same name collide
- uses: actions/upload-artifact@v4
  with:
    name: build-${{ matrix.os }}-${{ matrix.node-version }}
    path: dist

v4 behaves differently from v3

In upload-artifact@v4 each artifact is immutable and uploaded once: you cannot append to an existing name, and a second upload under the same name fails rather than merging. Downloads are also scoped to the current workflow run by default. Mixing v3 and v4 in one workflow does not work — they use incompatible storage backends.

Working with artifacts outside a workflow:

# List artifacts for a run
gh run view <run-id> --json displayTitle

# Download all artifacts of a run to the current directory
gh run download <run-id>

# Download one named artifact
gh run download <run-id> --name dist-files --dir ./dist

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

Best Practices

Best Practices

  • Build once, upload, and deploy the uploaded artifact. Rebuilding in the deploy job wastes minutes and breaks the guarantee that you shipped what you tested.
  • Upload only what the next step needs. dist/ is an artifact; node_modules/ is not.
  • Set retention-days explicitly — the repository default (up to 90 days) is usually far longer than anything is useful.
  • Name artifacts descriptively and uniquely, especially in matrix jobs where parallel uploads would otherwise collide.
  • Attach diagnostics with if: failure() so failed runs leave evidence behind.
  • Use if-no-files-found: error when an empty upload means the build silently produced nothing.
  • Set compression-level: 0 for already-compressed content (images, zips, videos) — it saves time without saving bytes.
  • Keep artifacts out of the repository itself. Committing build output back to Git is the anti-pattern artifacts exist to replace.

Security Best Practices

Security

  • Artifacts of a public repository are downloadable by anyone. Treat every upload as published: no .env files, no credentials, no internal hostnames.
  • Source maps and debug symbols leak implementation detail. Exclude them explicitly (!dist/**/*.map) unless you intend to ship them.
  • Never upload node_modules/, .git/, or a whole workspace directory. Broad paths sweep up configuration and tokens written during the run.
  • Secrets injected as environment variables can end up inside build output. Scan what you upload before making a repository public.
  • Downloading an artifact produced by an untrusted job (a fork PR, for example) means executing or deploying files you did not build. Keep deploy jobs on trusted triggers only.
  • Artifacts count against storage billing, so an attacker who can trigger workflows can also run up cost. Keep retention short and fork-PR approval on.
  • Deleting an artifact removes it from storage, but anything already downloaded is out of your control — rotate anything exposed rather than relying on deletion.

Do and Don't

✅ Do ❌ Don't
Upload dist/ and pass it to the deploy job Rebuild the project in every job
Set retention-days for short-lived output Leave everything at the 90-day default
Use unique artifact names in matrix jobs Upload the same name from parallel jobs
Attach logs with if: failure() Lose the evidence when a run fails
Exclude source maps and secrets Upload the whole workspace
Use an artifact for deliverables Use a cache for anything you must have
Keep v4 actions consistent across the workflow Mix upload-artifact@v3 and @v4

Common Mistakes

Common Mistakes

  • Expecting files to survive between jobs. Each job gets a clean runner; without an upload, dist/ does not exist downstream.
  • Forgetting needs. Without it the deploy job starts in parallel and the artifact isn't uploaded yet.
  • Path confusion on download. download-artifact extracts the artifact's contents into path:, so uploading dist and downloading into dist gives you dist/index.html, not dist/dist/index.html. Check with ls -R.
  • Uploading the parent folder by accident. path: dist stores the folder's contents; path: ./ stores the entire workspace.
  • Duplicate names in a matrix. v4 rejects the second upload instead of merging.
  • Assuming artifacts cross workflow runs. By default a download sees only the current run's artifacts; other runs need the REST API or extra configuration.
  • Relying on a cache as a handoff. Caches can be evicted; artifacts are guaranteed until they expire.
  • Skipping the upload step on failure. Steps after a failed step don't run unless marked if: failure() or if: always().

Breaks in production

A deploy job that rebuilds from source instead of downloading the tested artifact can ship different bytes than CI validated — a dependency resolved differently, a changed build-time environment variable — and the test results say nothing about what actually went live.

Troubleshooting

# What did the build job actually produce?  Add this before uploading.
ls -R dist

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

# Pull an artifact down locally to inspect it
gh run download <run-id> --name dist-files --dir /tmp/check && ls -R /tmp/check

# Check the repository's artifact retention setting
gh api repos/OWNER/REPO/actions/permissions --jq '.'
Symptom Likely cause
"No files were found with the provided path" Build output elsewhere, or the build step failed silently
Download step finds nothing Missing needs, wrong artifact name, or a different workflow run
Nested directory after download Mismatch between the uploaded path and the download path:
Upload fails with a conflict v4 artifact name already used in this run
Artifact missing from an older run Retention period elapsed
Deploy job can't see build output No upload/download pair — runners don't share a filesystem
Storage quota warnings Long retention plus large or frequent uploads
Mixed v3/v4 errors The two versions use incompatible backends

Tip

Artifacts appear in the run summary page under Artifacts once the run completes, with a download link per artifact. That page is also where you confirm the size, which is the fastest way to notice you uploaded node_modules/ by mistake.

Quick Recap

  • A job artifact is the output a job produces: binaries, website files, packages, reports, logs.
  • Runners are ephemeral, so anything worth keeping must be uploaded explicitly.
  • actions/upload-artifact stores files; actions/download-artifact retrieves them.
  • Artifacts can be downloaded manually via the GitHub UI or REST API, or automatically in another job.
  • Combine with needs so the consuming job runs after the producing one.
  • Set retention-days and upload only what is needed — storage is billed.
  • v4 artifacts are immutable and scoped to the current run; names must be unique.
  • Artifacts on public repositories are publicly downloadable.

Interview / Revision Notes

Quick revision

Q: What is a job artifact? Files or folders produced by a job and stored by GitHub beyond the life of the runner.

Q: Why are they needed? Each job runs on a fresh runner whose filesystem is discarded when the job ends.

Q: Give examples. Website build output, app binaries, installer packages, test reports, coverage data, log files.

Q: Two ways to consume an artifact? Download manually via the UI or REST API, or automatically in another job with download-artifact.

Q: Which actions handle artifacts? actions/upload-artifact and actions/download-artifact.

Q: Artifact vs cache? An artifact is a deliverable guaranteed to exist until it expires; a cache is an optimization that may be evicted.

Q: How do you keep logs from a failing job? Upload them in a step marked if: failure().

Q: How do you stop matrix jobs from clashing on upload? Include matrix values in the artifact name.

Q: What changed in v4? Artifacts became immutable, same-name uploads fail instead of merging, and downloads are scoped to the current run.

This topic can be expanded into sub-pages: upload-artifact and download-artifact inputs in depth, job outputs vs artifacts, caching dependencies, artifact retention and storage billing, and release asset publishing.