CI/CD com GitHub Actions: o Guia que Evita os Erros Mais Comuns
← Voltar para Codeshort

CI/CD com GitHub Actions: o Guia que Evita os Erros Mais Comuns

Configure pipelines de CI/CD com GitHub Actions sem cair nas armadilhas de cache, secrets e deploy que travam times inteiros.

DC
Dev Code Software
10 de julho de 2026·6 min de leitura

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

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: true em 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

ErroSintomaCorreção
Faltar permissions explícitoJob falha ao tentar comentar em PR ou publicar releaseDeclarar permissions: contents: write (ou o escopo necessário) no job
Usar latest em vez de versão fixaBuild quebra sem nenhuma mudança no códigoFixar actions/checkout@v4, não @main
Rodar jobs em paralelo sem needsDeploy acontece antes do teste terminarDeclarar needs: [test] explicitamente
Ignorar concurrencyDois pushes seguidos disparam deploys concorrentesConfigurar concurrency: { group: deploy, cancel-in-progress: true }
Não fixar node-versionCI passa, produção quebra por versão diferente do runtimeUsar .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":

  1. Confirme que pull_request dispara o CI, não só push em main.
  2. Adicione cache de dependências — é a mudança de maior impacto com menor esforço.
  3. Separe jobs de teste e deploy com needs explícito.
  4. Revise se secrets estão sendo passados via env, nunca impressos em run.
  5. Adicione concurrency no 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.