Skip to content

ci: harden npm release pipeline - #14

Merged
psd-coder merged 1 commit into
mainfrom
harden-release-pipeline
Jul 23, 2026
Merged

ci: harden npm release pipeline#14
psd-coder merged 1 commit into
mainfrom
harden-release-pipeline

Conversation

@psd-coder

Copy link
Copy Markdown
Owner

Overview

Reworks the release pipeline so no publishing credential exists to steal, releases can only originate from one workflow, and every release still requires a manual 2FA approval from the maintainer. Follows the secure-npm-package guide.

Problem Statement

The previous publish workflow ran the build, the install, and the publish in a single job holding id-token: write. Any dependency in that job — including the full dev toolchain and its transitive packages — could have published nanotags on its own. Publishing was also fully automatic: a tag push released straight to npm with no human gate. Supporting workflows kept credentials in the checkout, ran with default (write) token permissions, and one job pulled an unpinned pnpm@latest at runtime.

Solution Approach

  • Split publish.yml into test, build, and publish jobs. Only publish gets id-token: write, and it installs no dependencies, uses no cache, and consumes the build output as an artifact.
  • Publishing now uses npm stage publish --ignore-scripts: CI stages the release and it goes live only after the maintainer approves it with their 2FA key, so a compromised pipeline cannot ship a version on its own.
  • Added a zizmor workflow so CI keeps auditing workflow definitions on every push and pull request.
  • Applied least-privilege permissions per job, persist-credentials: false on every checkout, and refreshed all SHA-pinned actions; the bundle-size job no longer activates an unpinned pnpm.
  • Set a 3-day dependency cooldown (minimumReleaseAge), which blocks the large majority of malicious releases since most are pulled within hours.

Breaking Changes

None for consumers of the package. For maintainers, the release is no longer automatic: after pushing a version tag, the staged release must be approved in Staged Packages on npmjs.com or via npm stage approve.

Testing

  • Push a patch version tag and confirm the pipeline stages a release instead of publishing it, then approve it and verify the new version appears on npm with its provenance attestation.
  • The published payload is unchanged: a packing dry run from the built package produces the same file list as before.
  • zizmor reports no findings across the workflows.

Publish job now stages the release (npm stage publish) for manual 2FA
approval and installs no dependencies; build moved to a separate job.
Least-privilege permissions, persist-credentials off, zizmor linting,
3-day dependency cooldown.
@psd-coder psd-coder self-assigned this Jul 22, 2026
@github-actions

Copy link
Copy Markdown

size-limit report 📦

Path Size
.size-check/index.mjs 2.36 KB (0%)
.size-check/render.mjs 417 B (0%)
.size-check/context.mjs 402 B (0%)

@psd-coder
psd-coder merged commit f952641 into main Jul 23, 2026
11 checks passed
@psd-coder
psd-coder deleted the harden-release-pipeline branch July 23, 2026 11:15
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