Nosso pipeline de CI falhou por 13 dias seguidos.
Ninguem percebeu.
Mais de 20 falhas consecutivas. Builds vermelhos todos os dias, sem excecao. O pipeline de auto-deploy estava morto. Cada execucao E2E expirava apos 30 minutos, queimava creditos de CI e produzia um badge vermelho que ninguem olhava.
Durante 13 dias, o time estava mergeando codigo sem nenhuma validacao E2E. Cada PR que foi pra producao durante essas duas semanas chegou la sem a suite de testes que deveria detectar regressoes. A rede de seguranca tinha um buraco, e o time estava andando na corda bamba sem olhar pra baixo.
Dia 0: O PR que quebrou tudo
Um refactor de frontend foi mergeado. Trabalho necessario — o time estava centralizando as definicoes de endpoints de URL pela aplicacao toda. Chaves de rotas que estavam duplicadas em 17 arquivos foram consolidadas em um unico modulo de configuracao. Refactoring limpo. PR bem delimitado.
Todas as revisoes de codigo passaram. Todos os testes unitarios passaram. Todos os 4 aprovadores deram OK. O revisor mais experiente do PR ate sinalizou a area de mapeamento de URLs como um risco — e revisou com cuidado.
Mesmo assim, a brecha escapou.
A infraestrutura de testes E2E mantinha seu proprio mapeamento de URLs. Um arquivo de configuracao separado que associava nomes de rotas com URLs reais. Quando o frontend refatorou suas chaves de rotas, ninguem atualizou o mapeamento da infraestrutura de testes. Entao cada teste E2E que navegava para uma rota — ou seja, todos os testes E2E — tentava acessar uma URL que nao existia mais.
// test-config/routes.ts — THE BROKEN FILE
// These route keys matched the frontend's OLD naming convention.
// The frontend PR renamed them. This file was not updated.
export const ROUTES = {
dashboard: '/app/dashboard',
userProfile: '/app/user/profile',
settings: '/app/settings/general',
// ... 14 more routes
reports: '/app/reports/overview', // OLD: was renamed to 'reporting'
analytics: '/app/analytics/main', // OLD: was renamed to 'insights'
} as const;
// Every test used these routes:
test('user can view analytics', async ({ page }) => {
await page.goto(ROUTES.analytics); // Navigates to a URL that no longer exists
// Test times out waiting for a page that will never load
});Dias 1 a 12: A falha silenciosa
Eis o que aconteceu nos 12 dias seguintes: nada.
O pipeline E2E rodava a cada merge na main. Falhava. Enviava uma notificacao. A notificacao ia pra um canal que o time tinha silenciado meses atras porque era barulhento demais. A execucao do workflow aparecia vermelha na aba do GitHub Actions, que ninguem verificava porque os checks do PR (testes unitarios, linting) passavam todos.
Todos os dias, o pipeline:
- Baixava o codigo
- Instalava dependencias
- Lancava os navegadores
- Tentava navegar para rotas que nao existiam mais
- Esperava 30 segundos para cada pagina carregar
- Dava timeout
- Reportava falha
- Repetia para cada teste da suite
30 minutos de tempo de CI, queimados. Todos os dias. Por quase duas semanas.
Dia 13: A descoberta
Encontrei da mesma forma que a maioria das falhas silenciosas sao encontradas — por acidente. Eu estava investigando outro problema e abri a aba do GitHub Actions. O muro de vermelho era impossivel de nao ver uma vez que voce olhava. Mais de 20 falhas consecutivas no pipeline E2E da branch main.
A investigacao comecou com uma pergunta simples: quando isso comecou?
# Find the first failing run on main
gh run list --workflow=e2e.yml --branch=main --limit=30 --json conclusion,createdAt \
| jq '.[] | select(.conclusion == "failure") | .createdAt' | tail -1
# Result: 13 days agoDepois: o que mudou 13 dias atras?
# Find the merge commit from 13 days ago
git log --oneline --since="13 days ago" --until="12 days ago" --merges
# Cross-reference with the first failure timestamp
git log --oneline --all --after="2026-03-15T00:00:00" --before="2026-03-16T00:00:00"
# Found it: the URL consolidation PR, merged on March 15Entao: o que exatamente quebrou?
# Compare the test config routes against the frontend route definitions
# The frontend had renamed keys — the test config still used the old names
git diff HEAD~50..HEAD -- frontend/src/config/routes.ts
git diff HEAD~50..HEAD -- tests/config/routes.ts # This file had ZERO changesO diff contou a historia inteira na hora. O arquivo de rotas do frontend mostrava 17 chaves renomeadas em uma janela de 50 commits. O arquivo de rotas de testes mostrava zero mudancas no mesmo periodo. O mapeamento tinha desalinhado.
A correcao
A correcao foram 10 linhas. Atualizar a configuracao de rotas dos testes para bater com as novas chaves do frontend.
// test-config/routes.ts — FIXED
export const ROUTES = {
dashboard: '/app/dashboard',
userProfile: '/app/user/profile',
settings: '/app/settings/general',
// Updated to match the frontend refactor
reporting: '/app/reporting/overview', // Was 'reports'
insights: '/app/insights/main', // Was 'analytics'
// ...
} as const;O pipeline passou de timeouts de 30 minutos para execucoes verdes de 4 minutos. Instantaneamente.
A correcao levou 10 minutos. Encontrar o problema levou 30 minutos de arqueologia git. O fato de o problema ter existido por 13 dias e o que me tira o sono.
O contrato de mapeamento de URLs
A verdadeira correcao nao e a atualizacao de 10 linhas das rotas. A verdadeira correcao e garantir que essa categoria de falha nao possa passar despercebida de novo.
Mapeamentos de URL entre o roteamento do seu frontend e sua infraestrutura de testes sao um contrato de primeira classe. Eles precisam de sua propria validacao — nao apenas “espero que alguem se lembre de atualizar a config dos testes.”
Veja como um contrato entre as rotas do frontend e a infraestrutura de testes se parece em codigo:
// tests/contracts/route-sync.spec.ts
// This test validates that the test route config matches the frontend route config.
// It runs in CI on every PR and fails if the mappings drift.
import { ROUTES as TEST_ROUTES } from '../config/routes';
import { ROUTES as APP_ROUTES } from '../../frontend/src/config/routes';
test('test routes must match application routes', () => {
const testRouteKeys = Object.keys(TEST_ROUTES).sort();
const appRouteKeys = Object.keys(APP_ROUTES).sort();
// Every app route should have a corresponding test route
const missingInTests = appRouteKeys.filter(key => !testRouteKeys.includes(key));
const staleInTests = testRouteKeys.filter(key => !appRouteKeys.includes(key));
expect(missingInTests).toEqual([]);
expect(staleInTests).toEqual([]);
});
test('test route URLs must match application route URLs', () => {
for (const [key, url] of Object.entries(TEST_ROUTES)) {
expect(APP_ROUTES[key]).toBeDefined();
expect(APP_ROUTES[key]).toBe(url);
}
});Esse teste roda em menos de um segundo. Nao precisa de nenhum navegador. Ele pega exatamente a falha que passou despercebida por 13 dias. Se o PR do frontend tivesse incluido esse teste de contrato no codebase, o PR em si teria falhado no CI — porque mudar chaves de rotas no frontend sem atualizar a config de testes teria sido detectado como uma violacao de contrato.
O checklist de monitoramento de saude do pipeline
A falha de 13 dias foi possivel por causa de multiplas lacunas na observabilidade do pipeline. Veja o que implementei depois:
1. Alertas por falhas consecutivas
# .github/workflows/pipeline-health.yml
# Runs daily and alerts if the main branch E2E pipeline has failed
# more than 3 consecutive times
name: Pipeline Health Check
on:
schedule:
- cron: '0 9 * * 1-5' # Every weekday at 9 AM
jobs:
check-health:
runs-on: ubuntu-latest
steps:
- name: Check E2E pipeline status
run: |
RECENT=$(gh run list --workflow=e2e.yml --branch=main --limit=5 \
--json conclusion -q '.[].conclusion')
FAILURES=$(echo "$RECENT" | grep -c "failure" || true)
if [ "$FAILURES" -ge 3 ]; then
echo "🚨 E2E pipeline has failed $FAILURES of the last 5 runs"
# Send alert to team channel
exit 1
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}2. Dashboard de taxa de sucesso do pipeline
Acompanhe a taxa de sucesso em uma janela movel de 7 dias. Se cair abaixo de 80%, algo esta estruturalmente errado — nao e apenas um teste instavel.
3. Duracao maxima aceitavel de falha
Decidam como time: qual e o numero maximo de dias consecutivos em que o pipeline E2E pode falhar antes de se tornar uma prioridade bloqueante? Pro meu time, a resposta agora e 2 dias. Se o pipeline esta vermelho por 2 dias consecutivos, o problema e escalado.
4. Disciplina nos canais de notificacao
O time tinha silenciado o canal de notificacoes do CI porque era barulhento demais. Isso e um sintoma de outro problema — o pipeline mandava alertas de testes instaveis junto com falhas reais, e o sinal se perdeu no ruido. A solucao: canais separados para “falha do pipeline” (falhas duras que precisam de atencao) e “instabilidade de testes” (testes instaveis sendo rastreados separadamente).
5. Testes de contrato para configuracao compartilhada
Cada pedaco de configuracao compartilhada entre o codigo da aplicacao e a infraestrutura de testes precisa de um teste de contrato. Rotas, feature flags, variaveis de ambiente, URLs de endpoints de API — se a suite de testes depende de estar sincronizada com a aplicacao, valide essa sincronizacao no CI.
Por que falhas silenciosas sao as mais perigosas
Falhas barulhentas sao corrigidas. Um teste que falha em todo PR bloqueia o merge. Um deploy que da erro e revertido. Um build que crasha e investigado imediatamente.
Falhas silenciosas erodem a confianca gradualmente. O pipeline esta vermelho, mas PRs continuam sendo mergeados (porque a suite E2E roda na main, nao como check de PR). O dashboard mostra falhas, mas o time parou de olhar. O auto-deploy esta quebrado, mas deploys manuais ainda funcionam, entao ninguem esta realmente bloqueado.
No dia 5, o time se adaptou ao estado quebrado. No dia 10, o estado quebrado e o estado normal. No dia 13, alguem percebe e a reacao e “ah, isso ta quebrado faz tempo” em vez de “isso e uma emergencia.”
Esse e o modo de falha mais perigoso em automacao de testes. Nao e o teste que flakeia de vez em quando — esse e visivel e irritante. E o pipeline que falha silenciosamente por semanas, treinando o time a ignora-lo, ate que a rede de seguranca que ele oferece seja puramente teorica.
A licao mais profunda
Se seu pipeline de CI pode falhar por duas semanas sem que ninguem faca nada, voce nao tem CI. Voce tem um cron job mandando emails pra /dev/null.
Integracao continua significa continua. Nao “continua a menos que a suite E2E esteja quebrada, caso em que a gente so roda os testes unitarios e torce pro melhor.” O ponto inteiro do CI e pegar falhas cedo. Um pipeline que falha silenciosamente por 13 dias nao esta pegando nada. Esta gastando tempo de computacao pra produzir badges vermelhos que ninguem le.
A parte mais dificil de corrigir isso nao foi a atualizacao de 10 linhas das rotas. Nao foi o teste de contrato. Nao foi o workflow de monitoramento. Foi perceber. Todo o resto levou uma tarde. Perceber levou 13 dias — e teria levado mais se eu nao estivesse olhando a aba do Actions por um motivo totalmente diferente.
Construa sistemas que percebam por voce. Contratos que falhem rapido. Alertas que cheguem nas pessoas certas. O pipeline nunca deveria poder mentir pra voce por 13 dias.
Porque o pipeline nao era instavel. Estava quebrado. E a parte mais dificil nao foi consertar — foi perceber.


