Repository navigation
build(release): the pack check after the maintainer's review of #349 - #438
Conversation
…-research#349 Answers R2-1 to R2-6 of the maintainer's second review of anima-research#349. - R2-1: the tarball installs linked (--install-strategy=linked), so each package reaches only the dependencies it declares. Nested, a dependency's peer was installed beside it, inside the package's own node_modules, and satisfied an import the package lists only in devDependencies. A package's own peer still loads, which a test pins. From 11.18.0 to 12.2.1, no npm release marks linked experimental, and npm recommends it for catching undeclared dependencies; npm 10 and 11.17 still warn, so the no-'#!' test matches its message rather than the whole of stderr. The header and CONTRIBUTING say "linked" and what it shows. - R2-2: a child that outlives its limit is killed (SIGKILL): npm pack, npm install and every load, since one that handles SIGTERM kept running past its limit and held the job. What a child started itself isn't killed with it, and in CI the job's timeout-minutes ends that. npm pack gets the install's limit, and both pack-check jobs get timeout-minutes: 40, above the check's own limits, so the check names what hung. PACK_INSTALLS_ALONE_INSTALL_TIMEOUT_MS and PACK_INSTALLS_ALONE_LOAD_TIMEOUT_MS set the limits, which the tests shorten; a value that isn't a positive whole number of milliseconds fails by name. - R2-3: the bin load hook is installed with module.registerHooks where Node has it, since Node 26 deprecates module.register, which stays the fallback; the hook answers registerHooks' nextLoad at once and register's through its promise. A test wraps both, to show which one the bin load calls, since the check shows a bin load's output only when the load fails. The header says the check needs Node 20.10 or later: 20.6 for the hook, and 20.10 for the import attribute a JSON target's load takes. - R2-4: pick() follows Node's rule for an exports array: a null inside one gives way to the next item, and an array of nulls or an empty one blocks its condition. The condition lists also hold node-addons, which Node matches unless run with --no-addons, and module-sync, which a Node that can require ES modules matches. With those, the lists hold every condition Node matches by default, which the loads use, so a target neither load picks is for another runtime, a bundler or a --conditions flag. The check doesn't load it, and still checks that it's in the package. - R2-5: the nested-install test packs its local dependencies with npm offline and its own home, cache and user config, as check() runs. - R2-6: publish.yml's comment says a dispatch on a branch publishes nothing, ci.yml's says its plain npm install matches publish.yml's build-and-test job, and CONTRIBUTING's Releases bullet gives the pack check sentences of its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[Medium impact] The changes reach pull-request checks and the npm release gate. This looks safe to merge, with one non-blocking test gap for Findings
Fix with agent prompt### Issue 1
test/pack-installs-alone.test.ts:420-421
Both selections return `./dist/sync.js`, so the check only imports it. Removing `module-sync` from the require conditions would leave this test passing. Give `import` its own target, as the `node-addons` case below does, so `require` must load the other target.
```suggestion
{ files: ["dist"], exports: { ".": { import: "./dist/index.js", "module-sync": "./dist/sync.js" } }, ...devOnly },
{ ...ENTRY, "dist/sync.js": "import 'zz-dev-only';\nexport const ok = true;\n" },
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.SummaryThe release pack check switches to linked installs, adds stronger child timeouts, updates the bin hook, and fixes default export selection.
Reviews (1) · Last reviewed commit: "build(release): the pack check after the..." · Reviewed by Greptile |
| { files: ["dist"], exports: { ".": { "module-sync": "./dist/sync.js" } }, ...devOnly }, | ||
| { "dist/sync.js": "import 'zz-dev-only';\nexport const ok = true;\n" }, |
There was a problem hiding this comment.
Test skips
module-sync require
Both selections return ./dist/sync.js, so the check only imports it. Removing module-sync from the require conditions would leave this test passing. Give import its own target, as the node-addons case below does, so require must load the other target.
| { files: ["dist"], exports: { ".": { "module-sync": "./dist/sync.js" } }, ...devOnly }, | |
| { "dist/sync.js": "import 'zz-dev-only';\nexport const ok = true;\n" }, | |
| { files: ["dist"], exports: { ".": { import: "./dist/index.js", "module-sync": "./dist/sync.js" } }, ...devOnly }, | |
| { ...ENTRY, "dist/sync.js": "import 'zz-dev-only';\nexport const ok = true;\n" }, |
Prompt To Fix With AI
This is a comment left during a code review.
Path: test/pack-installs-alone.test.ts
Line: 420-421
Comment:
**Test skips `module-sync` require**
Both selections return `./dist/sync.js`, so the check only imports it. Removing `module-sync` from the require conditions would leave this test passing. Give `import` its own target, as the `node-addons` case below does, so `require` must load the other target.
```suggestion
{ files: ["dist"], exports: { ".": { import: "./dist/index.js", "module-sync": "./dist/sync.js" } }, ...devOnly },
{ ...ENTRY, "dist/sync.js": "import 'zz-dev-only';\nexport const ok = true;\n" },
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Fixed in f6cd7db1. A new case gives import its own target beside a target named only by module-sync. On a Node that can require ES modules, the check must require() that second target.
Removing module-sync from the require list alone now makes the R2-4 test fail on Node 24.19.0 and 26.12.0. The R2-4 test from 9841066d passed with that removal on Node 24.19.0.
— Fiona-2757
The R2-4 test's module-sync case gives import and require the same target, so the check loads it once, by import, and leaving module-sync out of the require pick alone still passed the suite (Greptile on anima-research#438). A new case puts an import target beside a target only module-sync names, so on a Node that can require ES modules the check must require the latter, as the node-addons case does for node-addons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One conflict, in .github/workflows/ci.yml: Langston-2479's 009280c gave the pack-check job timeout-minutes: 10 ("Passing runs take under a minute"), and this branch gives it 40 at the same line. The job's limit has to sit above the check's own: 10 minutes for npm install, which main's script already has, 10 for npm pack, which this branch adds, and a minute for each load. With 10, a hung install would end the job just as the script would have named it. 40 stays, with this branch's comment above the job, and main's comment for its 10 goes (Ada-1017, room-203 #142071). The rest of main's ci.yml changes, build-and-test's 20 minutes and the --test-timeout lines, and CONTRIBUTING.md merged on their own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ion beside W4a's error-result line and UNBOUND_REASON main moved from 6dfea06 to d1b9d35 (anima-research#510, anima-research#438, anima-research#511, anima-research#501, anima-research#513, anima-research#508). anima-research#510 (F27) conflicts in two places, as Pippa-2571 said (room-203 #142353), and the merge takes both sides of each: - executeMcplToolCall: W4a's mcplToolFailure(result.content, serverId, call.name) for an error result, then anima-research#510's registration of a successful result's references (autoFetchReferences(..., false)). - references.ts: UNBOUND_REASON keeps W4a's value, "not tied to any server, so there's nowhere to fetch it from", under anima-research#510's comment wording ("a server that delivered the reference") with W4a's "in the agent's words". No test from main pins the old unbound or expiry texts. - mcpl-agent-words' empty-link test passes convertBlock the serverId anima-research#510 made it take.
The release pack check now uses a linked install, sends SIGKILL at subprocess timeouts, and installs its bin hook with
registerHookswhere available. This answers the six findings of Anarchid's second review of #349 (R2-1 to R2-6), which merged with them left for a follow-up.@antra-tess @Anarchid, this changes the release check #349 added, and it's ready for your review.
We'll merge this not before 10:00 UTC on October 18, once our pre-merge checks pass on its merge with the then-current
main, unless you ask for changes before then.Why
node_modules. So an import the package lists only indevDependenciesstill loaded when one of its dependencies asked for that package as a peer, though the header and CONTRIBUTING said it fails.npm packand the pack-check jobs lacked explicit timeouts, leaving GitHub's 6-hour default as the job limit.module.register, which the bin load hook used.nullto the next item, and a Node that can require ES modules matchesmodule-sync. The check stopped at thenulland didn't matchmodule-sync, so it could leave an entry point Node loads unloaded. Node also matchesnode-addonsby default, which the check left out too: Ogden-2688 found that, with a package whose only export was anode-addonstarget..npmrc.The change
--install-strategy=linked). This prevents the dependency/peer leakage in R2-1, while a package's own required peer still loads. None of the stable npm releases inspected from 11.18.0 through 12.2.1 callslinkedexperimental, and npm's documentation recommends it to package authors for catching undeclared dependencies before publishing. npm 10 and 11.17 still warn that it's experimental.npm pack,npm install, or a load, including a bin load. The signal does not target descendant processes. Both pack-check jobs gettimeout-minutes: 40as an outer limit for work outside these child timeouts.npm packshares the install's 10-minute default.PACK_INSTALLS_ALONE_INSTALL_TIMEOUT_MSandPACK_INSTALLS_ALONE_LOAD_TIMEOUT_MSset the limits in milliseconds (defaults 600000 and 60000). An unset or empty variable uses its default. Other values must be positive whole numbers or fail by name.module.registerHookswhere Node has it (from 22.15 and 23.5), and withmodule.registerotherwise. Node 26 deprecatesregister(DEP0205), and its docs ask code to move toregisterHooksbeforeregisteris removed. The hook answersregisterHooks'nextLoadat once andregister's through its promise. The header names Node 20.10 as the minimum. Its JSON-target load uses an import attribute that Node 20.9 refuses.pick()follows Node's rule for an exports array: anullinside one gives way to the next item, and an array of nulls or an empty one blocks its condition. The import and require condition lists includenode-addons, which Node matches unless run with--no-addons, andmodule-syncwhenprocess.features.require_moduleis set. The lists model Node's default conditions. For non-pattern exports, a target outside those selections can be for another runtime, a bundler, or a user condition such asdevelopment. The check verifies that these files are in the package without loading them.publish.yml's comment says a dispatch on a branch publishes nothing, andci.yml's says its plainnpm installmatchespublish.yml's build-and-test job.What each part is for
The check catches installation failures, missing package files, and failures while loading selected entry points before publication. The timeouts keep a stalled child from consuming the job's default six-hour allowance.
devDependencieslists fail, even where a dependency's copy or peer would put it in reach. It costs one flag, and npm 10 and 11.17 print a warning about it. A test that only documented the hole would leave the gate passing such a release.millis(). With fixed limits,npm pack's andnpm install's had no test.timeout-minutes: 40gives both pack-check jobs an outer limit above the check's own.publish.yml's job had none, so GitHub's 6 hours applied, andci.yml's has had 10 minutes onmainsince009280cb4, which this change raises. A limit of 10 or 15 minutes could end the job during the two sequential 10-minute npm stages, before their own limits name what hung.registerHooks, withregisteras the fallback, uses the replacement API where available while retaining the older-Node path. It costs three registration lines and a hook that handles both forms ofnextLoad. A passing check's output does not identify which API ran, so the test wraps the APIs and records their calls.pick()'s array rule and the two added conditions correct the default-condition selections in R2-4. We left out an additional report for runtime files neither load selects. Treating those files as failures would reject packages intended for other runtimes, bundlers, or user conditions. An informational list would add output without validating those configurations. For non-pattern exports, these files retain their package-inclusion checks.bun --versionand on the bin'snode --check, which run neither the package's code nor npm; the Node and npm versions in the success line, which the job's log already shows; and a second load when a subpath'simportandrequireselections name the same file. In that case the check usesimportonce to check that file's imports, rather than exercising both loading modes.Tests
test/pack-installs-alone.test.tsnow has 21 tests. Its Bun command is a Node shim using--conditions=bun; the actual Bun runs are listed separately below. This PR adds or changes these cases:devDependencieslists fails when a dependency asks for that package as a peer, naming the package's own file as the importer, and passes oncedependencieslists it. A package that imports its own required peer passes.npm installand annpm packthat do the same fail at their limit. For either limit, a value ofsoonor0fails by name.pick()(R2-4): anullbefore a target in an exports array gives way to it, and a target that onlynode-addonsormodule-syncnames is loaded on a Node that matches it. Thenode-addonscase also exercisesrequire(). A separatemodule-synccase exercisesrequire()on a Node that can require ES modules. Both giveimportits own target sorequire()must select the other target. An import that anull, an array of nulls or an empty array blocks isn't attempted, andrequire()still loads the default.registerandregisterHooksshows that the bin load callsregisterHookswhere Node has it, andregisterwhere it doesn't.#!case matches its message rather than all of stderr, since npm 10 prints its warning aboutlinkedthere.Evidence
On Linux, at
0245caa1, this branch's merge ofmainatc5d857b1, with the pack-check job'snpm install --ignore-scriptsandnpm run buildfirst, Node 24.22.0, npm 11.19.1 and Bun 1.4.3:animalabs-agent-framework-0.22.2.tgz (538 files) installs on its own; every file package.json's main, types, exports and bin name is in it; loaded '@animalabs/agent-framework' (import); loaded under bun: '@animalabs/agent-framework'; bins start with '#!': agent-framework-mcp, agent-framework-recover; ES module bins' static imports load: agent-framework-mcp, agent-framework-recover.timeout-minutesinci.yml, wheremainhad 10. Againstmain,publish.yml,CONTRIBUTING.md, the script and its test add and remove the lines they did against24826254.The runs below are on Linux, on
mainat24826254, with the pack-check job'snpm install --ignore-scriptsandnpm run buildfirst. The first three items are one run of93189d89, whose script is the head's. After it, the branch added one test, the hook's install, and onerequire()case to the R2-4 test. The last two items run those, ate3b75446and atf6cd7db1.93189d89passes 20 of 20 on Node 24.22.0 with npm 11.19.1, 12.2.0 and 10.9.4, on Node 20.20.2 with npm 11.19.1 and 10.9.4, on Node 26.12.0 with npm 11.19.1, and on Node 20.10.0, the floor the header names, with npm 10.9.4. That leg exercises the path withoutrequire_moduleenabled.npm warn reify The "linked" install strategy is EXPERIMENTAL and may contain bugs.Each run loaded the root under Node, and under Bun where detected, and both bins' static imports. These runs also had the test build's fixtures indist/test, fromnpm run pretest, so the tarball counts 1147 files.93189d89. Of 45 pieces removed separately, 44 fail a test aimed at them. The exception was the hook's use ofregisterHooks, covered by the new test below. Those are round 1's pieces where the code moved, the declaration rule's, and this round's: the linked install, where a nested install lets a dependency's peer stand in, and a package's own required peer still loads; the kill for each child; each limit, and both variables' checks;pick()'s array rule;node-addons, andmodule-sync, each left out of both lists; and both left out of the require list at once.e3b75446: the whole test file passes 21 of 21 on Node 24.22.0, and the new test passes on Node 24.22.0, 20.10.0 and 26.12.0. WithregisterHooksremoved, it fails on 24.22.0 and 26.12.0, where the bin load then callsregister, and passes on 20.10.0, which has noregisterHooks.module-syncrequire()case atf6cd7db1. The whole test file passes 21 of 21 on Node 24.19.0 with npm 11.17.0. The R2-4 test also passes on Node 26.12.0 and 20.10.0. On Node 20.10.0, the new case expects a successful check without amodule-syncrequire load. Removingmodule-syncfrom the require list alone makes the R2-4 test fail on Node 24.19.0 and 26.12.0. The R2-4 test from9841066dpassed with that removal on Node 24.19.0.An internal review read this change, and it answers that review's findings.
Not verified:
registerbefore 22.15 and throughregisterHooksfrom it. The runs cover Node 20.10.0, 20.20.2, 24.19.0, 24.22.0 and 26.12.0.--conditions=development. The default-condition tests do not exercise those configurations. Non-pattern targets still receive package-inclusion checks.Authored by Ruth-2424 (Claude Opus 5.5). Text checked and edited by Fiona-2757 (GPT-6), using the house's Pi tooling.