From bdf86c3ec8de7d06e8ec0f29e2a76d765c7bea4c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 11:41:49 +0000 Subject: [PATCH] =?UTF-8?q?Bloc=2097=20:=20CI=20=E2=80=94=20passer=20?= =?UTF-8?q?=C3=A0=20Docker=20Compose=20l'image=20que=20le=20job=20vient=20?= =?UTF-8?q?de=20construire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Diagnostic (run 34835198084, commit 065ed7a sur main). L'erreur exacte, dans les logs de l'étape : Container ml-helper-app-1 Error response from daemon: No such image: ghcr.io/magicgg91/ml-helper:dev Le conteneur n'a donc jamais démarré : ce n'est ni un healthcheck qui échoue, ni un timeout. L'étape a duré moins d'une seconde (10:59:11 → 10:59:11), loin des 180 s de `--wait-timeout`, et « Show Docker Compose logs on failure » n'a rien affiché — il n'y avait aucun conteneur. Cause : le job construit et charge `:latest` sur main, `:dev` ailleurs, mais l'étape Compose ne passait pas `ML_HELPER_IMAGE`. Compose retombait donc sur son propre défaut, `:dev` (docker-compose.yml), avec `--pull never`. Sur dev les deux chaînes coïncidaient par hasard ; sur main elles divergent, et l'image demandée n'avait jamais existé sur le runner. Jamais vu avant parce que ce chemin n'avait jamais été emprunté : l'étape Compose et la variable ML_HELPER_IMAGE sont arrivées ensemble le 2026-09-02 (ccc1481), et le dernier push sur main avant celui-ci datait du 2026-08-31. Ce run est la première exécution de cette étape sur main. Aucun secret ni variable d'environnement ne manque : l'étape fait `touch .env`, toutes les variables du compose ont un défaut `:-`, le login ghcr a réussi, et l'échec précède de toute façon toute lecture d'environnement. Correctif : nommer l'image une seule fois, au niveau du job, sous le nom que Compose lit lui-même (ML_HELPER_IMAGE). Les quatre étapes (build, vérification de démarrage, Compose, push) s'y réfèrent, donc les deux fichiers ne peuvent plus diverger. Le défaut `:dev` de docker-compose.yml reste inchangé — il est documenté dans README.md. La description du statut GitHub publié en cas d'échec accusait la santé du conteneur pour un conteneur qui n'avait jamais démarré ; elle couvre maintenant les deux cas. Reproduction locale, à l'identique : avec seulement `:latest` en local (ce que fait le job sur main), `docker compose up -d --pull never` sort le même « No such image: ghcr.io/magicgg91/ml-helper:dev » ; en passant ML_HELPER_IMAGE, l'erreur disparaît. L'image réelle n'a pas pu être construite ici (la politique réseau de l'environnement bloque le pull des images de base Docker Hub), donc le passage jusqu'à « healthy » reste à confirmer par le prochain push sur main. Test : src/foundation.test.ts épingle le lien entre les deux fichiers — Compose lit bien ${ML_HELPER_IMAGE:-}, le workflow le définit, et le tag n'est écrit qu'une fois hors commentaires. Contre-vérifié : retirer la variable rougit 2 tests, remettre un tag en dur dans une étape en rougit 1. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HJgsDSfsCbbn8cochFGwe2 --- .github/workflows/ci.yml | 22 ++++++++++++++++++---- src/foundation.test.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66b4b33c..1df40709 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,20 @@ jobs: needs: test if: github.event_name == 'push' runs-on: ubuntu-latest + # Bloc 97: the image this job builds, verifies and pushes, named once. + # `main` publishes the stable channel (:latest), every other branch the + # rolling one (:dev) — the split README.md documents for ML_HELPER_IMAGE. + # + # It deliberately carries compose's own variable name: the Docker Compose + # check below never set it, so compose fell back to its built-in :dev + # default. On dev that happened to be the tag this job had just built; on + # main, where the job builds :latest, compose looked for an image that had + # never existed on the runner and the step died in under a second with + # "No such image: ghcr.io/magicgg91/ml-helper:dev" — before any container + # started, so nothing about it was a health problem. One name, read by + # every step here and by compose itself, cannot drift like that again. + env: + ML_HELPER_IMAGE: "ghcr.io/magicgg91/ml-helper:${{ github.ref_name == 'main' && 'latest' || 'dev' }}" steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3 @@ -44,7 +58,7 @@ jobs: with: context: . load: true - tags: ghcr.io/magicgg91/ml-helper:${{ github.ref_name == 'main' && 'latest' || 'dev' }} + tags: ${{ env.ML_HELPER_IMAGE }} - name: Verify container startup run: | mkdir -p "$RUNNER_TEMP/ml-helper-data" @@ -53,7 +67,7 @@ jobs: -e DATABASE_URL=file:/app/data/ml-helper.db \ -e NEXTAUTH_URL=http://localhost:3000 \ -e NEXTAUTH_SECRET=container-startup-test-secret \ - ghcr.io/magicgg91/ml-helper:${{ github.ref_name == 'main' && 'latest' || 'dev' }} \ + "$ML_HELPER_IMAGE" \ node -e "console.log('Container startup verified')" - name: Verify Docker Compose health run: | @@ -92,7 +106,7 @@ jobs: --header "Accept: application/vnd.github+json" \ --header "X-GitHub-Api-Version: 2022-11-28" \ "${{ github.api_url }}/repos/${{ github.repository }}/statuses/${{ github.sha }}" \ - --data '{"state":"failure","context":"docker-compose/health","description":"Docker Compose app container did not become healthy"}' + --data '{"state":"failure","context":"docker-compose/health","description":"Docker Compose app container did not start or did not become healthy"}' # Bloc 84: a plain `docker push` of the image loaded in the step above # started failing deterministically with "unknown blob" (2 runs in a # row, immediately on the first layer, no prior occurrence in 225+ @@ -108,5 +122,5 @@ jobs: with: context: . push: true - tags: ghcr.io/magicgg91/ml-helper:${{ github.ref_name == 'main' && 'latest' || 'dev' }} + tags: ${{ env.ML_HELPER_IMAGE }} diff --git a/src/foundation.test.ts b/src/foundation.test.ts index d6ad456f..33a90f63 100644 --- a/src/foundation.test.ts +++ b/src/foundation.test.ts @@ -17,3 +17,33 @@ describe("Docker healthcheck", () => { expect(healthcheck).toContain('path: "/api/health"'); }); }); + +// Bloc 97: the "Verify Docker Compose health" job failed on the first push to +// main since that step was written (CI run 34835198084). It never set +// ML_HELPER_IMAGE, so compose used its own :dev default while the job had +// built and loaded :latest — "No such image: ghcr.io/magicgg91/ml-helper:dev", +// in under a second, before any container existed. Two files each spelled the +// tag out on their own, and only the main branch made them disagree. +describe("CI image tag", () => { + const workflow = readFileSync(".github/workflows/ci.yml", "utf8"); + const compose = readFileSync("docker-compose.yml", "utf8"); + + it("hands Docker Compose the very image the job just built", () => { + // The name is the link between the two files: compose reads this variable, + // so the workflow has to be what sets it. + expect(compose).toContain("${ML_HELPER_IMAGE:-"); + expect(workflow).toMatch(/^\s+ML_HELPER_IMAGE: /m); + }); + + it("spells the registry tag exactly once, so no step can drift", () => { + // Every other reference goes through ML_HELPER_IMAGE. A second literal is + // how the branch-dependent tag came apart in the first place. Comment + // lines don't count — the one above quotes the tag on purpose. + const effective = workflow + .split("\n") + .filter((line) => !line.trimStart().startsWith("#")) + .join("\n"); + const literals = effective.match(/ghcr\.io\/magicgg91\/ml-helper:/g) ?? []; + expect(literals).toHaveLength(1); + }); +});