Skip to content

fix(create-objectstack): the scaffolded Dockerfile installs from the project's own lockfile - #22217

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-22150-scaffold-dockerfile-package-manager
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-22150-scaffold-dockerfile-package-manager

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22150
Clause-②: no

What was wrong

Where pnpm is on PATH, create-objectstack installs with pnpm, so the project gets pnpm-lock.yaml and no package-lock.json. The Dockerfile it writes copied package*.json and ran npm ci, so docker build stopped at RUN npm ci with EUSAGE before os build ran. content/docs/deployment/self-hosting.mdx showed 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.
    • RUN installs from the lockfile that is present: pnpm-lock.yaml gets corepack pnpm@10 install --frozen-lockfile, package-lock.json gets npm ci. With neither, the build stops with "No lockfile: run pnpm install or npm install, then commit it."
    • A four-line comment says the same in the file.
    • FROM node:22-slim AS build, COPY . ., RUN npx os build and the whole runtime stage are byte-identical to main.
  • content/docs/deployment/self-hosting.mdx: the Dockerfile block's build stage now shows the same lines. This is the cross-lane domain:devx edit 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.
  • New pin: packages/create-objectstack/src/dockerfile-build-stage.test.ts (below).
  • .changeset/22150-scaffold-dockerfile-lockfile.md: create-objectstack patch.
  • Not touched: 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:

  1. The CI docker leg scaffolds one way and installs another. scaffold-e2e.yml scaffold-local scaffolds with --skip-install on a runner whose pnpm is a Corepack shim (setup-pnpm runs corepack enable). It then runs npm install and docker builds the scaffolded Dockerfile. A stage emitted from the detected manager would be pnpm-shaped there and COPY a pnpm-lock.yaml that does not exist. The alternative is to emit only after a successful install. Then every --skip-install pnpm user keeps the npm ci stage, and the scaffolder already tells that user to run pnpm install.
  2. The detected manager is not stable there. Outside the repo, pnpm --version resolves 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-install followed 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 plus pnpm install --frozen-lockfile, npm gets npm ci, and the docs show the same file.

Readings

Reading
H1 reproduce Confirmed. I scaffolded with this branch's base scaffolder, pnpm on PATH, --skip-skills: the project has pnpm-lock.yaml and no package-lock.json. The old stage, run from a clean copy, copies only package.json. npm ci exits 1 with EUSAGE.
H2 emission point Not taken. The reasons are in the section above.
H3 what pnpm must copy package.json and pnpm-lock.yaml are enough for --frozen-lockfile to pass on pnpm 10.34.6. Without pnpm-workspace.yaml the install still exits 0 but prints Ignored 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 no packageManager field, so Corepack has nothing to read; see the version pin below.
H3 per manager detect-package-manager.ts answers pnpm or npm. A project with pnpm-lock.yaml gets the pnpm branch, one with package-lock.json gets npm ci. No manager was added. If both lockfiles exist, pnpm-lock.yaml wins, which is the one the template's ci.yml installs from. With no lockfile the build fails loudly; it does not run an unpinned install.
H4 unchanged The runtime stage and its pin code are byte-identical to main. runtime-image.test.ts and template-consistency.test.ts pass unchanged inside the full package run. RUN npx os build is byte-identical, and it built the artifact in both legs below.
H5 docs The block now shows the template's build stage. A new pin holds the two equal (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:

  • Unpinned (corepack enable pnpm then pnpm install --frozen-lockfile): Corepack resolves the registry's latest, pnpm 12.10.1, and dies with MODULE_NOT_FOUND on pnpm/12.10.1/bin/pnpm.cjs. pnpm 12 ships bin/pnpm.mjs plus a native wrapper.
  • COREPACK_DEFAULT_TO_LATEST=0: Corepack falls back to its bundled pnpm 10.13.1, which is below the template's engines.pnpm >=10.15.
  • corepack pnpm@10: resolves 10.34.6 and works. corepack pnpm@11.28.2 also runs.

The stage pins major 10, the same major the template's .github/workflows/ci.yml passes to pnpm/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 in node:22-slim. Each run gets a fresh HOME, a fresh COREPACK_HOME and a fresh package store, against the real registry. After the install it runs COPY . . (honouring the template's .dockerignore) and RUN npx os build.

