Ir para o conteúdo
Nextjs

Como configurar CI/CD para Next.js na Vercel com GitHub Actions

Marcos Soares
Atualizado em 
10 minutos de leitura
Ilustracao 3D de pipeline de vidro translucido representando CI/CD para Next.js com Vercel e GitHub Actions
Ouça este artigo
0:00Como configurar CI/CD para Next.js na Vercel com GitHub Actions--:--

Conteúdo técnico toda semana

Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.

Sem spam. Cancele a qualquer momento com 1 clique.

Neste artigo

O problema que ninguém fala

A Vercel faz deploy automático a cada push. Parece mágico. Até quebrar produção numa sexta-feira.

O deploy automático sem pipeline de qualidade é uma bomba-relógio. Sem testes, sem lint, sem verificação de build, qualquer console.log esquecido ou import quebrado vai direto para seus usuários.

A solução? Colocar GitHub Actions como gatekeeper antes que a Vercel faça o deploy. Você ganha controle total sem perder a experiência de preview deploys.

Arquitetura do pipeline

Antes de escrever YAML, entenda o fluxo completo:

Text
Push/PR → GitHub Actions → Lint → Testes → Build → Vercel Deploy

O GitHub Actions valida a qualidade. A Vercel cuida da infraestrutura. Cada um faz o que faz melhor.

Duas estratégias possíveis

Estratégia 1: Vercel faz deploy automático, Actions roda checks em paralelo. Simples, mas o deploy pode ir ao ar antes dos testes passarem.

Estratégia 2: Desabilita o auto-deploy da Vercel. Actions controla tudo: testes passam primeiro, depois dispara o deploy via CLI. Controle total.

Vamos com a estratégia 2. É a que funciona em times sérios.

Preparando o projeto Next.js

Se você já tem um projeto, pule para a próxima seção. Caso contrário:

Bash
npx create-next-app@latest meu-app --typescript --tailwind --app --src-dir
cd meu-app

Instale as dependências de qualidade que o pipeline vai usar:

Bash
npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/jest-dom

Configurando o Vitest

Crie o arquivo vitest.config.ts na raiz:

TYPESCRIPT
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'
 
export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    globals: true,
    css: false,
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
})

Crie o setup file em src/test/setup.ts:

TYPESCRIPT
import '@testing-library/jest-dom/vitest'

Primeiro teste de exemplo

Crie src/components/Counter.tsx:

TSX
'use client'
 
import { useState } from 'react'
 
export function Counter() {
  const [count, setCount] = useState(0)
 
  return (
    <div>
      <p data-testid="count">Contagem: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>Incrementar</button>
    </div>
  )
}

E o teste em src/components/Counter.test.tsx:

TSX
import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect } from 'vitest'
import { Counter } from './Counter'
 
describe('Counter', () => {
  it('incrementa o valor ao clicar', () => {
    render(<Counter />)
 
    expect(screen.getByTestId('count')).toHaveTextContent('Contagem: 0')
 
    fireEvent.click(screen.getByText('Incrementar'))
 
    expect(screen.getByTestId('count')).toHaveTextContent('Contagem: 1')
  })
})

Atualize os scripts do package.json:

JSON
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "test": "vitest run",
    "test:watch": "vitest",
    "test:coverage": "vitest run --coverage"
  }
}

Valide localmente antes de seguir:

Bash
npm run lint
npm run test
npm run build

Tudo verde? Próximo passo.

Configurando a Vercel CLI

Instale a Vercel CLI e conecte o projeto:

Bash
npm install -D vercel
npx vercel link

O comando vercel link vai pedir para conectar a um projeto existente ou criar um novo. Siga o wizard. Ele gera uma pasta .vercel com o project.json.

Desabilitando o auto-deploy

Esse passo é crítico. Crie o arquivo vercel.json na raiz:

JSON
{
  "github": {
    "enabled": false
  }
}

Isso diz à Vercel: "não faça deploy automático no push". O GitHub Actions assume o controle.

Tokens e secrets

Vá em vercel.com/account/tokens e crie um token. Depois, pegue o Org ID e Project ID do arquivo .vercel/project.json.

