Journal
5 min de lecture

Vert, ça veut dire prouvé vert

Mon premier garde-fou de merge laissait passer une PR sans aucun test. Comment une règle d'une ligne est devenue une porte en Python, et ce qu'elle refuse aujourd'hui.

« On ne merge que si la CI est verte. »

Tout le monde a cette règle. Je l'avais aussi, écrite noir sur blanc dans les consignes de mes agents. Et pendant des semaines, rien dans mon code ne la vérifiait.

Quand j'ai construit Foundry, c'est la première chose que j'ai voulu réparer : sortir la règle du texte, la mettre dans le code. Une porte, en Python, par laquelle tout merge doit passer.

La première version de la porte était fausse. Je l'ai su le jour même.

Zéro test, donc zéro échec

La règle de départ : le merge est refusé si tous les contrôles ne sont pas réussis.

Ça a l'air solide. Maintenant, prends une PR sur laquelle la CI n'a pas encore démarré. Combien de contrôles en échec ? Zéro. Donc tous les contrôles sont réussis. Donc c'est vert.

Une liste vide ne contient aucun échec. Elle ne contient aucune preuve non plus.

La relecture en a sorti un deuxième, plus vicieux. Sur une PR qui ne touche que de la documentation, mes workflows sont filtrés par chemin : tous les jobs sont ignorés. Aucun rouge, aucun en cours. Vert, encore.

Et un troisième. GitHub a deux façons de remonter un résultat de CI : l'API récente, et une plus ancienne, que Jenkins ou CircleCI utilisent encore. Ma porte ne lisait que la récente. Un build rouge sur l'ancienne ressemblait trait pour trait à « pas de CI ».

Trois trous dans un garde-fou d'une ligne.

Trop strict, aussi

Le réflexe, c'est de serrer : exiger que tout soit explicitement réussi.

Sauf que ça casse les CI saines. Un job conditionnel qui ne se lance pas est marqué « ignoré », et c'est son fonctionnement normal. Exiger du succès partout, c'est bloquer des merges légitimes tous les jours. Et un garde-fou qui bloque à tort, tu finis par le contourner.

La règle, la vraie

J'ai fini par l'écrire comme une décision d'architecture, pour ne plus y revenir :

  • au moins un contrôle réellement réussi, toutes sources confondues ;
  • rien d'en cours — on attend ;
  • rien en échec — on s'arrête ;
  • les contrôles ignorés sont tolérés à côté d'un vrai succès, mais ne prouvent jamais rien tout seuls ;
  • les deux API sont lues et fusionnées.

En code, le cœur tient en deux lignes :

proven = any(c.status == "completed" and c.conclusion == "success" for c in checks)
...
"passed": (proven or waived) and not pending and not failing,

Dernier verrou : le merge est épinglé au commit exact que la porte a vérifié. Si quelqu'un pousse entre la vérification et le merge, GitHub refuse. Tu ne peux pas faire valider un commit et en merger un autre.

La dérogation qui se mordait la queue

Reste le dépôt qui n'a pas de CI du tout. Là, il faut bien une sortie : une option explicite, qui laisse une trace.

Mais « pas de CI » et « la CI n'a pas encore démarré » se ressemblent beaucoup, pendant les secondes qui suivent un push. Alors la dérogation n'est accordée que si rien n'indique une CI active sur ce commit.

Et là, deuxième version fausse. Certains de mes dépôts ont des workflows planifiés, qui laissent en permanence une suite « en attente », vide. La porte y voyait une CI sur le point de démarrer. Pour toujours. La dérogation était donc refusée à vie sur ces dépôts.

Cerise sur le gâteau : le message de refus recommandait d'utiliser la dérogation. Celle qu'il venait de refuser.

Corrigé le lendemain : une suite en attente ne compte que si elle a moins de quinze minutes.

Le hook qui dit non

Une porte ne sert à rien si on peut passer à côté. Dans un dépôt piloté par Foundry, un hook intercepte les commandes avant qu'elles partent. gh pr merge tapé directement est refusé. gh pr create aussi. Un push sur la branche principale, pareil.

L'agent reçoit le refus avec le chemin à prendre à la place. Il n'insiste pas : il passe par la porte.

Je ne me raconte pas d'histoire sur ce que ça vaut. C'est écrit dans le code du hook : un outil de discipline, pas un bac à sable. Un agent qui voudrait vraiment contourner y arriverait. Mais les agents ne trichent pas par malice. Ils prennent le chemin le plus court. Il suffit que le chemin le plus court soit le bon.

Et avant la porte, Maigret

La CI ne dit qu'une chose : les tests passent. Elle ne dit pas que le code fait ce que le ticket demandait.

Ça, c'est le travail de Maigret. Il relit le diff sans rien savoir de la conversation qui l'a produit, en deux temps : d'abord chaque critère d'acceptation du ticket, un par un — couvert, pas couvert, contredit. Ensuite seulement, la qualité.

En août, j'ai suivi dix tickets de près. Huit ont été bloqués à la première relecture. L'un d'eux a été renvoyé quatre fois.

Huit sur dix. Du code que l'agent qui l'avait écrit considérait comme terminé, tests verts à l'appui.

Ce que ça change

La porte est lente, parfois. Elle me fait attendre une CI que je sais verte. Elle refuse des merges que j'aurais faits les yeux fermés.

C'est exactement le but. Les yeux fermés, c'est comme ça que je mergeais avant.

Ce que je livre aujourd'hui est nettement meilleur qu'au printemps. Pas parce que j'écris mieux. Parce que ce qui n'est pas prouvé ne passe plus.

— Pato