TL;DR
For a Next.js project on Node 22, Next 15.5.18, and build via scripts/build.cjs, we recommend one GitHub Actions pipeline: every PR = quality (lint + build), merge to main = deploy to Vercel. This repository does not yet have a .github/workflows directory — this article describes the target state to add now instead of relying on manual "works on my machine." Keep secrets (VERCEL_TOKEN, DATABASE_URL, API keys) only in GitHub Secrets.
Who this is for
- Teams of 1–5 developers on Next.js + Vercel
- Projects with Prisma (
postinstall:prisma generate, migrate in build script) - Companies wanting auditable deployment history and preview URL on every PR
- Monorepos or multiple apps in one repo (
paths:section)
Keywords (SEO)
ci cd nextjs github actions, nextjs pipeline 2026, vercel automatic deployment, github actions build nextjs, branch protection main, vercel rollback
Starting point: honest repo state
In many production projects the pipeline "exists in the team's heads," not in the repo. Here:
package.jsonrequires Node 22.x (engines.node),- Next.js 15.5.18, scripts:
build→node scripts/build.cjs,lint→eslint,postinstall→prisma generate, - No
.github/workflows/*.ymlfiles — CI must be added deliberately.
That is not a flaw — it is a normal stage. The flaw appears when laptop deploy skips the same steps as PR. The goal is one artifact: commit passed lint and build in Actions → same commit lands on Vercel.
What scripts/build.cjs does (and why CI must use it)
Plain next build in CI often is not enough when:
- Prisma — script loads
.env/.env.local(not in repo in Actions), runsprisma generate, and withDATABASE_URLalsoprisma migrate deploy, thennext build. - Locally the developer has a database; CI build may pass without migrations if you do not set the secret — but generate is still needed after
npm ci(also supported bypostinstall).
Pipeline conclusion: build job must call npm run build (not raw next build), with environment variables in GitHub Secrets / Environments. For PRs without a database you may use shadow DB URL or skip migrations — per team policy; production must have full secrets in production environment.
DATABASE_URL strategies in CI (Prisma)
| Strategy | When | Pros / cons |
|---|---|---|
| Same URL as staging | Small team, one test DB | Fast; risk of migration collision on parallel PRs |
| Separate DB per PR (Neon, Supabase branch) | Larger team | Isolation; higher cost / setup |
No DATABASE_URL on PR |
Static front only | Build may skip migrate deploy (log in build.cjs); schema must be current from repo |
Full URL only on main |
Compromise | PR: lint + tsc + build without migrate; prod: full pipeline |
In a DevStudio-style build, without DATABASE_URL the script logs a message and skips prisma migrate deploy, but still runs prisma generate — aligned with postinstall and prevents "Client not generated" errors.
Recommended split: PR vs main
| Step | Pull request | Push to main |
|---|---|---|
npm ci |
yes | yes |
npm run lint (ESLint) |
yes | yes |
npx tsc --noEmit |
yes (if TypeScript in project) | yes |
npm run build |
yes | yes |
| Unit tests | yes when added | yes |
| Vercel preview deploy | yes (Git integration or Actions) | — |
| Vercel production deploy | — | yes |
| Lighthouse CI | optional | optional (e.g. cron) |
Rule: merge nothing to main that did not pass the same build as production.
Full workflow example (.github/workflows/ci.yml)
The file below is ready to copy and adapt. Uses Node 22, npm cache, lint, typecheck, and build matching this repo.
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
quality:
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js 22
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: ESLint
run: npm run lint
- name: TypeScript (no emit)
run: npx tsc --noEmit
- name: Build (Prisma + Next via scripts/build.cjs)
run: npm run build
env:
# Required for full build — set in GitHub Secrets / Environment
DATABASE_URL: ${{ secrets.DATABASE_URL }}
NEXT_PUBLIC_SITE_URL: ${{ vars.NEXT_PUBLIC_SITE_URL || 'https://example.com' }}
# Add remaining NEXT_PUBLIC_* and API keys as in Vercel Production
deploy-preview:
if: github.event_name == 'pull_request'
needs: quality
runs-on: ubuntu-latest
environment: preview
steps:
- uses: actions/checkout@v4
- uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
github-token: ${{ secrets.GITHUB_TOKEN }}
scope: ${{ secrets.VERCEL_ORG_ID }}
deploy-production:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: quality
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
github-token: ${{ secrets.GITHUB_TOKEN }}
scope: ${{ secrets.VERCEL_ORG_ID }}Simpler alternative: enable Vercel Git Integration — Vercel builds preview on PR, Actions quality job stays the merge gate. Many teams use both: Actions = lint/tsc/build, Vercel = hosting + URL.
Secrets and variables — where to put what
| Name | Where | Notes |
|---|---|---|
VERCEL_TOKEN |
GitHub Secret | From Vercel Account Settings |
VERCEL_ORG_ID, VERCEL_PROJECT_ID |
GitHub Secret | From .vercel/project.json after vercel link |
DATABASE_URL |
Secret + Vercel Env | Same value as production for migrate deploy |
OPENAI_API_KEY, Stripe, etc. |
Secret / Vercel | Never in repo or Action logs |
NEXT_PUBLIC_* |
Variables (non-secret) or Vercel | Must be identical in PR build and prod or "works on my machine" returns |
In repo settings: Settings → Secrets and variables → Actions. For production use Environment production with optional Required reviewers (1–2 people).
Branch protection on main
Minimum worth enabling from day one:
- Require pull request before merging
- Require status check quality (job name from workflow)
- Require branch to be up to date
- Do not allow bypassing (even admin, if the team has discipline)
- Optional: require signed commits
Then nobody merges a build that did not pass npm run lint and npm run build on Node 22.
Preview URL on every PR
Vercel Git Integration (recommended to start):
- automatic URL like
project-git-branch-xyz.vercel.app, - bot comment on PR with link,
- QA and client verify before merge.
GitHub Actions + vercel-action (as in yaml above):
- full control, same token as prod deploy,
- useful when you want one pipeline in YAML instead of Vercel panel.
In both cases ensure NEXT_PUBLIC_* on preview is not empty — otherwise PR build "passes" but preview UI differs from production.
Env sync: in Vercel copy variable set from Production to Preview (or vercel env pull). In GitHub add the same NEXT_PUBLIC_* as Variables (visible in logs, not secret) — then quality builds the same code as preview. Keep secrets (DATABASE_URL, STRIPE_SECRET_KEY) only in Secrets.
Monorepo: do not build everything on every PR
When one repo has e.g. apps/web and packages/ui, add separate workflows with path filters:
on:
pull_request:
paths:
- 'apps/web/**'
- 'packages/ui/**'
- 'package-lock.json'You can use paths-ignore for docs (docs/**, content/blog/**) to skip an 8-minute build for a typo in an article — unless the blog is part of the same Next.js package (in a marketing monolith you often still build everything).
Rollback on Vercel in ~60 seconds
Prod deploy on Vercel is a new deployment; previous builds remain.
Emergency procedure:
- Vercel Dashboard → Deployments
- Find last working deployment (previous commit)
- ⋯ → Promote to Production
Often under one minute. Pipeline does not replace rollback — it ensures the promoted build was once green on PR. Avoid laptop deploy "quick fix" beside CI — then panel rollback may restore a different version than you think.
On self-hosted (Docker): image tag = commit SHA; rollback = previous tag in orchestrator. Same rule: one artifact from CI.
Security in CI/CD
permissions: contents: readby default (as in example) — minimalGITHUB_TOKENrights- Separate environment
productionwith approval - Dependabot /
npm auditin a weekly workflow - Never
echosecrets or full.envin logs — Actions maskssecrets.*, but custom scripts must stay clean - Rotate
VERCEL_TOKENand API keys quarterly or when a member leaves
Most common mistakes (and fixes)
| Mistake | Effect | Fix |
|---|---|---|
PR build without NEXT_PUBLIC_* |
Different bundle than prod | Variables in GitHub + Vercel Preview |
next build instead of npm run build |
Missing Prisma migrate/generate | Always repo script |
Node 20 in Actions, 22 in engines |
Random native module errors | node-version: '22' |
| No npm cache | 8 min instead of 3 | cache: 'npm' in setup-node |
| Laptop deploy + CI | Two sources of truth | Only merge → main → deploy |
| No branch protection | Broken prod Friday evening | Required quality check |
FAQ
Does GitHub Actions replace Vercel?
No. Actions = quality control and optional CLI deploy; Vercel = hosting, CDN, preview, rollback. Most often: Actions for lint/build, Vercel for running the app.
Must I run prisma migrate deploy in CI?
In this project scripts/build.cjs does it when DATABASE_URL is set. On PR you may use a test database or skip migrations — but ensure Prisma schema is consistent (generate is still required). On main/prod DATABASE_URL must be complete.
What if there are no tests?
Start with lint + tsc + build. Add tests when you have critical paths (payments, contract APIs). Pipeline still delivers ~80% of value.
Monorepo with three apps?
Three workflows with paths: or one workflow with strategy.matrix — not one global build without filters.
How to sync Node version with Vercel?
In Vercel Project Settings set Node.js 22.x, matching engines in package.json.
Troubleshooting: red build in Actions
prisma generatefailed — check Node (22), whethernpm cifinished, schema in repo valid.migrate deployfailed — DB URL unreachable from GitHub (IP firewall), bad migration, conflict with manual DB changes.next buildOOM — larger runner (rare) or drop heavy analysis; check heavy library imports.- Lint fail on PR — run
npm run lintlocally on Node 22; do not merge with--no-verify. - Preview works, prod does not — compare Vercel Production vs Preview Environment Variables; often one missing
NEXT_PUBLIC_*.
Action logs show the exact step — treat them as the only audit before touching production.
One-day rollout plan
- Add
.github/workflows/ci.yml(fragment above). - Configure Secrets + Variables (DATABASE_URL, Vercel, NEXT_PUBLIC_*).
- Enable branch protection on
main. - Connect repo to Vercel (if not yet).
- Open a test PR — verify green
quality+ preview URL. - Merge — verify production deployment and save rollback procedure in team README.
Summary
CI/CD for Next.js 15 on Node 22 is not magic — it is a repeatable set: npm ci → lint → typecheck → npm run build (Prisma + Next) on every PR, deploy to Vercel after merge, secrets outside repo, preview for acceptance, rollback via Promote in the panel. No workflow in the repository is a signal to add it now — before a manual deploy costs more than one day of production repair.
Want a pipeline for your project?
- Contact us — we review
package.json, Vercel, and Prisma for your case - Web applications — Next.js, hosting, and DevOps in one package