No GitHub, vá em Settings → Secrets and variables → Actions e adicione:

  • VERCEL_TOKEN: o token que você criou
  • VERCEL_ORG_ID: o orgId do project.json
  • VERCEL_PROJECT_ID: o projectId do project.json

O workflow completo do GitHub Actions

Crie .github/workflows/ci-cd.yml:

YAML
name: CI/CD Pipeline
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
 
env:
  VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
  VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
 
jobs:
  quality:
    name: Lint & Tests
    runs-on: ubuntu-latest
 
    steps:
      - name: Checkout
        uses: actions/checkout@v4
 
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
 
      - name: Install dependencies
        run: npm ci
 
      - name: Run ESLint
        run: npm run lint
 
      - name: Run tests
        run: npm run test
 
      - name: Check TypeScript
        run: npx tsc --noEmit
 
  deploy-preview:
    name: Deploy Preview
    needs: quality
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
 
    steps:
      - name: Checkout
        uses: actions/checkout@v4
 
      - name: Setup 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 -g vercel
 
      - name: Pull Vercel environment
        run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
 
      - name: Build project
        run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
 
      - name: Deploy to preview
        id: deploy
        run: |
          URL=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})
          echo "url=$URL" >> $GITHUB_OUTPUT
 
      - 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 deploy pronto!**\n\n${process.env.DEPLOY_URL}`
            })
        env:
          DEPLOY_URL: ${{ steps.deploy.outputs.url }}
 
  deploy-production:
    name: Deploy Production
    needs: quality
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
 
    steps:
      - name: Checkout
        uses: actions/checkout@v4
 
      - name: Setup 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 -g vercel
 
      - name: Pull Vercel environment
        run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
 
      - name: Build project
        run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
 
      - name: Deploy to production
        run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}

Vamos dissecar cada decisão.

Anatomia do workflow

O bloco concurrency

YAML
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Se você fizer 3 pushes rápidos, apenas o último roda. Os anteriores são cancelados. Isso economiza minutos de Actions e evita deploys desnecessários.

Job quality: o gatekeeper

Esse job roda em todo push e todo PR. Três verificações:

  1. ESLint: captura problemas de código
  2. Vitest: garante que nada quebrou
  3. TypeScript: tsc --noEmit verifica tipos sem gerar output

Se qualquer um falhar, os jobs de deploy nem iniciam. O needs: quality garante isso.

Job deploy-preview: feedback por PR

Roda apenas em pull requests. O fluxo:

  1. vercel pull baixa as configurações de ambiente (preview)
  2. vercel build compila localmente no runner do Actions
  3. vercel deploy --prebuilt envia o build pronto, sem rebuildar na Vercel
  4. Comenta na PR com a URL de preview

O --prebuilt é essencial. Sem ele, a Vercel rebuilda tudo do zero, desperdiçando o build que você já fez.

Job deploy-production: só na main

Roda apenas quando um push chega na main. Mesmo fluxo, mas com flags --prod.

A separação é clara: PR gera preview, merge gera produção.

Otimizando com cache avançado

O cache de npm no actions/setup-node já ajuda. Mas podemos ir além.

Cache do Next.js build

Adicione este step antes do build nos jobs de deploy:

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

Isso cacheia o .next/cache entre runs. Builds incrementais ficam 40-60% mais rápidos em projetos médios.

Paralelizando lint e testes

Para projetos grandes, separe o job quality em dois jobs paralelos:

YAML
  lint:
    name: Lint & 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: npm run lint
      - run: npx tsc --noEmit
 
  test:
    name: 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 run test
 
  deploy-preview:
    needs: [lint, test]
    # ...

Lint e testes rodam ao mesmo tempo. O deploy espera ambos.

Variáveis de ambiente seguras

Projetos reais têm env vars. Adicione-as como secrets no GitHub e passe para o build:

YAML
      - name: Build project
        run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
          NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}

Variáveis NEXT_PUBLIC_* são embutidas no bundle do cliente durante o build. As demais ficam apenas no servidor.

Dica importante: configure as mesmas variáveis no painel da Vercel (Settings → Environment Variables). O vercel pull baixa as variáveis de lá, mas secrets do GitHub dão uma camada extra de controle.

Adicionando proteção de branch

O pipeline só funciona de verdade com branch protection. No GitHub:

  1. Vá em Settings → Branches → Add rule
  2. Branch: main
  3. Marque Require status checks to pass before merging
  4. Selecione os checks Lint & Tests (ou lint e test se separou)
  5. Marque Require branches to be up to date

Agora é impossível mergear na main sem o pipeline verde. Ninguém bypassa. Nem você.

Workflow de E2E com Playwright (bônus)

Para projetos críticos, adicione testes end-to-end:

Bash
npm install -D @playwright/test
npx playwright install

Adicione um job ao workflow:

YAML
  e2e:
    name: E2E Tests
    needs: quality
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
 
    steps:
      - uses: actions/checkout@v4
 
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
 
      - run: npm ci
 
      - name: Install Playwright browsers
        run: npx playwright install --with-deps chromium
 
      - name: Build Next.js
        run: npm run build
 
      - name: Run E2E tests
        run: npx playwright test
        env:
          CI: true
 
      - name: Upload test report
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 7

O upload-artifact salva o relatório HTML quando testes falham. Você consegue debugar visualmente direto no GitHub.

Configure o deploy-preview para depender do E2E:

YAML
  deploy-preview:
    needs: [quality, e2e]

Preview só sobe se tudo passar. Sem exceções.

Monitorando o pipeline

Adicione um step de notificação no final do deploy de produção:

YAML
      - name: Notify Slack
        if: success()
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {
              "text": "✅ Deploy em produção concluído!\nCommit: ${{ github.sha }}\nAutor: ${{ github.actor }}"
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Times precisam de visibilidade. Um bot no Slack dizendo "deploy feito" evita aquele "já subiu?" no chat.

Troubleshooting comum

Erro: vercel pull falha com 403. Token expirado ou sem permissão. Gere um novo token com scope do projeto.

Build passa local, falha no Actions. Geralmente é env var faltando. Compare vercel env ls com seus secrets no GitHub.

Cache não funciona. Verifique se o package-lock.json está commitado. Sem ele, a key do cache muda a cada run.

Deploy demora demais. Use --prebuilt. Sem essa flag, a Vercel ignora seu build e recompila tudo.

Minha Opinião Sincera

Esse setup com Vercel + GitHub Actions é, na minha experiência, o melhor custo-benefício para projetos Next.js de pequeno a médio porte. Mas não é perfeito.

A Vercel cobra caro quando escala. O plano Pro é razoável, mas se seu time cresce e o uso de bandwidth e serverless functions explode, a conta assusta. Para projetos grandes, considere self-host com Docker + Coolify ou SST na AWS. Dá mais trabalho, mas o custo fica previsível.

GitHub Actions tem um problema de DX. Debugar workflows YAML é sofrido. Não tem como rodar local de verdade (o act tenta, mas é limitado). Comparado com o Dagger ou Earthly, a experiência é primitiva. Mas a integração nativa com GitHub compensa.

Alternativas que testei e descartei: GitLab CI tem pipelines melhores, mas o ecossistema é mais fechado. CircleCI é excelente, mas adicionar mais um serviço ao stack sem necessidade real é over-engineering. O Jenkins nem merece comentário em 2025.

O que eu faria diferente em um projeto enterprise? Adicionaria SAST com Semgrep, scan de dependências com npm audit no pipeline, e um approval gate manual antes do deploy de produção. O workflow que mostrei aqui é o alicerce. Adapte para sua realidade.

No fim, o que importa é simples: nenhum código chega em produção sem passar por lint, testes e build. Se seu projeto não tem isso hoje, pare tudo e configure. Leva uma hora. Economiza semanas de dor.

Marcos Soares

Escrito por

Marcos Soares

Fullstack Developer · CEO da Agência Poti

Fullstack Developer e CEO da Agência Poti. Mais de 20 anos construindo arquiteturas cloud-native com React, Next.js e sistemas distribuídos. Parceiro comercial do estúdio iellou design. Fundador do Vivo de Código.

Comentários

Participe da discussão

Seja o primeiro a comentar!

Continue Aprofundando

Guias de integração relacionados

Conteúdo técnico toda semana

Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.

Sem spam. Cancele a qualquer momento com 1 clique.