
Reduzi o tempo de validação do nosso CI em 95%.
Não foi com cache. Não foi com sharding. Não foi com uma ferramenta third-party sofisticada que adiciona mais uma dependência ao seu pipeline.
Foi com 15 linhas de bash. Zero dependências. Zero risco na cadeia de suprimentos.
O PR que entregou essa mudança era menor que a maioria das mensagens de commit. E economiza para o time 7-8 horas por mês.
O Problema
Todo PR rodava o pipeline completo de CI. Linting do backend, testes unitários do backend, testes unitários do frontend, testes E2E — a maratona inteira. Um desenvolvedor altera um único arquivo de teste E2E, faz push da branch e espera 8 minutos e 18 segundos para o linting do backend dizer que o código Python está tudo certo.
O código Python que ele nem tocou.
Esse é o comportamento padrão da maioria das configurações de CI. Um único arquivo de workflow, um único trigger, roda tudo em todo push. É simples. É seguro. E também está queimando tempo do desenvolvedor em validações que não têm como falhar para as mudanças que foram feitas.
A Solução: 15 Linhas de Bash
A ideia é simples: usar git diff --name-only para determinar quais arquivos mudaram e, condicionalmente, pular jobs que são irrelevantes para essas mudanças.
#!/bin/bash
# detect-changes.sh — Determine which CI jobs to run based on changed files
# Called by GitHub Actions workflow to set output variables
CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
RUN_BACKEND=false
RUN_FRONTEND=false
RUN_E2E=false
for file in $CHANGED_FILES; do
case "$file" in
backend/*|api/*|*.py) RUN_BACKEND=true ;;
frontend/*|src/*|*.ts|*.tsx) RUN_FRONTEND=true ;;
tests/e2e/*|playwright/*) RUN_E2E=true ;;
.github/*|docker-compose*|Makefile)
# Infrastructure changes: run everything as a safety net
RUN_BACKEND=true; RUN_FRONTEND=true; RUN_E2E=true ;;
esac
done
echo "run-backend=$RUN_BACKEND" >> "$GITHUB_OUTPUT"
echo "run-frontend=$RUN_FRONTEND" >> "$GITHUB_OUTPUT"
echo "run-e2e=$RUN_E2E" >> "$GITHUB_OUTPUT"É isso. O script lê o diff, classifica cada arquivo alterado por diretório e define flags de saída que os jobs downstream verificam antes de executar.
O Workflow do GitHub Actions: Antes e Depois
Antes: Rodar Tudo Sempre
# .github/workflows/ci.yml — BEFORE
name: CI
on: [push, pull_request]
jobs:
backend-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install ruff
- run: ruff check backend/
backend-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt
- run: pytest backend/tests/
frontend-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
e2e-test:
runs-on: ubuntu-latest
needs: [backend-test, frontend-test]
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright testTodo job roda em todo PR. Uma correção de typo num arquivo de teste E2E dispara linting do backend, testes do backend, testes do frontend e testes E2E. Tempo total: 8 minutos e 18 segundos.
Depois: Rodar Só o Que É Relevante
# .github/workflows/ci.yml — AFTER
name: CI
on: [push, pull_request]
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
run-backend: ${{ steps.changes.outputs.run-backend }}
run-frontend: ${{ steps.changes.outputs.run-frontend }}
run-e2e: ${{ steps.changes.outputs.run-e2e }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for accurate git diff
- id: changes
run: bash .github/scripts/detect-changes.sh
backend-lint:
needs: detect-changes
if: needs.detect-changes.outputs.run-backend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install ruff
- run: ruff check backend/
backend-test:
needs: detect-changes
if: needs.detect-changes.outputs.run-backend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt
- run: pytest backend/tests/
frontend-test:
needs: detect-changes
if: needs.detect-changes.outputs.run-frontend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
e2e-test:
needs: detect-changes
if: needs.detect-changes.outputs.run-e2e == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright testA única adição é o job detect-changes (roda em ~5 segundos) e os condicionais if: em cada job downstream. Todo o resto é idêntico.
Os Resultados
| Tipo de PR | Antes | Depois | Economia |
|---|---|---|---|
| Mudanças apenas em E2E | 8 min 18 seg | 39 seg | 95% |
| Mudanças apenas no frontend | 6 min 13 seg | 1 min 32 seg | 75% |
| Mudanças apenas no backend | 5 min 45 seg | 2 min 10 seg | 62% |
| Mudanças full-stack | 8 min 18 seg | 8 min 18 seg | 0% (roda tudo) |
| Mudanças em CI/infra | 8 min 18 seg | 8 min 18 seg | 0% (rede de segurança) |
Em uma semana típica — aproximadamente 40% PRs apenas de E2E, 30% apenas de frontend, 20% apenas de backend, 10% full-stack — isso projeta 7-8 horas por mês de tempo de espera economizado para os desenvolvedores.
A rede de segurança é importante: qualquer mudança em workflows do .github/, configurações Docker ou no Makefile dispara o pipeline completo. Você nunca pula a validação da infraestrutura que roda a sua validação.
Por Que Não Usar uma GitHub Action Para Isso?
É aqui que a história fica interessante. Eu auditei três GitHub Actions populares para filtragem de CI por caminho antes de escrever o script bash. O que encontrei me convenceu de que zero dependências era a decisão certa.
Action 1: Faz Chamadas Para uma API Comercial
Uma action popular de filtragem por caminho faz uma chamada de API para um serviço comercial em cada execução de CI. Leia de novo: seu pipeline de CI se comunica com um terceiro toda vez que roda. A action funciona corretamente quando o serviço está no ar e sua assinatura está ativa. Quando não está — e eu encontrei Issues no GitHub de usuários relatando isso — a action pode fazer seu build falhar com erro fatal. Seu pipeline de CI agora tem uma dependência de disponibilidade de um produto SaaS que você não controla.
Action 2: Alvo de Ataque à Cadeia de Suprimentos (CVE-2025-30066)
Em março de 2025, a amplamente utilizada GitHub Action tj-actions/changed-files — usada por mais de 23.000 repositórios — foi comprometida em um ataque à cadeia de suprimentos. O atacante modificou tags de versão existentes para apontar para código malicioso que fazia dump da memória do runner de CI, expondo secrets do workflow incluindo API keys, tokens e credenciais. A CISA emitiu um advisory formal (CVE-2025-30066). O ataque foi rastreado até um personal access token comprometido pertencente a uma conta bot com acesso de escrita ao repositório.
Isso não é um risco teórico. Isso aconteceu. Com uma action que faz exatamente o que meu script bash de 15 linhas faz.
Action 3: Funciona Bem, Mas é Desnecessária
A terceira action que avaliei é bem mantida, open-source e não tem problemas de segurança conhecidos. Funciona corretamente. Mas também adiciona uma dependência que precisa ter a versão fixada, ser monitorada para atualizações e auditada periodicamente. Para 15 linhas de bash que fazem a mesma coisa, a dependência não se justifica.
O Argumento de Segurança Para Zero Dependências
Toda GitHub Action que você adiciona ao seu workflow é código que você executa com acesso aos secrets do seu repositório, ao seu código-fonte e ao seu ambiente de CI. Cada action é uma decisão de confiança. O incidente do tj-actions/changed-files demonstrou que até actions populares e amplamente utilizadas podem ser comprometidas.
A abordagem com script bash tem um perfil de segurança diferente:
- Zero chamadas de rede. O script lê a saída do
git diff. Ele não busca nada, não se comunica com nada e não depende de nada fora do repositório. - Zero execução de código de terceiros. O script está versionado no seu repositório e revisado no mesmo processo de PR que o código da aplicação.
- Zero jogos de fixação de versão. Não existe tag
@v4para se preocupar. Nenhum vetor de ataque à cadeia de suprimentos. O código é seu. - Auditabilidade total. Qualquer pessoa do time consegue ler 15 linhas de bash e entender exatamente o que ele faz.
Às vezes, a melhor dependência é nenhuma dependência.
Casos Especiais e Redes de Segurança
Algumas coisas que eu trato e que uma implementação ingênua deixaria passar:
Arquivos novos sem uma categoria clara
# If a file doesn't match any known pattern, run everything.
# Better to over-run than under-run.
*)
RUN_BACKEND=true; RUN_FRONTEND=true; RUN_E2E=true ;;O caso padrão dispara uma execução completa. Um tipo de arquivo não reconhecido nunca deve silenciosamente pular a validação.
Arquivos deletados
git diff --name-only inclui arquivos deletados. Um arquivo de backend deletado ainda precisa que a suíte de testes do backend rode — a deleção pode quebrar um import.
Merge commits
O fetch-depth: 0 no step de checkout é crítico. Sem o histórico completo do git, o git diff contra a branch base produz resultados imprecisos. Já vi configurações de CI com fetch-depth: 1 (o padrão) que perdem mudanças porque o clone raso não contém a base do merge.
O Que Você Pode Fazer Segunda-Feira de Manhã
Meça sua baseline atual. Qual é a duração média do CI para o tipo de PR mais comum? Se você não sabe, confira as últimas 20 execuções do workflow.
Categorize seus jobs de CI por diretório. Quais jobs correspondem a quais partes do codebase? A maioria dos pipelines tem um mapeamento natural.
Escreva o script bash. Comece com a versão acima e adapte os padrões de diretório para a estrutura do seu repositório.
Adicione o job detect-changes. Conecte-o ao seu workflow com condicionais
if:.Adicione a rede de segurança. Qualquer mudança na configuração do CI, arquivos Docker ou scripts de infraestrutura deve disparar uma execução completa. Sempre.
Meça o resultado. Compare os mesmos tipos de PR antes e depois. Os números vão fazer o argumento melhor do que qualquer discussão.
A Lição Mais Ampla
O pipeline de CI é a infraestrutura mais negligenciada na maioria das organizações de engenharia. Times vão passar semanas otimizando queries de banco de dados que economizam 200ms por requisição, e depois assistem cada desenvolvedor esperar 8 minutos de validação irrelevante de CI 5 vezes por dia sem questionar.
A solução aqui não é inteligente. Não é inovadora. São 15 linhas de bash que fazem uma pergunta simples: as mudanças neste PR realmente afetam o código que este job valida?
Se a resposta é não, pule. Isso não é cortar caminho. Isso é respeitar o tempo do seu time.
15 linhas. Zero dependências. 95% mais rápido. Às vezes a solução mais simples é aquela que ninguém tenta porque parece simples demais para funcionar.

