Integração Contínua (CI): Automatizando Testes com Github Actions
Pipeline de CI completo para Node.js: cache de dependências, jobs paralelos para lint/test/typecheck, services para banco de dados de teste, matrix strategy e Branch Protection Rules.
Integração Contínua (CI) é a prática de verificar automaticamente cada mudança de código antes de integrá-la à branch principal. O objetivo é detectar problemas o mais cedo possível — quando ainda é barato corrigir. Um bug encontrado em um PR leva 5 minutos para corrigir; o mesmo bug em produção pode levar horas e impactar usuários reais.
O GitHub Actions transformou CI em algo acessível: gratuito para repositórios públicos, 2.000 minutos/mês gratuitos para privados, e configurado diretamente no repositório via arquivos YAML. Neste artigo vamos montar um pipeline profissional com cache de dependências, jobs paralelos, banco de dados de teste via services, e proteção de branch.
Conceitos Fundamentais do GitHub Actions
- Workflow — arquivo YAML em
.github/workflows/. Define quando e o que executar. - Trigger (on) — evento que dispara o workflow:
push,pull_request,schedule,workflow_dispatch(manual) - Job — unidade de execução isolada. Cada job roda em uma VM separada (runner). Jobs são paralelos por padrão.
- Step — tarefa dentro de um job. Executados sequencialmente dentro do job.
- Action — passo reutilizável do marketplace (ex:
actions/checkout,actions/setup-node) - Runner — a VM onde o job roda.
ubuntu-latesté a opção mais comum e gratuita.
Pipeline Básico com Cache
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
# Cancela runs anteriores do mesmo PR (economiza minutos)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ci:
name: Lint, Typecheck e Testes
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configurar pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Configurar Node.js
uses: actions/setup-node@v4
with:
node-version: 22
# Cache do pnpm store — evita baixar dependências em cada run
cache: 'pnpm'
- name: Instalar dependências
# --frozen-lockfile: falha se o lockfile estiver desatualizado
# Garante que o CI usa exatamente as versões do pnpm-lock.yaml
run: pnpm install --frozen-lockfile
- name: Typecheck
run: pnpm typecheck
- name: Lint
run: pnpm lint
- name: Testes unitários
run: pnpm test --coverage
- name: Upload de cobertura para Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.infoJobs Paralelos: Reduzindo o Tempo do Pipeline
Quando lint, typecheck e testes são sequenciais, um falha na etapa anterior bloqueia as outras e você não sabe quais outros problemas existem. Com jobs paralelos, todos rodam simultaneamente e você vê todos os erros de uma vez:
jobs:
typecheck:
name: TypeScript
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck
lint:
name: ESLint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
- run: pnpm lint
test:
name: Jest
runs-on: ubuntu-latest
needs: [] # Não depende de nada — roda em paralelo com typecheck e lint
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
- run: pnpm test --coverage
# Job de build depende dos três anteriores
build:
name: Build
runs-on: ubuntu-latest
needs: [typecheck, lint, test] # Só roda se os três passarem
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
- run: pnpm buildServices: Banco de Dados Real nos Testes de Integração
Testes de integração que testam a camada de repositório precisam de um banco de dados real. O GitHub Actions suporta service containers — instâncias Docker que sobem junto com o job:
jobs:
integration:
name: Testes de Integração
runs-on: ubuntu-latest
# Containers de serviço que ficam disponíveis durante o job
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
# Aguarda o PostgreSQL estar pronto antes de prosseguir
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
env:
# Variáveis de ambiente para os testes
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
REDIS_URL: redis://localhost:6379
JWT_SECRET: test-secret-only-for-ci
NODE_ENV: test
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
- name: Rodar migrations
run: pnpm typeorm migration:run
- name: Testes de integração
run: pnpm test:integrationMatrix Strategy: Testando em Múltiplas Versões
Para pacotes npm ou projetos que precisam suportar múltiplas versões do Node.js, use strategy.matrix para rodar o job em paralelo com versões diferentes:
jobs:
test:
runs-on: ubuntu-latest
strategy:
# Não cancela todas as versões se uma falhar
fail-fast: false
matrix:
node-version: [20, 22, 23]
name: Node.js ${{ matrix.node-version }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm testSecrets e Variáveis de Ambiente
Nunca coloque chaves de API, senhas ou tokens diretamente no YAML. Use GitHub Secrets (Settings → Secrets and variables → Actions):
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy para VPS
env:
# Secrets são mascarados nos logs — nunca aparecem em texto puro
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
VPS_HOST: ${{ secrets.VPS_HOST }}
VPS_USER: ${{ secrets.VPS_USER }}
run: |
echo "$SSH_PRIVATE_KEY" > /tmp/key.pem
chmod 600 /tmp/key.pem
ssh -i /tmp/key.pem -o StrictHostKeyChecking=no $VPS_USER@$VPS_HOST \
'cd /app && git pull && pm2 restart all'
# Variáveis públicas (não secretas) ficam em vars.*
- name: Notificar Slack
env:
SLACK_CHANNEL: ${{ vars.SLACK_DEPLOY_CHANNEL }}
SLACK_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}Branch Protection Rules: Tornando o CI Obrigatório
O CI só é efetivo se você não puder fazer merge sem ele passar. Configure em Settings → Branches → Add rule:
- Branch name pattern:
main(oumain,develop) - Require status checks to pass before merging: habilite
- Status checks: selecione os jobs do seu CI (ex:
TypeScript,ESLint,Jest) - Require branches to be up to date before merging: habilite para evitar merges desatualizados
- Do not allow bypassing the above settings: habilite para que nem admins possam ignorar
Use concurrency no workflow para cancelar runs anteriores do mesmo PR automaticamente. Sem isso, abrir um PR e fazer 3 commits em sequência dispara 3 runs simultâneos — desperdiçando minutos de CI. Com cancel-in-progress: true, apenas o run mais recente continua.
Conclusão
Um pipeline de CI bem configurado muda a cultura de uma equipe: o medo de fazer merge some quando você sabe que qualquer regressão será detectada automaticamente em minutos. Jobs paralelos reduzem o tempo de feedback, services permitem testes realistas contra bancos reais, e Branch Protection Rules garantem que nenhum código quebrado chega ao main. O custo é apenas o tempo de escrever o YAML uma vez.