Skip to content

fix(publish-npm): make publish steps fail directly when npm errors (follow-up to #82) - #83

Merged
Yan Xue (yanxue06) merged 1 commit into
mainfrom
ci/harden-publish-against-swallowed-exits
May 23, 2026
Merged

fix(publish-npm): make publish steps fail directly when npm errors (follow-up to #82)#83
Yan Xue (yanxue06) merged 1 commit into
mainfrom
ci/harden-publish-against-swallowed-exits

Conversation

@yanxue06

@yanxue06 Yan Xue (yanxue06) commented May 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #82. That PR added Layer 2 (verify on registry) and #82 was squash-merged before this Layer 1 hardening commit landed on the same branch — so main currently has only the post-publish backstop, not the in-step guard.

This PR adds Layer 1: the publish steps themselves now fail when npm publish errored, even when the publisher CLI swallows the non-zero exit (which is the spectrum-ts 1.10.0..1.11.1 failure mode and also the new 1.11.2 provenance-verification failure mode — bunx clean-publish printed npm error code E422 and still exited 0).

How it works

Both publish steps (OIDC primary, NPM token fallback) now:

  1. Capture publisher output via tee (still streams live to the GitHub Actions log).
  2. Read the publisher's real exit code from ${PIPESTATUS[0]} instead of the pipeline's $? (which is always 0 because of tee).
  3. Hard-fail when the captured output contains ^npm (error|ERR!) even if the publisher exited 0.

The OIDC branch additionally refuses to fall back to NPM_TOKEN on a swallowed-exit failure: a "successful" OIDC publish that printed npm errors signals a wrapper bug the token path can't fix, and falling back would just hide it.

LOG="$(mktemp)"
set +e
${{ inputs.publish-command }} ... 2>&1 | tee "$LOG"
PUBLISH_EXIT=${PIPESTATUS[0]}
set -e

if [ "$PUBLISH_EXIT" -ne 0 ]; then
  exit "$PUBLISH_EXIT"
fi

if grep -qE '^npm (error|ERR!)' "$LOG"; then
  echo "Publisher reported success but npm emitted error lines."
  exit 1
fi

Real evidence this is needed

The v1.11.2 spectrum-ts publish run shows the exact pathology:

🔒 Attempting publish via npm Trusted Publishing (OIDC, no token)...
...
npm error code E422
npm error 422 Unprocessable Entity - PUT https://registry.npmjs.org/spectrum-ts
✅ Published via OIDC Trusted Publishing.    ← lies — bunx clean-publish exited 0

That ✅ Published came from the un-hardened code on main after this PR's parent (#82) was merged without the hardening. With this PR, the same input would produce:

❌ OIDC publisher reported success but npm emitted error lines in its output.
   Refusing to fall back to NPM_TOKEN; failing the publish loudly instead.

Layer 2 (registry verify) catches it eventually, but at the cost of a misleading first. Layer 1 fails at the right step with the right message.

Why both layers

Layer 1 fails fast at the source — operators see "Publisher reported success but npm emitted error lines" inline with the publish output, no ambiguity about which step is wrong. Layer 2 is the publisher-agnostic canary that asserts the only thing that actually matters: did the bytes land on the registry. If a future publisher invents a new error format that doesn't match ^npm (error|ERR!), Layer 2 still catches it.

This eliminates the "publish reports success, registry has nothing" state entirely.

Backward compatibility

  • Successful publishes are unchanged
  • Failed publishes that already exit non-zero are unchanged
  • Failed publishes that were silently going green now hard-fail with a clear diagnostic — intended new behavior
  • dry-run: true is unaffected (publish-step hardening is a no-op on dry-run output unless packaging itself is broken, which is exactly when we want to surface it)

Test plan

  • YAML lints clean (verified)
  • Real publish where the publisher exits non-zero: publish step fails red on the explicit exit-code check
  • Real publish with a wrapper that swallows exits and emits npm error: publish step fails red on the grep check (this is the spectrum-ts 1.11.2 case)
  • Real successful publish: log shows ✅ Published via OIDC Trusted Publishing. followed by ✅ <name>@<version> is live on the npm registry.

Notes for reviewers

The cherry-pick is clean against current main since #82 only landed Layer 2. After this merges, publish-npm will have the full two-layer defense as described in #82's body.

Made with Cursor


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.

Layered on top of the registry-verification step: harden both publish
steps so they detect failures *during* publish instead of relying solely
on the after-the-fact registry probe.

Both the OIDC and token publish steps now:

  - Capture publisher output via `tee` (still streams live to the log)
  - Read the publisher's real exit code from `${PIPESTATUS[0]}` instead
    of the pipeline's $? (which would always be 0 because of tee)
  - Treat any `^npm (error|ERR!)` line in the output as a hard failure,
    even when the publisher itself exited 0 — this catches the case
    where `bunx clean-publish` swallowed npm's non-zero exit, which is
    exactly what kept spectrum-ts 1.10.0..1.11.1 silent for four releases

The OIDC branch also intentionally refuses to fall back to NPM_TOKEN on
a swallowed-exit failure: a "successful" OIDC publish that printed npm
errors signals a wrapper bug that the token path can't fix, and falling
back would hide it again. Real OIDC failures (non-zero exit) still fall
back to the token path, preserving the original opt-in semantics.

The registry-verification step from the previous commit stays as a final
backstop. Defense-in-depth: publish step fails directly → verify step
fails on missing version → no green CI without bytes on npm.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copilot AI review requested due to automatic review settings May 23, 2026 19:54
@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@yanxue06, we couldn't start this review because you've used your available PR reviews for now.

Your plan currently allows 3 reviews/hour. Refill in 3 minutes and 16 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5bb1551f-b331-40ca-986a-925808c717e7

📥 Commits

Reviewing files that changed from the base of the PR and between 9c587f3 and 62550b5.

📒 Files selected for processing (1)
  • .github/blocks/publish-npm/action.yaml
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/harden-publish-against-swallowed-exits

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown

📚 Skills documentation may need an update

This PR introduces changes that might not be reflected in the skills documentation.

Reason: publish-npm now supports npm OIDC Trusted Publishing with tokenless publish plus fallback/verification behavior, but _skills-repo/skills/buildspace-ci-cd/SKILL.md still only describes npm publishing in terms of NPM_TOKEN secrets and does not mention the new id-token: write permission or OIDC-based flow.

This is an automated check powered by AI. If the skills are intentionally unchanged, feel free to ignore this.

@yanxue06
Yan Xue (yanxue06) merged commit 719325e into main May 23, 2026
3 of 4 checks passed
@github-actions

Copy link
Copy Markdown

📄 README may need an update

This PR introduces changes that might not be reflected in README.md.

Reason: README.md does not document the publish-command input now exposed by .github/blocks/publish-npm/action.yaml, so it is missing a user-facing configuration option.

This is an automated check powered by AI. If the README is intentionally unchanged, feel free to ignore this.

@yanxue06
Yan Xue (yanxue06) removed the request for review from Copilot May 23, 2026 20:19
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