
Votre pipeline échoue une fois sur cinq sans raison apparente ? Timeout réseau, test instable, registry Docker indisponible — les erreurs transitoires sont le quotidien des pipelines CI/CD. Ce guide vous donne les mécanismes pour absorber les erreurs passagères sans masquer les vrais problèmes.
Un pipeline fiable ne veut pas dire un pipeline qui ne fail jamais. C’est un pipeline qui échoue pour les bonnes raisons : un vrai bug, un vrai problème de code, pas un glitch réseau à 3h du matin.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Configurer des retries ciblés par type d’erreur (
retry:when) - Définir des timeouts adaptés à chaque job
- Détecter et isoler les tests flaky
- Rendre un job idempotent (relançable sans effet de bord)
- Distinguer erreur transitoire, erreur de code et erreur d’infrastructure
Dans quel contexte ?
Section intitulée « Dans quel contexte ? »Votre pipeline tourne 50 fois par jour. Sur ces 50 exécutions, 3 à 5 échouent de façon aléatoire :
- Un
npm installqui timeout parce que le registry est lent - Un test d’intégration qui échoue parce que le service PostgreSQL n’était pas prêt
- Un
docker pullqui retourne un 503 du Docker Hub - Un test unitaire qui passe en local mais échoue 1 fois sur 10 en CI
Chaque échec transitoire déclenche une investigation manuelle, un re-run, et de la frustration. Ce guide transforme ces échecs en récupérations automatiques.
Retry ciblé
Section intitulée « Retry ciblé »Retry simple
Section intitulée « Retry simple »Le mécanisme le plus basique : relancer un job en cas d’échec.
test: script: npm test retry: 2 # Jusqu'à 2 relances (3 exécutions max au total)Problème : ce retry attrape toutes les erreurs. Un vrai bug dans les tests sera relancé 2 fois pour rien, gaspillant du temps.
Retry par type d’erreur (retry:when)
Section intitulée « Retry par type d’erreur (retry:when) »La bonne pratique : cibler les erreurs transitoires uniquement.
build:docker: script: - docker build -t myapp . - docker push registry.example.com/myapp:$CI_COMMIT_SHA retry: max: 2 when: - runner_system_failure - stuck_or_timeout_failure - scheduler_failureTypes d’erreurs disponibles
Section intitulée « Types d’erreurs disponibles »Valeur when | Signification | Retry recommandé ? |
|---|---|---|
always | Toute erreur (défaut) | ❌ Trop large |
unknown_failure | Erreur non classifiée | ✅ |
script_failure | Le script a retourné un code non-zéro | ❌ C’est souvent un vrai bug |
api_failure | Erreur API GitLab | ✅ |
stuck_or_timeout_failure | Job bloqué ou timeout | ✅ |
runner_system_failure | Le runner a crashé | ✅ |
runner_unsupported | Runner incompatible | ❌ Pas transitoire |
stale_schedule | Schedule périmé | ❌ |
job_execution_timeout | Timeout du job | ⚠️ Selon le contexte |
archived_failure | Projet archivé | ❌ |
unmet_prerequisites | Prérequis non satisfaits | ❌ |
scheduler_failure | Erreur de planification | ✅ |
data_integrity_failure | Intégrité des données | ✅ |
Patterns recommandés par type de job
Section intitulée « Patterns recommandés par type de job »build:docker: script: - docker build -t myapp:$CI_COMMIT_SHA . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA retry: max: 2 when: - runner_system_failure - stuck_or_timeout_failure - unknown_failureLes erreurs Docker sont souvent liées au réseau (pull d’images de base) ou au registry.
test:integration: services: - postgres:16-alpine script: - npm run test:integration retry: max: 1 when: - stuck_or_timeout_failure - runner_system_failureUn seul retry pour les erreurs de timing (service pas prêt). Les vrais échecs de test (script_failure) ne sont pas retried.
deploy: script: ./deploy.sh retry: max: 2 when: - runner_system_failure - stuck_or_timeout_failure resource_group: productionLe resource_group garantit qu’un seul déploiement tourne à la fois, même avec retry.
Timeouts
Section intitulée « Timeouts »Timeout par job
Section intitulée « Timeout par job »Chaque job peut avoir son propre timeout :
lint: script: npm run lint timeout: 5 minutes # Le lint ne devrait pas prendre plus de 5 min
test:unit: script: npm test timeout: 15 minutes
test:e2e: script: npm run test:e2e timeout: 30 minutes # Les tests end-to-end sont plus longs
build:docker: script: docker build . timeout: 20 minutesTimeout global du projet
Section intitulée « Timeout global du projet »Configurable dans Settings > CI/CD > General pipelines > Timeout (défaut : 60 minutes).
Le timeout du job ne peut pas dépasser le timeout du projet. Si le projet est à 30 minutes et le job à 45, le job sera coupé à 30.
Choisir le bon timeout
Section intitulée « Choisir le bon timeout »| Type de job | Timeout recommandé | Raison |
|---|---|---|
| Lint | 3-5 min | Très rapide, un timeout long cache un problème |
| Tests unitaires | 10-15 min | Dépend du nombre de tests |
| Tests d’intégration | 15-30 min | Services à démarrer + tests |
| Build Docker | 10-20 min | Dépend de l’image et du cache |
| Déploiement | 10-15 min | Si ça prend plus, il y a un problème |
| Tests E2E | 20-45 min | Les plus longs, selon la suite |
Gestion des tests flaky
Section intitulée « Gestion des tests flaky »Un test flaky (instable) passe parfois et échoue parfois, sans changement de code. C’est le poison des pipelines CI/CD : vous ne savez jamais si l’échec est réel ou transitoire.
Détecter les tests flaky avec GitLab
Section intitulée « Détecter les tests flaky avec GitLab »GitLab détecte automatiquement les tests flaky si vous utilisez les rapports JUnit :
test: script: npm test -- --reporters=jest-junit artifacts: reports: junit: junit.xmlGitLab marque comme flaky un test qui échoue puis passe (ou inversement) sur la même MR sans changement de code. Les résultats sont visibles dans Analytics > CI/CD Analytics > Test cases.
Stratégies face aux flaky tests
Section intitulée « Stratégies face aux flaky tests »Isolez les tests flaky dans une suite séparée avec allow_failure :
test:stable: script: npm test -- --testPathIgnorePatterns='flaky' # Ceux-là doivent passer — échec = vrai bug
test:flaky: script: npm test -- --testPathPattern='flaky' allow_failure: true # Monitorer — ne bloque pas le pipelineCertains frameworks de test (Jest, pytest, RSpec) supportent le retry natif :
# Jest avec jest-circustest: script: npm test -- --bail --retries=2
# pytest avec pytest-rerunfailurestest: script: pytest --reruns 2 --reruns-delay 1Avantage : le retry est sur le test individuel, pas sur tout le job. Un test flaky qui passe au 2e essai n’impacte pas les autres.
La vraie solution : corriger le test.
Causes fréquentes de flakiness :
- Timing :
sleep(1)dans un test → utiliser des attentes explicites - Ordre : tests qui dépendent de l’ordre d’exécution → isoler l’état
- Ressources partagées : base de données non nettoyée → transaction rollback
- Date/heure : test qui échoue à minuit → mocker le temps
allow_failure pour les jobs non bloquants
Section intitulée « allow_failure pour les jobs non bloquants »# Le pipeline passe même si ce job échouesecurity:scan: script: trivy image myapp:latest allow_failure: true # Informatif, ne bloque pas
# Variante : allow_failure seulement pour les exit codes spécifiqueslint:strict: script: npm run lint:strict allow_failure: exit_codes: - 2 # Warnings = ok, Errors = bloquantIdempotence des jobs
Section intitulée « Idempotence des jobs »Un job idempotent peut être relancé sans créer d’effet de bord. C’est la base de la fiabilité : si le retry crée des doublons ou des états incohérents, le remède est pire que le mal.
Principes
Section intitulée « Principes »| Principe | Exemple |
|---|---|
| Pas de création si existe déjà | Vérifier si l’image Docker existe avant de rebuild |
| Pas d’état global modifié | Utiliser des noms uniques basés sur $CI_COMMIT_SHA |
| Nettoyage avant exécution | rm -rf dist/ && npm run build |
| Opérations atomiques | Déployer dans un nouveau répertoire puis switcher le lien symbolique |
Exemples concrets
Section intitulée « Exemples concrets »# ❌ Non idempotent : crée des doublonsdeploy: script: - echo "$CI_COMMIT_SHA" >> /srv/deploy-history.log - cp -r dist/ /srv/app/
# ✅ Idempotent : le résultat est le même si relancédeploy: script: - rm -rf /srv/app-$CI_COMMIT_SHA/ - cp -r dist/ /srv/app-$CI_COMMIT_SHA/ - ln -sfn /srv/app-$CI_COMMIT_SHA /srv/app# ❌ Non idempotent : helm upgrade peut échouer si le release est dans un état intermédiairedeploy:k8s: script: - helm upgrade myapp ./chart --install
# ✅ Idempotent : force l'état souhaitédeploy:k8s: script: - helm upgrade myapp ./chart --install --atomic --timeout 5m # --atomic : rollback automatique en cas d'échecinterruptible : annuler les jobs obsolètes
Section intitulée « interruptible : annuler les jobs obsolètes »Quand vous poussez un nouveau commit, les jobs du commit précédent sont souvent inutiles. interruptible permet à GitLab de les annuler automatiquement :
default: interruptible: true # Tous les jobs sont annulables par défaut
lint: script: npm run lint # interruptible: true (hérité de default)
test: script: npm test # interruptible: true (hérité de default)
deploy:production: script: ./deploy.sh interruptible: false # JAMAIS interrompre un déploiement en cours resource_group: productionPour activer l’annulation automatique : Settings > CI/CD > General pipelines > Auto-cancel redundant pipelines.
Pattern complet : pipeline résilient
Section intitulée « Pattern complet : pipeline résilient »Voici un pipeline qui combine toutes les techniques :
workflow: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - if: $CI_COMMIT_TAG
default: interruptible: true retry: max: 1 when: - runner_system_failure - stuck_or_timeout_failure
stages: - lint - test - build - deploy
lint: stage: lint timeout: 5 minutes script: npm run lint
test:unit: stage: test timeout: 15 minutes script: npm test -- --reporters=jest-junit --retries=1 artifacts: reports: junit: junit.xml
test:integration: stage: test timeout: 20 minutes services: - postgres:16-alpine script: npm run test:integration retry: max: 2 when: - stuck_or_timeout_failure - runner_system_failure
build: stage: build timeout: 15 minutes script: - rm -rf dist/ - npm run build artifacts: paths: [dist/]
deploy:production: stage: deploy timeout: 10 minutes interruptible: false script: ./deploy.sh --atomic retry: max: 2 when: - runner_system_failure resource_group: production rules: - if: $CI_COMMIT_TAG =~ /^v\d+/ when: manualDépannage
Section intitulée « Dépannage »| Symptôme | Cause probable | Solution |
|---|---|---|
| Job relancé mais même erreur | script_failure inclus dans retry (vrai bug) | Utiliser retry: when: ciblé, exclure script_failure |
| Retry sans fin (3 essais, même timeout) | Timeout trop court pour le job | Augmenter le timeout ou optimiser le job |
| Test flaky non détecté par GitLab | Pas de rapport JUnit | Ajouter artifacts: reports: junit: |
| Pipeline non annulé après un nouveau push | interruptible non configuré | Ajouter default: interruptible: true |
| Déploiement interrompu à mi-chemin | interruptible: true sur le job de deploy | Mettre interruptible: false explicitement |
| Job qui crée des doublons au retry | Job non idempotent | Utiliser des noms uniques ($CI_COMMIT_SHA) et nettoyer avant |
À retenir
Section intitulée « À retenir »retry: when:cible les erreurs transitoires — ne retryez jamais lesscript_failuresauf cas spécifique- Les timeouts doivent être adaptés à chaque type de job (lint 5 min, deploy 10 min)
- Les tests flaky sont le premier ennemi de la fiabilité — détectez-les avec JUnit, corrigez-les en priorité
- Un job idempotent peut être relancé sans effet de bord — clé du retry fiable
interruptible: trueannule les jobs obsolètes — sauf les déploiements- La fiabilité ne vient pas d’un seul mécanisme mais de la combinaison : retry ciblé + timeout adapté + idempotence + interruptible
- Un pipeline fiable échoue pour les bonnes raisons : un vrai bug, pas un glitch réseau