Skip to content

fix(cli): scaffold both pnpm build-approval keys so a new project installs on pnpm 11 - #10522

Merged
os-elon merged 1 commit into
mainfrom
claude/issue-10405-scaffold-pnpm11-allowbuilds
Aug 21, 2026
Merged

fix(cli): scaffold both pnpm build-approval keys so a new project installs on pnpm 11#10522
os-elon merged 1 commit into
mainfrom
claude/issue-10405-scaffold-pnpm11-allowbuilds

Conversation

@os-elon

@os-elonos-elon commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Fixes#10405

objectstack init scaffolded a project whose very first pnpm install exited 1 on
pnpm 11. renderPnpmWorkspaceYaml() in packages/cli/src/commands/init.ts wrote only
onlyBuiltDependencies; pnpm 11 does not read that key at all, and it turned an
unapproved dependency build script from a warning into a hard error. The rendered file
now also carries allowBuilds, built from the same SCAFFOLD_BUILT_DEPENDENCIES list —
the shape packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml already
ships for the other scaffold path.

Measured before patching

Scaffolded with the published CLI, exactly as a newcomer would:

$ npx @objectstack/cli@17.1.0 init my-init-app -t app --no-install
$ cd my-init-app && pnpm install # pnpm 11.22.0
[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: better-sqlite3@13.0.3, esbuild@0.28.2
INSTALL_EXIT=1

Then confirmed the rendered file is the cause rather than something adjacent, by
substituting the two renderings of that one file into the same scaffold and changing
nothing else. Every run below is a clean install into a fresh directory with its own
--store-dir — an isolated store matters, because pnpm's side-effects cache will
otherwise hand a later run a build an earlier run performed, and the run then reads as
"the key worked" when the key was never read (this confounded the first pass here):

pnpm-workspace.yamlpnpmexitbuilds ranignoredBuilds
head, before this PR11.22.01neitherbetter-sqlite3@13.0.3, esbuild@0.28.2
this PR11.22.00both[]
this PR10.33.00both[]
this PR10.30.00both[]

"builds ran" is checked on the artifacts, not on the exit code: esbuild/bin/esbuild is
an ELF binary rather than the JS shim, and better-sqlite3/build/Release/ exists.

Which dependencies need a build, and why — exactly two, no wildcard

The scaffolded tree has 397 packages under node_modules/.pnpm. Two of them are build
candidates, and they are the two pnpm names in the error:

  • esbuild@0.28.2 — the only package in the whole tree that declares a lifecycle
    script: postinstall: node install.js, which installs/validates its platform binary.
    Without it, bin/esbuild stays a JS shim. The CLI uses esbuild to compile
    objectstack.config.ts, so this is on the path of dev, compile and validate.
  • better-sqlite3@13.0.3 — declares no install script and sets gypfile: false,
    but ships a binding.gyp, which pnpm treats as an implicit native build; that is why
    it appears in the ignored-builds list at all. It also ships prebuilds for eight
    platform triples, so on a platform with a matching prebuild it loads without
    compiling — but on any platform or ABI without one it must compile, or objectstack serve fails with "Could not locate the bindings file". It is the native driver behind
    the default standalone SQLite store.

Every other package with a scripts entry in that tree declares only prepare, which
pnpm does not run for registry dependencies. So the approved set stays the two packages
that were already approved — this PR grants no new build permission to anything, it only
states the existing grant in the second key. A test asserts the set is named packages
rather than a glob, and that both keys grant the same set.

Both keys are load-bearing — measured, not assumed

One clean install per pnpm version, isolated store, allowBuilds alone:

pnpmallowBuilds alone
10.15.0, 10.18.0, 10.22.0, 10.23.0, 10.24.0, 10.25.0ignored — build not run
10.26.0, 10.28.0, 10.30.0, 10.33.0honoured — build ran

and onlyBuiltDependencies alone on pnpm 11.22.0 exits 1 with exactly the ignored set a
file approving nothing at all produces. So the bands are 10.0–10.25 (needs
onlyBuiltDependencies), 10.26–10.34.x (either), 11.x (needs allowBuilds), and
neither key is redundant. The comment the renderer emits into the user's project states
those measured boundaries.

Note this differs from the boundary the blank template's comment states today
(>= 10.31 / 10.0–10.30). The template's behaviour is right and its two load-bearing
claims are right; only the version numbers in its prose are off. Filed as #10498 rather
than corrected here, to keep this diff inside packages/cli#10317 and #10323 are in
flight in packages/create-objectstack.

Reverse verification of the test

The new ratchets fail on the pre-fix renderer. With the fix committed and
packages/cli/src/commands/init.ts alone checked out from origin/main
(pnpm --filter @objectstack/cli exec vitest run test/init.test.ts):

 × sets allowBuilds.[pkg] = true, the only key pnpm 11 reads
× grants exactly the same set under both keys
Test Files 1 failed (1)
Tests 2 failed | 39 passed (41)

(The real test name spells that placeholder with angle brackets. It is written with
square brackets here because GitHub's body sanitizer deletes short angle-bracket
fragments — it silently ate the first version of this line, inside the code fence,
leaving sets allowBuilds. = true.)

Restoring the file returns Test Files 1 passed (1) / Tests 41 passed (41), and
git status is clean, so the restored tree is byte-identical to the commit. The other
two new tests (keeps listing … onlyBuiltDependencies, approves named packages only)
stay green in both legs by design — they hold on the old shape too, and exist to stop the
old key being dropped and a wildcard being pasted in later. No dist/ is involved in
either leg: the test imports ../src/commands/init, so nothing here depends on a
rebuild.

Scope

packages/cli/src/commands/init.ts (the renderer), packages/cli/test/init.test.ts
(four ratchets), one changeset. Nothing under packages/create-objectstack, so no overlap
with #10317 or #10323 — those two are not addressed here. Nothing under
content/docs/releases/**.

Existing scaffolds are unaffected: init writes this file with flag: 'wx' and never
overwrites one that already exists. The changeset carries the two lines an already
scaffolded project needs.

Out-of-scope findings filed

Gates run locally, at 2ee9cfb6f

Set re-derived after the final commit with node scripts/pm/dispatch-gates.mjs (no path
arguments — it takes the change set from the merge base itself), which named 9
path-derived families plus 5 convention-triggered ones for "adds or edits a test file".
Each line below is the gate's own verdict, not a shell exit code:

gateits verdict line
check:changeset-gate-self-tests✓ check-changeset-no-major --self-test: 116 assertions …
check:cross-package-test-inputsOK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
check:objectui-changeset✓ objectui-range --self-test: all checks passed
check:slot-lookup✓ slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new …
check-adr-0087-registration.mjs✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).
check-changeset-no-major.mjs✓ This diff introduces no major bump.
check-empty-changeset.mjs✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).
docs-audit/check-affected-docs.mjsexit 0, no affected-doc drift for this change set
check:query-options-erasure✓ query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new …
check:type-check-coveragecheck-type-check-coverage: OK — 64/77 workspace packages type-checked …
check:type-check-debtcheck-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured in 257.1s, 1924 raw tsc error(s) total, none above its recorded number.
check:engine-double-contractcheck-engine-double-contract: OK — 340 pinned, 133 in the DEBT ledger, 2 exempt.
check:where-matcher✓ where-matcher conformance holds: 266 matcher(s) discovered, 266 answer the combinator battery correctly or refuse it loudly …
check:nul-bytescheck-nul-bytes: OK (scanned 6144 text file(s) … no raw ASCII control bytes).

check:type-check-debt is the one worth calling out: packages/cli/tsconfig.json has
include: ["src"], so the package's tests sit outside its own typecheck script — a new
test file here lands in the TEST_DEBT layer, not in the clean tsc --noEmit run. It was
run the way CI runs it, on the built workspace closure
(turbo run build --filter=./packages/* --filter=./packages/*/* first).

