Skip to content

Infra: CI de garde, healthcheck réel, runtime épinglé - #16

Merged
Asgarrrr merged 11 commits into
mainfrom
infra/ci-healthcheck-runtime
Sep 13, 2026
Merged

Asgarrrr merged 11 commits into
mainfrom
infra/ci-healthcheck-runtime

Conversation

@Asgarrrr

Copy link
Copy Markdown
Owner

Contexte

Trois manques, vérifiés avant d'écrire une ligne :

  1. Aucune CI. .github/ n'existait pas. Le seul garde-fou était le
    tsc --noEmit niché dans le build Railway — une suite de tests rouge
    atteignait la production et n'apparaissait qu'après le déploiement.
  2. Le healthcheck ne testait rien. healthcheckPath: "/" sert la coquille
    SPA et répond 200 même Postgres à terre.
  3. Aucun runtime épinglé. Railpack choisissait sa version de bun.

Cette PR porte aussi les 8 commits dont main local était en avance sur
origin : ils passent la CI en entrant, plutôt qu'après coup.

Ce qui change

  • .github/workflows/ci.ymllinttestverifybuild sur push
    vers main et sur PR, un job Ubuntu, concurrence annulant les runs dépassés.
    Pas de service Postgres : aucun test n'importe src/db/index.ts ni ne lit
    DATABASE_URL, la suite reste sans état.
  • lint passe à oxlint --max-warnings=12 — l'invariant d'AGENTS.md devient
    exécutable.
  • packageManager: "bun@1.4.0" — Railpack le lit (vérifié dans la source :
    core/providers/node/package_manager.go), et setup-bun répète le littéral.
  • bun.lock régénéré : il avait dérivé de package.json (entrées mortes
    @prisma/client, better-sqlite3…). La dérive précède cette branche —
    vérifié en rejouant --frozen-lockfile sur un worktree de main.
  • /api/health (src/routes/api/health.tssrc/server/health.ts) exécute
    select 1 et répond 200 {"status":"ok"} ou 503 {"status":"degraded"},
    cache-control: no-store. healthcheckPath pointe dessus.
  • docker-compose.yml passe en postgres:18-alpine, le major de production.

Comment vérifier

En local, tout vert : bun run lint && bun run test && bun run verify && bun run build
(12 avertissements, 340 tests, 22 niveaux certifiés).

Le seuil mord : un nouveau module de route porte le compte à 13 et lint sort
en 1.

Le healthcheck, serveur bâti, base injoignable :

