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 automatically detects changed packages since the last commit and only runs necessary 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 - 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 use git-based filtering
- Test with small change to verify selective builds
Implementation hints:
--filterselects specific packages...includes dependents (packages that depend on this one)^...includes dependencies (packages this one depends on)[main]compares against main branch for changed packages
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/ui...Output:
• 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 only packages changed since main branch
turbo build --filter=[main]If you changed packages/ui:
• Packages in scope: @geniusgarage/ui, @geniusgarage/web, @geniusgarage/snippet-manager
• Running build in 3 packages (changed: ui, dependents: web, app)
Turborepo automatically includes dependents!
How it works
# What changed?
git diff main...HEAD --name-only
packages/ui/src/button.tsx
# 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: node_modules/.cache/turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- name: Install dependencies
run: pnpm install
- name: Run build, lint, and test (changed packages only)
run: turbo build lint test --filter=[origin/main]Key changes:
fetch-depth: 0- Fetch full git history (needed for comparison)--filter=[origin/main]- Compare against remote main branch
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:
--filter=[main]CI optimization strategy
Branch builds (PRs):
# Only changed packages
run: turbo build lint test --filter=[origin/main]Main branch builds:
# Full build (no filter)
run: turbo build lint testConditional example:
- name: Run tasks
run: |
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
turbo build lint test
else
turbo build lint test --filter=[origin/main]
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=[HEAD^]and saw only changed packages - Updated CI with fetch-depth: 0
- Updated CI with --filter=[origin/main]
- 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 Vercel remote cache so your team shares build artifacts across machines, enabling instant cache hits even on fresh clones.
Was this helpful?