Package suites: pnpm --filter @objectstack/cli testTest Files 138 passed (138) /
Tests 1534 passed (1534); pnpm --filter @objectstack/cli typechecktsc --noEmit,
exit 0.


Generated by Claude Code

…talls on pnpm 11 (#10405)
`renderPnpmWorkspaceYaml()` wrote only `onlyBuiltDependencies`. pnpm 11 does
not read that key, and it turned an unapproved dependency build script from a
warning into a hard error — so `objectstack init my-app && cd my-app &&
pnpm install` exited 1 with ERR_PNPM_IGNORED_BUILDS, on the first command a
newcomer runs after scaffolding.
The rendered file now also carries `allowBuilds`, built from the same
`SCAFFOLD_BUILT_DEPENDENCIES` list, which is the only key pnpm 11 reads. This
is the shape the blank template already ships for the other scaffold path.
Measured, one clean install per pnpm version, each with its own store, on a
project scaffolded by `objectstack init -t app`:
10.0.0 – 10.25.0 read onlyBuiltDependencies only
10.26.0 – 10.34.x read either key
11.x read allowBuilds only
so both keys are load-bearing and neither is redundant. Build permission is
still granted to exactly two named packages, no wildcard: esbuild (postinstall
installs its platform binary, used to compile objectstack.config.ts) and
better-sqlite3 (ships a binding.gyp, which pnpm treats as a native build).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 2 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx(via os init (command))
  • content/docs/getting-started/examples.mdx(via os init (command))
  • content/docs/getting-started/your-first-project.mdx(via os init (command))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via os init (command))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 23 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 2e3cf955405e3e5e0c95fe50a56784f167bea727packageMentionDocs.

Which tree this was computed on

This run read content/docs from cbba7e30d265cc8a41253c17c6898608962b9f11 — the merge of head 2ee9cfb6f2d3c7f467fdc2ab3dac8245ffcef2b8 into base 2e3cf955405e3e5e0c95fe50a56784f167bea727, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin cbba7e30d265cc8a41253c17c6898608962b9f11 && git checkout cbba7e30d265cc8a41253c17c6898608962b9f11
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2e3cf955405e3e5e0c95fe50a56784f167bea727 2ee9cfb6f2d3c7f467fdc2ab3dac8245ffcef2b8 && git checkout -B drift-repro 2e3cf955405e3e5e0c95fe50a56784f167bea727 && git merge --no-ff 2ee9cfb6f2d3c7f467fdc2ab3dac8245ffcef2b8
node scripts/docs-audit/affected-docs.mjs --json 2e3cf955405e3e5e0c95fe50a56784f167bea727

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 2e3cf955405e3e5e0c95fe50a56784f167bea727 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

objectstack init scaffolds a project whose pnpm install exits 1 on pnpm 11 — its rendered pnpm-workspace.yaml omits allowBuilds

2 participants

@os-elon@claude