Lesson 7 of 30
Build once, deploy the exact bytes you tested — not a fresh rebuild that might not match. Artifacts are how one job hands its output to another.
14 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A pipeline is usually made of more than one job — build, test, deploy — and by default those jobs don't share a filesystem. Each one starts on a fresh runner with nothing on it. If your deploy job needs the production build, the naive approach is to just run the build again inside the deploy job. That works, but it means the thing you actually ship was never the thing your test job verified — it's a second, separately-produced build that merely should be identical.
An artifact is how you avoid that: one job builds the output, uploads it as a named artifact, and any later job in the same run can download that exact artifact — the same files, byte for byte, that already passed whatever came before. "Build once, deploy the same bytes everywhere" is the whole point. It also means a slow build step only runs once per pipeline run instead of once per job that needs its output.
This repo's own ci.yml doesn't split build and deploy into separate jobs — pnpm build is the last step of the single verify job, and deployment happens separately via Vercel's own GitHub integration. But the artifact pattern is exactly what you'd add if you wanted an explicit deploy job in the same workflow, rather than handing off to an external platform:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 11.5.0
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build
- name: Upload production build
uses: actions/upload-artifact@v4
with:
name: production-build
path: .next
retention-days: 1
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download production build
uses: actions/download-artifact@v4
with:
name: production-build
path: .next
- name: Deploy
run: ./scripts/deploy.sh
needs: build is what makes this work — it tells GitHub Actions the deploy job can't start until build finishes successfully, and it's what gives deploy something to download. Without needs, the two jobs would run in parallel and deploy would fail immediately with nothing to fetch. Notice deploy never runs pnpm install or pnpm build at all — it has no reason to; the artifact already contains everything the build step produced.
path on upload and download doesn't have to match, but it's the easiest thing to get wrong. If build uploads from .next and deploy downloads to a different directory expecting the same structure, the deploy step fails on a missing file with an error that looks like a build problem, not an artifact-path mismatch.Caching and artifacts solve two different problems that look similar. A cache is for something that's expensive to reproduce but shouldn't have changed — dependencies keyed on a lockfile hash, reused across many runs. An artifact is for something a job just produced in this specific run that a later job in the same run needs exactly as-is. Reach for caching when the question is "has this changed since last time?" and for an artifact when the question is "how does this job's output get to that job?"