Aller au contenu
Retour aux Articles

Les checks CI qu'un linter ne peut pas faire : contrat d'API, patterns mobiles, cliquet d'avertissements et tests qui épinglent les bugs Stripe

Byte Smith · · 14 min de lecture

ESLint était vert. PHPStan était vert. TypeScript compilait. La suite de tests passait. Et le bouton « Payer en ligne » sur chaque facture envoyée par Zenpresa pendant plusieurs mois ne faisait rien au clic, parce que la route vers laquelle ce bouton pointait n’avait jamais été enregistrée. Le contrôleur existait. Le frontend appelait le bon chemin. Aucun outil du pipeline ne regardait les deux côtés à la fois.

Ce billet parle des checks que nous avons ajoutés à l’intégration continue après des bugs comme celui-là : des bugs syntaxiquement parfaits, corrects au niveau des types, et faux. Aucun d’eux n’est un linter. Tous s’exécutent sur chaque pull request dans le même workflow GitHub Actions que les linters, et chacun existe parce qu’un bug précis a été livré.

Le check de contrat d’API

Le bug du paiement de facture avait une forme simple. L’application React avait une fonction qui construisait une URL vers l’endpoint de paiement en ligne. Le backend Laravel avait une méthode de contrôleur qui le gérait. Le fichier de routes qui aurait dû les relier contenait la ligne d’un endpoint de paiement différent, et les deux avaient dérivé au cours d’un refactoring. Chaque couche était cohérente en interne ; l’écart se trouvait entre les couches.

Le correctif est un script qui regarde les deux côtés. Il parcourt le client API du frontend et extrait chaque chemin qu’il peut appeler. Il demande au backend de lister ses routes enregistrées. Puis il les compare : chaque chemin que le frontend appelle doit résoudre vers une route enregistrée avec une méthode compatible, et chaque route destinée au frontend doit avoir au moins un appelant ou une annotation explicite « utilisée en externe ».

Le script fait moins de deux cents lignes et s’exécute en quelques secondes. Ce n’est pas un framework complet de contract testing, et il n’essaie délibérément pas de valider la forme des requêtes ou des réponses ; c’est le rôle des clients API typés et des tests fonctionnels. Il répond à une seule question : est-ce que chaque bouton pointe vers quelque chose qui existe ? Dans les mois qui ont suivi, il a fait échouer quatre pull requests, chaque fois pour une route qui aurait autrement été livrée morte.

Le check des patterns mobiles

Deux défauts ont été livrés à répétition sur mobile, et tous deux étaient du JSX valide auquel aucun linter ne pouvait rien reprocher.

Le premier, c’est le rognage par le wrapper. Un tableau de données dans un conteneur dont le débordement horizontal est masqué perd silencieusement ses colonnes de droite sur un écran étroit. Les boutons d’action des factures et des rendez-vous vivent dans la colonne la plus à droite. Sur un téléphone, ils n’existaient pas. C’est arrivé trois fois dans trois tableaux différents, parce qu’à chaque fois c’était un nouveau composant écrit par quelqu’un qui testait sur un ordinateur portable.

Le second, ce sont les contrôles visibles uniquement au survol. Une ligne qui révèle ses boutons d’édition et de suppression au survol est un pattern desktop normal. Les écrans tactiles ne survolent pas. Les boutons n’apparaissaient jamais, et la fonctionnalité était invisible pour tout utilisateur sur téléphone, ce qui, pour un prestataire de services entre deux rendez-vous, représente la majorité d’entre eux.

Le check est un petit script qui parse les fichiers de composants du frontend et signale deux patterns : un tableau ou un élément large à l’intérieur d’un wrapper dont les classes masquent le débordement horizontal sans conteneur de défilement interne, et un élément dont le seul déclencheur de visibilité est une variante hover sans repli focus ou toujours-visible. C’est une heuristique, elle a une liste d’exceptions pour la poignée de cas légitimes, et elle produit occasionnellement des faux positifs. Elle a aussi attrapé les deux bugs avant merge à chaque fois depuis son ajout, ce qui est la seule métrique qui compte.

Le cliquet d’avertissements lint

Le job de lint du frontend échouait autrefois sur n’importe quelle erreur et ignorait les avertissements. Résultat, les avertissements s’accumulaient, parce que rien ne les arrêtait. Quand nous avons essayé le correctif évident, échouer sur n’importe quel avertissement, le job est passé au rouge dès la première exécution avec un compte dans les centaines, personne n’avait le temps de les nettoyer, et en une semaine le job a été marqué « échec attendu » et totalement ignoré. Zéro était la mauvaise cible.