/            → 200      (l'ancienne cible : saine sans base)
/api/health  → 503 en 2.0s  {"status":"degraded"}

Risque / rollback

  • healthcheckPath ne s'applique qu'au prochain déploiement de web ;
    railway config apply n'a pas été lancé et attend une décision.
  • Aucun mode de panne ajouté au déploiement : preDeployCommand: bun run db:migrate exige déjà que la base réponde.
  • Le bump Postgres est destructif pour le volume local uniquement :
    docker compose down -v puis bun run db:migrate.
  • Rollback : revert des trois commits ; rien n'est appliqué côté Railway.

Non couvert

Le cas 200 de /api/health n'a pas pu être exercé de bout en bout en local
(démon Docker indisponible). Le chemin est couvert par un test unitaire ; le cas
503 et le délai de 2 s le sont sur le serveur bâti.

Asgarrrr added 11 commits July 31, 2026 03:11
Un hors-série est l'inverse d'une famille sur tous les points : pas de
palier, pas de seuil, pas de valeur faciale à gravir ; on le détient ou
non ; et un profil qui ne le détient pas n'affiche RIEN, parce qu'un
timbre jamais mis en vente ne peut pas manquer à un album. C'est cette
asymétrie qui lui vaut son propre module plutôt qu'une cinquième famille :
en émettre un ne peut jamais élargir la série courante, donc la mise en
page de la feuille reste bornée quoi qu'on ajoute.

Sissi est gravée couchée sur un plateau, ce qu'elle fait réellement. Le
plateau est en cases carrées — celui du jeu l'est. Elle est unie, gris et
blanc : ni rayures sur le dos ni anneaux à la queue, une bicolore n'est
pas une tigrée.

Rien ne l'attribue pour l'instant. L'easter egg qui la donne n'est pas
décidé, donc le timbre est émis mais indécrochable et chaque profil porte
une liste vide. Quand le déclencheur existera il n'aura qu'à la remplir.

Au passage, le chrome du timbre (dentelure, papier, burelage, cadre,
bandeau de pied, oblitération) sort en composant partagé : une valeur de
série et un hors-série sont le même objet avec une gravure différente au
milieu, et il n'y a toujours qu'une seule source du dessin.
Ni notFoundComponent ni errorComponent sur la racine : une URL inconnue ou
un loader en échec tombait sur l'écran par défaut du routeur — hors direction
artistique, non localisé, sans issue vers le jeu.

Un seul FallbackScreen rend les deux frontières ; l'erreur n'est jamais
affichée au visiteur, elle reste dans les logs serveur.
Le serveur ne garde qu'une ligne par (niveau, joueur), pas un historique :
un run sans retouche qui ne battait pas le record ne laissait aucune trace,
et son sceau mourait avec le localStorage du joueur.

Une colonne level_score.ever_clean, écrite par une seconde instruction —
l'upsert est gardé par beatenBy, donc il ne peut pas la porter. Le sceau des
tableaux ne bouge pas : il décrit la ligne affichée, pas la progression.
Trente-deux symboles sans consommateur hors de leur propre fichier :
vingt-six perdent leur export, trois sont supprimés (signOut, SHADE_HEX,
FAMILIES), trois relations Drizzle restent intactes — le framework les
consomme par convention, pas par import.

Aucun changement de comportement.
src/solver est exclu du tsconfig de l'app, donc ni build ni CI ne le
couvraient : son propre projet échouait depuis un moment sur six TS2345
dans hunt.test.ts (mods littéral readonly contre MechanicId[] mutable).

HuntOpts.mods et randomLevel prennent un readonly — le hunt ne fait que
lire cette liste — et le niveau généré copie l'array au lieu de l'aliaser.
L'app avait des comptes et aucun moyen d'en sortir : sur un poste partagé,
c'était la session d'un autre qui jouait. Le tri des exports morts l'a
révélé — signOut était exporté sans appelant parce que l'écran n'existait pas.

Un bouton sous le colophon de /profile/me, gouverné par une prop optionnelle
que le profil public ne passe pas. La navigation part de onSuccess : le
client better-auth ne rejette pas, un aller-retour manqué laisserait le
joueur croire qu'il est déconnecté.
La directive Sitemap: exige une URL absolue, que seul le serveur connaît
(BETTER_AUTH_URL) : les garder statiques imposait de coder l'origine de
production en dur. public/robots.txt disparaît, une route et un fichier de
même URL se disputeraient l'adresse.

Le sitemap ne liste que les routes statiques, dérivées de LEVELS. Les profils
en sont absents : publiables un par un, pas énumérables dans un fichier servi
à tout le monde. Sans origine il répond 404 plutôt que d'enseigner des URL
relatives à un crawler ; robots, lui, reste utile et perd juste sa ligne.

README et AGENTS.md remis en phase avec le dépôt.
Rien ne tournait automatiquement : le seul garde-fou était le `tsc --noEmit`
niché dans le build Railway, donc une suite rouge atteignait la production et
n'était visible qu'après le déploiement.

`--max-warnings=12` rend exécutable l'invariant que AGENTS.md n'énonçait qu'en
prose : les douze avertissements react(only-export-components) sont inhérents
aux modules de route TanStack, un treizième est une régression.

`packageManager` épingle bun pour Railpack et pour setup-bun d'un même
littéral ; bun.lock avait dérivé de package.json, ce que --frozen-lockfile
refusait — régénéré ici.
`healthcheckPath: "/"` servait la coquille SPA et répondait 200 avec Postgres à
terre : une instance web incapable de joindre sa base était déclarée saine.

`probe()` court contre un minuteur de 2 s parce que le Pool n'a aucun délai
configuré — sans lui, une base injoignable fait traîner la requête au lieu de
la faire échouer. La course vit dans probe() et non sur le Pool partagé :
imposer un délai à toutes les requêtes du serveur est une autre décision.

Le client est un paramètre plutôt qu'un import de db/index.ts, qui lève à
l'import sans DATABASE_URL — la CI n'a pas de base, et une sonde qu'on ne peut
pas pointer vers un client en panne ne peut pas être testée en panne.

Le corps reste opaque en échec : la route est publique et un message d'erreur
du driver nomme l'hôte et l'utilisateur. La raison part dans les logs.
docker-compose fixait postgres:17-alpine quand la production tourne
postgres-ssl:18 — un major d'écart entre développement et production.

Destructif pour le volume local : Postgres refuse de démarrer sur un répertoire
de données d'un major précédent. `docker compose down -v` puis
`bun run db:migrate`.

AGENTS.md consigne la CI, le healthcheck et ce major.
@Asgarrrr
Asgarrrr merged commit f8465d1 into main Sep 13, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant