TL;DR
Vercel’s Skipping unaffected projects feature appears to compare the pushed commit against its immediate parent (HEAD^..HEAD) rather than against the merge-base with the PR’s target branch. As a result, real source changes in a workspace are silently considered “not affected” — and the preview is canceled — in two common situations:
-
A multi-commit push where the workspace change is not in the last commit.
-
A PR branch that merges
main(or the target branch) back into itself, where the merge commit’s first-parent diff is empty for the workspace.
Setup
-
Monorepo with two front-end apps under
apps/app-aandapps/app-b, each set up as a separate Vercel project (Root Directory set, Bun workspaces, validturbo.json, lockfile-aware skipping enabled). -
“Skip deployment” toggle: enabled (the default).
-
The PR’s diff vs
origin/mainclearly contains source-code changes (.tsxfiles,i18n/*.json, etc.) inside the workspace. -
The PR comment shows: “Build Canceled — this project was not affected.”
Repro 1 — Multi-commit push
A (main)
└── B refactor backend
└── C remove a front-end component <- changes apps/app-a/src/\*\*
└── D more backend tests
└── E more backend tests <- HEAD pushed
Single push of B..E to a brand-new PR branch. Vercel deploys only E. The diff D..E doesn’t touch apps/app-a/, so app-a is “not affected” and skipped — even though C clearly modified its source files.
Repro 2 — Merge commit from main
A (main) ───┐
│
B feat ────┤
C feat ────┤ <- apps/app-a/src/\*\* changes here
D feat ────┤
└── M "Merge branch 'main' into feat/..." <- HEAD
git diff M^1..M (first parent = D) is empty for apps/app-a/, so the workspace is reported “not affected.” The merge commit hides every change behind it. The real diff (origin/main...M or M^2..M) does show them.
Expected behavior
When the build is triggered by a PR (and the target branch is known), the affected-projects detection should compare against the merge-base with the PR’s target branch, not against the pushed commit’s parent. This matches how git diff origin/main...HEAD works on PRs and how a reviewer would reason about “what does this PR change.”
For pushes that aren’t tied to a PR, comparing against the last successful deployment SHA on the same project + branch (rather than HEAD^) would already cover Repro 1.
Workarounds we tried
-
vercel.json#ignoreCommandwithnpx turbo-ignore <name> --fallback=origin/main— doesn’t help, because Skip-unaffected runs before the Ignored Build Step, so the Ignored Build Step never executes. -
Bumping
versionin the workspace’spackage.json— doesn’t help, presumably because the feature filterspackage.jsonchanges that don’t impact dependencies. -
Pushing a real source-file change in a single trailing commit — works, but is exactly the friction the feature is supposed to remove.
-
Disabling the “Skip deployment” toggle and relying solely on
turbo-ignore --fallback=origin/main— works, but at the cost of build slots.
Suggested fix
Use the PR’s merge-base when the deployment is triggered from a PR context (which Vercel already knows via the GitHub integration). For non-PR pushes, fall back to “last successful deployment SHA for this project on this branch” before falling back to HEAD^.
Happy to share concrete commit SHAs privately if it helps reproduce.