dev_tools
Back to guides

GitHub Actions CI/CD Pipeline

Set up a complete CI/CD pipeline that automatically lints, type-checks, tests, and builds your Next.js or Node.js app on every push and pull request — with optional deployment to Vercel.

GitHub Actions
CI/CD
DevOps
Automation
Next.js
1

Create the Workflows Directory

GitHub Actions reads workflow files from .github/workflows/. Create this directory in your project root.

mkdir -p .github/workflows
2

Create the CI Workflow

This workflow runs on every push and pull request to main. It uses a matrix strategy to test against multiple Node.js versions. Jobs run in parallel by default — build waits for test to pass first.

.github/workflows/ci.yml
name: CI

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

jobs:
  # ── Job 1: Lint + Type-Check ─────────────────────────────────────
  lint:
    name: Lint & Type-Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm          # cache node_modules between runs

      - name: Install dependencies
        run: npm ci           # ci is faster + reproducible vs npm install

      - name: Run ESLint
        run: npm run lint

      - name: Type-check
        run: npx tsc --noEmit

  # ── Job 2: Test ──────────────────────────────────────────────────
  test:
    name: Run Tests
    runs-on: ubuntu-latest
    needs: lint               # only run tests if lint passed
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test -- --ci --coverage
        env:
          CI: true

      - name: Upload coverage
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: coverage-report
          path: coverage/

  # ── Job 3: Build ─────────────────────────────────────────────────
  build:
    name: Build
    runs-on: ubuntu-latest
    needs: test               # only build if tests passed
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build
        env:
          NODE_ENV: production

      - name: Upload build artifact
        uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: .next/          # change to dist/ for non-Next projects
          retention-days: 1
3

Add a Separate Deploy Workflow

Create a separate workflow that only runs on merges to main. It downloads the build artifact from CI and deploys to Vercel. Using separate files keeps concerns cleanly separated.

.github/workflows/deploy.yml
name: Deploy to Vercel

on:
  push:
    branches: [main]

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    environment:
      name: production
      url: ${{ steps.deploy.outputs.preview-url }}
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Deploy to Vercel
        id: deploy
        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'
💡Add VERCEL_TOKEN, VERCEL_ORG_ID, and VERCEL_PROJECT_ID as repository secrets in GitHub → Settings → Secrets and variables → Actions.
4

Add Required npm Scripts

Make sure package.json has the scripts that the workflows call. lint and test must exit with a non-zero code on failure for the workflow to fail correctly.

package.json (scripts)
"scripts": {
  "dev":   "next dev",
  "build": "next build",
  "start": "next start",
  "lint":  "next lint",
  "test":  "jest"
}
5

Cache Dependencies for Faster Runs

The cache: npm option in actions/setup-node caches ~/.npm automatically. For even faster cache hits, add a cache-dependency-path to scope the cache to your exact lockfile.

.github/workflows/ci.yml (cache snippet)
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: npm
    cache-dependency-path: package-lock.json  # invalidates cache if lockfile changes
💡With caching, subsequent workflow runs typically complete in under 60 seconds instead of 3–4 minutes.
6

Add a Status Badge to README

Paste this markdown into your README.md to show the CI status at a glance. Replace owner and repo with your GitHub username and repository name.

README.md
![CI](https://github.com/owner/repo/actions/workflows/ci.yml/badge.svg)
![Deploy](https://github.com/owner/repo/actions/workflows/deploy.yml/badge.svg)
7

Commit and Push to Trigger the Pipeline

Once you push these files, GitHub automatically detects the workflows and starts the CI run.

git add .github && git commit -m 'ci: add GitHub Actions CI/CD pipeline' && git push
💡Go to the Actions tab on your GitHub repository to watch the jobs run in real time. Failed steps show the exact command output.

Found this guide helpful? Check out more tools and guides.

Browse All Guides