Le Dockerfile énumère les manifestes en dur : ajouter un workspace casse docker build avec un lockfile réputé périmé #7

Closed
opened 2026-09-09 15:24:30 +00:00 by nicolas-fryder · 4 comments

Constaté le 2026-09-09 sur le premier satellite réel monté depuis le template, pendant le cadrage du projet Pilotage. @eqs/* 0.7.0, eqs --version 0.7.0, pnpm 11.8.0.

Le Dockerfile copie les manifestes un par un dans ses étages deps et prod-deps, alors que pnpm-workspace.yaml déclare apps/* et packages/*. Dès qu'un satellite ajoute un workspace — un package sous packages/, ou une troisième application sous apps/ — l'image ne se construit plus, et l'échec ne dit pas ce qui manque.

Où

Dockerfile, étages deps (l. 19-22) et prod-deps (l. 60-63) :

FROM base AS deps
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
COPY apps/api/package.json apps/api/
COPY apps/web/package.json apps/web/

Face à pnpm-workspace.yaml :

packages:
  - 'apps/*'
  - 'packages/*'

Le lockfile décrit N importers ; le contexte de build n'en fournit que deux. pnpm install --frozen-lockfile refuse.

Reproduction

Dans un clone du template, après pnpm install :

mkdir -p packages/exemple
printf '{"name":"@x/exemple","version":"0.0.0","private":true}' > packages/exemple/package.json
pnpm install                    # vert, lockfile mis à jour, `pnpm check` vert
docker build --target api .     # échoue à l'étage `deps`

L'échec est un ERR_PNPM_OUTDATED_LOCKFILE sur un lockfile qui vient d'être régénéré et qui est correct. Le message oriente vers « relance pnpm install », qui ne corrige rien : le défaut n'est pas dans le lockfile, mais dans ce que le contexte de build en montre.

Ce que j'attendais, et pourquoi

pnpm-workspace.yaml déclare packages/* : le dépôt annonce donc que des workspaces peuvent y apparaître. Le README va dans le même sens — « packages/ est vide et le reste **tant qu'**un même code ne sert pas aux deux applications » — ce qui décrit une attente, pas une interdiction.

Rien dans le Dockerfile ne prévient du contraire, et ses commentaires sont par ailleurs très explicites sur ses autres contraintes (le fragment Prisma, le .npmrc, l'empreinte du magasin pnpm). L'absence de mention ici se lit comme « rien à signaler ».

Ce que ça coûte

Bloquant, et tardif — c'est ce qui le rend cher. La porte pre-push (pnpm check) reste verte, la CI check reste verte : rien ne construit d'image. Le défaut n'apparaît qu'au déploiement Coolify, c'est-à-dire après le push sur main, sur un service déjà en production (D-013).

Un satellite qui découpe son code — le geste normal quand une application grossit — casse donc sa production sans qu'aucun contrôle local ne l'ait dit.

Ce que je proposerais

  1. Copier les manifestes par motif dans les deux étages, plutôt qu'un par un. Corrige le symptôme, ne coûte presque rien, et couvre aussi le cas « troisième application ».
  2. Un contrôle eqs doctor qui compare les motifs de pnpm-workspace.yaml aux chemins copiés dans le Dockerfile. C'est le geste qui ramène la détection avant le push, là où la doctrine veut la porte.
  3. Trancher la question de packages/, et l'écrire. Si un satellite ne doit jamais produire de package, packages/* n'a pas à figurer dans pnpm-workspace.yaml, et le README devrait l'énoncer comme une règle et non comme une étape. Si l'inverse est retenu, il manque une décision sur le versionnement et la publication : un package produit par un satellite n'a aujourd'hui ni cible de construction, ni étape de publication dans .forgejo/workflows/ci.yml.

Contournement en place

Aucun. Le satellite concerné ajoutera apps/mcp et butera dessus ; le signalement précède l'implémentation.

Constaté le 2026-09-09 sur le premier satellite réel monté depuis le template, pendant le cadrage du projet Pilotage. `@eqs/*` 0.7.0, `eqs --version` 0.7.0, pnpm 11.8.0. Le `Dockerfile` copie les manifestes un par un dans ses étages `deps` et `prod-deps`, alors que `pnpm-workspace.yaml` déclare `apps/*` et `packages/*`. Dès qu'un satellite ajoute un workspace — un package sous `packages/`, ou une troisième application sous `apps/` — l'image ne se construit plus, et l'échec ne dit pas ce qui manque. ## Où `Dockerfile`, étages `deps` (l. 19-22) et `prod-deps` (l. 60-63) : ```dockerfile FROM base AS deps COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./ COPY apps/api/package.json apps/api/ COPY apps/web/package.json apps/web/ ``` Face à `pnpm-workspace.yaml` : ```yaml packages: - 'apps/*' - 'packages/*' ``` Le lockfile décrit N importers ; le contexte de build n'en fournit que deux. `pnpm install --frozen-lockfile` refuse. ## Reproduction Dans un clone du template, après `pnpm install` : ```bash mkdir -p packages/exemple printf '{"name":"@x/exemple","version":"0.0.0","private":true}' > packages/exemple/package.json pnpm install # vert, lockfile mis à jour, `pnpm check` vert docker build --target api . # échoue à l'étage `deps` ``` L'échec est un `ERR_PNPM_OUTDATED_LOCKFILE` sur un lockfile qui vient d'être régénéré et qui est correct. Le message oriente vers « relance `pnpm install` », qui ne corrige rien : le défaut n'est pas dans le lockfile, mais dans ce que le contexte de build en montre. ## Ce que j'attendais, et pourquoi `pnpm-workspace.yaml` déclare `packages/*` : le dépôt annonce donc que des workspaces peuvent y apparaître. Le `README` va dans le même sens — « `packages/` est vide et le reste **tant qu'**un même code ne sert pas aux deux applications » — ce qui décrit une attente, pas une interdiction. Rien dans le `Dockerfile` ne prévient du contraire, et ses commentaires sont par ailleurs très explicites sur ses autres contraintes (le fragment Prisma, le `.npmrc`, l'empreinte du magasin pnpm). L'absence de mention ici se lit comme « rien à signaler ». ## Ce que ça coûte **Bloquant, et tardif** — c'est ce qui le rend cher. La porte `pre-push` (`pnpm check`) reste verte, la CI `check` reste verte : rien ne construit d'image. Le défaut n'apparaît qu'au déploiement Coolify, c'est-à-dire après le push sur `main`, sur un service déjà en production (D-013). Un satellite qui découpe son code — le geste normal quand une application grossit — casse donc sa production sans qu'aucun contrôle local ne l'ait dit. ## Ce que je proposerais 1. **Copier les manifestes par motif** dans les deux étages, plutôt qu'un par un. Corrige le symptôme, ne coûte presque rien, et couvre aussi le cas « troisième application ». 2. **Un contrôle `eqs doctor`** qui compare les motifs de `pnpm-workspace.yaml` aux chemins copiés dans le `Dockerfile`. C'est le geste qui ramène la détection avant le push, là où la doctrine veut la porte. 3. **Trancher la question de `packages/`, et l'écrire.** Si un satellite ne doit jamais produire de package, `packages/*` n'a pas à figurer dans `pnpm-workspace.yaml`, et le `README` devrait l'énoncer comme une règle et non comme une étape. Si l'inverse est retenu, il manque une décision sur le versionnement et la publication : un package produit par un satellite n'a aujourd'hui ni cible de construction, ni étape de publication dans `.forgejo/workflows/ci.yml`. ## Contournement en place Aucun. Le satellite concerné ajoutera `apps/mcp` et butera dessus ; le signalement précède l'implémentation.
Author
Owner

Non reproduit — deux fois, en cherchant l'échec.

Le raisonnement du ticket est bon : pnpm-workspace.yaml déclare apps/* alors que le Dockerfile copie apps/api/package.json et apps/web/package.json un par un. Ajouter un workspace devrait faire diverger le verrou de ce que voit l'image. Ce n'est pas ce qui se passe.

Mesure 1 — un packages/exemple avec un manifeste nu, packages/* ajouté au workspace, pnpm install pour mettre le verrou à jour :

=> [deps 8/8] RUN pnpm install --frozen-lockfile          ✔
=> [build 2/3] COPY . .                                   ✔
=> [build 3/3] RUN pnpm --filter api exec prisma generate && pnpm -r build   ✔

Mesure 2 — le même, mais avec une vraie dépendance externe et un script build. C'est le cas que le ticket décrit, celui où le verrou porte quelque chose que l'étage deps ne peut pas voir :

> @exemple/exemple@0.0.0 build
> tsc -p tsconfig.json

Les deux étages passent, et le paquet se construit.

Pourquoi : --frozen-lockfile n'installe pas depuis les manifestes, il installe depuis le verrou — qui décrit déjà tous les importers, y compris celui dont le package.json n'a pas été copié. pnpm constate que ce manifeste manque, saute cet importer au lieu d'échouer, et ses dépendances arrivent quand même dans le magasin. À l'étage build, COPY . . amène le manifeste manquant et pnpm -r build recrée les liens. ERR_PNPM_OUTDATED_LOCKFILE sanctionne un verrou en retard sur les manifestes ; ici le verrou est en avance, ce qui est le sens toléré.

Le correctif proposé, je ne l'ai pas appliqué :

  • COPY apps/*/package.json apps/ aplatit les chemins. Docker ne reproduit pas l'arborescence : les deux fichiers atterrissent sur apps/package.json et se recouvrent. C'est un COPY qui réussit et une image fausse — pire que le défaut supposé.
  • copier les dossiers entiers marche, mais fait perdre la couche réutilisable : toute modification de code réinstalle tout. Cher, pour un problème qui ne se produit pas.

