Repository navigation
fix(create-objectstack): the scaffolded Dockerfile installs from the project's own lockfile - #22217
Merged
objectstack-fleet[bot] merged 4 commits intoOct 8, 2026
Conversation
…ect's own lockfile The build stage copied package*.json and ran `npm ci`, but a project scaffolded where pnpm is on PATH carries only pnpm-lock.yaml, so `npm ci` exited 1 with EUSAGE and `docker build` stopped before `os build`. The stage now copies whichever lockfile exists and installs from it: pnpm-lock.yaml -> `corepack pnpm@10 install --frozen-lockfile` (the major the template's CI workflow pins), package-lock.json -> `npm ci`, neither -> a loud failure naming the remedy. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
…om a clean copy Pins, for a pnpm-installed and an npm-installed scaffold, that the stage's COPY takes that project's lockfile and its RUN line installs from it under /bin/sh, offline, with a stub Corepack standing in for the repository's own pinned pnpm. A project with no lockfile must stop with the remedy. The self-hosting guide shows the same build stage the template ships, and the stage's pnpm major stays the one the template's CI workflow pins. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
…ld stage Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
… pnpm workspace file Under the old npm-only stage the npm leg went red only because it also required pnpm-workspace.yaml in the npm copy set. It now asserts the npm lockfile is copied, the pnpm lockfile is not, and npm ci ran, so the old stage reddens the pnpm leg alone. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
Contributor
📓 Docs Drift Check
What this run could not see
Coarse fallback — 1 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This was referenced Oct 8, 2026
objectstack-fleet
Bot
deleted the
claude/issue-22150-scaffold-dockerfile-package-manager
branch
October 8, 2026 08:20
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #22150
Clause-②: no
What was wrong
Where pnpm is on PATH,
create-objectstackinstalls with pnpm, so the project getspnpm-lock.yamland nopackage-lock.json. TheDockerfileit writes copiedpackage*.jsonand rannpm ci, sodocker buildstopped atRUN npm ciwithEUSAGEbeforeos buildran.content/docs/deployment/self-hosting.mdxshowed the same stage.What changed
packages/create-objectstack/src/templates/blank/Dockerfile, build stage only:COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml* package-lock.json* ./copies whichever of these exist.RUNinstalls from the lockfile that is present:pnpm-lock.yamlgetscorepack pnpm@10 install --frozen-lockfile,package-lock.jsongetsnpm ci. With neither, the build stops with "No lockfile: run pnpm install or npm install, then commit it."FROM node:22-slim AS build,COPY . .,RUN npx os buildand the whole runtime stage are byte-identical tomain.content/docs/deployment/self-hosting.mdx: the Dockerfile block's build stage now shows the same lines. This is the cross-lanedomain:devxedit declared on the claim. The block's image-tag line and the page's other tag lines (the ones the Version Packages PR chore: version packages #21988 edits) are untouched.packages/create-objectstack/src/dockerfile-build-stage.test.ts(below)..changeset/22150-scaffold-dockerfile-lockfile.md:create-objectstackpatch.index.ts,runtime-image.ts,detect-package-manager.ts,packages/cli,packages/spec.Route: the stage follows the lockfile; the scaffolder does not rewrite it (differs from the PM's H2 lean)
The PM leaned toward emitting the build stage after install, from the detected package manager. Before writing that, I measured two reasons against it:
scaffold-e2e.ymlscaffold-localscaffolds with--skip-installon a runner whosepnpmis a Corepack shim (setup-pnpmrunscorepack enable). It then runsnpm installanddocker builds the scaffolded Dockerfile. A stage emitted from the detected manager would be pnpm-shaped there andCOPYapnpm-lock.yamlthat does not exist. The alternative is to emit only after a successful install. Then every--skip-installpnpm user keeps thenpm cistage, and the scaffolder already tells that user to runpnpm install.pnpm --versionresolves through Corepack's LastKnownGood or the registry's latest. Today that is pnpm 12, which the probe below shows Corepack 0.34 cannot run, so the same runner can detect either manager.A stage that reads the lockfile at build time is right on every path: a pnpm install, an npm install,
--skip-installfollowed by either, and a later switch of package manager. It needs no scaffolder code, and the docs page can show the one file. The ruling's outcome is unchanged: pnpm gets Corepack pluspnpm install --frozen-lockfile, npm getsnpm ci, and the docs show the same file.Readings
--skip-skills: the project haspnpm-lock.yamland nopackage-lock.json. The old stage, run from a clean copy, copies onlypackage.json.npm ciexits 1 withEUSAGE.package.jsonandpnpm-lock.yamlare enough for--frozen-lockfileto pass on pnpm 10.34.6. Withoutpnpm-workspace.yamlthe install still exits 0 but printsIgnored build scripts: better-sqlite3@13.0.3, esbuild@0.28.2, because the template's build approvals live in that file. So the stage copies it. The template has nopackageManagerfield, so Corepack has nothing to read; see the version pin below.detect-package-manager.tsanswerspnpmornpm. A project withpnpm-lock.yamlgets the pnpm branch, one withpackage-lock.jsongetsnpm ci. No manager was added. If both lockfiles exist,pnpm-lock.yamlwins, which is the one the template'sci.ymlinstalls from. With no lockfile the build fails loudly; it does not run an unpinned install.main.runtime-image.test.tsandtemplate-consistency.test.tspass unchanged inside the full package run.RUN npx os buildis byte-identical, and it built the artifact in both legs below.Why pnpm is pinned to a major. These readings use Node 22.22.0 and its bundled Corepack 0.34.0, each run with a fresh
COREPACK_HOME:corepack enable pnpmthenpnpm install --frozen-lockfile): Corepack resolves the registry's latest, pnpm 12.10.1, and dies withMODULE_NOT_FOUNDonpnpm/12.10.1/bin/pnpm.cjs. pnpm 12 shipsbin/pnpm.mjsplus a native wrapper.COREPACK_DEFAULT_TO_LATEST=0: Corepack falls back to its bundled pnpm 10.13.1, which is below the template'sengines.pnpm >=10.15.corepack pnpm@10: resolves 10.34.6 and works.corepack pnpm@11.28.2also runs.The stage pins major 10, the same major the template's
.github/workflows/ci.ymlpasses topnpm/action-setup, and a pin keeps the two equal.Measured: the stage run from clean copies (no Docker daemon here)
Each run takes the COPY and RUN text from the committed Dockerfile itself. It COPYs with BuildKit glob semantics into an empty directory (no
node_modules), then runs the RUN line under/bin/sh, which is dash here and innode:22-slim. Each run gets a fresh HOME, a freshCOREPACK_HOMEand a fresh package store, against the real registry. After the install it runsCOPY . .(honouring the template's.dockerignore) andRUN npx os build.package.jsononly;npm ciexit 1EUSAGEpackage.json pnpm-lock.yaml pnpm-workspace.yaml; pnpm 10.34.6 "Lockfile is up to date"; install exit 0;os buildexit 0,dist/objectstack.jsonwrittenpackage-lock.json package.json pnpm-workspace.yaml; "added 524 packages"; install exit 0;os buildexit 0The pin, and its ablation
dockerfile-build-stage.test.tsreads the stage from scaffolded output (copyDir) and emulates the COPY into an empty directory. It then runs the RUN line under/bin/shfor three legs:To stay hermetic:
pnpm install --frozen-lockfileanswers "Already up to date" even with no lockfile. With one dependency, a missing lockfile givesERR_PNPM_NO_LOCKFILE/EUSAGEand a drifted one givesERR_PNPM_OUTDATED_LOCKFILE/EUSAGE, offline.corepackis a stub that runs the repository's own pinned pnpm (resolved from inside the package), and it refuses when that pnpm is outside the major the stage asks for. pnpm itself is not on the stage's PATH.npm_config_offline=true.Two more cases in the file: the docs block's build stage equals the template's, and the Dockerfile's pnpm major equals
ci.yml's.Ablation, run once inside one lock hold on head
cc944d6b, viascripts/ablation-replace.mjs. It put the oldCOPY package*.json ./andRUN npm cilines back: anchor 1 to 0, blobce699d28904ato04316706900a, on-disk counts of the new line 0 and the old line 1.Tests 5 passed (5).Tests 3 failed | 2 passed (5).npm error code EUSAGE, the card's failure), the docs-equality pin and the pnpm-major pin.ce699d28904a, which equals HEAD, andgit diff HEADis empty.An earlier ablation also reddened the npm leg. The only cause was that the leg pinned
pnpm-workspace.yamlin the npm copy set. Commitcc944d6bnarrowed that assertion to whatnpm cineeds, and the run above is on that commit.Verification
Everything below ran on head
cc944d6b, the final commit, with a clean worktree.pnpm --filter 'create-objectstack^...' build(@objectstack/spec), exit 0.pnpm --filter create-objectstack typecheck: exit 0.tsc --listFilesincludes all 17src/*.test.ts, the new one among them.pnpm --filter create-objectstack test: exit 0,Test Files 17 passed (17),Tests 254 passed (254). The package has no integration tier.turbo run build --filter='!@objectstack/docs' --concurrency=2: 72/72 tasks successful.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderives 93 commands for this diff, the same 93 the dispatch order lists; the order addspnpm lint.pnpm lint, exited 0.--ranover that record: "93 derived, 93 run, 0 NOT-MEASURED, 0 UNRUN".PREREQUISITE NOT MET) before the builds above. Their exit-0 runs here are real measurements.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 readscontent/docsthrough the existing$TURBO_ROOT$/content/**input ofcreate-objectstack#test.check:docs-image-tag: "OK (3/3 enumerated surface(s) read, 9 concrete pin(s) compared …)".check-empty-changeset: "1 declaring changeset(s) added".pnpm lint(eslint . --no-inline-config): exit 0, no findings, about 108 s on a shared box..tstest. The.mdx, the changeset and theDockerfileeach answer "File ignored because no matching configuration was supplied". Result: 0 errors, 0 warnings.eslint.config.mjsnever enables type-aware linting, so this change cannot move a verdict on an untouched file.scaffold-e2e.ymldocker build, which exercises thenpm cibranch.Acceptance notes
scaffold-e2e.ymlscaffold-local) installs withnpm install, so it exercises thenpm cibranch alone. The pnpm branch is proven by the measurement above and the hermetic pin, not by a CIdocker build. This is a coverage note, not filed;scaffold-e2e.ymlis outside this card's file surface.node:22-slimtracks the newest 22.x, so a later Corepack may run pnpm 12; the major pin keeps the image on the same pnpm line as CI either way.Generated by Claude Code