Node caps a captured subprocess at 1 MiB (maxBuffer) and raises ENOBUFS past it. That has now caused two incidents:
#1748 bounds the release path. The class is wider. Audit of captured subprocesses in scripts/ that do not set maxBuffer:
| Script |
Capture |
Risk |
check-package-manager.ts:155 |
git ls-files -z over the whole tree |
grows with the repository; the most likely next failure |
check-affected.ts:69 |
git diff name lists |
grows with the size of a branch's diff |
resolve-release-upgrade-images.ts:16 |
registry/docker output |
image indexes are small but unbounded in principle |
release-upgrade-test.ts:20 |
docker output |
container logs are unbounded |
coolify-preview.ts:522, check-preview-stack.ts:241, check-artifact-source-contract-immutability.ts |
git / API output |
bounded today, unbounded by contract |
postgres-major-upgrade-test.ts and verify-revert.ts already set it, which is the shape to copy.
Scope
One home for the fact. A shared helper in scripts/lib/ that captures a subprocess with a sane bound, and the call sites above moved onto it — rather than the same literal repeated in a dozen files, which is how the two that already set it came to disagree on the value.
Then a contract test asserting no captured subprocess in scripts/** relies on the default. Derive it from the source the way ci-contract.test.ts derives the import closure, so a new script cannot reintroduce it.
Deliberately not raising the bound to infinity: an unbounded capture of a runaway process is its own failure. The point is that the limit is chosen and stated once, not inherited by accident.
Non-deferrable core
- The contract test. Fixing the call sites without it is how the third incident happens
check-package-manager.ts and check-affected.ts specifically — both scale with the repository and both run on every developer's machine
Node caps a captured subprocess at 1 MiB (
maxBuffer) and raisesENOBUFSpast it. That has now caused two incidents:gh apirelease listings down to four fields, because full release bodies overflow the default.cosign verify-attestation, whose DSSE envelope carries the whole SPDX SBOM base64-encoded. This fired after the images were tagged.#1748 bounds the release path. The class is wider. Audit of captured subprocesses in
scripts/that do not setmaxBuffer:check-package-manager.ts:155git ls-files -zover the whole treecheck-affected.ts:69gitdiff name listsresolve-release-upgrade-images.ts:16release-upgrade-test.ts:20coolify-preview.ts:522,check-preview-stack.ts:241,check-artifact-source-contract-immutability.tspostgres-major-upgrade-test.tsandverify-revert.tsalready set it, which is the shape to copy.Scope
One home for the fact. A shared helper in
scripts/lib/that captures a subprocess with a sane bound, and the call sites above moved onto it — rather than the same literal repeated in a dozen files, which is how the two that already set it came to disagree on the value.Then a contract test asserting no captured subprocess in
scripts/**relies on the default. Derive it from the source the wayci-contract.test.tsderives the import closure, so a new script cannot reintroduce it.Deliberately not raising the bound to infinity: an unbounded capture of a runaway process is its own failure. The point is that the limit is chosen and stated once, not inherited by accident.
Non-deferrable core
check-package-manager.tsandcheck-affected.tsspecifically — both scale with the repository and both run on every developer's machine