Acelerar um pipeline lento
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.
1. Meça por estágio antes de mexer
Seção intitulada “1. Meça por estágio antes de mexer”Otimizar por palpite é como otimizar código sem profiler. Comece com os números.
# GitHub Actions: duração de cada job nas últimas 50 execuções do workflowgh 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)"' doneMonte 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.
2. Paralelize o que é independente
Seção intitulada “2. Paralelize o que é independente”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=maxTrê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.
4. Rode só o que foi afetado
Seção intitulada “4. Rode só o que foi afetado”Em monorepo, esta é a maior economia disponível.
npx turbo run build test --filter='...[origin/main]'npx nx affected -t build,test --base=origin/mainSem 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.
6. Elimine o teste instável
Seção intitulada “6. Elimine o teste instável”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.
# quais testes falham de forma intermitentegh 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.
Se o tempo não cair
Seção intitulada “Se o tempo não cair”- 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.
Checklist de pronto
Seção intitulada “Checklist de pronto”- 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.