Pular para o conteúdo

Acelerar um pipeline lento

Intermediário14 min de leituracicd

Pipeline lento cobra caro de um jeito difícil de enxergar: as pessoas trocam de contexto enquanto esperam, agrupam mudanças em lotes maiores e evitam publicar perto do fim do dia. O alvo prático é feedback do pull request em menos de 10 minutos.

Este guia é sobre cortar tempo sem cortar sinal — remover teste e scan resolve o número e transfere o atraso para o incidente.

Otimizar por palpite é como otimizar código sem profiler. Comece com os números.

Janela do terminal
# GitHub Actions: duração de cada job nas últimas 50 execuções do workflow
gh run list --workflow ci.yml --limit 50 --json databaseId \
--jq '.[].databaseId' | while read id; do
gh api "repos/:owner/:repo/actions/runs/$id/jobs" \
--jq '.jobs[] | "\(.name)\t\(.started_at)\t\(.completed_at)"'
done

Monte a lista de estágios ordenada por duração média e anote também a variação: um job que às vezes leva 3 e às vezes 15 minutos costuma ser cache que não acerta ou teste instável.

Separe ainda o tempo de espera (fila por runner disponível) do tempo de execução. Se a fila domina, o problema é capacidade, e nenhuma otimização de build resolve.

Lint, tipos, testes unitários e build de imagem não dependem um do outro. Rodar em sequência é desperdício puro.

jobs:
verificar:
strategy:
fail-fast: false # veja todas as falhas de uma vez, não uma por execução
matrix:
tarefa: [lint, tipos, unitarios]
steps:
- run: npm run ${{ matrix.tarefa }}
imagem:
runs-on: ubuntu-latest # começa junto, não depois
steps:
- run: docker build .

Para suítes grandes, divida os testes em N partes por tempo histórico (a maioria dos runners de teste tem essa opção). Dividir por ordem alfabética desequilibra e o job mais lento define o tempo total.

3. Cache: dependências, build e camadas de imagem

Seção intitulada “3. Cache: dependências, build e camadas de imagem”
# dependências
- uses: actions/setup-node@v4
with: { node-version: 22, cache: npm }
# build de imagem com cache remoto — o ganho maior costuma estar aqui
- uses: docker/build-push-action@v6
with:
cache-from: type=registry,ref=registry.exemplo.com/loja:buildcache
cache-to: type=registry,ref=registry.exemplo.com/loja:buildcache,mode=max

Três regras que fazem o cache funcionar de verdade:

  • Chave certa: hash do lockfile, não do repositório inteiro.
  • Ordem no Dockerfile: copie o manifesto de dependências e instale antes de copiar o código; senão toda mudança de código invalida a instalação.
  • Meça o acerto. Cache que nunca acerta é tempo gasto duas vezes: baixando e reconstruindo. Se o hit rate for baixo, a chave está errada.

Em monorepo, esta é a maior economia disponível.

Janela do terminal
npx turbo run build test --filter='...[origin/main]'
npx nx affected -t build,test --base=origin/main

Sem ferramenta de grafo, um filtro por caminho já resolve boa parte:

on:
pull_request:
paths: ['servicos/pedidos/**', '.github/workflows/pedidos.yml']

Cuidado com o falso negativo: mudança em biblioteca compartilhada precisa disparar os consumidores. É por isso que o grafo de dependências é melhor que a lista de caminhos.

5. Tire a verificação pesada do caminho crítico

Seção intitulada “5. Tire a verificação pesada do caminho crítico”

Nem tudo precisa bloquear o pull request. Reorganize por objetivo:

Onde O que roda Alvo
Pull request lint, tipos, unitários, build, scan do diff < 10 min
Merge na main integração, e2e, scan completo, imagem assinada < 30 min
Noturno teste de carga, DAST, matriz completa de versões sem limite

Um cuidado: mover teste para depois do merge só é seguro com reversão rápida e com o hábito de reverter sem drama. Sem isso, você trocou espera por instabilidade.

Teste que falha aleatoriamente custa duas vezes: a reexecução e a confiança perdida na suíte inteira. Trate como bug de prioridade alta.

Janela do terminal
# quais testes falham de forma intermitente
gh run list --workflow ci.yml --limit 100 --json conclusion,databaseId \
--jq '.[] | select(.conclusion=="failure") | .databaseId'

O procedimento: identifique, coloque em quarentena com prazo e dono, corrija a causa (quase sempre espera fixa, ordem de execução ou estado compartilhado) e devolva à suíte. Quarentena sem prazo é o mesmo que apagar o teste.

  • Fila de runners: aumente a capacidade ou use runners maiores para os jobs pesados — às vezes é a solução mais barata em horas de engenharia.
  • Checkout lento: repositório grande pede fetch-depth: 1 (e só busque o histórico onde você precisa dele).
  • Imagem base gigante: build multi-stage e base mínima cortam minutos de push e pull.
  • Serviço externo no teste: substitua por dublê; rede é a maior fonte de lentidão e de instabilidade.
  • Tempo medido por estágio, antes e depois.
  • Jobs independentes rodando em paralelo.
  • Cache de dependências e de camadas com acerto medido.
  • Só o afetado é executado no pull request.
  • Verificação pesada fora do caminho crítico, com reversão rápida garantida.
  • Nenhum teste instável ativo sem prazo de correção.