Seu pipeline de CI provavelmente está mentindo pra você. Ele passa verde, o merge acontece, e três dias depois alguém descobre que o deploy de sexta-feira quebrou o ambiente de staging porque o workflow nunca testou aquele branch de verdade. Isso é mais comum do que parece, e a raiz do problema quase sempre está na configuração do GitHub Actions, não no código.
Este guia cobre como montar um pipeline de CI/CD que realmente protege o seu main, sem os erros que fazem devs perderem tarde inteira debugando YAML.
Índice
- Por que a maioria dos pipelines falha antes do deploy
- Anatomia de um workflow do GitHub Actions
- Build e testes automatizados na prática
- Cache de dependências: o vilão silencioso da esteira lenta
- Deploy automatizado da main para produção
- Secrets e variáveis sem vazar credencial
- Erros comuns que travam workflows
- FAQ
- Próximos passos
Por que a maioria dos pipelines falha antes do deploy
Times configuram CI achando que "ter um arquivo .yml verde" é sinônimo de qualidade. Não é. Um workflow mal desenhado testa a versão errada do código, ignora falhas silenciosas em steps opcionais ou simplesmente nunca roda no cenário que importa — o merge para main.
O erro mais frequente é tratar o CI como checklist burocrático em vez de gate de qualidade. Se o pipeline não bloqueia merge quando os testes falham, ele é decorativo. Se ele não roda em pull requests, só serve para avisar depois que o estrago já foi feito.
⚠️ Atenção: um workflow que usa
continue-on-error: trueem um step de teste sem motivo explícito está, na prática, desligado. O pipeline passa verde mesmo com teste quebrado.
Anatomia de um workflow do GitHub Actions
Todo workflow vive em .github/workflows/*.yml e tem essa estrutura básica:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm ci
- run: npm test
Três peças importam mais do que parece à primeira vista. O gatilho (on), que define quando o workflow roda. O runner (runs-on), que é a máquina onde tudo executa. E os steps, que são sequenciais e param no primeiro erro — a menos que você diga o contrário.
Por que isso importa? Porque um workflow que só dispara em push para main nunca vai barrar um PR problemático antes do merge. Você só vai descobrir o problema depois que ele já está na branch principal.
Build e testes automatizados na prática
Um job de CI decente precisa fazer três coisas na ordem certa: instalar dependências de forma reprodutível, rodar lint, e só depois rodar os testes.
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
# ❌ instala e ignora o lockfile — versões podem divergir do ambiente local
- run: npm install
# ✓ usa o lockfile exato, falha se ele estiver desatualizado
- run: npm ci
- run: npm run lint
- run: npm test -- --ci
A diferença entre npm install e npm ci parece cosmética, mas não é. npm ci respeita o package-lock.json à risca e falha se ele estiver fora de sincronia com o package.json. Isso pega exatamente o tipo de bug que "funciona na minha máquina" costuma esconder.
Cache de dependências: o vilão silencioso da esteira lenta
Isso apareceu num PR meu em 2022: o CI levava 6 minutos só para instalar dependências, todo run, sem exceção. O revisor comentou "por que isso demora tanto?". Boa pergunta — a resposta era falta de cache.
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
Com uma linha (cache: "npm"), o setup-node passou a reaproveitar o cache de dependências entre execuções. O tempo de instalação caiu de 6 minutos para menos de 30 segundos na maioria dos runs.
Para projetos com cache mais específico, dá pra usar actions/cache diretamente:
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
A chave (key) baseada no hash do lockfile garante que o cache invalida sozinho quando as dependências mudam. Sem isso, você corre o risco de rodar testes contra pacotes desatualizados sem perceber.
Deploy automatizado da main para produção
Deploy automático exige um cuidado que testes não exigem: ele muda o mundo real. Um workflow de deploy típico separa build e deploy em jobs distintos, com dependência explícita entre eles.
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy
run: ./scripts/deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
O needs: test garante que o deploy só roda se os testes passarem. O if: github.ref == 'refs/heads/main' impede que PRs de outras branches disparem deploy por engano — um erro clássico em projetos que copiam workflow de outro repositório sem revisar as condições.
Secrets e variáveis sem vazar credencial
Secrets do GitHub Actions ficam em Settings > Secrets and variables > Actions e são referenciados como ${{ secrets.NOME }}. O problema não é declarar o secret — é usá-lo errado.
# ❌ imprime o valor do secret no log, mesmo que mascarado parcialmente
- run: echo "Token é ${{ secrets.DEPLOY_TOKEN }}"
# ✓ passa o secret como variável de ambiente, sem expor em comandos de log
- run: ./deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
💡 Dica: o GitHub mascara secrets automaticamente nos logs, mas essa proteção é baseada em correspondência de string exata. Se o secret passar por um comando que o transforma (base64, concatenação, etc.), a máscara não funciona mais.
Outro ponto que passa despercebido: workflows disparados por pull_request vindos de forks não têm acesso a secrets por padrão. Isso é proposital — é uma proteção contra PRs maliciosos tentando exfiltrar credenciais. Se seu deploy depende de secret e falha só em PRs externos, é isso.
Erros comuns que travam workflows
| Erro | Sintoma | Correção |
|---|---|---|
Faltar permissions explícito | Job falha ao tentar comentar em PR ou publicar release | Declarar permissions: contents: write (ou o escopo necessário) no job |
Usar latest em vez de versão fixa | Build quebra sem nenhuma mudança no código | Fixar actions/checkout@v4, não @main |
Rodar jobs em paralelo sem needs | Deploy acontece antes do teste terminar | Declarar needs: [test] explicitamente |
Ignorar concurrency | Dois pushes seguidos disparam deploys concorrentes | Configurar concurrency: { group: deploy, cancel-in-progress: true } |
Não fixar node-version | CI passa, produção quebra por versão diferente do runtime | Usar .nvmrc ou versão explícita igual à de produção |
O erro de concurrency é sorrateiro porque só aparece sob carga: dois merges rápidos seguidos, dois deploys tentando escrever no mesmo ambiente ao mesmo tempo. Ninguém nota até acontecer numa sexta-feira à tarde.
FAQ
GitHub Actions é gratuito? Para repositórios públicos, sim, sem limite prático de minutos. Para repositórios privados, cada plano do GitHub inclui uma cota mensal de minutos gratuitos (o Free, por exemplo, inclui 2.000 minutos/mês), e o excedente é cobrado por minuto de runner usado.
Dá para reutilizar o mesmo workflow em vários projetos?
Sim, com reusable workflows. Você declara on: workflow_call no workflow reutilizável e chama ele de outro repositório com uses: org/repo/.github/workflows/ci.yml@main. Evita duplicar YAML entre projetos parecidos.
Como debugar um step que falha só no CI e não localmente?
Ative ACTIONS_STEP_DEBUG: true como secret para logs verbosos, e reproduza o ambiente localmente com uma imagem Docker equivalente ao runner (ubuntu-latest corresponde a uma versão específica do Ubuntu, documentada pelo GitHub). Diferenças de versão de runtime são a causa mais comum.
Vale a pena usar self-hosted runners? Só se você tem necessidade real: hardware específico, rede interna, ou custo alto com minutos de runner hospedado. Para a maioria dos times, o runner padrão do GitHub é mais simples de manter e já resolve.
Como evitar que um workflow rode em toda alteração, incluindo mudanças em README?
Use paths-ignore no gatilho:
on:
push:
paths-ignore:
- "**.md"
Próximos passos
Se você tem um workflow rodando hoje, vale essa checklist rápida antes de considerar "resolvido":
- Confirme que
pull_requestdispara o CI, não sópushemmain. - Adicione cache de dependências — é a mudança de maior impacto com menor esforço.
- Separe jobs de teste e deploy com
needsexplícito. - Revise se secrets estão sendo passados via
env, nunca impressos emrun. - Adicione
concurrencyno job de deploy para evitar corrida entre releases.
Nenhum desses passos exige reescrever o pipeline do zero. São ajustes pontuais que eliminam a maior parte dos incidentes de CI/CD que aparecem em produção.