Skip to content

GitHub Actions Artifacts: Storing and Passing Build Output

Lesson 9 of 11Intermediate5 min readGitHub Actions & CI/CD · Advanced ActionsVerified: actions/upload-artifact v7, actions/download-artifact v8, August 2026

Jobs share nothing. Two jobs in the same workflow run on separate machines with separate filesystems, so the build job’s dist/ directory simply does not exist for the deploy job. Artifacts are how you move files across that boundary — and how you get them out of the run entirely.

ArtifactCache
PurposeOutput you want to keep or pass onSpeed optimisation
Downloadable by peopleYes, from the run pageNo
RetentionExplicit, up to 90 days (configurable)LRU eviction against a quota
Missing oneBreaks the dependent jobOnly makes the job slower
Scoped by branchNo — visible with repository read accessYes

The clarifying question: if this disappears, does the pipeline break or just get slower? Breaks means artifact. Slower means cache.

- uses: actions/upload-artifact@v7
with:
name: dist
path: ./dist
retention-days: 7
if-no-files-found: error

if-no-files-found defaults to warn, which is a poor default for a build output. A build that silently produced nothing then uploads an empty artifact, and the deploy job downloads nothing and deploys nothing — with green ticks all the way. Set error.

Multiple paths and exclusions:

path: |
dist/**
!dist/**/*.map
reports/junit.xml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: npm ci && npm run build
- uses: actions/upload-artifact@v7
with:
name: dist
path: ./dist
if-no-files-found: error
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v8
with:
name: dist
path: ./dist
- run: ./deploy.sh ./dist

What it doesMoves a directory from one job to another through artifact storage.

Why we run itJobs run on separate runners with separate filesystems. Rebuilding in the deploy job would deploy something the test job never saw.

Expected resultThe deploy job works with the exact bytes the build job produced.

This is the “build once, deploy that” principle, and it is the reason to bother. Rebuilding in the deploy job means deploying an artifact nothing tested — a different dependency resolution, a different timestamp, potentially different code.

Omitting name: on download fetches all artifacts, each into a subdirectory named after it. Useful for a job that collects matrix results.

An artifact name can be uploaded once per run. A second upload under the same name fails.

This bites hardest on a matrix, where every leg runs the same upload step:

- uses: actions/upload-artifact@v7
with:
name: coverage-${{ matrix.os }}-${{ matrix.node }}
path: coverage/

Include enough of the matrix in the name to make it unique. Then merge downstream if you need one combined artifact — download-artifact with a pattern and merge-multiple: true collects them.

- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-${{ matrix.os }}
path: reports/

Every step carries an implicit if: success(). Without always(), test reports, logs and screenshots are uploaded only when the tests passed — precisely the case where nobody needs them. This is the most valuable one-line change on this page.

For a failing browser test, uploading the screenshot and the trace turns “it failed in CI and I can’t reproduce it” into an actual diagnosis.

retention-days on the step overrides the repository default; the repository default can be up to 90 days, and organisation policy can cap it lower.

Artifact storage is billed against the account, and it accumulates quietly — a pipeline uploading 200 MB per run, ten runs a day, at 90-day retention holds a lot. Set retention to what the artifact is actually for:

ArtifactReasonable retention
Test reports and logs7–14 days
Coverage14–30 days
Pull request build output1–7 days
Release binariesLong, or move them to a release

Release binaries are the case where artifacts are the wrong home: attach them to a GitHub Release instead, which is permanent, versioned and publicly linkable.

An artifact is downloadable by anyone with read access to the repository, and on a public repository that is everyone, for the whole retention period.

  • Credentials of any kind, including files a build wrote them into — .npmrc, .docker/config.json, kubeconfig, .aws/credentials.
  • Terraform plan files. They embed resource attributes including generated passwords. See Terraform CI.
  • .env files, whether or not you think they are populated in CI.
  • Whole workspace directories. path: . uploads the checkout, the caches, and anything any step wrote — which is how credentials end up in artifacts without anyone deciding to put them there.
Terminal window
gh run download <run-id> --name dist
gh run download <run-id> # everything
gh api repos/OWNER/REPO/actions/artifacts

Downloading an artifact from another repository, or from a different workflow run, needs a token with actions: read and the artifact must not have expired.

The workflow_run pattern uses this deliberately: an untrusted pull_request workflow uploads a sanitised report as an artifact, and a separate trusted workflow — running from the default branch, with write permissions — downloads and publishes it. That separation is how you get pull request comments from fork contributions without handing the fork a writable token. See workflow security.

  1. Build in one job, upload dist, download it in a second job with needs:. Confirm the second job has files it never built.

  2. Remove if-no-files-found: error and make the build produce nothing. Confirm the pipeline stays green and deploys an empty directory.

  3. Add the flag back and confirm the same scenario now fails at upload.

  4. Add a matrix and upload with a fixed artifact name. Read the collision error, then fix it with a name containing the matrix values.

  5. Add if: always() to a report upload, make a test fail, and confirm the report is still there.

  6. Check your repository’s artifact storage in the billing page. Set retention deliberately.

GitHub Actions Security ChecklistAudit your workflows against the failure modes that actually cause incidents. Free and complete.

Want production-ready workflow templates? The Professional Toolkit has five, with permissions set correctly.