Repository navigation
[finding] create-objectstack: the scaffolded Dockerfile runs npm ci, but a pnpm-scaffolded project ships only pnpm-lock.yaml — docker build fails at the build stage #22150
Description
Filing gate: ① product defect with a named landing spot and a reproduction (finding class (a): reproducible defect).
reach: public entry measured once — the Dockerfile's own build stage (COPY package*.json ./ + RUN npm ci) run against a fresh npm create objectstack@latest project exits 1 with EUSAGE, before os build ever runs.
Reader: domain:cli seat (create-objectstack owns the template); fix lands in packages/create-objectstack/src/templates/blank/Dockerfile or the scaffolder's package-manager-aware emission, plus the matching Dockerfile in content/docs/deployment/self-hosting.mdx.
Dedup: search_issues "scaffolded Dockerfile npm ci fails pnpm-lock.yaml package-lock.json docker build create-objectstack" (open + closed) → 0 cards on this defect; nearest are the registry-canary cards #20382 / #19510 / #16500 (closed, about the npm install itself, not the Dockerfile).
Filed on the maintainer's instruction in this session: 「设想你是一个新人,第一次打开 github objectstack 项目主页,了解本项目,并按照文档指引执行完整的试用流程,并对阅读文档和试用过程中遇到的问题立 issue」.
Summary
npm create objectstack@latest my-app detects pnpm when it is on PATH, installs with pnpm, and writes pnpm-lock.yaml (no package-lock.json). The Dockerfile it writes into the same project hard-codes the build stage as:
FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx os buildnpm ci refuses to run without package-lock.json / npm-shrinkwrap.json, so the first command the README's Ship it section tells a new user to run (docker build -t my-app . && docker compose up -d) fails before the artifact is compiled. The README states the scaffolded project is "container-ready", and content/docs/deployment/self-hosting.mdx repeats the same npm ci Dockerfile.
The scaffolder is already package-manager aware elsewhere: the closing summary prints pnpm run dev, and the generated .github/workflows/ci.yml uses pnpm/action-setup + pnpm install --frozen-lockfile and comments that pnpm-lock.yaml "has to be committed". Only the Dockerfile still assumes npm.
Reproduction
Environment: create-objectstack@17.7.0, Node v22.22.0, pnpm 10.31.0 on PATH, npm 10.9.4.
npm create objectstack@latest my-app # scaffolder reports "→ Installing dependencies..." via pnpm
cd my-app
ls *lock* # pnpm-lock.yaml skills-lock.json (no package-lock.json)
grep -n 'npm ci' Dockerfile # 16:RUN npm ciRunning the build stage exactly as the Dockerfile does (copy the tree without node_modules, run npm ci):
npm error code EUSAGE
npm error
npm error The `npm ci` command can only install with an existing package-lock.json or
npm error npm-shrinkwrap.json with lockfileVersion >= 1. Run an install with npm@5 or
npm error later to generate a package-lock.json file, then try again.
Exit code 1, so docker build stops at RUN npm ci. (This session had no Docker daemon, so the stage was reproduced by running the same two commands in a copy of the project; the failure does not depend on Docker.)
Expected
docker build -t my-app . on a freshly scaffolded project succeeds with whichever package manager the scaffolder actually used — or the scaffolder writes a Dockerfile that matches the lockfile it just produced.
Actual
The build stage fails with EUSAGE for every project scaffolded on a machine where pnpm is present.
Suggested direction
Either keeps the Dockerfile and the lockfile in agreement:
- Emit the build stage from the detected package manager (the scaffolder already carries
detect-package-manager.ts): for pnpm,COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./+corepack enable && pnpm install --frozen-lockfile; for npm, the currentnpm ci. - Or make the stage package-manager neutral (
COPY package.json ./+npm install --no-audit --no-fund), accepting the loss of lockfile pinning inside the image.
The self-hosting docs page should follow whichever shape lands.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 8, 2026
Path: the road — start: a scaffolded project deploys | 缺项 | P2
Triage: first grade,
bug·priority:p2·domain:cli·area:devpath·pm:queue(findingremoved). Direction: the scaffolded Dockerfile follows the project's package managerTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-08T04:58Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/create-objectstack/src/templates/blank/Dockerfileand the scaffolder's emission, plus the mirrored Dockerfile incontent/docs/deployment/self-hosting.mdx⇒domain:cli; rationale:create-objectstackis that lane's; the docs page is declared todomain:devxcross-lane. Read onmainec8f37c890.docker buildof a fresh pnpm-scaffolded project fails before the build stage (measured). A newcomer's first deploy breaks.corepack+pnpm install --frozen-lockfile; npm:npm ci)os buildinside the image is unchanged.Clause-②: no. Patch changeset forcreate-objectstack.objectstack-fleet commentedon Oct 8, 2026
Claim: PM loop round 8
Session:
session_01RWZbGvPFcRKvUqASZtunCUAccount:
os-warren(the seat's linked user asget_meanswers it; the card's assignee)Branch:
claude/issue-22150-scaffold-dockerfile-package-managerWorktree:
objectstack-issue-22150Domain:
domain:cliSeat:
domain:cli#1File surface, per triage
6052631473, read onorigin/main6ed0c0f3:packages/create-objectstack/src/templates/blank/Dockerfile: its build stage (COPY package*.json ./+RUN npm ci,:15–:16).packages/create-objectstack/src/index.ts(the post-install Dockerfile step about:510, besidepinRuntimeImage) andruntime-image.tsor a new module beside it, together withdetect-package-manager.ts's answer.packages/create-objectstack/src/(pnpm-scaffolded and npm-scaffolded projects each get a build stage that matches their lockfile).domain:devx:content/docs/deployment/self-hosting.mdx, the mirrored Dockerfile block only. It is declared on the devx seat post..changeset/*.md:create-objectstackpatch.:latestwhile its own comment says to pin the CLI version #9017) are unchanged. ⛔ Nopackages/cli. (Stop on breach and explain in the report.)Container & model:
S,mode:subagent,model: default (opus).dispatch-gates --tierover the path gives no path-derived mandate.Clause-②: no
Responsibility:
platform code: create-objectstack's blank template Dockerfile hard-codes COPY package*.json + npm ci (templates/blank/Dockerfile :15-:16) while the scaffolder installs with the detected package manager|none: no test builds the scaffolded Dockerfile's build stage against the lockfile the scaffolder wrote|every newcomer who scaffolds with pnpm on PATH and follows the README's Ship it section (measured once in the card)Thread-read: 6052631473
Serial constraints cleared: read 2026-10-08T05:14Z:
filename): PR chore: version packages (next) #21988 (Version Packages) touchescontent/docs/deployment/self-hosting.mdx(the runtime image tag lines,:75and:93) andcreate-objectstack'sCHANGELOG.md/package.json. That is release machinery in a different region of the page; ordinary concurrency.6051334172) writes onecreate-objectstackfile, the template'sobjectstack.config.ts^17handshake. That is not this claim's files.envelope-caller-census.test.tswalks.cache/, sopnpm testgoes red onmainright after the documentedpnpm objectui:build— the objectui checkout it leaves behind is counted as a 29th SDK call site #22153) touch nothing here.domain:cliseat ·session_01RWZbGvPFcRKvUqASZtunCU· 2026-10-08T05:14Zobjectstack-fleet commentedon Oct 8, 2026
os-dev-report$TURBO_ROOT$ /content/** input); check-empty-changeset '1 declaring changeset(s) added'. CI convergence: in_progress, not awaited.",
{
"issue": 22150,
"status": "done",
"branch": "claude/issue-22150-scaffold-dockerfile-package-manager",
"pr": "#22217",
"session": "session_01RWZbGvPFcRKvUqASZtunCU",
"premise_still_valid": true,
"summary": "Reproduced H1 on a real pnpm scaffold: the old stage copies only package.json and
npm ciexits 1 with EUSAGE. The template's build stage now copiespackage.json pnpm-lock.yaml* pnpm-workspace.yaml* package-lock.json*and installs from whichever lockfile is present. pnpm-lock.yaml getscorepack pnpm@10 install --frozen-lockfile; package-lock.json getsnpm ci; with neither, the build fails loudly with the remedy. Measured from clean copies against the registry: pnpm- and npm-scaffolded projects both install and runos buildgreen. The self-hosting docs block shows the same stage, and a new hermetic pin in create-objectstack runs the stage's real install, with an ablation on record. Route differs from the PM's H2 lean (no scaffold-time emission); the measured reasons are under deviations, and the ruling's outcome is unchanged.","tests": "HEAD cc944d6. Package:
pnpm --filter create-objectstack testexit 0, 'Test Files 17 passed (17) / Tests 254 passed (254)';typecheckexit 0, andtsc --listFilesincludes all 17 src/*.test.ts (the new one among them). The dependency closure (pnpm --filter 'create-objectstack^...' build, i.e. spec) was built first, exit 0. The package has no integration tier. New pin src/dockerfile-build-stage.test.ts: 5 passed. Its legs: a pnpm-installed fixture, an npm-installed fixture, and a no-lockfile project, each running the stage's COPY+RUN from a clean dir under /bin/sh, offline, with a stub corepack that runs the repo-pinned pnpm and refuses a major mismatch. Two more cases: the docs build stage equals the template's, and the Dockerfile pnpm major equals ci.yml's. ABLATION: scripts/ablation-replace.mjs, inside one lock hold on cc944d6; no build/dist on the path, since the test reads src/templates through copyDir. It put backCOPY package*.json ./+RUN npm ci: anchor 1 to 0, blob ce699d28904a to 04316706900a, on-disk new-line count 0 and old-line count 1. Result 'Tests 3 failed | 2 passed (5)'. Red: the pnpm leg (npm error code EUSAGE), the docs pin and the major pin. Green: the npm and no-lockfile legs. Restore: blob back to ce699d28904a, equal to HEAD;git diff HEADempty; porcelain clean. REAL-REGISTRY MEASUREMENT, one-off, with a scratch harness that emulates each stage from a clean copy (BuildKit glob COPY, /bin/sh=dash, fresh HOME/COREPACK_HOME/store), with stage text taken from the committed file. pnpm-scaffolded project (lockfile written by pnpm 10.28.0): the old stage exits 1 with EUSAGE; the new stage installs with pnpm 10.34.6 ('Lockfile is up to date'), exit 0, thennpx os buildexit 0 and dist/objectstack.json is written. npm-scaffolded project: the new stage reports 'added 524 packages', exit 0, and os build exit 0; the old stage also exits 0. No lockfile: exit 1 with the remedy line. Corepack 0.34.0 / Node 22.22.0: unpinned, it resolves pnpm 12.10.1 and fails with MODULE_NOT_FOUND on bin/pnpm.cjs; COREPACK_DEFAULT_TO_LATEST=0 gives 10.13.1, below engines.pnpm >=10.15; pnpm@10 gives 10.34.6 and works; pnpm@11.28.2 also runs. Without pnpm-workspace.yaml, the frozen install exits 0 but prints 'Ignored build scripts: better-sqlite3@13.0.3, esbuild@0.28.2'.","mcp_calls": "0 — no MCP GitHub tool called, read or write",
"api_writes": "3, all as one repository_dispatch each to the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches), executed as objectstack-fleet[bot]. (1) pr_create: POST /repos/objectstack-ai/objectstack/pulls, draft #22217; body read back as 11483 bytes sent and stored, identical. (2) label-write --assign os-warren: POST /repos//issues/22217/assignees; read back as assignee os-warren. (3) post-stamped: POST /repos//issues/22150/comments, this report. Zero label writes: the order names no label, and skip-changeset does not apply because a patch changeset ships.
git pushx5 is not REST. The card's assignee was not written.","files_changed": [
".changeset/22150-scaffold-dockerfile-lockfile.md (+14/-0)",
"content/docs/deployment/self-hosting.mdx (+8/-2, the Dockerfile block build stage only; the image-tag lines are untouched)",
"packages/create-objectstack/src/dockerfile-build-stage.test.ts (+329/-0)",
"packages/create-objectstack/src/templates/blank/Dockerfile (+8/-2, the build stage only; the runtime stage is byte-identical)"
],
"gates": "HEAD cc944d6, clean tree.
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderives 93 commands, identical to the order's list minus thepnpm lintthe order adds. Ran all 93 pluspnpm lint: 94/94 exit 0.dispatch-gates --ran: '93 derived, 93 run, 0 NOT-MEASURED, 0 UNRUN'. Five gates exited 3 (PREREQUISITE NOT MET) on an earlier pass before builds: lint check:doc-formula-expressions, lint check:doc-security-posture, check:docs-transcript-drift, check:dual-build-cjs-loads, check:lean-entry-closure. They exit 0 in the final union, afterturbo run build --filter='!@objectstack/docs' --concurrency=2(72/72).pnpm lint(eslint . --no-inline-config) exit 0, no findings, about 108s on a shared box. Narrowed lint was also run: eslint lints 1 of the 4 changed paths (the other three: 'File ignored because no matching configuration was supplied'), 0 errors and 0 warnings, and eslint.config.mjs never enables type-aware linting. Quoted verdicts: check:nul-bytes 'OK (scanned 10161 text file(s) ... no raw ASCII control bytes)'; check:cross-package-test-inputs 'OK: 30 package(s) read outside themselves, all declared' (the new test reads content/docs under the existing"line_budget": "n/a. No skills/** and no governed ledger touched. Diff: 4 files, +359/-4, under the 5000-line human-merge threshold.",
"deviations": [
"Route (PM reading H2, a suggested route): I did NOT emit the build stage post-install from the detected package manager. Measured: scaffold-e2e.yml scaffold-local scaffolds with --skip-install on a runner whose pnpm is a Corepack shim, then runs
npm installanddocker builds the scaffolded Dockerfile. A stage emitted from the detected manager would COPY a pnpm-lock.yaml that does not exist there. Emitting only after a successful install instead leaves --skip-install pnpm users onnpm ci. And outside a project, the manager detected on such a runner depends on Corepack's LastKnownGood or latest. So I shipped one stage that reads the lockfile at build time, which is right on every path. The ruling's outcome holds: pnpm gets Corepack + frozen install, npm gets npm ci, and the docs show the same file. index.ts, runtime-image.ts and detect-package-manager.ts, named on the claim's surface, are untouched; the surface used is a subset.","The ruling's 'corepack + pnpm install --frozen-lockfile' is spelled
corepack pnpm@10 install --frozen-lockfile. An unpinned Corepack resolves pnpm 12, which Corepack 0.34 cannot run, and the bundled default is below engines.pnpm. Major 10 is the major the template's ci.yml pins.","One test file beyond the triage pins also holds two equalities: the docs block's build stage equals the template's, and the Dockerfile's pnpm major equals ci.yml's. No turbo.json change: content/** is already a declared input of create-objectstack#test.",
"The first ablation run also reddened the npm leg. The cause was an over-specific assertion in my own test, which required pnpm-workspace.yaml in the npm copy set. Commit cc944d6 fixed it, and the ablation was re-run on that head.",
"Attribution: commits carry the order's model-free trailer pair. The harness reminder's model-named trailer and its two-line PR footer were not used (os-dev.md and AGENTS.md take precedence). The PR body ends with the AGENTS session-URL footer block only.",
"Process: one background PID was read back with
pgrep -f. It was confirmed withps -pto be the gate runner beforetail --pidwaited on it; nothing was killed by name. The first lock attempt for the new test returned 99 (NOT MEASURED) behind another agent's long hold; it was re-acquired on the kept slot."],
"open_questions": [],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted in the PR's Acceptance notes, not filed. CI's only docker build of a scaffold (scaffold-e2e.yml scaffold-local) installs with
npm install, so it exercises only thenpm cibranch. The pnpm branch is covered by the hermetic pin and the one-off real-registry measurement. Coverage note, not a defect; scaffold-e2e.yml is outside this card's surface. Dedupe words: scaffold-e2e docker pnpm branch, scaffolded Dockerfile pnpm docker build CI, create-objectstack docker leg npm only"]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 8, 2026
ACCEPT — PR #22217 at
cc944d6b, pending CI and the seat's self-testdomain:cliseat ·session_01RWZbGvPFcRKvUqASZtunCU· read on GitHub 2026-10-08T06:43ZChecked on GitHub and in the diff, not from the report:
main;Fixes #22150, line 2Clause-②: no;os-warren;create-objectstackpatch.templates/blank/Dockerfile):package.json pnpm-lock.yaml* pnpm-workspace.yaml* package-lock.json*.corepack pnpm@10 install --frozen-lockfile, ornpm ci..dockerignoredoes not exclude any of the copied lockfiles.pnpm@10is the major the template's.github/workflows/ci.ymlpins (pnpm/action-setup,version: 10), and a pin holds the two in step.content/docs/deployment/self-hosting.mdxshows the same build stage, and a pin holds that too. Only the Dockerfile block moved; the image-tag lines the release PR chore: version packages (next) #21988 changes are untouched. The cross-lane edit is declared todomain:devx(6052853912,6052860601).scaffold-e2e.yml's--skip-installscaffold, which then runsnpm installanddocker builds the file. It would also leave--skip-installpnpm users onnpm ci. One stage that reads the lockfile at build time is right on every path, and it delivers the ruling's outcome (pnpm gets Corepack plus a frozen install, npm getsnpm ci, and the docs show the same file).dockerfile-build-stage.test.ts, 5 cases, hermetic):/bin/sh;ci.yml's.npm cistage put back): the pnpm leg turns red withEUSAGE, the npm and no-lockfile legs stay green, and the tree was restored to a blob equal to HEAD.os buildwritesdist/objectstack.json;EUSAGEon pnpm.docker buildran (no daemon here); the stage commands were run directly, as the card measured too.Kept as Acceptance notes, no carrier:
docker build(scaffold-e2e.yml) installs withnpm install, so it exercises thenpm cibranch only. The pnpm branch is held by the hermetic pin.Evidence (the dev's, at
cc944d6b):create-objectstacktests: 17 files and 254 tests;dispatch-gates --ran: 93 of 93;pnpm lintexits 0.The seat's own: the new test adds
mkdtempSyncsites (standing lesson 1).node scripts/pm/dispatch-gates.mjs --self-testatcc944d6bis running and is read before landing.CI on
cc944d6b, read 2026-10-08T06:43Z: 23 success · 3 skipped · 8 in progress · 0 red. ⛔ Not green.Clause-②: no: no contract review is owed (noClause-②: yes, nopackages/spec, no governed surface). Landing follows CI green and the self-test.objectstack-fleet commentedon Oct 8, 2026
Landed: PR #22217 →
5f98778dce, a single-parent queue squashdomain:cliseat ·session_01RWZbGvPFcRKvUqASZtunCU· 2026-10-08T08:21Z5f98778dcehas one parent.origin/main; the pre-merge headcc944d6bis not.Fixes #22150closed this card as completed.origin/main:Dockerfilecopiespackage.json pnpm-lock.yaml* pnpm-workspace.yaml* package-lock.json* ./and installs from whichever lockfile is present:corepack pnpm@10 install --frozen-lockfile, elsenpm ci, else a refusal naming the missing lockfile (:19–:21);content/docs/deployment/self-hosting.mdxshows the same build stage (:145–:146);packages/create-objectstack/src/dockerfile-build-stage.test.tsand.changeset/22150-scaffold-dockerfile-lockfile.md(create-objectstackpatch) are present.6054112427atcc944d6b, in one round.Clause-②: no; no contract review was owed.dispatch-gates --self-testpassed (1976 cases) for the new temp-directory site.domain:devx(6052853912,6052860601), and no objection came.pnpm-lock.yaml, instead ofnpm cifailing for want of apackage-lock.json. An npm project builds exactly as before. A project with no lockfile is refused with a sentence that says why, rather than an unpinned install.fix(create-objectstack): the scaffolded Dockerfile installs from the …