From 91013bc080f4551ad720cc9b75d1607cb499c1b8 Mon Sep 17 00:00:00 2001 From: Lucas Carlson Date: Wed, 23 Sep 2026 15:49:58 -0700 Subject: [PATCH] ci: build before npm publish reads the manifest Every release logged this twice: npm warn package-json solid-objects@0.16.0 No bin file found at dist/executable.js npm validates `bin` against the working tree, and the publish job left the build to `prepack`, which npm runs eleven seconds later. The published package was never wrong: `package/dist/executable.js` is in the 0.16.0 tarball, `node_modules/.bin/solid-objects` links to it, and `npx solid-objects --help` prints usage from a clean consumer install. Only the warning was wrong, and it reads like a broken package. The job now builds before it invokes npm and publishes with `--ignore-scripts`, so the build runs once instead of twice. A dry run produces the same 377 files the published 0.16.0 contains. `check-package.mjs` asserted `dist/executable.js` by name, so pointing `bin` at a path the build does not produce still passed. It reads the manifest now and fails with: bin solid-objects points at dist/renamed-cli.js, which the build does not produce Validation: pnpm run pack:check, pnpm test (528), check, format:check. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 3 ++- CHANGELOG.md | 14 ++++++++++++++ scripts/check-package.mjs | 18 ++++++++++++++---- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1631dd..b35138e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,6 +167,7 @@ jobs: - run: corepack enable - run: pnpm install --frozen-lockfile - run: npm install --global npm@11 + - run: pnpm run build - name: Validate release tag id: release run: | @@ -188,7 +189,7 @@ jobs: echo "exists=false" >> "$GITHUB_OUTPUT" fi - if: steps.registry.outputs.exists != 'true' - run: npm publish --access public + run: npm publish --access public --ignore-scripts - name: Write release notes run: node scripts/release-notes.mjs "${GITHUB_REF_NAME#v}" > "${RUNNER_TEMP}/release-notes.md" - name: Create GitHub release diff --git a/CHANGELOG.md b/CHANGELOG.md index 01dc270..d79a993 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## Unreleased + +- Build before `npm publish` reads the manifest. npm validates `bin` against the + working tree before `prepack` produces `dist`, so every release logged + `No bin file found at dist/executable.js` twice. The published package was + always correct, and `npx solid-objects` has always worked, but a warning that + says the binary is missing is a poor thing to print while publishing one. + The publish job builds first and publishes with `--ignore-scripts`, so the + build runs once rather than twice. +- Check the `bin` entry the manifest declares rather than a hard-coded path. + `check-package.mjs` asserted `dist/executable.js` directly, so renaming the + build output would have shipped a `bin` that resolves to nothing while the + check still passed. + ## 0.16.0 - 2026-09-23 - Find a message whose reference a caller lost. `runtime.findBy({ requestId })` diff --git a/scripts/check-package.mjs b/scripts/check-package.mjs index 6afb9e3..b49588d 100644 --- a/scripts/check-package.mjs +++ b/scripts/check-package.mjs @@ -1,9 +1,19 @@ import fs from "node:fs" import path from "node:path" -const executablePath = path.resolve("dist/executable.js") -const executableMode = fs.statSync(executablePath).mode +const manifest = JSON.parse(fs.readFileSync(path.resolve("package.json"), "utf8")) +const binaries = Object.entries(manifest.bin ?? {}) -if ((executableMode & 0o111) === 0) { - throw new Error(`${executablePath} is not executable`) +if (binaries.length === 0) { + throw new Error("package.json declares no bin entry") +} + +for (const [name, declared] of binaries) { + const target = path.resolve(declared) + if (!fs.existsSync(target)) { + throw new Error(`bin ${name} points at ${declared}, which the build does not produce`) + } + if ((fs.statSync(target).mode & 0o111) === 0) { + throw new Error(`${target} is not executable`) + } }