Filtering and git-based filtering
Your CI builds everything every time. If you change one line in apps/web, CI rebuilds packages/ui, packages/utils, and apps/snippet-manager too - wasting time. You need to run tasks only for changed packages and their dependents.
Turborepo's --filter flag lets you scope tasks to specific packages. Combined with git-based filtering (--filter=...[main]), CI detects which packages changed, plus everything that depends on them, and only runs those tasks.
Outcome
Use --filter to run selective tasks and enable git-based filtering in CI for incremental builds.
Fast track
- Learn
--filtersyntax for targeting packages - Use
--filter=...[main]for changed packages and their dependents - Update CI to use git-based filtering
- See massive time savings on incremental changes
Hands-on exercise 7.2
Implement selective task execution with filtering.
Requirements:
- Run tasks for single package:
--filter @geniusgarage/web - Run tasks with dependencies:
--filter=@geniusgarage/web... - Use git-based filtering:
--filter=...[main] - Update GitHub Actions to run only affected packages with
--affected - Test with small change to verify selective builds
Implementation hints:
--filterselects specific packages...before the name (...pkg) includes dependents;...after the name (pkg...) includes dependencies^with...(pkg^...) leaves the target package itself out of the selection[main]selects only the packages changed since main. Put...in front (...[main]) to add the packages that depend on them
Filter syntax
Filter single package
# Build only apps/web
turbo build --filter @geniusgarage/webOutput:
• Packages in scope: @geniusgarage/web
• Running build in 1 package
@geniusgarage/web:build: cache miss, executing
Tasks: 1 successful, 1 total
Only apps/web builds!
Filter with dependencies
# Build apps/web and everything it depends on
turbo build --filter @geniusgarage/web...Output:
• Packages in scope: @geniusgarage/ui, @geniusgarage/web
• Running build in 2 packages
@geniusgarage/ui:build ✓
@geniusgarage/web:build ✓
Tasks: 3 successful, 3 total
Builds web + its dependencies (ui, config).
Filter dependents
# Build packages/ui and everything that depends on it
turbo build --filter ...@geniusgarage/uiOutput:
• Packages in scope: @geniusgarage/ui, @geniusgarage/web, @geniusgarage/snippet-manager
• Running build in 3 packages
Builds ui + apps that use it (web, app).
Git-based filtering
Filter changed packages
# Build packages changed since main
turbo build --filter=[main]If you changed packages/ui:
• Packages in scope: @geniusgarage/ui
That looks efficient, and it's a trap. The Button in packages/ui is what apps/web and apps/snippet-manager render, and neither of them got rebuilt. A bare [main] selects changed packages only. Ship this to CI, and a broken Button sails through every check.
Put ... in front to include the dependents:
# Build changed packages plus everything that depends on them
turbo build --filter=...[main]• Packages in scope: @geniusgarage/snippet-manager, @geniusgarage/ui, @geniusgarage/web
• Running build in 3 packages
That's the selection we want: ui changed, so the two apps that use it build too.
How it works
# What changed?
git diff main...HEAD --name-only
packages/ui/src/button.tsx
# With ...[main], Turbo includes:
# - packages/ui (changed)
# - apps/web (depends on ui)
# - apps/snippet-manager (depends on ui)Try it
1. Test filtering locally
Make a change to packages/ui:
// Add comment
export function Button() {
// Changed!Run with git filter:
git add .
git commit -m "test: change button"
turbo build --filter=...[HEAD^]Output:
• Packages in scope: @geniusgarage/ui, @geniusgarage/web, @geniusgarage/snippet-manager
Only ui + dependents build!
2. Update CI with git filtering
Update .github/workflows/ci.yml:
name: CI
on:
push:
branches:
- main
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for git filtering
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Cache Turborepo
uses: actions/cache@v3
with:
path: .turbo/cache
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- name: Install dependencies
run: pnpm install
- name: Run build, lint, and test (affected packages only)
run: pnpm turbo build lint test --affectedKey changes:
fetch-depth: 0- Fetch full git history (needed for comparison)--affected- Shorthand for--filter=...[main...HEAD]: changed packages plus their dependents. In GitHub Actions it reads the PR's base branch on its own, and on a shallow clone it falls back to running everything (--affectedreference)
3. Test incremental CI
Make a small change:
echo "# Update" >> apps/web/README.md
git add .
git commit -m "docs: update web readme"
git pushCI output:
• Packages in scope: @geniusgarage/web
• Running build in 1 package
• Skipped: @geniusgarage/snippet-manager, @geniusgarage/ui, etc.
Tasks: 1 successful, 1 total
Time: 23s (was 2m 45s!)
2m 45s → 23s on incremental change!
Filter patterns
Exact package:
--filter @geniusgarage/webPackage + dependencies:
--filter @geniusgarage/web...Package + dependents:
--filter ...@geniusgarage/uiMultiple packages:
--filter @geniusgarage/web --filter @geniusgarage/snippet-managerAll apps:
--filter "./apps/*"All packages:
--filter "./packages/*"Changed since main, plus dependents:
--filter=...[main]Affected packages (the CI shorthand):
--affectedCI optimization strategy
Branch builds (PRs):
# Only affected packages
run: pnpm turbo build lint test --affectedMain branch builds:
# Full build (no filter)
run: pnpm turbo build lint testConditional example:
- name: Run tasks
run: |
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
pnpm turbo build lint test
else
pnpm turbo build lint test --affected
fiCommit
git add .github/workflows/ci.yml
git commit -m "ci: add git-based filtering for incremental builds"Done-when
Verify filtering works:
- Ran
--filter @geniusgarage/weband saw only web build - Ran
--filter @geniusgarage/web...and saw web + dependencies - Ran
--filter ...@geniusgarage/uiand saw ui + dependents - Ran
--filter=[main], saw it skip the apps, then ran--filter=...[main]and saw them included - Updated CI with fetch-depth: 0
- Updated CI with
--affected - Tested incremental change and saw selective builds
- Understood ... suffix for dependencies and dependents
- Understood [branch] syntax for git-based filtering
What's Next
Filtering speeds up CI, but cache is still local to each machine. Next lesson: Remote Caching - you'll configure Edgenivo remote cache so your team shares build artifacts across machines, enabling instant cache hits even on fresh clones.
Was this helpful?