Skip to content

Add npm publish preflight to releases#1619

Merged
ponderingdemocritus merged 1 commit into
masterfrom
ponderingdemocritus/npm-404s
Mar 22, 2026
Merged

Add npm publish preflight to releases#1619
ponderingdemocritus merged 1 commit into
masterfrom
ponderingdemocritus/npm-404s

Conversation

@ponderingdemocritus

Copy link
Copy Markdown
Contributor

Summary

Add an npm publish preflight to the release workflows so bad npm credentials fail before versioning or publish steps run.
Refactor scripts/changeset-publish.ts to support --preflight-only, probe scope access with npm, and translate common npm auth and permission failures into clearer errors.
Add unit coverage for the new publish-argument parsing and npm failure classification helpers.

Verification

bun test scripts/tests/changeset-publish-utils.test.ts
LUCID_SKIP_NPM_PUBLISH_PREFLIGHT=1 bun run scripts/changeset-publish.ts --preflight-only

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
lucid-agents 1efa97a Commit Preview URL

Branch Preview URL
Mar 22 2026, 01:16 AM

@greptile-apps

greptile-apps Bot commented Mar 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds an npm publish preflight check to both release workflows (release.yml and release-bot.yml) that verifies npm credentials — via npm whoami and npm access list collaborators — before any versioning or publish steps run. It also refactors scripts/changeset-publish.ts to support a --preflight-only flag, translates common npm auth/permission error codes into actionable messages, and adds unit tests for the new error-classification and argument-parsing helpers.

Key changes:

  • New scripts/changeset-publish-utils.ts with pure helpers (describeNpmAccessFailure, describeNpmPublishFailure, partitionPublishArgs) — well-tested and cleanly separated.
  • exec() now captures stdout/stderr so error output can be classified; however this buffers all output until subprocess exit, which means changeset publish produces no real-time CI log output — potentially minutes of silence during a publish run.
  • runPublish() still runs the full package-manifest sanitization phase before the preflight check even when --preflight-only is passed; a manifest resolution error here would produce a confusing non-auth error during what should be a credential probe.
  • The preflight step in release-bot.yml has no condition guard, so it fires on every push to master even when there are no pending changesets.

Confidence Score: 4/5

  • Safe to merge — the core preflight logic is correct and will catch bad credentials before any irreversible versioning or publish steps run.
  • The primary goal (fail fast on bad npm credentials) is achieved correctly. The three flagged issues are all P2 style/best-practice concerns: output buffering, unnecessary sanitization during preflight, and the unconditional preflight in release-bot. None of these would cause incorrect behaviour or a failed release — they affect observability and minor CI noise. A targeted fix for the output-buffering issue would be the most valuable follow-up.
  • scripts/changeset-publish.ts — output buffering and preflight sanitization concerns.

Important Files Changed

Filename Overview
scripts/changeset-publish.ts Refactored to support --preflight-only, capture subprocess output for error classification, and provide richer error messages on publish failure. Two P2 issues: output buffering hides real-time CI progress for changeset publish, and package sanitization runs unnecessarily when --preflight-only is passed.
scripts/changeset-publish-utils.ts New utility module with pure helper functions for scope extraction, npm error classification, and argument parsing. Clean implementation, well-tested.
scripts/tests/changeset-publish-utils.test.ts Unit tests covering E401/E404 access failure classification, PUT 404 publish failure detection, and partitionPublishArgs. Good coverage of the new utility functions.
.github/workflows/release.yml Moved "Verify npm token" and "Configure npm auth" steps before the new preflight step, correctly gated on should_publish == 'true'. Ordering is now correct — credentials are configured before the preflight runs.
.github/workflows/release-bot.yml Adds preflight step after npm auth configuration. The step has no condition guard, so it fires on every push to master regardless of whether there are pending changesets.

Sequence Diagram

sequenceDiagram
    participant CI as GitHub Actions
    participant Script as changeset-publish.ts
    participant npm as npm CLI

    CI->>Script: bun run scripts/changeset-publish.ts --preflight-only

    Script->>Script: sanitiseManifest (writes/restores package.json)

    Script->>npm: npm whoami
    alt auth fails (E401)
        npm-->>Script: exit ≠ 0
        Script-->>CI: ❌ "NPM_TOKEN is missing or invalid"
    else auth succeeds
        npm-->>Script: exit 0 (username)
    end

    loop for each npm scope
        Script->>npm: npm view <pkg> version --json
        alt package found
            npm-->>Script: exit 0
            Script->>npm: npm access list collaborators <pkg> --json
            alt access denied (E403/E404)
                npm-->>Script: exit ≠ 0
                Script-->>CI: ❌ "lacks collaborator or publish access"
            else access granted
                npm-->>Script: exit 0
            end
        else no published package found
            Script-->>Script: warn and skip scope
        end
    end

    Script-->>CI: ✅ "npm publish preflight succeeded."

    Note over CI,Script: If not --preflight-only, continues to<br/>bun x changeset publish
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: scripts/changeset-publish.ts
Line: 307-333

Comment:
**Output buffering breaks real-time CI streaming**

The `exec` function was changed from `stdout: "inherit"` / `stderr: "inherit"` to `stdout: "pipe"` / `stderr: "pipe"`, with output flushed only after the subprocess exits. For quick commands like `npm whoami` this is harmless, but `bun x changeset publish` publishes every package in the monorepo sequentially — potentially running for several minutes — and CI logs will show nothing until the entire command completes.

If the publish stalls, hangs, or is killed by the 30-minute job timeout, there will be zero diagnostic output in the log. Consider streaming output in real time while still capturing it for error reporting, e.g. via a `PassThrough` tee or by writing to the log in chunks as each line arrives.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: scripts/changeset-publish.ts
Line: 251-278

Comment:
**Package sanitization runs unnecessarily during `--preflight-only`**

`runPublish` writes (and then restores) modified `package.json` files before calling `verifyNpmPublishAccess()`. When `--preflight-only` is passed, this disk I/O is entirely unnecessary — the sanitization step exists to prepare manifests for publishing, not to check credentials.

More importantly, if `sanitiseManifest` throws (e.g. an unresolved workspace dependency), the preflight step will fail with a manifest error instead of an npm auth error, which is confusing.

Consider skipping the sanitization block when `parsedArgs.preflightOnly` is true:

