Sommaire
Regardez un agent de code travailler pendant cinq minutes. Il modifie un fichier, lance les tests, corrige, relance, ajuste un import, relance encore. Le temps de converger vers une version qui passe, il a exécuté la suite une dizaine de fois. Il pousse alors son commit, et la CI démarre la même suite sur un runner vierge, sans rien savoir de ce qui vient de se passer.
Ce doublon existait déjà avec les développeur·euse·s, mais à petite dose : personne ne relance sa suite complète quinze fois en une heure. Un agent le fait sans y penser. Et comme il écrit aussi plus de code, il ouvre plus de pull requests, qui déclenchent chacune leur pipeline. La facture grossit des deux côtés : plus d’exécutions locales jetées aussitôt, et plus de minutes de CI derrière.
Solomon Hykes, fondateur de Docker et cofondateur de Dagger, a donné un nom au phénomène dans un billet du 15 septembre 2026 : le Great CI Bottleneck of 2026. Il ne croit pas que des machines plus rapides régleront la question : tant qu’une CI se résume à « lancer des scripts shell sur des VM », elle exécute des tâches qu’il juge « bêtes » et « gaspilleuses », et ajouter des machines revient à payer toujours plus cher à mesure que les agents produisent du code.
Ne pas recalculer ce qui l’a déjà été, c’est le principe de ccache pour la compilation C, du cache de build de Gradle, de l’extension de cache de Maven, de Bazel ou de Nix. Turborepo et Nx l’appliquent à l’échelle d’un monorepo : chaque tâche (build, lint, test) reçoit une clé calculée à partir du code source concerné, des dépendances et de la configuration. Si cette clé a déjà été vue, le résultat est resservi depuis le cache au lieu d’être recalculé.
Ce cache peut être distant. Bazel propose du remote caching depuis des années, avec des backends comme Google Cloud Storage, S3 ou un simple serveur HTTP. Turborepo fait de même côté JavaScript, pour éviter de refaire le même travail d’une machine à l’autre. Nx a son équivalent, Nx Replay. Dans les trois cas, une tâche jouée sur un poste pousse sa clé et son résultat dans un stockage partagé, et la CI qui retombe sur la même clé récupère le résultat.
Un cache distant : le poste pousse la clé et le résultat de la tâche, la CI les récupère si elle retombe sur la même clé.
Ce mécanisme est différent de la détection de périmètre affecté que proposent aussi Nx et Turborepo. Le calcul du périmètre affecté part du graphe de dépendances pour déduire ce qui devrait être rejoué. Le cache, lui, ressert un résultat qui a réellement été obtenu. C’est ce second mécanisme qui nous intéresse ici.
Sur le papier, tout est donc là pour qu’un poste de travail et la CI partagent leurs résultats. En pratique, peu d’équipes le mettent en place entre le laptop et le pipeline : un·e développeur·euse ne répète pas assez ses exécutions pour que le jeu en vaille la chandelle. Un agent qui relance dix fois la même suite avant chaque commit change ce calcul.
La boucle d’un agent tient en quatre temps : générer, exécuter, lire le résultat, corriger. Chaque tour relance à peu près la même commande, avec les mêmes tests, le même lint, parfois le même build. Et l’agent tourne souvent dans un environnement très proche de celui de la CI, avec les mêmes dépendances, parfois dans le même conteneur.
Au moment où il pousse, l’agent connaît donc déjà la réponse que la CI va recalculer. Seulement, rien ne garde la trace de cette réponse : l’itération suivante l’écrase, et le commit part sans elle.
L'agent itère jusqu'à obtenir des tests verts, puis la CI relance la même suite sur un runner vierge.
Plusieurs projets cherchent à rapprocher la boucle de l’agent et la CI. Agent CI, créé par l’équipe de RedwoodJS, fait tourner le vrai runner GitHub Actions en local, dans un conteneur léger, avec le store pnpm et le toolcache montés depuis la machine hôte. Un agent comme Claude Code peut ainsi faire passer tout le pipeline avant de pousser, et un pnpm install qui prenait plusieurs minutes tombe à zéro une fois le cache chaud.
Ce qu’Agent CI met en cache, ce sont les dépendances. Les tests, eux, tournent toujours en entier dans la session de l’agent, puis une seconde fois sur la CI.
GitHub explore une piste voisine avec ses Agentic Workflows, présentés comme une troisième brique à côté de l’intégration et du déploiement continus. Le projet se concentre sur l’orchestration d’agents dans le pipeline, avec le mainteneur qui garde la main via la revue de pull request. La question de réutiliser un résultat déjà calculé n’y est pas abordée.
Aucun de ces projets ne propose de format pour transmettre à la CI le résultat d’une tâche que l’agent a déjà exécutée.
Il n’est pas nécessaire d’attendre un cache distant complet pour tester l’idée. La version la plus simple tient dans un fichier : un manifeste que l’agent écrit après une exécution réussie, et qu’il pousse comme artefact de la pull request, comme on le ferait d’un rapport de couverture.
{
"commit": "a1b2c3d",
"tasks": [
{
"id": "test:unit",
"inputs_hash": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"status": "passed",
"log_ref": "artifact://agent-run/4821/unit-tests.log"
},
{
"id": "lint",
"inputs_hash": "sha256:3fa8b2e1e8f4a1c0d7b9e2f5a6c8d1b3e4f5a6c7d8e9f0a1b2c3d4e5f6a7b8c9",
"status": "passed"
}
]
}
Chaque entrée associe une tâche au hash de ses entrées déclarées et au statut obtenu. Le fichier ne contient aucun secret et ne transporte pas les logs eux-mêmes, seulement une référence vers eux.
Avant de lancer une tâche, la CI consulte ce manifeste. En pseudo-code, la décision ressemble à ceci :
for task in pipeline.tasks:
entry = manifest.find(task.id, commit=current_commit)
if entry and hash(task.declared_inputs) == entry.inputs_hash and entry.status == "passed":
skip(task, reason=f"vérifié par l'agent, hash {entry.inputs_hash}")
else:
run(task)
Tout repose sur hash(task.declared_inputs). La CI le recalcule elle-même à partir du commit et de la configuration du dépôt, sans jamais le relire dans le manifeste. Si le code a bougé depuis le passage de l’agent, les deux hashs divergent et la tâche est rejouée normalement.
La CI recalcule le hash des entrées et ne saute la tâche que s'il correspond à celui du manifeste et que le statut est « passed ».
Le gain se compte en minutes de pipeline, et aussi en temps d’attente : la pull request passe au vert plus tôt, pour l’équipe qui relit comme pour l’agent lorsqu’il attend le verdict de la CI avant de poursuivre.
La boucle ci-dessus est courte. La rendre fiable demande beaucoup plus de travail, sur trois points au moins.
La clé doit couvrir tout ce qui peut faire varier le résultat, et rien d’autre. Si elle embarque un élément sans incidence, comme un horodatage ou un chemin absolu, le cache ne sert jamais. Si elle en oublie un, c’est plus grave : l’environnement de l’agent n’est presque jamais identique à celui de la CI, et une version d’outil différente ou une variable d’environnement absente de la clé suffisent à resservir un résultat que la CI n’aurait jamais obtenu.
Le risque dépasse l’erreur de bonne foi. En juin 2026, Nx a montré qu’un cache partagé, chez Nx comme chez Turborepo, peut être empoisonné dès que la clé omet un fichier réellement lu par la tâche. Un attaquant modifie ce fichier non déclaré, la clé ne bouge pas, et la victime récupère un artefact piégé sans que rien n’apparaisse dans le diff. La parade de Nx s’appelle le sandboxing, proposé dans Nx Cloud : chaque accès fichier est tracé pendant l’exécution, et la tâche échoue si elle lit ou écrit en dehors de ses entrées et sorties déclarées.
Notre manifeste est exposé au même problème. Si les entrées déclarées sont incomplètes, la CI recalcule le même hash incomplet que l’agent, constate l’égalité, et saute une tâche dont le résultat aurait changé. Sans sandboxing, rien ne garantit que la comparaison porte sur la bonne question.
La comparaison de hashs prouve que le résultat porte sur ce code-là. Elle ne dit rien de sa véracité. Un manifeste qui annonce le bon hash avec un "status": "passed" inventé passerait le contrôle, et le job serait sauté.
Sans attestation, la CI ne peut pas distinguer un vrai résultat d'un fichier qui affirme « passed ».
Pour renoncer à une vérification, la CI a besoin d’une garantie de provenance : ce résultat vient d’une exécution réelle, dans un environnement qu’elle reconnaît. C’est le rôle des attestations de build signées, avec SLSA et in-toto pour les spécifications, et actions/attest-build-provenance côté GitHub. Vu sous cet angle, le manifeste proposé plus haut ressemble beaucoup à une attestation, avec un champ en plus pour le hash des entrées.
Mettre en cache une suite de tests entière est simple, mais le moindre test modifié invalide tout. Descendre au niveau du test individuel permet de réutiliser beaucoup plus de résultats, au prix de milliers de clés à calculer, stocker et purger. Entre les deux, le découpage par module ou par projet d’un monorepo, que Nx et Turborepo pratiquent déjà, semble le point de départ le plus raisonnable.
Même une fois ces points réglés, la CI conserve son rôle d’arbitre. Elle doit pouvoir rejouer n’importe quelle tâche quand le manifeste manque, quand il paraît suspect, ou simplement quand l’équipe le décide, par exemple sur la branche principale ou avant une release.
Un billet publié sur dev.to met en garde contre la tentation inverse : déplacer les vérifications dans la session locale de l’agent sans y reproduire l’isolation et la reproductibilité de la CI. On recréerait alors le problème que la CI a été inventée pour résoudre. Avec le manifeste, c’est la CI qui décide : elle saute un calcul seulement quand elle peut vérifier qu’il a déjà eu lieu dans des conditions équivalentes.
Nx et Bazel ont déjà le plus dur : une clé de cache calculée à partir des entrées déclarées, et un sandboxing qui vérifie que la tâche n’a rien lu d’autre. Il leur faudrait accepter en entrée un résultat produit hors de leurs propres runners, accompagné d’une attestation. Les outils d’agent comme Agent CI ont l’autre moitié, un environnement fidèle à la CI, mais aucune notion de clé. Le pont peut venir d’un côté comme de l’autre, par exemple sous la forme d’un plugin Nx capable de lire une attestation produite par Agent CI.
Le hashing des entrées, le sandboxing et les attestations signées tournent déjà en production. Ce qui manque relève de la plomberie : une convention de manifeste et quelques lignes de configuration pour que la CI la lise. Avant d’aller plus loin, il faudra aussi mesurer sur de vrais projets combien de minutes cela fait gagner.
En attendant, une équipe peut déjà se préparer : déclarer précisément les entrées et sorties de chaque tâche dans son nx.json, son turbo.json ou ses règles Bazel, et activer le sandboxing quand l’outil le propose. C’est utile dès aujourd’hui pour le cache distant classique, et ce sera le prérequis le jour où un agent pourra présenter ses résultats à la CI.
- Bazel Remote Caching - Partager les résultats de build entre machines
- Turborepo Remote Caching - Le cache distant côté monorepo JavaScript
- Nx Replay - Réutilisation des résultats de tâches en CI
- SLSA - Le cadre des attestations de provenance de build
- The Great CI Bottleneck of 2026 - Le billet de Solomon Hykes sur la CI face aux agents
- Can you trust your build cache? - L’analyse de Nx sur l’empoisonnement de cache partagé
- The Agentic Dev Loop - Agent CI, le runner GitHub Actions en local
- Continuous AI - Les Agentic Workflows de GitHub Next
- When the agent codes in seconds, CI becomes the slow neighbour - Le billet dev.to sur le rôle de la CI face aux agents