Compare commits

...

4 commits

Author SHA1 Message Date
0355b1cd99 ci(test): purge aussi les conteneurs postgres-* orphelins au démarrage
Some checks failed
CI / lint (push) Successful in 3m35s
CI / build (push) Successful in 3m44s
CI / intent-service-test (push) Successful in 5m31s
CI / e2e (push) Failing after 13m53s
CI / test (push) Failing after 17m41s
Les essais 1-2 de cette branche (nom de conteneur Postgres par run) ont
laissé des conteneurs `postgres-122` / `postgres-125` en marche sur le
runner persistant : leur teardown `if: always()` n'a pas tourné (ce runner
saute les étapes finales après un échec) et rien d'autre ne cible ce
préfixe. Un 2e Postgres inactif affame la RAM du runner → requêtes ~5×
plus lentes → `resetDatabase()` dépasse le timeout Mocha. L'étape « Start
Postgres » retire maintenant tout `postgres-*` en plus du `postgres` nu ;
plus rien ne crée ce préfixe, donc ça ne touche que ces résidus.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 10:35:08 +02:00
b5c557c3f6 ci(test): remplace le verrou docker par une attente des jobs test antérieurs
Some checks failed
CI / lint (push) Successful in 3m7s
CI / build (push) Successful in 3m5s
CI / intent-service-test (push) Successful in 4m41s
CI / test (push) Failing after 10m41s
CI / e2e (push) Successful in 10m49s
Les deux approches précédentes de cette branche ne tenaient pas sur ce
runner :

- Nom de conteneur Postgres par run + `$GITHUB_ENV` (721a1ec) : laissait
  tourner le conteneur `postgres` nu de l'ancien ci.yml du run précédent →
  deux Postgres sur un runner à court de RAM → I/O DB ~5× plus lent →
  `resetDatabase()` dépasse le timeout Mocha de 10 s, cascade. Run 125 a
  échoué ainsi *en solo*.
- Verrou `docker network create ci-test-lock` (785d97b) : ce runner
  (forgejo-runner v3.5.0) n'exécute PAS l'étape de libération `if:
  always()` après l'échec d'une étape → le verrou fuit à chaque run rouge.

