"Skipping unaffected projects" misses changes hidden behind merge commits and multi-commit pushes

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:

  1. A multi-commit push where the workspace change is not in the last commit.

  2. 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-a and apps/app-b, each set up as a separate Vercel project (Root Directory set, Bun workspaces, valid turbo.json, lockfile-aware skipping enabled).

  • “Skip deployment” toggle: enabled (the default).

  • The PR’s diff vs origin/main clearly contains source-code changes (.tsx files, 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#ignoreCommand with npx turbo-ignore <name> --fallback=origin/maindoesn’t help, because Skip-unaffected runs before the Ignored Build Step, so the Ignored Build Step never executes.

  • Bumping version in the workspace’s package.jsondoesn’t help, presumably because the feature filters package.json changes 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.