Erreur CI : 4 causes fréquentes hors du code
Votre code passe en local mais échoue en CI ? Identifiez les dépendances, environnements, fichiers ou caches en cause et rétablissez un diagnostic fiable.

Erreur CI : 4 causes fréquentes hors du code
Votre code passe en local, mais échoue en CI : les causes à vérifier
Vous n'avez modifié aucune ligne liée au problème. Et pourtant, ça compile ou ça teste chez vous... mais ça casse dans le pipeline d'intégration continue. La première tentation, c'est de chercher une régression dans le code. Parfois, oui. Mais souvent, le souci vient simplement d'un détail dans l'environnement.
Une dépendance publiée entre deux exécutions, une version de Python différente, un fichier oublié dans Git ou encore un cache qui réutilise un ancien artefact peuvent suffire à produire un résultat contradictoire. Et dans tous ces cas, le point commun est le même : le résultat devient difficile à reproduire, donc plus coûteux à corriger.
La CI n'est pas seulement là pour « vérifier le code ». Elle sert aussi à exposer les hypothèses cachées que votre machine tolère. L'idée ici : isoler l'écart, passer en revue les causes les plus fréquentes (sans deviner), puis décider quoi corriger - sans désactiver la CI à l'aveugle.
Pourquoi la CI révèle des problèmes invisibles en local
Sur votre poste, le projet ne tourne pas « dans le vide ». Il s'exécute dans un système déjà occupé : outils installés depuis des mois, paquets obtenus manuellement, fichiers de configuration locaux... et parfois des variables d'environnement que vous avez oubliées. Tout ça peut accélérer votre quotidien, mais ça brouille aussi la réalité : vous ne testez pas seulement votre dépôt, vous testez aussi votre machine.
Un runner de CI démarre, lui, d'une image ou d'un environnement préparé pour exécuter une suite d'étapes : il clone le dépôt, installe (ou non) des dépendances, compile, puis lance les tests selon le pipeline. Il ne « hérite » pas nécessairement des fichiers ignorés par .gitignore, ni des paquets installés globalement chez vous.
C'est justement l'intérêt : vérifier que le dépôt contient assez d'informations pour être construit et testé dans un environnement défini. Si le résultat diffère, vous tenez une information exploitable. Ça ne prouve pas que la CI a tort - et ça ne prouve pas non plus automatiquement qu'il y a un défaut dans le code. Mais ça indique clairement qu'il y a un écart à comprendre.
Ce que signifie réellement un résultat différent
- Réussir localement veut dire : « ça marche dans les conditions de votre poste ».
- Réussir en CI veut dire : « ça marche dans les conditions du runner ».
Dès que les conditions changent, vous ne comparez plus deux exécutions équivalentes.
Les différences peuvent porter sur :
- les versions de l'interpréteur, des outils et des bibliothèques ;
- le système d'exploitation, l'architecture ou le répertoire de travail ;
- les fichiers disponibles et les variables d'environnement ;
- les caches, les services externes ou les paramètres régionaux.
La bonne question n'est donc pas seulement « pourquoi ça échoue ? », mais aussi « qu'est-ce qui change entre ces deux exécutions ? ». Si vous voulez mieux cerner comment les workflows sont construits, vous pouvez consulter la documentation de GitHub Actions sur les workflows.
Quatre causes fréquentes à examiner
Quand le même commit donne des résultats différents, il y a quatre grands suspects à vérifier avant de réécrire quoi que ce soit. Ils ne couvrent pas tout, mais ils expliquent une grosse partie des échecs « sans explication » qui surgissent après coup.
1. Une dépendance n'est pas suffisamment maîtrisée
Une contrainte du type bibliotheque>=1.4 autorise l'installation de versions ultérieures. Résultat : si une nouvelle version sort pendant l'intervalle, une installation faite le lendemain peut résoudre un ensemble de paquets différent, alors que votre dépôt n'a pas bougé.
Ça peut venir :
- de la bibliothèque déclarée directement ;
- ou d'une dépendance transitive (indirecte) installée par une autre bibliothèque.
Une mise à jour peut modifier une API, changer un comportement ou imposer une version incompatible d'un paquet dont vous dépendiez... indirectement.
Mettre une borne supérieure peut réduire les surprises. Par exemple : sqlmodel>=0.0.45,<0.0.48 empêche d'aller au-delà de la version maximale spécifiée. Mais ce n'est pas une recette miracle : multiplier les bornes manuelles peut compliquer les mises à jour et laisser traîner des dépendances vulnérables.
Dans un projet d'application, un fichier de verrouillage utilisé par la CI est souvent le chemin le plus simple : il consigne les versions effectivement résolues. Dans le cas d'une bibliothèque destinée à d'autres développeurs, les plages doivent aussi rester compatibles pour les utilisateurs. L'objectif, dans tous les cas, c'est une règle explicite et des mises à jour testées - pas un figement permanent « par prudence ».
Pour Python, la doc de pip sur les fichiers de contraintes explique une façon de contrôler les versions. Et vérifiez surtout un point : la CI utilise-t-elle vraiment le fichier prévu ? Le déposer dans le dépôt ne suffit pas si la commande de CI ne s'en sert pas.
2. L'environnement local n'est pas celui du pipeline
Chez vous, vous utilisez peut-être Python 3.12, tandis que le runner est en Python 3.11. Votre linter peut être plus ancien, ou un paquet requis existe déjà globalement sur la machine - sans figurer dans les dépendances du projet.
Dans ce scénario, votre poste « réussit » parce qu'il possède déjà ce dont le dépôt a besoin. La CI, elle, repart de zéro (ou presque) dans un environnement contrôlé. C'est exactement ce qui révèle les dépendances manquantes.
L'écart peut aussi venir de détails système :
- système d'exploitation ;
- fuseau horaire ;
- locale ;
- variables comme
PATH.
Un test sensible au format d'une date, par exemple, peut changer de comportement selon la configuration régionale.
Commencez par comparer ce qui est réellement utilisé, pas ce que vous pensez avoir installé :
- version du langage et de l'interpréteur ;
- versions du gestionnaire de paquets, du linter et des outils de test ;
- commande exacte exécutée dans le pipeline ;
- système d'exploitation et variables d'environnement pertinentes.
Ensuite, reproduisez cette commande dans un environnement « neuf », en utilisant les mêmes versions. Les environnements virtuels Python sont documentés dans le guide officiel de Python. Ils ne garantissent pas à eux seuls une reproduction parfaite, mais ils évitent déjà de confondre les paquets du projet avec ceux installés globalement.
3. Un fichier lu par les tests n'est pas versionné
Un test peut dépendre d'un fichier de configuration, d'un petit jeu de données, d'un certificat de test... présent sur votre disque, mais absent du dépôt. S'il est ignoré par .gitignore ou jamais ajouté, le test passera localement puis échouera dans un clone propre.
Ce type d'échec paraît parfois « absurde » : le message semble pointer ailleurs (fichier introuvable, configuration invalide, donnée vide). Mais la question à trancher reste simple : le fichier existe-t-il réellement dans l'historique Git et est-il disponible au moment où le test s'exécute ?
Faites l'inventaire des fichiers lus par le test (y compris ceux chargés indirectement via une configuration), puis vérifiez :
- qu'ils sont suivis par Git ;
- qu'ils sont présents dans le dépôt.
git status aide à repérer les changements non suivis. Et git ls-files permet de confirmer si un chemin est indexé.
Attention aux secrets : un fichier requis localement ne doit pas forcément être commité. Les clés, mots de passe et certificats privés doivent être fournis via un gestionnaire de secrets ou par les variables sécurisées du pipeline. À l'inverse, une configuration de test sans secret peut être versionnée si elle est nécessaire à la reproduction.
4. Un cache masque l'état réel du projet
Les caches accélèrent les installations et les compilations, mais ils ajoutent une complexité supplémentaire. Un cache incomplet ou mal invalidé peut réutiliser un artefact construit avec d'anciennes dépendances, donc dans un état différent de celui attendu.
Un indice assez parlant : le même commit échoue quand le cache est désactivé, puis passe quand il est activé (ou l'inverse). Ça ne prouve pas immédiatement quelle donnée est périmée, mais ça montre que le cache influe.
Dans ce cas, il faut inspecter :
- la clé utilisée pour restaurer le cache ;
- les fichiers censés invalider ce cache ;
- ce qui est réellement réutilisé.
Et surtout : ne supprimez pas systématiquement tous les caches « pour de bon ». Vous risquez de faire disparaître le symptôme tout en ralentissant les builds, sans corriger la cause. Pour diagnostiquer, une exécution sans cache est utile. Ensuite, soit vous liez correctement les clés aux versions et aux fichiers concernés, soit vous renoncez à mettre en cache ce dont l'état ne peut pas être maîtrisé.
Une méthode de diagnostic reproductible
Quand un pipeline devient rouge, le but est de réduire le nombre d'hypothèses. Modifier plusieurs choses à la fois, relancer, vider tous les caches, puis « ça remarche »... c'est confortable, mais vous ne saurez jamais pourquoi ça cassait. Et ça a toutes les chances de revenir au prochain changement.
Suivre l'ordre des vérifications
- Lire l'erreur exacte. Identifiez le job, la commande, le fichier et la ligne signalés par le runner. Le message peut être incomplet, mais c'est le meilleur point de départ.
- Comparer les versions. Notez l'interpréteur, les dépendances et les outils utilisés en CI, puis comparez avec le local. Ne « devinez » pas les versions à partir de la configuration habituelle : vérifiez-les au moment de l'exécution.
- Vérifier les entrées. Confirmez que les fichiers, variables et services attendus sont bien présents. Séparez clairement ce qui est versionné de ce qui est injecté par la CI sous forme de secrets.
- Reproduire dans un environnement propre. Créez un environnement neuf, installez les dépendances selon la procédure du projet, lancez la même commande - idéalement sans cache si celui-ci peut influencer le résultat.
- Ne changer qu'un paramètre à la fois. Si la reproduction marche, réintroduisez ensuite le cache ou les options une par une. Vous pourrez ainsi isoler ce qui fait varier le résultat.
Si vous arrivez à reproduire exactement l'échec avec les mêmes versions et les mêmes entrées, le code ou les tests deviennent une piste prioritaire. Si vous ne pouvez pas reproduire, continuez à comparer l'environnement plutôt que de conclure trop vite que le runner est « en cause ».
Construire une reproduction utile
Une bonne reproduction n'a pas besoin d'être un copier-coller complet de la CI sur votre machine. Elle doit reproduire ce qui change réellement le résultat : version du langage, commande, dépendances et fichiers d'entrée.
Si la CI s'appuie sur un service externe, documentez aussi comment ce service est démarré et configuré.
L'idéal : consigner tout ça dans le dépôt ou dans la définition du pipeline. Un fichier indiquant la version de Python, une commande de test standard, et des dépendances verrouillées rend le diagnostic beaucoup plus rapide - et réduit les écarts entre les postes, la préproduction et la CI.
Il reste des limites : services réseau, architectures matérielles, et certaines images système peuvent varier. L'objectif n'est pas de rendre chaque détail identique. Le bon but, c'est de maîtriser les variables qui comptent et d'identifier clairement celles qui restent différentes.
Rendre le pipeline plus fiable dans la durée
Un pipeline fiable ne veut pas dire « figer l'environnement pour l'éternité ». Cela veut dire : les changements d'environnement doivent être visibles, évalués et attribuables. Quand une dépendance bouge, le changement doit être explicite et vérifié, plutôt que de s'introduire silencieusement lors d'une installation ultérieure.
Voici des pratiques qui offrent souvent un bon équilibre entre effort et bénéfice :
- Déclarer les versions importantes. Fixez les versions du langage et des outils, et utilisez un fichier de verrouillage quand c'est pertinent.
- Installer depuis le dépôt. Le processus CI doit fonctionner à partir d'un clone propre, sans dépendre de paquets installés manuellement.
- Partager les commandes. Utilisez les mêmes commandes de test et de lint en local et dans le pipeline, idéalement documentées dans le projet.
- Rendre les entrées explicites. Versionnez les fichiers non sensibles nécessaires ; injectez les secrets via un mécanisme dédié.
- Traiter le cache comme une optimisation. Le build doit rester compréhensible sans cache. Les clés doivent refléter les éléments dont dépend le contenu mis en cache.
- Mettre à jour de façon contrôlée. Automatisez les propositions de mise à jour si c'est utile, mais faites passer les tests avant d'intégrer les nouvelles versions.
Le but n'est pas de reproduire chaque poste au détail près. Souvent, c'est trop cher à maintenir. Le meilleur compromis consiste à documenter les paramètres qui comptent, limiter les dépendances implicites et garder une procédure de reproduction suffisamment courte pour être réellement utilisée.
Si vous travaillez en équipe, ajoutez au compte rendu d'échec les versions observées, la commande lancée, et le résultat d'une exécution sans cache. Ces informations sont bien plus utiles qu'un simple « ça marche chez moi » : elles transforment un désaccord entre environnements en cas vérifiable.
Conclusion : comparez les environnements avant de réécrire le code
Quand un commit passe en local mais échoue en CI, commencez par les vérifications les plus concrètes : dépendances réellement installées, versions d'outils, fichiers suivis par Git et influence des caches. Lire l'erreur exacte, puis reproduire la commande dans un environnement propre, permet souvent de transformer un échec opaque en différence mesurable.
La CI n'a pas toujours raison, et un test peut parfois révéler un vrai défaut dans le code. Mais tant que les environnements ne sont pas comparables, il est trop tôt pour trancher. Relevez d'abord les versions et les entrées du job, puis tentez une reproduction sans cache.
C'est une petite discipline qui évite les corrections au hasard et rend les prochains incidents plus simples à diagnostiquer. Si votre pipeline reste difficile à reproduire, documentez d'abord une seule commande de test exécutée de la même façon en local et en CI.