Project (scaffolded for real) Old stage New stage
pnpm-installed (pnpm 10.28.0 wrote the lockfile) copies package.json only; npm ci exit 1 EUSAGE copies package.json pnpm-lock.yaml pnpm-workspace.yaml; pnpm 10.34.6 "Lockfile is up to date"; install exit 0; os build exit 0, dist/objectstack.json written
npm-installed (scaffolded with pnpm absent from PATH) install exit 0 copies package-lock.json package.json pnpm-workspace.yaml; "added 524 packages"; install exit 0; os build exit 0
no lockfile n/a exit 1, "No lockfile: run pnpm install or npm install, then commit it."

The pin, and its ablation

dockerfile-build-stage.test.ts reads the stage from scaffolded output (copyDir) and emulates the COPY into an empty directory. It then runs the RUN line under /bin/sh for three legs:

  • a pnpm-installed project,
  • an npm-installed project,
  • a project with no lockfile.

To stay hermetic:

  • The fixture swaps the registry dependencies for one local-directory dependency. With no dependencies at all, pnpm install --frozen-lockfile answers "Already up to date" even with no lockfile. With one dependency, a missing lockfile gives ERR_PNPM_NO_LOCKFILE / EUSAGE and a drifted one gives ERR_PNPM_OUTDATED_LOCKFILE / EUSAGE, offline.
  • corepack is 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.
  • Every install runs with 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, via scripts/ablation-replace.mjs. It put the old COPY package*.json ./ and RUN npm ci lines back: anchor 1 to 0, blob ce699d28904a to 04316706900a, on-disk counts of the new line 0 and the old line 1.

  • Green before: Tests 5 passed (5).
  • Mutated: Tests 3 failed | 2 passed (5).
    • Red: the pnpm leg (npm error code EUSAGE, the card's failure), the docs-equality pin and the pnpm-major pin.
    • Green: the npm leg and the no-lockfile leg.
  • Restored: blob back to ce699d28904a, which equals HEAD, and git diff HEAD is empty.

An earlier ablation also reddened the npm leg. The only cause was that the leg pinned pnpm-workspace.yaml in the npm copy set. Commit cc944d6b narrowed that assertion to what npm ci needs, and the run above is on that commit.

Verification

Everything below ran on head cc944d6b, the final commit, with a clean worktree.

  • Package. The dependency closure was built first: pnpm --filter 'create-objectstack^...' build (@objectstack/spec), exit 0.
    • pnpm --filter create-objectstack typecheck: exit 0. tsc --listFiles includes all 17 src/*.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.
  • Builds for gates that read built packages. turbo run build --filter='!@objectstack/docs' --concurrency=2: 72/72 tasks successful.
  • Gate union. node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derives 93 commands for this diff, the same 93 the dispatch order lists; the order adds pnpm lint.
    • All 93, plus pnpm lint, exited 0.
    • --ran over that record: "93 derived, 93 run, 0 NOT-MEASURED, 0 UNRUN".
    • Five gates had first answered exit 3 (PREREQUISITE NOT MET) before the builds above. Their exit-0 runs here are real measurements.
  • Verdict lines:
    • 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 through the existing $TURBO_ROOT$/content/** input of create-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.
  • Narrowed lint, also run. eslint lints 1 of the 4 changed paths, the new .ts test. The .mdx, the changeset and the Dockerfile each answer "File ignored because no matching configuration was supplied". Result: 0 errors, 0 warnings.
    • eslint.config.mjs never enables type-aware linting, so this change cannot move a verdict on an untouched file.
  • Left to CI: the path-scheduled scaffold-e2e.yml docker build, which exercises the npm ci branch.

Acceptance notes

  • CI's only docker build of a scaffold (scaffold-e2e.yml scaffold-local) installs with npm install, so it exercises the npm ci branch alone. The pnpm branch is proven by the measurement above and the hermetic pin, not by a CI docker build. This is a coverage note, not filed; scaffold-e2e.yml is outside this card's file surface.
  • The Corepack readings are from Node 22.22.0 / Corepack 0.34.0 in this container. node:22-slim tracks 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.
  • A project scaffolded before this release keeps its own Dockerfile. The changeset says how to update one.

Generated by Claude Code

claude added 4 commits October 8, 2026 05:28
…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
… 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
@github-actions github-actions Bot added the size/m label Oct 8, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/create-objectstack/src/templates/blank/Dockerfile), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/create-objectstack/src/templates/blank/Dockerfile) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 1 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 7d7943dd0dfba6c98afa2200a08983ec85f9849a → packageMentionDocs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants