Lesson 17 of 30
Running the whole pipeline on every commit stops scaling the moment a repo holds more than one package. The fix is knowing which packages a change actually affects.
16 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 monorepo holds multiple packages in one repository — say a shared ui component library, a web app that consumes it, and an unrelated api service. Point a naive pipeline at the repo root and every PR, no matter how small, rebuilds and retests all three: a one-line fix in api still pays for ui's and web's full test suites. That's correct but wasteful, and the waste compounds — a repo with a dozen packages spends most of its CI minutes re-verifying code that didn't change.
The fix isn't "only run pipelines for the package whose files changed" — that's necessary but not sufficient, because packages depend on each other. If ui changes and web imports it, web needs re-testing too even though nobody touched a web file. What you actually want is affected-package detection: given a changed set of files, walk the workspace's real dependency graph (declared in each package's own package.json) and compute the changed packages plus everything that depends on them, transitively. Tools like Turborepo and Nx exist specifically to compute that set and run tasks scoped to it, instead of you hand-rolling glob-based path filters that don't know the graph exists.
packages/
ui/ — design-system components, no internal dependencies
api/ — backend service, no internal dependencies
web/ — the app; package.json lists "ui" as a workspace dependency
A PR touches exactly one file: packages/ui/src/Button.tsx. Here's a CI job that builds and tests only what that change actually affects:
name: CI
on:
pull_request:
jobs:
affected:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history — turbo needs it to diff against origin/main
- 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
- name: Build and test only affected packages
run: pnpm turbo run build test --filter='...[origin/main]'
--filter='...[origin/main]' means: find every package with at least one file changed since origin/main, then the leading ... expands that set to include every package that depends on one of them. Turborepo resolves "depends on" from the workspace's actual package.json dependency declarations, not from guesswork. For this PR, that resolves to {ui, web} — ui because it changed directly, web because it declares ui as a dependency — while api is excluded entirely, because nothing in its dependency graph changed. api's job doesn't run slowly, doesn't run cached — it doesn't run at all.
Three packages share a repo: ui, api, and web. web depends on ui — api stands alone.
fetch-depth: 0 isn't optional here. The default shallow checkout (fetch-depth: 1) gives the runner no history to diff against origin/main, so Turborepo or Nx can't compute what changed at all — affected-detection silently falls back to either "everything" or "nothing," depending on the tool, neither of which is what you want.HEAD^ instead of the actual target branch (origin/main) gives wrong answers the moment someone force-pushes, rebases, or squashes — the comparison needs to be against the branch this PR is actually merging into.This is the same "don't redo unchanged work" idea from the dependency-caching lesson, keyed differently. There, the unit of reuse is the whole install, invalidated by a lockfile hash. Here, the unit of reuse is a single package's build-and-test cycle, invalidated by that package's own files — or one of its dependencies' — actually changing. Same shape of problem, one layer more granular.