Lesson 2 of 30
Stages, jobs, and steps aren't interchangeable words — they're nested structural units, and knowing which one you're looking at is how you read (and debug) any pipeline.
15 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
Every CI/CD pipeline, no matter the provider, is built from the same three nested units:
pnpm install," "run the test suite." It's the smallest thing that can pass or fail.On top of that structure sits a trigger — the event that starts the whole thing. The four common ones: a push (code lands on a branch), a pull_request (someone opens or updates a PR), a schedule (a cron expression, e.g. a nightly run), and workflow_dispatch (a human clicks a button to run it manually).
This is .github/workflows/ci.yml, word for word, as it runs on every push and PR against this codebase right now:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
verify:
name: Type-check, lint, test, validate content, 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
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Type-check
run: pnpm exec tsc --noEmit
- name: Lint
run: pnpm exec eslint .
- name: Run lib/ test suite
run: |
set -e
for f in $(find lib -name "*.test.ts"); do
echo "--- $f ---"
pnpm exec tsx "$f"
done
- name: Validate vercel.json against Vercel's schema
run: pnpm exec tsx scripts/validate-vercel-json.ts
- name: Validate authored content against schemas
run: pnpm exec tsx scripts/validate-content.ts
- name: Production build
run: pnpm build
Mapped onto the vocabulary above: the trigger is on: push (to main) plus on: pull_request (any branch, any target) — two different events both start the same workflow. There's exactly one job, named verify, running on one ubuntu-latest machine. Inside it are ten steps, executed strictly top to bottom: checkout, install pnpm itself, install Node with its cache wired to pnpm, install dependencies, type-check, lint, run every test file under lib/, validate vercel.json, validate content, then build. If pnpm exec tsc --noEmit fails at step five, steps six through ten never run — the job stops and reports failure immediately. That's the "same machine, sequential, first failure wins" rule for jobs in action.
A push to the repo fires "on: push" — that trigger is what starts the workflow.
pull_request and push are genuinely different events, not two names for the same thing. This workflow's on: push: branches: [main] only fires for commits landing directly on main; on: pull_request fires separately for every PR, targeting any branch. A PR into main triggers the pull_request event, not the push event — the push event only fires later, once that PR is merged.Every uses: and run: line in this file is GitHub Actions' own workflow syntax — the specific vocabulary for declaring jobs, steps, and triggers in YAML. That syntax, and the difference between running someone else's packaged action versus running a raw shell command, is the next lesson.