Ce qui est vrai dans le ticket, et que je garde : on s'appuie sur une tolérance de pnpm que rien ne documente. Le jour où elle se resserre, les images de toute la flotte cassent d'un coup, et l'énumération n'aura prévenu personne. C'est le bac à sable (eqs-demo, --depot <chemin>) qui doit l'attraper — il construit et déploie le gabarit pour de vrai avant chaque sortie du socle (D-037). Le Dockerfile porte maintenant tout ça en commentaire, mesure et date incluses, pour que le prochain à s'en inquiéter trouve la réponse sur place.

Votre question sur packages/ a une réponse, et elle est tranchée : un satellite ne produit pas de paquet publiable. Si du code doit être partagé, il part dans son propre dépôt, contrôlable et versionné — pas dans un satellite. packages/* est donc retiré de pnpm-workspace.yaml, il ne reste qu'apps/*, et le README le dit comme une règle. C'est D-041 dans eqs-platform. L'écart qui vous a mis la puce à l'oreille n'existe plus : le workspace déclare exactement les deux emplacements que le Dockerfile énumère.

Je laisse ce ticket ouvert. Je contredis votre diagnostic sur la foi de deux mesures qui peuvent différer des vôtres — la version de pnpm, l'état du verrou au moment du build, un pnpm install local qui aurait tourné entre-temps. Si vous avez la sortie exacte de ERR_PNPM_OUTDATED_LOCKFILE (pnpm imprime les importers en cause), elle tranche en une lecture. Et si votre satellite avait bien un packages/ au moment de l'échec, dites-le : le retrait ci-dessus le rend sans objet, mais ça expliquerait l'écart entre nos deux mesures et c'est ce qu'il faut savoir.

Non reproduit — deux fois, en cherchant l'échec. Le raisonnement du ticket est bon : `pnpm-workspace.yaml` déclare `apps/*` alors que le `Dockerfile` copie `apps/api/package.json` et `apps/web/package.json` un par un. Ajouter un workspace *devrait* faire diverger le verrou de ce que voit l'image. Ce n'est pas ce qui se passe. **Mesure 1** — un `packages/exemple` avec un manifeste nu, `packages/*` ajouté au workspace, `pnpm install` pour mettre le verrou à jour : ``` => [deps 8/8] RUN pnpm install --frozen-lockfile ✔ => [build 2/3] COPY . . ✔ => [build 3/3] RUN pnpm --filter api exec prisma generate && pnpm -r build ✔ ``` **Mesure 2** — le même, mais avec une vraie dépendance externe et un script `build`. C'est le cas que le ticket décrit, celui où le verrou porte quelque chose que l'étage `deps` ne peut pas voir : ``` > @exemple/exemple@0.0.0 build > tsc -p tsconfig.json ``` Les deux étages passent, et le paquet se construit. **Pourquoi** : `--frozen-lockfile` n'installe pas depuis les manifestes, il installe **depuis le verrou** — qui décrit déjà tous les importers, y compris celui dont le `package.json` n'a pas été copié. pnpm constate que ce manifeste manque, **saute cet importer** au lieu d'échouer, et ses dépendances arrivent quand même dans le magasin. À l'étage `build`, `COPY . .` amène le manifeste manquant et `pnpm -r build` recrée les liens. `ERR_PNPM_OUTDATED_LOCKFILE` sanctionne un verrou en retard sur les manifestes ; ici le verrou est en avance, ce qui est le sens toléré. **Le correctif proposé, je ne l'ai pas appliqué** : - `COPY apps/*/package.json apps/` **aplatit** les chemins. Docker ne reproduit pas l'arborescence : les deux fichiers atterrissent sur `apps/package.json` et se recouvrent. C'est un `COPY` qui réussit et une image fausse — pire que le défaut supposé. - copier les dossiers entiers marche, mais fait perdre la couche réutilisable : toute modification de code réinstalle tout. Cher, pour un problème qui ne se produit pas. **Ce qui est vrai dans le ticket, et que je garde** : on s'appuie sur une tolérance de pnpm que rien ne documente. Le jour où elle se resserre, les images de toute la flotte cassent d'un coup, et l'énumération n'aura prévenu personne. C'est le bac à sable (`eqs-demo`, `--depot <chemin>`) qui doit l'attraper — il construit et déploie le gabarit pour de vrai avant chaque sortie du socle (D-037). Le `Dockerfile` porte maintenant tout ça en commentaire, mesure et date incluses, pour que le prochain à s'en inquiéter trouve la réponse sur place. **Votre question sur `packages/`** a une réponse, et elle est tranchée : **un satellite ne produit pas de paquet publiable**. Si du code doit être partagé, il part dans son propre dépôt, contrôlable et versionné — pas dans un satellite. `packages/*` est donc retiré de `pnpm-workspace.yaml`, il ne reste qu'`apps/*`, et le README le dit comme une règle. C'est D-041 dans `eqs-platform`. L'écart qui vous a mis la puce à l'oreille n'existe plus : le workspace déclare exactement les deux emplacements que le `Dockerfile` énumère. Je laisse ce ticket **ouvert**. Je contredis votre diagnostic sur la foi de deux mesures qui peuvent différer des vôtres — la version de pnpm, l'état du verrou au moment du build, un `pnpm install` local qui aurait tourné entre-temps. Si vous avez la sortie exacte de `ERR_PNPM_OUTDATED_LOCKFILE` (pnpm imprime les importers en cause), elle tranche en une lecture. Et si votre satellite avait bien un `packages/` au moment de l'échec, dites-le : le retrait ci-dessus le rend sans objet, mais ça expliquerait l'écart entre nos deux mesures et c'est ce qu'il faut savoir.
Author
Owner

Les sorties exactes — et une rectification : je me suis trompé sur le symptôme

Nous avons ajouté un troisième workspace (apps/mcp) et joué le build pour de vrai. Mon corps de ticket décrit mal ce qui se passe, et la réalité est plus insidieuse que ce que j'annonçais.

Ce que j'avais annoncé, et qui est faux

J'écrivais que pnpm install --frozen-lockfile échouerait avec un ERR_PNPM_OUTDATED_LOCKFILE. Il n'échoue pas. Il n'y a aucune erreur, aucun avertissement, et le build continue.

Ce qui se passe réellement

L'étage deps, avec le Dockerfile d'origine, sur un dépôt qui compte quatre workspaces :

#15 [deps 7/7] RUN --mount=type=secret,id=npmrc,target=/root/.npmrc     pnpm install --frozen-lockfile
#15 1.225 Scope: all 3 workspace projects

Scope: all 3 workspace projects — alors que le lockfile en déclare quatre. pnpm installe silencieusement le sous-ensemble dont il voit les manifestes, et n'en dit rien : ceux qu'on ne lui a pas copiés n'existent tout simplement pas de son point de vue.

Après correction, le même étage :

#18 [deps 10/10] RUN --mount=type=secret,id=npmrc,target=/root/.npmrc     pnpm install --frozen-lockfile
#18 1.198 Scope: all 4 workspace projects

Une installation partielle muette, donc, et pas un échec franc. C'est pire que ce que j'annonçais : un échec aurait au moins arrêté le build. Là, l'image se construit — avec un workspace en moins. Chez nous, l'étage build rattrapait le manquant par une réinstallation implicite, ce qui masquait complètement le problème et le rendait dépendant de l'ordre des étages. Sur un autre montage, l'image partirait en production amputée.

Le signal existe pourtant, et il est parfaitement lisible : ce Scope: all N workspace projects que personne ne regarde. Un contrôle qui compare ce N au nombre d'importers du lockfile serait sans doute plus simple à écrire qu'un contrôle qui relit le Dockerfile.

Ma proposition n°1 ne marche pas — le joker aplatit les chemins

J'avais proposé « copier les manifestes par motif ». Testé :

COPY apps/*/package.json apps/

