Vercel’s default behavior is “deploy on every push.” Connect the repo, and every commit to every branch triggers a build. Fast feedback, zero config — great for a single-app project.
In a monorepo with multiple deploy targets (a dashboard and a marketing site, say), a few things start to grate:
- A red CI run still ships. Vercel deploys concurrently with your test suite, not after it. By the time the tests fail, production has already rebuilt.
- Every push deploys every project. A typo fix in the dashboard rebuilds the landing page too, burning minutes and build credits on a no-op deploy.
- Preview deploys bypass signal. A preview URL is useful for review, but when the underlying tests haven’t run yet, you’re reviewing code that hasn’t been vetted.
Our setup
Two pieces make this work.
First, a disable switch in each Vercel project’s vercel.json:
{
"git": { "deploymentEnabled": false }
}
This keeps the Git integration connected (env var sync, vercel pull auth) but stops the automatic deploy on push. Every future deploy now has to come from somewhere else.
Second, a Deploy workflow in GitHub Actions that fires via workflow_run after the CI workflow completes with success. It runs vercel pull → vercel build → vercel deploy --prebuilt using the Vercel CLI. That’s the “somewhere else.”
Per-project change detection
The Deploy workflow includes a per-project check: it calls a small bash script that diffs the current commit against the previous deploy’s SHA and checks whether any files under that project’s watched paths changed. If nothing relevant changed, the job exits early.
if git diff --quiet "$BASE" HEAD -- "${PATHS[@]}"; then
exit 0 # skip
fi
Simple, idempotent, and no extra infrastructure.
Trade-offs
The big one: preview deploys on PRs go away. If you want review links without CI, you either revert to Vercel’s default or add a second workflow that deploys previews after CI. We opted for the former: no preview deploys until tests pass. It shifts friction earlier in the loop.
A secondary gotcha: if a CI run fails and later commits don’t touch the project’s watched paths, the pending changes never auto-deploy. Adding a workflow_dispatch trigger with a force=true input covers this — a manual button to deploy whatever’s on main, bypassing the change check. We hit this within the first week.
When to adopt this
If your repo has one app, Vercel’s defaults are fine. Reach for CI-gated deploys once you have:
- Multiple deploy targets that pollute each other’s deploys
- Tests that matter enough to block production on
- A build budget you’re watching
Each of these raises the cost of “deploy first, ask questions later.”