Notre pipeline CI a echoue pendant 13 jours d’affilee.
Personne ne l’a remarque.
Plus de 20 echecs consecutifs. Des builds rouges chaque jour, sans exception. Le pipeline d’auto-deploiement etait mort. Chaque execution E2E expirait apres 30 minutes, brulait des credits CI, et produisait un badge rouge que personne ne regardait.
Pendant 13 jours, l’equipe a merge du code sans aucune validation E2E. Chaque PR livree durant ces deux semaines est passee en production sans la suite de tests censee detecter les regressions. Le filet de securite avait un trou, et l’equipe marchait sur la corde raide sans regarder en bas.
Jour 0 : la PR qui a tout casse
Un refactoring frontend a ete merge. Un travail necessaire — l’equipe centralisait les definitions d’endpoints URL a travers l’application. Des cles de routes dupliquees dans 17 fichiers ont ete regroupees dans un seul module de configuration. Du refactoring propre. Une PR bien delimitee.
Toutes les revues de code ont ete validees. Tous les tests unitaires sont passes. Les 4 approbateurs ont signe. Le relecteur le plus experimente sur la PR avait meme signale la zone de mapping d’URL comme un risque — et l’avait relue attentivement.
La faille a quand meme glisse entre les mailles du filet.
L’infrastructure de tests E2E maintenait son propre mapping d’URL. Un fichier de configuration separe qui associait les noms de routes aux URL reelles. Quand le frontend a refactorise ses cles de routes, personne n’a mis a jour le mapping de l’infrastructure de tests. Donc chaque test E2E qui naviguait vers une route — c’est-a-dire tous les tests E2E — tentait d’atteindre une URL qui n’existait plus.
// 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
});Jours 1 a 12 : l’echec silencieux
Voici ce qui s’est passe pendant les 12 jours suivants : rien.
Le pipeline E2E s’executait a chaque merge sur main. Il echouait. Il envoyait une notification. La notification arrivait dans un canal que l’equipe avait mis en sourdine depuis des mois parce qu’il etait trop bruyant. L’execution du workflow apparaissait en rouge dans l’onglet GitHub Actions, que personne ne consultait parce que les checks de PR (tests unitaires, linting) passaient tous.
Chaque jour, le pipeline :
- Recuperait le code
- Installait les dependances
- Lancait les navigateurs
- Tentait de naviguer vers des routes qui n’existaient plus
- Attendait 30 secondes que chaque page se charge
- Expirait par timeout
- Rapportait un echec
- Repetait pour chaque test de la suite
30 minutes de temps CI, brulees. Chaque jour. Pendant presque deux semaines.
Jour 13 : la decouverte
Je l’ai trouve comme la plupart des echecs silencieux sont trouves — par accident. J’enquetais sur un autre probleme et j’ai ouvert l’onglet GitHub Actions. Le mur de rouge etait impossible a rater une fois qu’on le regardait. Plus de 20 echecs consecutifs sur le pipeline E2E de la branche main.
L’investigation a commence par une question simple : quand est-ce que ca a commence ?
# 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 agoEnsuite : qu’est-ce qui a change il y a 13 jours ?
# 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 15Puis : qu’est-ce qui a exactement casse ?
# 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 changesLe diff a raconte toute l’histoire immediatement. Le fichier de routes frontend montrait 17 cles renommees sur une fenetre de 50 commits. Le fichier de routes de tests montrait zero modification sur la meme periode. Le mapping avait derive.
Le correctif
Le correctif faisait 10 lignes. Mettre a jour la configuration des routes de test pour correspondre aux nouvelles cles du 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;Le pipeline est passe de timeouts de 30 minutes a des executions vertes de 4 minutes. Instantanement.
Le correctif a pris 10 minutes. Le trouver a pris 30 minutes d’archeologie git. Le fait que le probleme ait existe pendant 13 jours est ce qui m’empeche de dormir.
Le contrat de mapping d’URL
Le vrai correctif n’est pas la mise a jour de 10 lignes des routes. Le vrai correctif, c’est de s’assurer que cette categorie de defaillance ne puisse plus passer inapercue.
Les mappings d’URL entre votre routage frontend et votre infrastructure de tests sont un contrat de premier ordre. Ils necessitent leur propre validation — pas juste “j’espere que quelqu’un pensera a mettre a jour la config des tests.”
Voici a quoi ressemble un contrat entre les routes frontend et l’infrastructure de tests, en code :
// 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);
}
});Ce test s’execute en moins d’une seconde. Il ne necessite aucun navigateur. Il detecte exactement la defaillance qui est passee inapercue pendant 13 jours. Si la PR frontend avait inclus ce test de contrat dans la base de code, la PR elle-meme aurait echoue en CI — parce que changer les cles de routes dans le frontend sans mettre a jour la config de tests aurait ete detecte comme une violation de contrat.
La checklist de surveillance de sante du pipeline
L’echec de 13 jours a ete possible a cause de multiples lacunes dans l’observabilite du pipeline. Voici ce que j’ai mis en place ensuite :
1. Alertes sur echecs consecutifs
# .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. Tableau de bord du taux de reussite du pipeline
Suivez le taux de reussite glissant sur 7 jours. S’il descend en dessous de 80 %, quelque chose est structurellement casse — ce n’est pas juste un test instable.
3. Duree d’echec maximale acceptable
Decidez en equipe : quel est le nombre maximum de jours consecutifs ou le pipeline E2E peut echouer avant que cela devienne une priorite bloquante ? Pour mon equipe, la reponse est maintenant 2 jours. Si le pipeline est rouge pendant 2 jours consecutifs, l’alerte est escaladee.
4. Discipline des canaux de notification
L’equipe avait mis en sourdine le canal de notifications CI parce qu’il etait trop bruyant. C’est le symptome d’un autre probleme — le pipeline envoyait des alertes pour des tests instables en meme temps que des echecs reels, et le signal s’est perdu dans le bruit. La solution : des canaux separes pour “echec du pipeline” (echecs durs qui necessitent une action) et “instabilite des tests” (tests instables suivis separement).
5. Tests de contrat pour la configuration partagee
Chaque element de configuration partage entre le code applicatif et l’infrastructure de tests a besoin d’un test de contrat. Routes, feature flags, variables d’environnement, URL des endpoints d’API — si la suite de tests depend de sa synchronisation avec l’application, validez cette synchronisation en CI.
Pourquoi les echecs silencieux sont les plus dangereux
Les echecs bruyants sont corriges. Un test qui echoue sur chaque PR bloque le merge. Un deploiement qui plante est rollback. Un build qui crash est investigue immediatement.
Les echecs silencieux erodent la confiance progressivement. Le pipeline est rouge, mais les PR continuent d’etre mergees (parce que la suite E2E tourne sur main, pas comme check de PR). Le tableau de bord montre des echecs, mais l’equipe a arrete de le consulter. L’auto-deploy est casse, mais les deploiements manuels fonctionnent encore, donc personne n’est vraiment bloque.
Au jour 5, l’equipe s’est adaptee a l’etat casse. Au jour 10, l’etat casse est l’etat normal. Au jour 13, quelqu’un remarque et la reaction est “ah oui, ca fait un moment que c’est casse” au lieu de “c’est une urgence.”
C’est le mode de defaillance le plus dangereux en automatisation des tests. Pas le test qui flake de temps en temps — ca, c’est visible et agacant. Le pipeline qui echoue silencieusement pendant des semaines, entrainant l’equipe a l’ignorer, jusqu’a ce que le filet de securite qu’il fournit soit purement theorique.
La lecon plus profonde
Si votre pipeline CI peut echouer pendant deux semaines sans que personne n’agisse, vous n’avez pas de CI. Vous avez une tache cron qui envoie des emails a /dev/null.
L’integration continue signifie continue. Pas “continue sauf si la suite E2E est cassee, auquel cas on lance juste les tests unitaires et on croise les doigts.” L’objectif meme du CI est de detecter les echecs tot. Un pipeline qui echoue silencieusement pendant 13 jours ne detecte rien. Il depense du temps de calcul pour produire des badges rouges que personne ne lit.
La partie la plus difficile pour corriger ca n’etait pas la mise a jour de 10 lignes des routes. Ce n’etait pas le test de contrat. Ce n’etait pas le workflow de monitoring. C’etait de le remarquer. Tout le reste a pris un apres-midi. Le remarquer a pris 13 jours — et ca aurait pris encore plus longtemps si je n’avais pas consulte l’onglet Actions pour une raison totalement differente.
Construisez des systemes qui remarquent pour vous. Des contrats qui echouent rapidement. Des alertes qui atteignent les bonnes personnes. Le pipeline ne devrait jamais pouvoir vous mentir pendant 13 jours.
Parce que le pipeline n’etait pas instable. Il etait casse. Et la partie la plus difficile n’etait pas de le reparer — c’etait de le remarquer.


