Un garde-fou pour les échecs qui n’arrivent que sur la plateforme
Deux choses avaient mis la production à terre, et ni l’une ni l’autre ne pouvait échouer en local : un import sans extension qu’un bundler masque et que Node refuse, et un motif dans vercel.json qui ne produit aucun déploiement du tout. Les deux sont désormais vérifiés avant le build, et le garde-fou a été prouvé en réintroduisant chacun des deux bugs.
Les types passent, le bundler construit, le serveur de développement répond — et le déploiement répond 500 à chaque requête. Cet écart est ce pour quoi ce script existe. Ces trois outils résolvent les modules et lisent la configuration comme un bundler ; la fonction déployée ne fait ni l’un ni l’autre, et le script vérifie cette différence. Il s’exécute dans npm run build et sort en erreur, si bien qu’aucune des deux erreurs ne peut plus atteindre un push.
Ce qu’il rejette
Un import relatif sans extension. L’API tourne en ESM sur Node, où ./faq ne se résout pas et ./faq.js oui. Une seule ligne de ce genre, atteinte depuis le graphe d’import du serveur, a fait répondre chaque route /api avec FUNCTION_INVOCATION_FAILED.
Un import .json dans ce graphe. import { version } from '../package.json' passe la vérification de types, se construit et fonctionne dans le serveur de développement. Le paquet déployé contient des modules et non le dépôt, le fichier n’est donc simplement pas là, et l’import échoue au chargement du module — c’est-à-dire à chaque requête, si bien que l’API entière renvoyait 500.
Un motif source que le routeur de la plateforme ne sait pas analyser. Le symptôme ici n’est pas un déploiement en échec : un motif invalide est rejeté avant que le build commence, il n’y a donc aucun déploiement, et la production reste sur le commit précédent. Les motifs sont analysés avec la même bibliothèque que la plateforme.
Une version qui se contredit elle-même. shared/version.ts doit être égal à package.json. C’est une copie, précisément parce qu’un import JSON est l’erreur décrite plus haut, et une copie que personne ne vérifie devient obsolète ; le manifeste de l’extension et le tag de version lisent l’une, le connecteur lit l’autre.
Comment il choisit ce qu’il regarde
La règle s’applique aux fichiers que la fonction déployée charge réellement, et cet ensemble n’est pas « tout ce qui se trouve sous server/ » — il suit les imports où qu’ils mènent, et c’est ainsi qu’un fichier sous src/lib est devenu une partie du serveur. Il commence au point d’entrée de la fonction et remonte de là. Un import qui ne sert qu’aux types est ignoré : il est effacé à la compilation, et son chemin n’a donc jamais besoin d’être résolu à l’exécution.
Ce qu’il n’est pas
Ni une suite de tests, ni un linter. Il connaît quatre différences précises entre la plateforme et un ordinateur portable, et rien d’autre ; il ne remarquera pas une erreur de logique, et il avertit plutôt que d’échouer si la bibliothèque qui analyse les motifs n’est pas installée, car le but est d’attraper l’erreur sur la machine où elle est commise. Chaque règle a été prouvée en réintroduisant son bug et en observant la vérification échouer.
Autres lectures : la documentation qui vit dans le dépôt, et publier du Markdown depuis GitHub Actions.