Como 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:
Push/PR → GitHub Actions → Lint → Testes → Build → Vercel DeployO 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:
npx create-next-app@latest meu-app --typescript --tailwind --app --src-dir
cd meu-appInstale as dependências de qualidade que o pipeline vai usar:
npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/jest-domConfigurando o Vitest
Crie o arquivo vitest.config.ts na raiz:
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:
import '@testing-library/jest-dom/vitest'Primeiro teste de exemplo
Crie src/components/Counter.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:
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:
{
"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:
npm run lint
npm run test
npm run buildTudo verde? Próximo passo.
Configurando a Vercel CLI
Instale a Vercel CLI e conecte o projeto:
npm install -D vercel
npx vercel linkO 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:
{
"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ê criouVERCEL_ORG_ID: o orgId do project.jsonVERCEL_PROJECT_ID: o projectId do project.json
O workflow completo do GitHub Actions
Crie .github/workflows/ci-cd.yml:
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
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: trueSe 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:
- ESLint: captura problemas de código
- Vitest: garante que nada quebrou
- TypeScript:
tsc --noEmitverifica 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:
vercel pullbaixa as configurações de ambiente (preview)vercel buildcompila localmente no runner do Actionsvercel deploy --prebuiltenvia o build pronto, sem rebuildar na Vercel- 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:
- 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:
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:
- 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:
- Vá em Settings → Branches → Add rule
- Branch:
main - Marque Require status checks to pass before merging
- Selecione os checks
Lint & Tests(oulintetestse separou) - 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:
npm install -D @playwright/test
npx playwright installAdicione um job ao workflow:
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: 7O 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:
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:
- 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.

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.


