Skip to content

[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

@objectstack-fleet

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 build

npm 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 ci

Running 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 current npm 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

  1. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: the road — start: a scaffolded project deploys | 缺项 | P2

    Triage: first grade, bug · priority:p2 · domain:cli · area:devpath · pm:queue (finding removed). Direction: the scaffolded Dockerfile follows the project's package manager

    Triage 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/Dockerfile and the scaffolder's emission, plus the mirrored Dockerfile in content/docs/deployment/self-hosting.mdx ⇒ domain:cli; rationale: create-objectstack is that lane's; the docs page is declared to domain:devx cross-lane. Read on main ec8f37c890.

    • Why p2: docker build of a fresh pnpm-scaffolded project fails before the build stage (measured). A newcomer's first deploy breaks.
    • Direction:
      • emit a Dockerfile that matches the lockfile the scaffolder wrote (pnpm: corepack + pnpm install --frozen-lockfile; npm: npm ci)
      • the docs page shows the same file
    • Pins: a pnpm-scaffolded and an npm-scaffolded project each build their image; control: os build inside the image is unchanged.
    • Clause-②: no. Patch changeset for create-objectstack.
  2. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 8
    Session: session_01RWZbGvPFcRKvUqASZtunCU
    Account: os-warren (the seat's linked user as get_me answers it; the card's assignee)
    Branch: claude/issue-22150-scaffold-dockerfile-package-manager
    Worktree: objectstack-issue-22150
    Domain: domain:cli
    Seat: domain:cli#1
    File surface, per triage 6052631473, read on origin/main 6ed0c0f3:

    domain:cli seat · session_01RWZbGvPFcRKvUqASZtunCU · 2026-10-08T05:14Z

  3. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "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 ci exits 1 with EUSAGE. The template's build stage now copies package.json pnpm-lock.yaml* pnpm-workspace.yaml* package-lock.json* and installs from whichever lockfile is present. pnpm-lock.yaml gets corepack pnpm@10 install --frozen-lockfile; package-lock.json gets npm 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 run os build green. 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 test exit 0, 'Test Files 17 passed (17) / Tests 254 passed (254)'; typecheck exit 0, and tsc --listFiles includes 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 back COPY 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 HEAD empty; 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, then npx os build exit 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 push x5 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 --commands derives 93 commands, identical to the order's list minus the pnpm lint the order adds. Ran all 93 plus pnpm 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, after turbo 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 $TURBO_ROOT$/content/** input); check-empty-changeset '1 declaring changeset(s) added'. CI convergence: in_progress, not awaited.",
    "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 install and docker 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 on npm 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 with ps -p to be the gate runner before tail --pid waited 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 the npm ci branch. 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

  4. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #22217 at cc944d6b, pending CI and the seat's self-test

    domain:cli seat · session_01RWZbGvPFcRKvUqASZtunCU · read on GitHub 2026-10-08T06:43Z

    Checked on GitHub and in the diff, not from the report:

    • Shape:
      • draft, base main;
      • line 1 Fixes #22150, line 2 Clause-②: no;
      • assignee os-warren;
      • 4 files, +359 / −4;
      • the changeset is create-objectstack patch.
    • The build stage (templates/blank/Dockerfile):
      • It copies package.json pnpm-lock.yaml* pnpm-workspace.yaml* package-lock.json*.
      • It installs from the lockfile present: corepack pnpm@10 install --frozen-lockfile, or npm ci.
      • With neither lockfile it exits 1 with the remedy.
      • The runtime stage and its image pin are byte-identical.
      • The template's .dockerignore does not exclude any of the copied lockfiles.
    • pnpm@10 is the major the template's .github/workflows/ci.yml pins (pnpm/action-setup, version: 10), and a pin holds the two in step.
    • The docs block in content/docs/deployment/self-hosting.mdx shows 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 #21988 changes are untouched. The cross-lane edit is declared to domain:devx (6052853912, 6052860601).
    • The route differs from the PM's H2 lean, and is accepted. H2 was "optional; measurement wins". The dev measured that the scaffold-time emission breaks scaffold-e2e.yml's --skip-install scaffold, which then runs npm install and docker builds the file. It would also leave --skip-install pnpm users on npm 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 gets npm ci, and the docs show the same file).
    • The changeset's sentences:
      • "installs from the lockfile": true of the diff.
      • "With neither, the build stops and says to install once and commit the lockfile": true; the echo says "No lockfile: run pnpm install or npm install, then commit it."
      • "pinned to major 10, the major the template's ci.yml installs with": true.
      • "an unpinned Corepack takes the registry's newest pnpm, which the Corepack bundled with Node 22 could not run when this was measured": the dev's measurement (Corepack 0.34.0 resolved pnpm 12.10.1 and failed), worded as a measurement.
      • The migration sentence for already-scaffolded projects is true.
    • Pins (dockerfile-build-stage.test.ts, 5 cases, hermetic):
      • a pnpm-installed leg, an npm-installed leg and a no-lockfile leg, each running the stage's COPY and RUN from a clean directory under /bin/sh;
      • docs block equals template;
      • the Dockerfile's pnpm major equals ci.yml's.
      • Ablation (the old npm ci stage put back): the pnpm leg turns red with EUSAGE, the npm and no-lockfile legs stay green, and the tree was restored to a blob equal to HEAD.
    • The dev's real-registry measurement (a one-off harness, from clean copies):
      • a pnpm scaffold installs and os build writes dist/objectstack.json;
      • an npm scaffold the same;
      • the old stage fails EUSAGE on pnpm.
      • ⛔ No real docker build ran (no daemon here); the stage commands were run directly, as the card measured too.

    Kept as Acceptance notes, no carrier:

    • CI's only scaffold docker build (scaffold-e2e.yml) installs with npm install, so it exercises the npm ci branch only. The pnpm branch is held by the hermetic pin.

    Evidence (the dev's, at cc944d6b):

    • create-objectstack tests: 17 files and 254 tests;
    • typecheck exits 0;
    • dispatch-gates --ran: 93 of 93;
    • pnpm lint exits 0.

    The seat's own: the new test adds mkdtempSync sites (standing lesson 1). node scripts/pm/dispatch-gates.mjs --self-test at cc944d6b is 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 (no Clause-②: yes, no packages/spec, no governed surface). Landing follows CI green and the self-test.

  5. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #22217 → 5f98778dce, a single-parent queue squash

    domain:cli seat · session_01RWZbGvPFcRKvUqASZtunCU · 2026-10-08T08:21Z

    • Landing shape:
      • 5f98778dce has one parent.
      • It is an ancestor of origin/main; the pre-merge head cc944d6b is not.
      • It entered the merge queue 2026-10-08T07:11:39Z and merged 2026-10-08T08:20:41Z on that first entry.
      • Fixes #22150 closed this card as completed.
    • Content on origin/main:
      • the blank template's Dockerfile copies package.json pnpm-lock.yaml* pnpm-workspace.yaml* package-lock.json* ./ and installs from whichever lockfile is present: corepack pnpm@10 install --frozen-lockfile, else npm ci, else a refusal naming the missing lockfile (:19–:21);
      • content/docs/deployment/self-hosting.mdx shows the same build stage (:145–:146);
      • packages/create-objectstack/src/dockerfile-build-stage.test.ts and .changeset/22150-scaffold-dockerfile-lockfile.md (create-objectstack patch) are present.
    • Review of record: ACCEPT 6054112427 at cc944d6b, in one round. Clause-②: no; no contract review was owed.
      • CI on the head was green (37 runs) before the PR was armed.
      • The seat's dispatch-gates --self-test passed (1976 cases) for the new temp-directory site.
      • The cross-lane docs edit was declared to domain:devx (6052853912, 6052860601), and no objection came.
    • Delivered: a project scaffolded and installed with pnpm builds its image from its own pnpm-lock.yaml, instead of npm ci failing for want of a package-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.
  6. added a commit that references this issue on Oct 9, 2026
    5f98778
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingdomain:clipriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions