CI/CD for Next.js with GitHub Actions: Testing, Preview, and Production Deployment
Learn how to build a complete CI/CD pipeline for Next.js using GitHub Actions in 2026. This guide covers workflow YAML, caching, Vitest testing, type checking, preview deploys with Vercel CLI, secrets management, branch protection, and production deployments.
CI/CD for Next.js with GitHub Actions: Testing, Preview, and Production Deployment
By Elhassane Mehdioui, Full Stack Web Developer
Shipping a Next.js application without a solid CI/CD pipeline is like driving without a seatbelt. You might be fine most of the time, but one bad merge can take down production and cost you hours of debugging. In 2026, GitHub Actions Next.js CI/CD has become the industry standard for automating the entire lifecycle from a pull request to a live production URL — and for good reason. It is free for public repositories, deeply integrated with GitHub, and flexible enough to handle any deployment target.
In this guide you will build a production-grade pipeline that runs on every push and pull request. By the end you will have automated Vitest tests, TypeScript type checking, preview deployments on every PR using the Vercel CLI, and a locked-down production deploy that fires only when code lands on main.
Why CI/CD Matters for Next.js Projects
Next.js applications grow fast. What starts as a handful of pages quickly becomes a complex app with API routes, middleware, server components, and a mix of client and server state. Without automation, the following problems appear:
- A developer merges broken TypeScript that only surfaces at runtime
- A staging environment drifts from production because deployments are manual
- Reviewers cannot see a live preview of the PR they are reviewing
- Secrets end up hard-coded because there is no secure injection mechanism
A well-designed Next.js automated deployment workflow eliminates all of these pain points. GitHub Actions sits inside the same platform where your code already lives, so the integration is zero-friction.
Repository Structure Assumptions
The examples below assume a standard Next.js project at the repository root with the following scripts in package.json:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"typecheck": "tsc --noEmit"
}
}
All workflow files live in .github/workflows/.
Step 1: The CI Workflow — Tests and Type Checking
Create .github/workflows/ci.yml. This workflow runs on every push to any branch and on every pull request targeting main.
# .github/workflows/ci.yml
name: CI
on:
push:
branches: ["**"]
pull_request:
branches: [main]
jobs:
ci:
name: Test & Type Check
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run TypeScript type check
run: npm run typecheck
- name: Run Vitest tests
run: npm run test
env:
CI: true
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
retention-days: 7
Caching node_modules the Right Way
The cache: "npm" option on actions/setup-node automatically caches the npm cache directory keyed by package-lock.json. This means a cache hit skips the network download of every package and reduces install time from 60-90 seconds down to under 10 seconds on most projects.
If you use pnpm or yarn, swap the cache value accordingly:
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
And add a pnpm setup step before it:
- uses: pnpm/action-setup@v3
with:
version: 9
Step 2: Preview Deployments on Pull Requests
Every PR should produce a live URL that reviewers can click. Vercel's CLI makes this straightforward. You do not need the Vercel GitHub integration — the CLI gives you more control from within Actions.
Create .github/workflows/preview.yml:
# .github/workflows/preview.yml
name: Preview Deploy
on:
pull_request:
branches: [main]
jobs:
deploy-preview:
name: Deploy Preview to Vercel
runs-on: ubuntu-latest
environment:
name: preview
url: ${{ steps.deploy.outputs.preview_url }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Pull Vercel environment (preview)
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
- name: Build project artifacts
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy to Vercel (preview)
id: deploy
run: |
DEPLOY_URL=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})
echo "preview_url=$DEPLOY_URL" >> $GITHUB_OUTPUT
- name: Comment PR with preview URL
uses: actions/github-script@v7
with:
script: |
const url = "${{ steps.deploy.outputs.preview_url }}";
const body = `### Preview Deployment\n\nYour changes are live at: [${url}](${url})`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body,
});
The environment block ties the deployment to a GitHub Environment named preview, which lets you set environment-specific secrets and require approvals if needed.
Step 3: Secrets Management
Never hard-code API keys, database URLs, or tokens. GitHub Secrets are encrypted at rest and injected as environment variables at runtime. They are never printed in logs.
Required Secrets for This Pipeline
Navigate to Settings > Secrets and variables > Actions in your repository and add:
| Secret Name | Description |
|---|---|
VERCEL_TOKEN | Personal access token from vercel.com/account/tokens |
VERCEL_ORG_ID | Found in .vercel/project.json after vercel link |
VERCEL_PROJECT_ID | Found in .vercel/project.json after vercel link |
DATABASE_URL | Your production database connection string |
NEXT_PUBLIC_API_URL | Any public-facing API base URL |
Reference secrets in your workflow like this:
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
Environment-Level Secrets
For production-only secrets, create a GitHub Environment named production and attach secrets there. This way the secrets are only available to jobs that explicitly target that environment, and you can add a required reviewer approval gate before any production deploy runs.
Step 4: Production Deployment on Merge to Main
Create .github/workflows/production.yml. This workflow runs only when a pull request is merged into main — not on every push.
# .github/workflows/production.yml
name: Production Deploy
on:
push:
branches: [main]
jobs:
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
environment:
name: production
url: https://yourdomain.com
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Pull Vercel environment (production)
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- name: Build project artifacts
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy to Vercel (production)
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
The --prod flag promotes the deployment to your production domain. Because this workflow is gated on a push to main, it only fires after a PR is reviewed and merged.
Step 5: Branch Protection Rules
Automating deployments is only safe if you also protect the main branch from direct pushes. Configure branch protection under Settings > Branches > Add rule:
- Branch name pattern:
main - Require a pull request before merging: enabled
- Require status checks to pass before merging: enabled
- Add
Test & Type Check(fromci.yml) as a required check
- Add
- Require branches to be up to date before merging: enabled
- Restrict who can push to matching branches: enabled (only repo admins or a bot account)
- Do not allow bypassing the above settings: enabled
With these rules in place, no code reaches production without passing your CI checks and receiving at least one approval.
Step 6: Putting It All Together
Here is the full lifecycle of a change in this pipeline:
- A developer opens a feature branch and pushes commits.
- The CI workflow runs automatically — Vitest tests execute, TypeScript is checked, and any failure blocks the PR.
- The Preview workflow deploys a live URL to Vercel and posts it as a comment on the PR.
- A reviewer checks the live preview, reviews the code, and approves.
- The developer merges the PR into
main. - The Production workflow fires, rebuilds the project with production environment variables, and promotes the deployment to the production domain.
The entire process is auditable. Every deployment is tied to a specific commit SHA, and every failed check is visible directly on the pull request.
Performance Tips for Faster Pipelines
Split test and typecheck into parallel jobs. Vitest and tsc do not depend on each other. Running them in parallel cuts wall-clock time roughly in half:
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: "npm" }
- run: npm ci
- run: npm run typecheck
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: "npm" }
- run: npm ci
- run: npm run test
Use Turborepo remote caching if this is a monorepo. Turbo can cache Next.js build outputs across CI runs, turning a 3-minute build into a 10-second cache restore.
Pin action versions to a full SHA in security-sensitive workflows to prevent supply chain attacks:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Common Mistakes to Avoid
- Using
npm installinstead ofnpm ci—npm ciis deterministic and faster in CI because it uses the lockfile exactly.npm installcan silently upgrade packages. - Storing secrets in workflow YAML — always use
${{ secrets.YOUR_SECRET }}. Never inline values even in non-sensitive-looking variables. - Skipping the
CI: trueenvironment variable — some tools (including older versions of Create React App and certain Vitest reporters) behave differently whenCIis not set and may not exit with a non-zero code on test failure. - Deploying before tests pass — use
needsto enforce job order if you combine CI and deployment in the same workflow file:
jobs:
ci:
...
deploy:
needs: ci
...
Conclusion
A GitHub Actions tutorial 2026 is not complete without acknowledging how much the ecosystem has matured. The combination of GitHub Actions, the Vercel CLI, and Vitest gives Next.js teams a pipeline that is fast, secure, and free to run on most projects.
The workflow described in this post covers every stage a professional team needs: automated testing, static analysis, ephemeral preview environments, secret injection, branch protection, and gated production releases. Each piece is independently useful, but together they create a safety net that lets your team ship with confidence.
Start with the CI workflow, add preview deploys once that is stable, then lock down main with branch protection and wire in production deploys. The whole setup takes less than an hour and pays for itself the first time it catches a broken build before it reaches your users.
Elhassane Mehdioui is a Full Stack Web Developer specialising in Next.js, TypeScript, and modern DevOps practices.