GetLaunchpad
Back to blog
6 min read

GitHub Actions CI/CD for Next.js: type checking, linting, and tests

Set up a CI pipeline that catches TypeScript errors, ESLint violations, and failed tests before they reach production — with parallel jobs, dependency caching, branch protection rules, and Vercel integration.

Every Next.js SaaS should have a CI pipeline that runs type checks, linting, and tests before code reaches production. Without it, a broken build or failed type check makes it to Vercel — where you find out from a broken production site instead of a failed check.

Here's how to set up a practical GitHub Actions CI pipeline for a Next.js SaaS, with the jobs that actually matter.

What your pipeline should check

For a Next.js SaaS, run these in CI:

  1. TypeScript: npx tsc --noEmit
  2. ESLint: npm run lint
  3. Unit tests: npm test (if you have them)
  4. Build check: npm run build (optional but catches import errors)

Run type checking and linting first — they're fast (under 30 seconds). Only run the full build in CI if you have a reason (Vercel already runs it on deploy, so it's redundant unless you need artifacts).

Basic CI workflow

Create .github/workflows/ci.yml:

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  typecheck:
    name: TypeScript
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "npm"
      - run: npm ci
      - run: npx tsc --noEmit

  lint:
    name: ESLint
    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 lint

Each job runs independently and in parallel — TypeScript and ESLint check simultaneously. Both must pass before the PR can be merged.

Adding environment variables for CI

If your build or tests need environment variables (API keys for integration tests, or env vars required for the build), add them as GitHub repository secrets:

  1. GitHub repo → Settings → Secrets and variables → Actions
  2. Add each secret (STRIPE_SECRET_KEY, SUPABASE_SERVICE_ROLE_KEY, etc.)

Reference them in the workflow:

env:
  NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
  NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
  SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
  STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
  # Add all vars your build needs...

For type checking only (tsc --noEmit), you usually don't need real API keys — the types don't depend on runtime values.

Adding unit tests with Vitest

If you have unit tests, add a test job:

  test:
    name: Unit tests
    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 test

In package.json, configure the test script:

{
  "scripts": {
    "test": "vitest run"
  }
}

Use vitest run (not just vitest) in CI to run tests once and exit, rather than in watch mode.

Caching node_modules

The cache: "npm" option in setup-node caches the npm cache directory, which speeds up npm ci on repeat runs. Without caching, npm downloads all packages every time — which adds 30–60 seconds to every CI run.

GitHub Actions automatically invalidates the cache when package-lock.jsonchanges.

Branch protection rules

CI checks are only useful if you enforce them. Add branch protection to main:

  1. GitHub repo → Settings → Branches → Add rule
  2. Branch name pattern: main
  3. Enable: “Require status checks to pass before merging”
  4. Add your CI jobs as required checks: TypeScript, ESLint
  5. Enable: “Require branches to be up to date before merging”

With this configuration, PRs can't be merged until all CI jobs pass. Direct pushes to main also go through the checks.

Vercel integration

Vercel automatically creates preview deployments for each PR and deploys main to production on merge. This gives you:

You don't need a separate deployment job in GitHub Actions — Vercel's GitHub integration handles it. Just make sure your CI checks run before reviewers can approve merges.

Full workflow example

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  typecheck:
    name: TypeScript
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "npm"
      - run: npm ci
      - run: npx tsc --noEmit

  lint:
    name: ESLint
    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 lint

  test:
    name: Unit tests
    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 test

What GetLaunchpad adds

GetLaunchpad doesn't include a GitHub Actions workflow by default (Vercel handles deployment automatically), but the codebase is set up to pass CI from day one:tsconfig.json is strict, .eslintrc is configured, and all utilities are type-safe. Adding the workflow above requires only creating the .github/workflows/ci.yml file.

Share this article:Share on X

Ready to ship faster?

GetLaunchpad gives you everything covered in this guide — pre-configured, tested, and production-ready. Skip the setup and focus on your product.

Get the boilerplate →

More articles