```typescript
async function runPublish() {
  const parsedArgs = partitionPublishArgs(process.argv.slice(2));
  const backups: Backup[] = [];
  const sanitisedPackages: string[] = [];

  if (!parsedArgs.preflightOnly) {
    for (const pkg of packages) {
      // … sanitisation …
    }
  }
  //
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: .github/workflows/release-bot.yml
Line: 55-59

Comment:
**Preflight fires unconditionally on every push to `master`**

Unlike `release.yml` where the preflight is gated by `steps.metadata.outputs.should_publish == 'true'`, this step has no condition. It will run on every push to `master` even when there are no pending changesets and the `changesets/action` step would not publish anything. This adds two npm API calls (`npm whoami` + `npm access list collaborators`) on every merge to `master`.

More practically, repositories forked without `NPM_TOKEN` configured will see this step fail on every push, which is noisy. Consider adding a guard or at least documenting the intentional unconditional behaviour.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "fix release npm publ..."

Comment on lines +307 to 333
async function exec(
argv: string[],
opts: { allowFailure?: boolean } = {}
): Promise<ExecResult> {
const proc = Bun.spawn(argv, {
cwd: repoRoot,
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
stdout: "pipe",
stderr: "pipe",
});
const code = await proc.exited;
if (code !== 0) {

const [stdout, stderr, code] = await Promise.all([
proc.stdout ? new Response(proc.stdout).text() : "",
proc.stderr ? new Response(proc.stderr).text() : "",
proc.exited,
]);

if (stdout) process.stdout.write(stdout);
if (stderr) process.stderr.write(stderr);

const output = [stdout, stderr].filter(Boolean).join("\n");
if (code !== 0 && !opts.allowFailure) {
throw new Error(`${argv.join(" ")} exited with code ${code}`);
}

return { code, output };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Output buffering breaks real-time CI streaming

The exec function was changed from stdout: "inherit" / stderr: "inherit" to stdout: "pipe" / stderr: "pipe", with output flushed only after the subprocess exits. For quick commands like npm whoami this is harmless, but bun x changeset publish publishes every package in the monorepo sequentially — potentially running for several minutes — and CI logs will show nothing until the entire command completes.

If the publish stalls, hangs, or is killed by the 30-minute job timeout, there will be zero diagnostic output in the log. Consider streaming output in real time while still capturing it for error reporting, e.g. via a PassThrough tee or by writing to the log in chunks as each line arrives.

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/changeset-publish.ts
Line: 307-333

Comment:
**Output buffering breaks real-time CI streaming**

The `exec` function was changed from `stdout: "inherit"` / `stderr: "inherit"` to `stdout: "pipe"` / `stderr: "pipe"`, with output flushed only after the subprocess exits. For quick commands like `npm whoami` this is harmless, but `bun x changeset publish` publishes every package in the monorepo sequentially — potentially running for several minutes — and CI logs will show nothing until the entire command completes.

If the publish stalls, hangs, or is killed by the 30-minute job timeout, there will be zero diagnostic output in the log. Consider streaming output in real time while still capturing it for error reporting, e.g. via a `PassThrough` tee or by writing to the log in chunks as each line arrives.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 251 to +278
@@ -191,8 +272,28 @@ async function runPublish() {
}

try {
const extraArgs = process.argv.slice(2);
await exec(["bun", "x", "changeset", "publish", ...extraArgs]);
await verifyNpmPublishAccess();
if (parsedArgs.preflightOnly) {
console.log("npm publish preflight succeeded.");
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Package sanitization runs unnecessarily during --preflight-only

runPublish writes (and then restores) modified package.json files before calling verifyNpmPublishAccess(). When --preflight-only is passed, this disk I/O is entirely unnecessary — the sanitization step exists to prepare manifests for publishing, not to check credentials.

More importantly, if sanitiseManifest throws (e.g. an unresolved workspace dependency), the preflight step will fail with a manifest error instead of an npm auth error, which is confusing.

Consider skipping the sanitization block when parsedArgs.preflightOnly is true:

async function runPublish() {
  const parsedArgs = partitionPublishArgs(process.argv.slice(2));
  const backups: Backup[] = [];
  const sanitisedPackages: string[] = [];

  if (!parsedArgs.preflightOnly) {
    for (const pkg of packages) {
      // … sanitisation …
    }
  }
  // …
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/changeset-publish.ts
Line: 251-278

Comment:
**Package sanitization runs unnecessarily during `--preflight-only`**

`runPublish` writes (and then restores) modified `package.json` files before calling `verifyNpmPublishAccess()`. When `--preflight-only` is passed, this disk I/O is entirely unnecessary — the sanitization step exists to prepare manifests for publishing, not to check credentials.

More importantly, if `sanitiseManifest` throws (e.g. an unresolved workspace dependency), the preflight step will fail with a manifest error instead of an npm auth error, which is confusing.

Consider skipping the sanitization block when `parsedArgs.preflightOnly` is true:

```typescript
async function runPublish() {
  const parsedArgs = partitionPublishArgs(process.argv.slice(2));
  const backups: Backup[] = [];
  const sanitisedPackages: string[] = [];

  if (!parsedArgs.preflightOnly) {
    for (const pkg of packages) {
      // … sanitisation …
    }
  }
  //
}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +55 to +59
- name: Preflight npm publish access
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: bun run scripts/changeset-publish.ts --preflight-only

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Preflight fires unconditionally on every push to master

Unlike release.yml where the preflight is gated by steps.metadata.outputs.should_publish == 'true', this step has no condition. It will run on every push to master even when there are no pending changesets and the changesets/action step would not publish anything. This adds two npm API calls (npm whoami + npm access list collaborators) on every merge to master.

More practically, repositories forked without NPM_TOKEN configured will see this step fail on every push, which is noisy. Consider adding a guard or at least documenting the intentional unconditional behaviour.

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/release-bot.yml
Line: 55-59

Comment:
**Preflight fires unconditionally on every push to `master`**

Unlike `release.yml` where the preflight is gated by `steps.metadata.outputs.should_publish == 'true'`, this step has no condition. It will run on every push to `master` even when there are no pending changesets and the `changesets/action` step would not publish anything. This adds two npm API calls (`npm whoami` + `npm access list collaborators`) on every merge to `master`.

More practically, repositories forked without `NPM_TOKEN` configured will see this step fail on every push, which is noisy. Consider adding a guard or at least documenting the intentional unconditional behaviour.

How can I resolve this? If you propose a fix, please make it concise.

@ponderingdemocritus ponderingdemocritus merged commit f421c2d into master Mar 22, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant