Skip to main content
Retour aux billets
Oct. 20269 min de lecture

L'intégration continue d'Actual Budget a laissé passer un bogue qui enregistrait 10.23 comme 1,023. J'ai confié la publication à GitLab Duo.

Still Works déplace Actual Budget sur GitLab avec un pipeline qui ne publie une version que si elle fait encore tout ce que la production fait déjà. GitLab Duo écrit des vérifications de navigateur, garde celles qui passent en production, et une version retenue passe à Duo Developer et à Duo Code Review jusqu'à ce que la barrière la laisse sortir. Rejoué sur onze régressions étiquetées par les responsables : 2 à 3 arrêtées, contre 1 pour un test de fumée.

GitLab TranscendGitLab DuoDuo Agent PlatformCI/CDPlaywrightGoogle Cloud RunOpenTofuActual BudgetHackathon
J'ai écrit ce billet pour le GitLab Transcend Hackathon, Life After Code (octobre 2026, Path B, Hands-off), et j'ai bâti Still Works pour la même soumission. Le code, et chaque pipeline, demande de fusion et ticket mentionnés plus bas, sont publics sur GitLab à gitlab.com/gitlab-ai-hackathon/transcend-october-2026/3846496/showcase. Le déploiement Cloud Run sera retiré après le jugement, alors il n'y a pas de bouton vers une version en ligne ici.
La revendication en une phrase : rejouée sur onze régressions étiquetées par les responsables d'Actual Budget, chacune ayant passé leur intégration continue et été publiée, la barrière en a arrêté 3 (en écrivant des vérifications sans les exécuter) ou 2 (en se calibrant d'abord sur la production, la version qui tourne en direct), contre 1 pour un test de fumée sans Duo, corrigé sur les images publiées par les responsables eux-mêmes pour la version d'avant, la version brisée et le correctif. Ça veut quand même dire que la plupart des régressions passent, et la barrière retient aussi certaines versions qui ont changé un comportement exprès; les deux sont comptés plus bas.
Le README de Still Works sur GitLab : le titre Still Works, la ligne « A release only ships if it still does everything production already does », et l'histoire du bogue de montant de la 25.7.0 retenu en préproduction.
Still Works : Actual Budget déplacé sur GitLab, où une version ne sort que si elle fait encore tout ce que la production fait déjà.

L'intégration continue teste ce que l'auteur a pensé tester

En juillet 2025, Actual Budget a publié la version 25.7.0. Tapez 10.23 dans le montant d'une nouvelle opération planifiée et elle enregistrait 1,023. Le changement avait passé tous les tests de l'intégration continue du projet, et il est resté deux jours dans les budgets des gens avant que la 25.7.1 le corrige. Ce genre de bogue n'est pas rare. Quand j'ai sorti la liste le 19 octobre, les responsables d'Actual avaient étiqueté 122 tickets comme régressions, et chacune avait aussi passé l'intégration continue, parce que celle-ci teste ce que l'auteur d'un changement a pensé tester. Rien ne vérifie qu'une version fait encore ce que la production fait déjà.

Le ticket GitHub #5266 sur actualbudget/actual, « Cannot insert a decimal in any of the amount fields in dialog boxes », étiqueté regression et fermé, où la personne explique que taper 10.23 insère 1,023.
Actual Budget 25.7, juillet 2025 : taper 10.23 dans un champ de montant donnait 1,023. Les responsables l'ont étiqueté comme régression.

Aucun modèle ne décide de publier. Un navigateur le fait.

Une fusion vers main est testée, analysée et empaquetée, OpenTofu configure Cloud Run, et la version part en préproduction. Ensuite, le flux Release Guard de GitLab Duo lit les commits entre la production et la version, nomme ce sur quoi les utilisateurs comptent et que ce changement pourrait briser, et écrit des vérifications Playwright pour ça. Il exécute chaque vérification sur la production d'abord et ne garde que celles qui y passent : une vérification qui échoue en production se trompe sur l'application, ce n'est pas une preuve contre la version. Duo ne voit jamais la nouvelle version tourner.

Ensuite, le pipeline décide sans rien demander au modèle. Les vérifications gardées sont exécutées deux autres fois en production, puis en préproduction. Si une vérification qui tenait en production échoue en préproduction, la version reste en préproduction et le flux Regression Report ouvre un ticket : le commit qu'il blâme, la vérification qui a échoué, ce que la préproduction a montré et ce qu'un correctif doit préserver. Quand tout tient, la version passe en production avec une GitLab Release, les notes de version de Duo et une note sur chaque ticket qu'elle ferme, et les vérifications qui l'ont appuyée roulent sur la production toutes les six heures.

Le pipeline GitLab #2984538340, réussi, avec trois étapes : deploy (deploy-staging, production-baseline), guard (calibrate-on-production, guard-staging, write-release-checks) et release (create-release, promote-production, record-production-checks, write-release-notes, et un promote-held-release manuel).
Chaque fusion se déploie en préproduction sur Cloud Run. Duo écrit des vérifications de navigateur, elles sont calibrées sur la production, la barrière les exécute, puis vient la promotion.

Le refus

J'ai remis le changement derrière la 25.7.0 sous forme de demande de fusion. Duo Code Review, avec la consigne de révision de ce fork sur les montants et l'argent, l'a signalé (« typing 12. returns 12, so the . is dropped ») et je l'ai fait passer quand même, comme on contourne une révision en pratique. Duo a lu la version et écrit une vérification qui tape 12.50 dans une nouvelle opération planifiée. La production affichait 12.50. La préproduction affichait 1,250. La version est restée en préproduction, Duo a ouvert un ticket qui nomme le commit, et la production n'a jamais été touchée.

Le ticket GitLab #7 créé par Regression Report, « Typing 12.50 as a new schedule amount is listed as 1,250 instead of 12.50 », étiqueté regression et release-blocked, avec les sections What the check expected, What staging showed et Suspected commit 717eec8d.
Le refus : la préproduction affichait 12.50 comme 1,250. La version n'a jamais quitté la préproduction, et Duo Regression Report a ouvert ce ticket.

Duo vérifie Duo

Ensuite, je voulais savoir si Duo pouvait aussi corriger ce qu'il attrapait. J'ai remis le changement une autre fois et confié chaque ticket ouvert par la barrière à Duo Developer. Il a fallu sept demandes de fusion. Duo Code Review a approuvé les sept avant leur fusion, et la barrière en a quand même retenu cinq. Neuf des échecs attrapés après cette approbation étaient de mauvais montants : 5.5+2.5 enregistré comme 30.50, 1250.50 enregistré comme 125.050,00, 1,00 tapé par-dessus un montant sélectionné appliqué comme 100.00. Aucun n'a atteint la production. La production affiche maintenant les séparateurs de milliers pendant la saisie, une fonction que les responsables d'Actual ont retirée deux fois pour ce même genre de bogue.

Un tableau du README qui liste les correctifs !2 à !8 : pour chacun, le ticket auquel il répond, ce que Duo Code Review a dit, le résultat de la barrière en préproduction (retenu 3 sur 6, retenu 3 sur 5, retenu 1 sur 5, retenu 2 sur 4, promu, retenu 1 sur 5, promu) et ce que la barrière a attrapé.
Duo Code Review a approuvé sept correctifs écrits par Duo. La barrière en a quand même retenu cinq : neuf mauvais montants qui n'ont jamais atteint la production.

Du ticket à la production, sans personne entre les deux

Dans cette boucle, je fusionnais encore chaque correctif et j'assignais chaque ticket moi-même. Alors j'ai déplacé les deux dans le pipeline, et je lui ai donné un bogue signalé en amont par de vrais utilisateurs et toujours ouvert dans la 26.10.0 : on saute la prochaine date d'une facture, on renomme l'opération planifiée, et la facture redevient due (#9167). Je l'ai reproduit en production, je l'ai déclaré et je l'ai assigné à Duo Developer à 19 h 58. Personne n'y a touché ensuite. Duo Developer a ouvert un correctif neuf minutes plus tard. Duo Code Review l'a renvoyé une fois, parce que le nouveau test pouvait passer sans le correctif, et le pipeline a remis ça à Duo Developer. La deuxième version a été approuvée à 20 h 26 et le pipeline l'a fusionnée. Duo a écrit cinq vérifications pour la modification d'opérations planifiées, les cinq ont tenu en production et en préproduction, et le correctif était en production à 21 h 40.

La demande de fusion !10, « Keep a skipped schedule date when editing unrelated fields », fusionnée : GitLab Duo a demandé des changements, la tâche drive-fix du pipeline a mentionné Duo Developer, et Duo Developer a répondu qu'il a déplacé un appel MockDate pour que le test ne dépende plus de l'ordre, en listant les tests qu'il a exécutés.
Sans les mains : Duo Code Review a demandé des changements, le pipeline les a remis à Duo Developer, et Duo Developer a poussé le correctif.
Le journal de la tâche guard-staging : cinq vérifications d'opérations planifiées, chacune marquée pass, suivies de « verdict for the staging URL: promote (0 blocking of 5) » et Job succeeded.
Duo a écrit cinq vérifications pour la modification d'opérations planifiées. Les cinq ont tenu en production et en préproduction, et le verdict de la barrière était de promouvoir.
La page Schedules du budget de démonstration d'Actual Budget en production, avec Internet bill, Phone bills, Utilities, Wedding et Broadband, leurs prochaines dates, leurs statuts et leurs montants.
La production après le correctif : on saute une facture, on la renomme, et elle reste sautée. Le ticket #9167 en amont est toujours ouvert dans la 26.10.0.

Comment je sais que ça marche

Je n'ai pas écrit les bogues. J'ai pris onze régressions étiquetées par les responsables d'Actual, chacune ayant passé l'intégration continue, été publiée puis corrigée dans une version suivante, avec des images publiées pour chaque version. Chacune a été rejouée à partir d'une branche qui ne contient que la version d'avant et celle qui l'a brisée, alors le correctif n'existait pas pendant que Duo écrivait ses vérifications. Une régression compte comme arrêtée seulement si une vérification passait avant, échouait sur la version brisée et passait sur le correctif. Un test de fumée sans Duo en a arrêté 1 sur 11. La barrière qui écrit des vérifications sans les exécuter en a arrêté 3. La barrière qui essaie d'abord ses vérifications sur la production, celle qui tourne en direct, en a arrêté 2. Chaque prise a été rejouée cinq autres fois sur chacune de ses trois versions, et un script fait échouer la construction si le README et les données ne concordent pas.

Ce qui n'est pas revendiqué

Le rejeu est corrigé par les propres étiquettes et images publiées des responsables, pas par moi, mais ce sont onze régressions, et la barrière laisse passer la plupart. La version en direct en arrête 2 sur 11, une de moins que la version qui ne se calibre pas, parce que la calibration garde plus de vérifications et retient plus de changements voulus (14 blocages qui ne sont pas des prises contre 6). Elle fige ce que la production fait aujourd'hui, y compris ses propres bogues. Elle rate les régressions dans des endroits que le diff ne touche pas de façon évidente, comme une page vide. Elle ne retient que ce que ses vérifications couvrent : une version a laissé passer un mauvais montant (effacer deux chiffres de 1,250 appliquait 1.20) parce qu'aucune vérification ne le tapait, et un correctif de Duo plus tard a publié la correction. Elle ne peut pas montrer qu'un correctif fonctionne, seulement qu'il n'a rien brisé de ce que la production fait déjà; pour #9167, le test unitaire de Duo, Code Review et ma propre vérification en production après la publication ont couvert le correctif. Les versions qui changent un comportement exprès attendent qu'une personne appuie sur promote-held-release, et une exécution de la barrière prend environ une demi-heure de temps de Duo.

Projet associé

Still Works: Actual Budget moved onto GitLab, where GitLab Duo writes browser checks from what production already does and a release only ships if it still does all of it

Voir le projet