Docker aplatit les chemins des sources : les trois fichiers atterrissent sur un unique apps/package.json, chacun écrasant le précédent. pnpm ne retrouve aucun workspace.

COPY --parents apps/*/package.json ./ préserverait l'arborescence, mais --parents n'existe que sur le canal expérimental de BuildKit (# syntax=docker/dockerfile:1-labs). Nous l'avons écarté : ça n'a pas sa place dans un gabarit destiné à trente dépôts.

Nous avons donc fait le plus bête — une ligne COPY explicite par workspace, dans les deux étages. Ça marche, et ça ne résout évidemment pas le problème de fond : le quatrième workspace du prochain satellite reproduira exactement le même trou.

Vérifié après correction

docker build --target api --secret id=npmrc,src=$HOME/.npmrc   → succès
docker build --target web --secret id=npmrc,src=$HOME/.npmrc   → succès

Les deux cibles, sur un dépôt à quatre workspaces. C'était la première construction d'image jouée sur ce satellite.

Un détail qui a coûté du temps et qui vous intéressera peut-être

Le postinstall d'apps/mcp régénère un client depuis openapi.json. L'étage deps ne copie que les manifestes : il a donc fallu y ajouter les fichiers dont ce postinstall a besoin — exactement comme les trois lignes déjà présentes pour le fragment Prisma d'apps/api, et pour la même raison, documentée dans le Dockerfile.

Autrement dit, ce n'est pas un cas particulier : tout workspace avec un postinstall non trivial devra ajouter ses propres lignes à l'étage deps. Si la copie des manifestes est un jour repensée, ce point mérite d'entrer dans la réflexion.

Je corrige le corps du ticket ? Dites-moi si vous préférez que je le réécrive ou que ce commentaire suffise.

## Les sorties exactes — et une rectification : je me suis trompé sur le symptôme Nous avons ajouté un troisième workspace (`apps/mcp`) et joué le build pour de vrai. **Mon corps de ticket décrit mal ce qui se passe**, et la réalité est plus insidieuse que ce que j'annonçais. ### Ce que j'avais annoncé, et qui est faux J'écrivais que `pnpm install --frozen-lockfile` **échouerait** avec un `ERR_PNPM_OUTDATED_LOCKFILE`. **Il n'échoue pas.** Il n'y a aucune erreur, aucun avertissement, et le build continue. ### Ce qui se passe réellement L'étage `deps`, avec le `Dockerfile` d'origine, sur un dépôt qui compte quatre workspaces : ``` #15 [deps 7/7] RUN --mount=type=secret,id=npmrc,target=/root/.npmrc pnpm install --frozen-lockfile #15 1.225 Scope: all 3 workspace projects ``` **`Scope: all 3 workspace projects`** — alors que le lockfile en déclare quatre. pnpm installe silencieusement le sous-ensemble dont il voit les manifestes, et n'en dit rien : ceux qu'on ne lui a pas copiés n'existent tout simplement pas de son point de vue. Après correction, le même étage : ``` #18 [deps 10/10] RUN --mount=type=secret,id=npmrc,target=/root/.npmrc pnpm install --frozen-lockfile #18 1.198 Scope: all 4 workspace projects ``` **Une installation partielle muette**, donc, et pas un échec franc. C'est pire que ce que j'annonçais : un échec aurait au moins arrêté le build. Là, l'image se construit — avec un workspace en moins. Chez nous, l'étage `build` rattrapait le manquant par une réinstallation implicite, ce qui masquait complètement le problème et le rendait dépendant de l'ordre des étages. Sur un autre montage, l'image partirait en production amputée. Le signal existe pourtant, et il est parfaitement lisible : ce `Scope: all N workspace projects` que personne ne regarde. Un contrôle qui compare ce N au nombre d'importers du lockfile serait sans doute plus simple à écrire qu'un contrôle qui relit le `Dockerfile`. ### Ma proposition n°1 ne marche pas — le joker aplatit les chemins J'avais proposé « copier les manifestes par motif ». Testé : ```dockerfile COPY apps/*/package.json apps/ ``` Docker **aplatit les chemins des sources** : les trois fichiers atterrissent sur un unique `apps/package.json`, chacun écrasant le précédent. pnpm ne retrouve aucun workspace. `COPY --parents apps/*/package.json ./` préserverait l'arborescence, mais `--parents` n'existe que sur le canal expérimental de BuildKit (`# syntax=docker/dockerfile:1-labs`). Nous l'avons écarté : ça n'a pas sa place dans un gabarit destiné à trente dépôts. Nous avons donc fait le plus bête — une ligne `COPY` explicite par workspace, dans les deux étages. Ça marche, et ça ne résout évidemment pas le problème de fond : le quatrième workspace du prochain satellite reproduira exactement le même trou. ### Vérifié après correction ``` docker build --target api --secret id=npmrc,src=$HOME/.npmrc → succès docker build --target web --secret id=npmrc,src=$HOME/.npmrc → succès ``` Les deux cibles, sur un dépôt à quatre workspaces. C'était la première construction d'image jouée sur ce satellite. ### Un détail qui a coûté du temps et qui vous intéressera peut-être Le `postinstall` d'`apps/mcp` régénère un client depuis `openapi.json`. L'étage `deps` ne copie que les manifestes : il a donc fallu y ajouter les fichiers dont ce `postinstall` a besoin — exactement comme les trois lignes déjà présentes pour le fragment Prisma d'`apps/api`, et pour la même raison, documentée dans le `Dockerfile`. Autrement dit, ce n'est pas un cas particulier : **tout workspace avec un `postinstall` non trivial devra ajouter ses propres lignes à l'étage `deps`.** Si la copie des manifestes est un jour repensée, ce point mérite d'entrer dans la réflexion. Je corrige le corps du ticket ? Dites-moi si vous préférez que je le réécrive ou que ce commentaire suffise.
Author
Owner

Votre Scope: all 3 workspace projects est la ligne qui manquait, et elle renverse ma conclusion.

J'avais mesuré que --frozen-lockfile n'échoue pas sur un manifeste absent, et j'en avais tiré « le cas est bénin ». C'est l'inverse : pnpm installe le sous-ensemble qu'il voit, sans un mot, et l'image se construit — amputée. Un échec franc aurait au moins arrêté le build. J'ai pris l'absence d'erreur pour une absence de problème, et c'est exactement la faute que le socle est censé ne pas commettre.

Chez moi le manquant était rattrapé à l'étage suivant, comme chez vous — c'est ce qui m'a fait conclure trop vite. Vous le dites mieux que moi : le résultat dépend de l'ordre des étages, ce qui est la définition d'un défaut qui attend son moment.

Et vous confirmez de première main les deux impasses que j'avais annoncées sans les avoir jouées en Docker : le joker aplatit les chemins, et COPY --parents demande le canal expérimental de BuildKit. Merci de les avoir vérifiées plutôt que de me croire.

Ce que ça donne : un contrôle, puisqu'il n'y a pas de raccourci.

tooling.workspaces-dockerfile, dans @eqs/tooling, bloquant et statique :

  • il lit les importers de pnpm-lock.yaml — ce que pnpm lit vraiment dans l'image — et les compare aux COPY …/package.json du Dockerfile ;
  • il compte les étages qui lancent pnpm install. Un manifeste copié dans deps et oublié dans prod-deps refait le trou dans l'image finale, celle qui part en production. C'est le cas que votre correction manuelle aurait pu laisser passer.
bloquant  Dockerfile ne copie jamais apps/mcp/package.json, que le verrou déclare
bloquant  Dockerfile copie apps/web/package.json 1 fois pour 2 étages qui installent

Vérifié dans les deux sens : il nomme les deux cas, et il se tait sur un Dockerfile correct. Vert sur le gabarit tel qu'il est.

Sur votre proposition de contrôler le Scope: all N : elle est plus élégante, et je ne l'ai pas retenue pour une raison de placement. Cette ligne n'existe qu'au moment du docker build, c'est-à-dire après le push sur main, sur un service déjà déployé (D-013). Lire le Dockerfile est moins joli mais tourne dans pnpm check, avant que les commits ne quittent la machine — là où la doctrine veut la porte. Le vrai argument contre votre version est le moment, pas la difficulté.

Sur les postinstall non triviaux : point noté, et c'est le meilleur du commentaire. Le contrôle ne le voit pas, et je l'écris dans son code plutôt que de le laisser se découvrir — tout workspace qui régénère quelque chose à l'installation devra ajouter ses propres lignes à l'étage deps, comme apps/api le fait déjà pour le fragment Prisma. Si la copie des manifestes est un jour repensée, c'est la contrainte qui décidera de la forme.

Ne réécrivez pas le corps du ticket : ce commentaire est plus utile que le corps corrigé — on y voit ce qu'on croyait, ce qu'on a mesuré, et l'écart entre les deux. C'est ce qu'on relit dans six mois.

Branche fix/workspaces-dockerfile. Je ferme.

Votre `Scope: all 3 workspace projects` est la ligne qui manquait, et elle renverse ma conclusion. J'avais mesuré que `--frozen-lockfile` **n'échoue pas** sur un manifeste absent, et j'en avais tiré « le cas est bénin ». C'est l'inverse : pnpm installe le sous-ensemble qu'il voit, **sans un mot**, et l'image se construit — amputée. Un échec franc aurait au moins arrêté le build. J'ai pris l'absence d'erreur pour une absence de problème, et c'est exactement la faute que le socle est censé ne pas commettre. Chez moi le manquant était rattrapé à l'étage suivant, comme chez vous — c'est ce qui m'a fait conclure trop vite. Vous le dites mieux que moi : le résultat dépend de l'ordre des étages, ce qui est la définition d'un défaut qui attend son moment. Et vous confirmez de première main les deux impasses que j'avais annoncées sans les avoir jouées en Docker : le joker aplatit les chemins, et `COPY --parents` demande le canal expérimental de BuildKit. Merci de les avoir vérifiées plutôt que de me croire. **Ce que ça donne : un contrôle, puisqu'il n'y a pas de raccourci.** `tooling.workspaces-dockerfile`, dans `@eqs/tooling`, **bloquant** et **statique** : - il lit les `importers` de `pnpm-lock.yaml` — ce que pnpm lit vraiment dans l'image — et les compare aux `COPY …/package.json` du `Dockerfile` ; - il compte les étages qui lancent `pnpm install`. Un manifeste copié dans `deps` et oublié dans `prod-deps` refait le trou dans l'image **finale**, celle qui part en production. C'est le cas que votre correction manuelle aurait pu laisser passer. ``` bloquant Dockerfile ne copie jamais apps/mcp/package.json, que le verrou déclare bloquant Dockerfile copie apps/web/package.json 1 fois pour 2 étages qui installent ``` Vérifié dans les deux sens : il nomme les deux cas, et il se tait sur un `Dockerfile` correct. Vert sur le gabarit tel qu'il est. **Sur votre proposition de contrôler le `Scope: all N`** : elle est plus élégante, et je ne l'ai pas retenue pour une raison de placement. Cette ligne n'existe qu'au moment du `docker build`, c'est-à-dire après le push sur `main`, sur un service déjà déployé (D-013). Lire le `Dockerfile` est moins joli mais tourne dans `pnpm check`, avant que les commits ne quittent la machine — là où la doctrine veut la porte. Le vrai argument contre votre version est le moment, pas la difficulté. **Sur les `postinstall` non triviaux** : point noté, et c'est le meilleur du commentaire. Le contrôle ne le voit pas, et je l'écris dans son code plutôt que de le laisser se découvrir — tout workspace qui régénère quelque chose à l'installation devra ajouter ses propres lignes à l'étage `deps`, comme `apps/api` le fait déjà pour le fragment Prisma. Si la copie des manifestes est un jour repensée, c'est la contrainte qui décidera de la forme. **Ne réécrivez pas le corps du ticket** : ce commentaire est plus utile que le corps corrigé — on y voit ce qu'on croyait, ce qu'on a mesuré, et l'écart entre les deux. C'est ce qu'on relit dans six mois. Branche `fix/workspaces-dockerfile`. Je ferme.
Author
Owner

Repris en 0.8.0 — la réponse est meilleure que ma proposition, mais elle ne se charge pas

Repris le 2026-09-10 sur le satellite qui avait ouvert ce ticket.

Sur la façon dont vous l'avez fermé

Vous n'avez pas modifié le Dockerfile, et vous avez eu raison : le gabarit ne connaît que deux workspaces et ne peut pas deviner ceux d'un satellite. À la place, un contrôle statique — tooling.workspaces-dockerfile — qui compare les importers du lockfile aux COPY …/package.json.

C'est plus fin que ce que je proposais. Ma piste n°1 (copier par motif) ne marchait pas — Docker aplatit les chemins — et ma piste n°2 (un contrôle qui relit le Dockerfile) était la bonne mais moins bien posée : partir du lockfile plutôt que du fichier attrape aussi le cas où quelqu'un ajoute un workspace sans y penser. Le signal Scope: all N workspace projects que je citais est exactement ce que votre contrôle rend inutile à surveiller à la main.

Le problème : nous n'avons pas pu le vérifier

$ pnpm exec eqs doctor
  ! @eqs/tooling : @eqs/tooling/conformite n'a pas pu être chargé :
    Cannot find module '…/@eqs/tooling/scripts/workspaces-dockerfile.js'

scripts/ n'est pas dans le champ files de @eqs/tooling@0.8.0. L'import étant en tête de module, tous les contrôles du paquet tombent avec lui. Ticket ouvert : eqs-platform#44.

Donc : le défaut de ce ticket-ci est bien traité en intention, mais le garde-fou qui devait le fermer ne tourne chez personne aujourd'hui. Nous le rejouerons dès la republication et je viendrai le confirmer ici.

Ce que nous gardons, et qui n'est plus un contournement

Nos lignes COPY apps/mcp/… et les fichiers supplémentaires d'apps/web restent, mais leur commentaire a été réécrit : ce n'est plus « un contournement en attente d'arbitrage amont », c'est notre propre besoin — nous avons un troisième workspace que le gabarit ignore légitimement.

Une chose que la vérification a révélée, et qui pourrait vous intéresser

En jouant docker build pour de vrai, nous avons découvert que notre copie était incomplète pour nos propres besoins : apps/web a, comme apps/mcp, un postinstall qui régénère un client Orval. Il a fallu ajouter à l'étage deps leurs tsconfig.json, leurs orval.config.ts, et pour apps/web le module de mutateur que sa configuration référence.

C'est le même mécanisme que les trois lignes déjà présentes pour le fragment Prisma d'apps/api, et documentées comme telles dans le Dockerfile. Autrement dit : tout workspace avec un postinstall non trivial doit ajouter ses propres fichiers à l'étage deps, et votre contrôle ne le verra pas — il compare des manifestes, pas des besoins de postinstall.

Ce n'est pas une critique du contrôle, qui fait ce qu'il annonce. Juste un cas voisin qui restera à la charge de chaque satellite, et qui ne se manifeste qu'à la construction d'image.

Vérifications faites ici

docker build --target api --secret id=npmrc,src=$HOME/.npmrc   → succès
docker build --target web --secret id=npmrc,src=$HOME/.npmrc   → succès

Quatre workspaces, Scope: all 4 workspace projects aux deux étages.

## Repris en 0.8.0 — la réponse est meilleure que ma proposition, mais elle ne se charge pas Repris le 2026-09-10 sur le satellite qui avait ouvert ce ticket. ### Sur la façon dont vous l'avez fermé Vous n'avez pas modifié le `Dockerfile`, et vous avez eu raison : le gabarit ne connaît que deux workspaces et ne peut pas deviner ceux d'un satellite. À la place, un contrôle statique — `tooling.workspaces-dockerfile` — qui compare les `importers` du lockfile aux `COPY …/package.json`. C'est plus fin que ce que je proposais. Ma piste n°1 (copier par motif) ne marchait pas — Docker aplatit les chemins — et ma piste n°2 (un contrôle qui relit le `Dockerfile`) était la bonne mais moins bien posée : partir du **lockfile** plutôt que du fichier attrape aussi le cas où quelqu'un ajoute un workspace sans y penser. Le signal `Scope: all N workspace projects` que je citais est exactement ce que votre contrôle rend inutile à surveiller à la main. ### Le problème : nous n'avons pas pu le vérifier ``` $ pnpm exec eqs doctor ! @eqs/tooling : @eqs/tooling/conformite n'a pas pu être chargé : Cannot find module '…/@eqs/tooling/scripts/workspaces-dockerfile.js' ``` `scripts/` n'est pas dans le champ `files` de `@eqs/tooling@0.8.0`. L'import étant en tête de module, **tous** les contrôles du paquet tombent avec lui. Ticket ouvert : [eqs-platform#44](https://git.equilibre-structures.fr/Infrastructure/eqs-platform/issues/44). Donc : le défaut de ce ticket-ci est bien traité **en intention**, mais le garde-fou qui devait le fermer ne tourne chez personne aujourd'hui. Nous le rejouerons dès la republication et je viendrai le confirmer ici. ### Ce que nous gardons, et qui n'est plus un contournement Nos lignes `COPY apps/mcp/…` et les fichiers supplémentaires d'`apps/web` **restent**, mais leur commentaire a été réécrit : ce n'est plus « un contournement en attente d'arbitrage amont », c'est notre propre besoin — nous avons un troisième workspace que le gabarit ignore légitimement. ### Une chose que la vérification a révélée, et qui pourrait vous intéresser En jouant `docker build` pour de vrai, nous avons découvert que notre copie était **incomplète pour nos propres besoins** : `apps/web` a, comme `apps/mcp`, un `postinstall` qui régénère un client Orval. Il a fallu ajouter à l'étage `deps` leurs `tsconfig.json`, leurs `orval.config.ts`, et pour `apps/web` le module de mutateur que sa configuration référence. C'est le même mécanisme que les trois lignes déjà présentes pour le fragment Prisma d'`apps/api`, et documentées comme telles dans le `Dockerfile`. Autrement dit : **tout workspace avec un `postinstall` non trivial doit ajouter ses propres fichiers à l'étage `deps`**, et votre contrôle ne le verra pas — il compare des manifestes, pas des besoins de `postinstall`. Ce n'est pas une critique du contrôle, qui fait ce qu'il annonce. Juste un cas voisin qui restera à la charge de chaque satellite, et qui ne se manifeste qu'à la construction d'image. ### Vérifications faites ici ``` docker build --target api --secret id=npmrc,src=$HOME/.npmrc → succès docker build --target web --secret id=npmrc,src=$HOME/.npmrc → succès ``` Quatre workspaces, `Scope: all 4 workspace projects` aux deux étages.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Infrastructure/eqs-satellite-template#7
No description provided.