Le cliquet (ratchet) est la bonne. Le workflow enregistre le nombre actuel d’avertissements dans un fichier, avec la date à laquelle il a été fixé. Le job de lint échoue si le compte monte. N’importe qui peut abaisser le nombre quand il nettoie des avertissements, et l’abaisser est encouragé dans le template de pull request. Personne ne peut le relever. Le compte a baissé régulièrement depuis, non pas parce que quelqu’un a planifié un nettoyage, mais parce que chaque ingénieur qui touche un fichier contenant un avertissement a tendance à le corriger tant qu’il y est, et le cliquet garantit que ce travail n’est jamais défait.

Le même cliquet s’applique à la baseline PHPStan côté backend et au nombre de commentaires @ts-expect-error. Le principe est général : quand l’état actuel est loin de l’objectif, faites porter le gate sur la direction du mouvement, pas sur la destination.

Épingler les bugs de destination charge Stripe avec des tests

Zenpresa utilise Stripe Connect pour que les clients paient l’entreprise directement et que la plateforme prélève une commission. Trois bugs ont été livrés dans cette intégration, chacun subtil, chacun désormais épinglé par un test qui échoue s’il revient un jour.

Limiter Stripe.js au compte connecté. Quand un client paie une facture, l’élément de paiement dans le navigateur doit être initialisé pour le compte connecté de l’entreprise, pas celui de la plateforme. Initialisé pour la plateforme, le paiement réussit et l’argent atterrit au mauvais endroit. Le test rend la page de paiement pour un tenant doté d’un compte connecté et vérifie que l’initialisation côté client porte l’identifiant du compte connecté.

Webhooks sans champ de compte. Stripe envoie les événements des comptes connectés avec un champ identifiant le compte auquel ils appartiennent. Les événements du compte propre de la plateforme ne portent pas ce champ. Le handler de webhook supposait que le champ était toujours présent, et les événements au niveau de la plateforme faisaient planter le handler, ce qui signifiait que les événements d’abonnement de la plateforme elle-même étaient perdus. Le test rejoue les deux types d’événement à travers le handler et vérifie que les deux sont traités.

Remboursements dans le scope de la plateforme. Rembourser une destination charge depuis le contexte de la plateforme, plutôt que celui du compte connecté, produit une erreur ou, pire, un remboursement sur le mauvais solde. Le test émet un remboursement pour une charge de compte connecté et vérifie que la requête est faite avec le compte connecté dans le scope.

La leçon plus large de ces trois bugs a remodelé le fonctionnement du flux de paiement. Le webhook est la source de vérité. Quand Stripe dit qu’un paiement a réussi, c’est cet événement qui marque la facture comme payée. Le callback « paiement confirmé » du navigateur, qui arrive en premier et semble faire autorité, n’est traité que comme un accélérateur : il déclenche une vérification qui peut marquer la facture payée plus tôt, mais rien ne dépend de son arrivée. Les deux chemins convergent vers un unique compare-and-set : marquer payée seulement si actuellement impayée, en une seule instruction atomique, de sorte que l’accélérateur et le webhook peuvent se faire la course et la facture est quand même marquée payée exactement une fois. Un test pilote les deux chemins dans les deux ordres et vérifie une seule transition.

Testez la branche qui déploie

La toute première version du workflow s’exécutait sur les pull requests et sur les pushes vers la branche principale. Le staging se déployait depuis une branche différente, et cette branche n’était pas dans la liste des déclencheurs. Pendant un temps, le staging a fait tourner du code que la CI n’avait jamais vu, parce qu’un hotfix poussé directement sur la branche de staging sautait tous les checks.

C’est d’une simplicité embarrassante à corriger et d’une fréquence tout aussi embarrassante. Le workflow se déclenche désormais sur chaque branche depuis laquelle un environnement se déploie, et un commentaire dans la liste des déclencheurs nomme l’environnement que chaque branche alimente. Si une branche déploie, elle est testée. Il n’y a aucune exception, y compris pour la personne qui a écrit le workflow.

Où vit chaque check

Le pipeline compte onze jobs, et les checks ci-dessus s’y insèrent plutôt que d’ajouter de nouvelles étapes de premier niveau :

  • Tests backend, sur une matrice de versions PHP, incluant les tests Stripe et les tests d’isolation des tenants du billet sur la base par tenant. Les tests des chemins malheureux de l’assistant IA du billet sur la sécurité des outils tournent ici aussi.
  • Style de code backend et analyse statique backend, cette dernière avec la baseline à cliquet.
  • Vérification des types frontend, lint frontend avec le cliquet d’avertissements et le check des patterns mobiles, tests frontend et build frontend. Le check de contrat d’API s’exécute dans le job de build, parce qu’il a besoin à la fois du frontend construit et de la liste des routes du backend.
  • Widgets embarquables, typés et construits séparément parce qu’ils sont livrés sur des pages tierces.
  • Validation du build Docker, qui construit l’image de production de la même manière que le déploiement et échoue si le Dockerfile a dérivé de ce dont l’application a besoin.
  • Audit des dépendances contre les vulnérabilités connues dans les arbres Composer et npm.
  • Tests de sécurité applicative statiques sur l’ensemble du dépôt.

Tout sauf la matrice de tests backend se termine en quelques minutes. Le pipeline entier est le gate pour merger sur toute branche qui déploie. La structure suit le pattern de notre billet sur la CI/CD d’un monorepo SPA React ; les jobs de chaîne d’approvisionnement suivent le billet sur la sécurité de la chaîne d’approvisionnement logicielle ; et le billet sur la sécurisation des workflows d’agents de coding IA explique pourquoi le même pipeline est le gate pour le code écrit par des agents, pas seulement par des humains.

Coût vs couverture

Chaque check ci-dessus est bon marché à exécuter et a coûté cher à ne pas avoir. Le check de contrat et le check des patterns mobiles prennent ensemble moins d’une minute. Le cliquet est une comparaison de fichier. Les tests Stripe tournent contre un faux fournisseur et prennent quelques secondes.

Ce qu’ils coûtent, c’est l’écriture : quelques heures chacun, généralement l’après-midi suivant la découverte du bug, pendant que la forme de la défaillance était encore fraîche. C’est le pattern que nous recommanderions à n’importe quelle équipe. Quand un bug que le pipeline aurait dû attraper est livré, ne vous contentez pas de corriger le bug. Demandez-vous quel check l’aurait attrapé, écrivez ce check le jour même, et faites-le échouer sur le commit précédant le correctif. Un pipeline construit ainsi est le registre de toutes les façons dont le produit a réellement cassé, et il ne laisse pas la même chose casser deux fois.

La série qui a commencé avec le lancement de Zenpresa est l’histoire d’une petite équipe qui livre une plateforme avec beaucoup de surface : de nombreux tenants, de nombreux modules, un agent capable d’agir, et des paiements qui doivent être justes. Le pipeline, c’est ce qui permet à une petite équipe de dormir. Si vous construisez quelque chose de forme similaire, les checks de ce billet représentent chacun un bon après-midi de travail, et le produit qu’ils protègent est ouvert en essai gratuit si vous voulez voir ce qu’ils tiennent ensemble.

FAQ : les checks CI au-delà du linting

Qu’est-ce qu’un check de contrat d’API en CI ?

Un script qui extrait chaque chemin d’endpoint que le frontend peut appeler, liste les routes enregistrées du backend, et fait échouer le build si un appel frontend n’a aucune route correspondante ou si une route destinée au frontend n’a aucun appelant. Il ne valide pas la forme des requêtes ou des réponses ; il garantit seulement que chaque bouton pointe vers quelque chose qui existe. Il existe parce qu’un bouton de paiement a été livré en pointant vers une route non enregistrée.

Comment imposer automatiquement des patterns adaptés au mobile en React ?

Avec un script heuristique sur les fichiers de composants qui signale deux patterns : des éléments larges dans des wrappers qui masquent le débordement horizontal sans conteneur de défilement interne, et des contrôles dont le seul déclencheur de visibilité est une variante hover sans repli focus ou toujours-visible. Il a une liste d’exceptions et des faux positifs occasionnels, et il a attrapé les deux vrais bugs avant merge à chaque fois depuis son ajout.

Qu’est-ce qu’un cliquet d’avertissements lint ?

Un nombre d’avertissements enregistré auquel le job de lint se compare. Le build échoue si le compte monte ; n’importe qui peut l’abaisser. Il remplace un gate « zéro avertissement » qui était inatteignable et donc ignoré. La même approche s’applique aux baselines d’analyse statique et aux commentaires de suppression. Faites porter le gate sur la direction du mouvement quand la destination est loin.

Comment un flux de paiement doit-il décider qu’une facture est payée ?

Traitez le webhook du fournisseur de paiement comme la source de vérité et le callback de confirmation du navigateur comme un accélérateur qui peut marquer la facture payée plus tôt mais dont rien ne dépend. Les deux chemins convergent vers un unique compare-and-set atomique, de sorte que la facture est marquée payée exactement une fois, quel que soit celui qui arrive en premier. Épinglez chaque bug déjà livré avec un test qui rejoue le scénario exact.

Pourquoi la CI doit-elle tourner sur les branches de staging, et pas seulement sur la branche principale ?

Parce que toute branche qui se déploie quelque part est de la production pour quelqu’un. Si le staging se déploie depuis une branche sur laquelle la CI ne se déclenche pas, les hotfixes qui y sont poussés sautent tous les checks. Listez chaque branche qui déploie dans le déclencheur du workflow et nommez l’environnement que chacune alimente.