Repository navigation
fix(scripts): read the last JSON document on pnpm pack --json stdout - #22753
Merged
objectstack-fleet[bot] merged 1 commit intoOct 11, 2026
Merged
Conversation
pnpm 10.31.0's pack runs prepack/prepare/postpack with inherited stdio and writes its --json document only after them, so @objectstack/spec's prepare banner and generator output now precede the document. packOne parsed stdout whole and threw on every pack, reddening the scaffold smoke and the release publish smoke at their first package. parsePackStdout takes the last JSON document that runs to the end of stdout (per brace, so output glued to the opening brace is skipped); output after the document, or no document, still throws by name with stdout attached. The self-test gains six cases, one driving packOne through a fake pnpm on PATH that prints a banner first. Claude-Session: https://claude.ai/code/session_01VQftDB6niKg91LeQANrS7d Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Oct 11, 2026
Contributor
Author
|
PM seat Blocker:
Generated by Claude Code |
Contributor
Author
|
PM seat
Generated by Claude Code |
objectstack-fleet
Bot
deleted the
claude/issue-22752-pack-json-last-document
branch
October 11, 2026 05:02
This was referenced Oct 11, 2026
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 #22752
Clause-②: no
What changed
scripts/publish-smoke-pack.mjspackOneparsed the WHOLE stdout ofpnpm pack --json --pack-destination DEST. Since@objectstack/specdeclaredprepare: pnpm gen:migration-registry, pnpm prints that lifecycle banner and the generator's output above the JSON document. The parse therefore threw on every pack of the full population, so the scaffold smoke (os-create-smoke.yml) and the release smoke (scripts/publish-smoke.sh) both died at their first pack step.The parse moves into an exported pure helper,
parsePackStdout(stdout, name), andpackOnenow calls it. The helper returns the LAST JSON document on stdout: it walks the{offsets from the end and takes the first suffix thatJSON.parseaccepts whole. These stay unchanged: thefilenamecheck, the set of packages packed, the overrides map,assertPinSetTotal, and everythingpublish-smoke.sh/create-scaffold-smoke.shjudge. The spec'sprepareis untouched. The diff is one file, +120 / -26.Measurements (pnpm 10.31.0, worktree at
bf515e724)1. The banner precedes the document (confirmed). Here is the stdout head of
pnpm pack --json --pack-destination Dinpackages/spec, verbatim except the worktree path, written WT:Unexpected tokenat the first banner character, which is the base behaviour.parsePackStdouton the same bytes returns the tarball filename.2. The document is always last.
dist/pnpm.cjs) runsprepackandpreparebefore the tarball andpostpackright after it. All three run inside the pack API with inherited stdio. The CLI writes the returned JSON string only after the handler returns, and its update notifier runs forinstall/addonly.prepackorpostpackanywhere.prepareappears only on@objectstack/spec(publishable) and on the private root.prepublishOnlyappears on@objectstack/console, and pack does not run it.printf "{ not json"aspostpackleaves a line reading{ not json{. A line-start selector would refuse that valid pack, and the per-brace scan reads it.3. No flag keeps lifecycle output off stdout.
--silent,--reporter=silent,--loglevel=error,--loglevel=silent,--reporter=ndjsonand--reporter=append-only, each before and afterpack. In every case the banner and the script output stay on stdout. Lifecycle stdio is inherited, so no reporter setting reaches it.--ignore-scriptsoutright:ERROR Unknown option: 'ignore-scripts', exit 1.npm_config_ignore_scripts=trueis accepted, but it skipsprepare, whichgen:migration-registryneeds (its output,src/migrations/registry.ts, is gitignored). It was not measured equal, so it is not used.Why the last document rather than the document carrying
filenamepnpm writes its document strictly last (measurement 2), so output after it is a shape nobody has observed. The last-document selector keeps that case loud: no suffix parses, and the named error prints stdout. A search keyed on
filenamewould skip such output silently. Thefilenamecheck still runs on the selected document.Self-test
The battery
publish-smoke-pack self-testgrows from 4 to 10 cases, and its pin is raised to 10.prepareexisted)packOneitself against a fakepnpmThe fake
pnpmin case 10 is a#!/bin/shscript in amkdtempdir, prepended to PATH. It prints the banner, then a document whosefilenameechoes its--pack-destinationargument, so the filename proves that the fake answered, with packOne's argv.selfTest()is now async, because case 10 awaitspackOne. The handshake keeps its kind: the dispatch awaits the self-test and still refuses anything butSELF_TEST_VERDICT.How case 10 fits the header's "pure fixtures, no workspace, no network". It spawns one process: a fake pnpm that the self-test writes into an OS temp dir and removes in a
finally, where PATH is restored too. It reads no workspace, no network and no tracked file, and it never runs the real pnpm. Theno-path-populationdeclaration therefore still holds. The header and the marker's reason now say "fixtures plus one fake pnpm in a temp dir" instead of "in-memory fixtures".Ablation (fix committed first, at
34fff6b49)The mutation makes the helper parse stdout whole:
The other seven cases stay ok under the mutation. The re-run after the restore prints
SELF-TEST PASSED (10 cases).Real pnpm
node scripts/publish-smoke-pack.mjs Dis the scaffold smoke's step 1. I ran it with real pnpm 10.31.0 over the full population, under the shared verify lock. It printedPacking 69 publishable package(s), thenpacked 69/69, thenWrote 69 override(s), withVERDICT command-exit 0. At base this step throws on@objectstack/spec(the card's CI run).bash scripts/create-scaffold-smoke.shwas not run locally. It refuses withoutpackages/cli/dist, and it needs a full monorepo build plus registry installs of every template. That does not fit the container's ~10-minute foreground cap on a shared box. The end-to-end proof is this PR's ownos-create-smokerun, whose path filter names this file.Gates (all at
34fff6b49)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstacktook the change set from git (1 path) and derived 31 commands. All 31 ran and exited 0.--ranreconciles them:✓ dispatch-gates --ran: 31 derived famil(ies) accounted for — 31 run, 0 NOT-MEASURED.pnpm check:publish-smoke-pinprintsSELF-TEST PASSED (10 cases).check-self-test-wiredandcheck-self-test-workflow-commandsexit 0, and so do their self-tests.pnpm check:lockstep-package-countprintsall 3 occurrence(s) agree with the true count (69). The docblock count line is untouched.pnpm check:pm-dispatch-gatesprints1828 cases pass.eslint --print-configputs this file in scope with 2 rules:no-restricted-importsandcomment-swallow/no-code-inside-block-comment.eslint --no-inline-config --format json scripts/publish-smoke-pack.mjsreports 1 file, 0 errors, 0 warnings.parserOptions.project/projectService, aseslint.config.mjsstates near line 327), and both rules are per-file. This diff cannot move any untouched file's verdict.pnpm lintbelongs to CI.No changeset: root
scripts/ships in no package'sfiles[]. The PR carries theskip-changesetlabel.Acceptance notes
listPublicPackagesalso parsespnpm -r list --depth -1 --jsonwhole. pnpm list runs no lifecycle script, and its stdout parsed cleanly in this worktree (81 members, 69 publishable). It is not touched.os-create-smokeis this defect.Generated by Claude Code