Skip to content

feat(actions): hardened npm publish composite action#23

Merged
jan-kubica merged 2 commits into
mainfrom
feat/npm-publish-hardened-action
May 13, 2026
Merged

feat(actions): hardened npm publish composite action#23
jan-kubica merged 2 commits into
mainfrom
feat/npm-publish-hardened-action

Conversation

@jan-kubica
Copy link
Copy Markdown
Contributor

Summary

Item #4 of the supply-chain hardening pass — a composite action that codifies OIDC trusted publishing with SLSA v1 provenance for use across the @stll publishing repos.

`.github/actions/npm-publish-hardened/` contains:

  • `action.yml` — composite manifest, one `package-dir` input, optional `tag`
  • `publish.sh` — the publish logic: NPM_TOKEN refusal, npm version check, idempotency, publish, eventual-consistency retry
  • `README.md` — caller requirements + the one-time npm UI configuration for trusted publishing

What it does, briefly

  1. Hard-fails if `NPM_TOKEN` or `NODE_AUTH_TOKEN` is set in env. Trusted publishing is via OIDC token exchange; a legacy token in env would silently shortcut that.
  2. Verifies the npm CLI is v11.5.1+ (cutoff for trusted publishing).
  3. Skips if `name@version` is already on the registry (re-runs are no-ops).
  4. `npm publish --provenance --access public --tag `. npm auto-detects the GitHub Actions OIDC env vars and exchanges them for a one-shot registry token.
  5. Retries if publish reports failure but registry visibility lags.

Composite vs reusable workflow

Composite scopes the abstraction to the part that's actually shared. The surrounding build/test scaffolding diverges across publishing repos (napi-rs cross-compilation vs pure JS), and a reusable workflow would need many inputs to handle every shape. Each repo's release.yml stays readable end-to-end; only the publish step is centralized.

Migration

Out of scope here. After this lands, each publishing repo gets a small follow-up PR that replaces its inline publish step with a `- uses: stella/.github/.github/actions/npm-publish-hardened@` block and drops `NPM_TOKEN` from env. Migration is gated on trusted-publishing being configured for the package on the npm side (confirmed already done across @stll).

Test plan

  • `bash -n publish.sh` syntax check
  • `shellcheck publish.sh` clean
  • CI green on this PR (CLA check)
  • post-merge: validate via a real migration of one publishing repo (likely `stella/stdnum` as the smallest)

OIDC-only trusted publishing with SLSA v1 provenance, idempotent over
package version, refuses to fall back to NPM_TOKEN. Designed to
replace the per-package inline publish step in the stella publishing
repos (anonymize, regex-set, aho-corasick, fuzzy-search, stdnum,
text-search).

What it does:
- hard-fails if NPM_TOKEN/NODE_AUTH_TOKEN is set (defence in depth)
- verifies npm 11.5.1+ is on PATH (the cutoff for trusted publishing)
- skips publish if name@version is already on the registry
- runs `npm publish --provenance --access public --tag <tag>`; npm
  detects the GitHub Actions OIDC env vars automatically
- retries with backoff if the publish fails but registry visibility
  eventually catches up

Caller requirements (documented in README):
- permissions: id-token: write on the calling job
- setup-node + npm 11+ already on PATH
- per-package trusted-publisher record configured on npmjs.com first;
  without it the publish fails with 401

Migration of the existing publishing repos is per-repo and out of
scope for this PR.
@jan-kubica jan-kubica requested a review from nnad3N as a code owner May 13, 2026 12:25
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f894b4fca8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +18 to +20
NPM_VERSION=$(npm --version)
NPM_MAJOR=${NPM_VERSION%%.*}
if (( NPM_MAJOR < 11 )); then
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 Badge Enforce the full npm 11.5.1 minimum

When the runner has npm 11.0.0 through 11.5.0, this check passes even though the action contract and npm's trusted-publishing docs say the OIDC publish path requires npm CLI version 11.5.1 or later; those jobs will proceed to npm publish and fail with an auth error instead of getting the intended early, actionable failure. Compare the full semver, not just the major version.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new composite GitHub Action, 'npm-publish-hardened', designed to publish npm packages securely using OIDC trusted publishing and SLSA v1 provenance. The action includes idempotency checks to skip already-published versions and prevents the use of legacy tokens. The review feedback highlights that the current npm version check is insufficient as it only validates the major version, potentially allowing unsupported minor versions of npm 11. A code suggestion was provided to implement a more precise semver comparison using Node.js to ensure the required 11.5.1+ version is met.

Comment on lines +19 to +24
NPM_MAJOR=${NPM_VERSION%%.*}
if (( NPM_MAJOR < 11 )); then
printf '::error::npm %s is too old; trusted publishing requires 11.5.1+.\n' \
"${NPM_VERSION}" >&2
exit 2
fi
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The current version check only verifies the major version of npm (NPM_MAJOR < 11), which allows versions like 11.0.0. However, the documentation and PR description specify that npm v11.5.1+ is required for trusted publishing support. Using an older version of npm 11 may result in the OIDC exchange being skipped, leading to authentication failures or insecure fallbacks.

Since Node.js is already a requirement for this action, you can use it to perform a more precise semver comparison.

Suggested change
NPM_MAJOR=${NPM_VERSION%%.*}
if (( NPM_MAJOR < 11 )); then
printf '::error::npm %s is too old; trusted publishing requires 11.5.1+.\n' \
"${NPM_VERSION}" >&2
exit 2
fi
if ! node -e "
const [v, min] = process.argv.slice(2);
const p = s => s.split('.').map(n => parseInt(n, 10) || 0);
const va = p(v), ma = p(min);
for (let i = 0; i < 3; i++) {
if (va[i] > ma[i]) process.exit(0);
if (va[i] < ma[i]) process.exit(1);
}
" "$NPM_VERSION" "11.5.1"; then
printf '::error::npm %s is too old; trusted publishing requires 11.5.1+.\n' "${NPM_VERSION}" >&2
exit 2
fi

The previous check only compared the major version, so npm 11.0.0
through 11.5.0 — which lack OIDC trusted publishing support — would
pass and then fail at publish time with a confusing 401. Compare
all three semver components.

Addresses bot review (codex P2, gemini medium) on PR #23.
@jan-kubica
Copy link
Copy Markdown
Contributor Author

CC on behalf of @jan-kubica

Addressed in the latest commit. Replaced the major-only check with a full semver comparison so npm 11.0.0–11.5.0 (which lack OIDC trusted publishing support) is now correctly rejected. Unit-tested locally against 10.9.0, 11.0.0, 11.4.99, 11.5.0 → reject; 11.5.1, 11.5.1-beta, 11.11.1, 12.0.0 → accept.

Thanks codex + gemini.

@jan-kubica jan-kubica merged commit 21f3b21 into main May 13, 2026
1 check passed
@jan-kubica jan-kubica deleted the feat/npm-publish-hardened-action branch May 13, 2026 12:49
@github-actions github-actions Bot locked and limited conversation to collaborators May 13, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant