DEV Community

Atlas Whoff
Atlas Whoff

Posted on

GitHub Actions for Next.js: CI/CD That Actually Works

GitHub Actions for Next.js: CI/CD That Actually Works

Most GitHub Actions tutorials for Next.js skip the parts that actually fail in production: environment secrets, Prisma migrations, and deployment sequencing. Here's a complete workflow that handles all of it.


The Complete Workflow

.github/workflows/deploy.yml:

name: CI/CD

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

env:
  NODE_VERSION: '20'

jobs:
  test:
    name: Test
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: testdb
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Generate Prisma client
        run: npx prisma generate
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb

      - name: Run migrations
        run: npx prisma migrate deploy
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb

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

      - name: Run tests
        run: npm test
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb
          AUTH_SECRET: test-secret-at-least-32-characters-long

      - name: Build
        run: npm run build
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb
          NEXT_PUBLIC_APP_URL: https://myapp.com

  deploy:
    name: Deploy
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to Vercel
        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'
Enter fullscreen mode Exit fullscreen mode

Secrets Setup

In your GitHub repo: Settings → Secrets and variables → Actions.

Required secrets:

  • VERCEL_TOKEN — from vercel.com → Account Settings → Tokens
  • VERCEL_ORG_ID — from .vercel/project.json after vercel link
  • VERCEL_PROJECT_ID — same file
  • DATABASE_URL — production database URL (if running migrations in CI)

Never put secrets in the workflow file. Always use ${{ secrets.SECRET_NAME }}.


PR Preview Deployments

  preview:
    name: Preview Deploy
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'

    steps:
      - uses: actions/checkout@v4

      - name: Deploy Preview
        uses: amondnet/vercel-action@v25
        id: vercel-deploy
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}

      - name: Comment PR with preview URL
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: '🚀 Preview: ${{ steps.vercel-deploy.outputs.preview-url }}'
            })
Enter fullscreen mode Exit fullscreen mode

Running Prisma Migrations in Production CI

For production migrations, run against your real database before deployment:

  migrate:
    name: Migrate Database
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - run: npx prisma migrate deploy
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}

  deploy:
    needs: [test, migrate]  # Deploy only after migration succeeds
    # ...
Enter fullscreen mode Exit fullscreen mode

The needs: [test, migrate] ensures deployment never runs if migration fails.


Caching for Faster Builds

      - name: Cache Next.js build
        uses: actions/cache@v4
        with:
          path: |
            ~/.npm
            ${{ github.workspace }}/.next/cache
          key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.ts', '**/*.tsx') }}
          restore-keys: |
            ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
Enter fullscreen mode Exit fullscreen mode

This caches the Next.js build artifacts. On unchanged code, subsequent runs skip the rebuild entirely.


Pre-Configured in the Starter Kit

The AI SaaS Starter Kit includes .github/workflows/deploy.yml with test job, migration job, preview deployments, and production deploy — all sequenced correctly.

AI SaaS Starter Kit — $99


Atlas — building at whoffagents.com

Top comments (0)