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 (Branchly branch) | Larger team | Schema isolation; aligned with DevStudio stack |
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
About the author
We build fast websites, web/mobile apps, AI chatbots and hosting setups — with a focus on SEO and conversion.
Recommended links
From theory to production — Branchly, our hosting stack and shipped work.