Retour au ci.yml de `main` (qui passe 614/614) + une seule étape en plus :
« Wait for any earlier test job » boucle tant qu'un conteneur
`GITEA-ACTIONS-TASK-<id>_..._JOB-test` de task id inférieur au sien tourne.
Le runner retire toujours le conteneur de job (succès comme échec), donc
rien à libérer ; task id le plus bas d'abord → pas d'interblocage.
`DATABASE_URL` et l'étape « Start Postgres » redeviennent identiques à
`main`. `timeout-minutes: 45` conservé.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 10:20:23 +02:00
785d97ba9e ci: sérialise vraiment le job test via un verrou docker inter-run
Some checks failed
CI / lint (push) Successful in 5m6s
CI / intent-service-test (push) Successful in 5m7s
CI / build (push) Successful in 5m20s
CI / e2e (push) Successful in 12m41s
CI / test (push) Failing after 18m58s
Le run 122 (branche avec l'isolation par conteneur du commit précédent) a
quand même échoué : sa base `postgres-122` était bien isolée et migrée
proprement, mais le run 123 (ancien ci.yml) tournait en parallèle sur le
même runner. Deux jobs `test` concurrents saturent le CPU (chacun entraîne
un modèle spaCy pour son intent-service *et* déroule toute la suite Mocha
DB) : un hook `resetDatabase()` dépasse alors le timeout Mocha de 10 s,
Mocha enchaîne sur le hook suivant pendant que le TRUNCATE + reseed du
précédent tourne encore, et les deux se percutent *dans la base d'un seul
run* (`deadlock detected`, `Unique constraint failed on (key)`,
intent-service affamé → F1 0.000).

L'isolation par base ne suffit donc pas ; il faut « un seul job test à la
fois », ce que `concurrency:` ferait nativement — indisponible ici
(Forgejo 9.0.3). À la place : un verrou via le seul point commun à tous
les jobs du runner, le démon docker. `docker network create ci-test-lock`
échoue si le nom est pris → le premier run le détient, les autres bouclent
jusqu'à libération (file d'attente, pas d'annulation). Libération en étape
`if: always()` gardée par un marqueur `/tmp` (un run qui a renoncé ne doit
pas supprimer le verrou du détenteur) + purge d'un verrou de plus de
~35 min (détenteur tué avant sa libération). `timeout-minutes` passe à 60
pour couvrir l'attente en file. L'isolation `postgres-${GITHUB_RUN_ID}`
reste en défense en profondeur (fenêtre de transition où d'autres branches
sont encore sur l'ancien fichier, sans verrou).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 09:27:12 +02:00
721a1ecf81 ci: isole la base Postgres du job test par run
Some checks failed
CI / lint (push) Successful in 1m45s
CI / build (push) Successful in 1m47s
CI / intent-service-test (push) Successful in 3m36s
CI / test (push) Failing after 13m51s
CI / e2e (push) Successful in 10m41s
Deux jobs `test` qui se chevauchent (push de branche peu après un merge
sur `main`) se connectaient au *même* conteneur `--name postgres` sur le
réseau partagé du runner Forgejo : leurs `prisma migrate deploy` +
`seedReferenceData` (beforeAll racine Mocha) se télescopaient — P2002 sur
`key`, « No pending migrations » à tort, décalage des ids de
`tech_step`/`diet` cassant des assertions sans rapport. Vu 3× en une
session (PR #47, #53, #54), à chaque fois re-déclenché par un commit vide
sur un runner au repos.

`concurrency:` serait le correctif naturel mais ce Forgejo (9.0.3) est
antérieur à son support côté Actions (arrivé en Forgejo v14 / Gitea 1.26,
et workflow-level uniquement même là) : la clé est ignorée silencieusement.

À la place, le conteneur prend un nom unique par run
(`postgres-${GITHUB_RUN_ID}`), poussé dans `DATABASE_URL` via `$GITHUB_ENV`
pour les étapes suivantes — deux runs ne partagent plus jamais de base.
Nettoyage : étape de teardown `if: always()` + purge des conteneurs
abandonnés (bornée par l'âge) au démarrage, pour un job tué net avant son
teardown. `timeout-minutes: 40` borne aussi le cas « job zombie ~1h »
observé cette session (Mocha sans `--exit`).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 08:59:38 +02:00

View file

@ -61,17 +61,70 @@ jobs:
test:
runs-on: ubuntu-latest
# ~16-19 min of actual work (runs 117-124); the extra budget covers time
# spent queued in "Wait for any earlier test job" below. Also a runaway
# ceiling — Mocha has no `--exit` (`apps/api/.mocharc.json`), so one
# leaked handle would otherwise hang the job until the runner's global
# timeout (a `test` job zombied ~1h once). If a hung job hits this
# ceiling the runner removes its container, which unblocks the queue.
timeout-minutes: 45
steps:
- name: Install curl and the docker CLI
run: |
apt-get update && apt-get install -y curl docker.io
# Serialize `test` across every concurrent run. Two `test` jobs must
# never overlap: they share one `--name postgres` container on the
# runner's shared docker network (next step), so their `prisma migrate
# deploy` + `seedReferenceData` (root Mocha `beforeAll`) race —
# `Unique constraint failed on (key)` in `seedReferenceData`, "No
# pending migrations" when there should be some, `tech_step`/`diet` id
# shifts breaking unrelated assertions; and even past that they starve
# each other's RAM/CPU (each also trains a spaCy model) until
# `resetDatabase()` overruns Mocha's 10s hook timeout and cascades.
# Hit repeatedly (PRs #47, #53, #54), each needing an empty-commit
# re-trigger once the runner was idle.
#
# GitHub's `concurrency:` is the native fix but this Forgejo (9.0.3)
# predates Actions `concurrency` — it arrived in Forgejo v14 / Gitea
# 1.26 and is silently ignored here. A lock object (network, container,
# file) would leak: this runner (forgejo-runner v3.5.0) does NOT run a
# trailing `if: always()` step after an earlier step fails, so there is
# no reliable in-job release. Instead, key off the one thing the runner
# *does* always clean up — the job container itself (removed on pass,
# fail, and cancel alike): wait while any lower-numbered `test` job's
# container is still running. Lower task id goes first, so two jobs
# that start together can't deadlock; nothing to release.
- name: Wait for any earlier test job
run: |
my_id="$(printf '%s\n' "${JOB_CONTAINER_NAME:-}" | sed -n 's/.*TASK-\([0-9]\{1,\}\).*/\1/p')"
echo "this test job is task ${my_id:-<unknown, not serializing>}"
# ~25 min cap (75 * 20s): comfortably covers one earlier run
# (the realistic overlap is two runs); a rarer 3-way pile-up whose
# wait exceeds this just fails and gets an empty-commit re-run.
for attempt in $(seq 1 75); do
earlier="$(docker ps --format '{{.Names}}' 2>/dev/null \
| sed -n 's/.*TASK-\([0-9]\{1,\}\)_.*_JOB-test$/\1/p' \
| awk -v me="${my_id:-0}" 'NF && ($1 + 0) < (me + 0)')"
if [ -z "$earlier" ]; then
[ "$attempt" -gt 1 ] && echo "earlier test job(s) finished — proceeding"
exit 0
fi
echo "waiting for earlier test job(s): $(printf '%s ' $earlier)(attempt ${attempt})"
sleep 20
done
echo "still blocked by an earlier test job after ~25 min — giving up; a re-run should get through" >&2
exit 1
# Postgres as a plain background container instead of a `services:`
# block — see this workflow's top-level `env.DATABASE_URL` comment for
# why. Reachable as `postgres` from this job (the runner mounts the
# docker socket and runs jobs on a shared user-defined network, so a
# `--name postgres` sibling resolves by name); the network is
# discovered from the job container itself rather than hardcoded.
# discovered from the job container itself rather than hardcoded. The
# `docker rm -f postgres` below also clears a container left behind by
# the previous run (this runner has no reliable post-job cleanup), and
# the serialization step above means no other run is using it now.
- name: Start Postgres in the background
run: |
net="$(docker inspect "$(hostname)" \
@ -79,6 +132,15 @@ jobs:
| awk '{print $1}')"
echo "job network: ${net:?could not determine the job container network}"
docker rm -f postgres >/dev/null 2>&1 || true
# Also drop any `postgres-<suffix>` container. A since-reverted
# attempt at per-run Postgres naming on this branch left some
# running on this persistent runner (their `if: always()` teardown
# never fired — this runner skips trailing steps after a failure),
# and nothing else removes them. A second idle Postgres here starves
# the runner's RAM and slows every query ~5x → `resetDatabase()`
# blows Mocha's 10s hook timeout. Nothing creates `postgres-*` any
# more, so this only ever clears that debris.
docker ps -aq --filter 'name=^postgres-' | xargs -r docker rm -f >/dev/null 2>&1 || true
docker run -d --name postgres --network "$net" \
-e POSTGRES_USER=ci -e POSTGRES_PASSWORD=ci -e POSTGRES_DB=batchcooking_ci \
postgres:16-alpine