Repository navigation
ci: publish to npm on version tags - #3
Conversation
There was a problem hiding this comment.
🟢 Approval recommended
No blocking defects were identified, and the updated lockfile remains consistent with the manifest.
No open issues 🎉
What changed in this PR
Automates npm publishing and GitHub Releases for version tags.
Changes:
- Validates tag/version and changelog consistency, then builds, tests, and publishes with provenance.
- Creates GitHub Releases with notes, tarballs, checksums, and a CycloneDX SBOM.
- Removes unused publishing hooks and development dependencies.
| File | Description |
|---|---|
| package.json | Removes pinst hooks and unused dependencies. |
| package-lock.json | Updates the dependency tree to match removals. |
| .github/workflows/publish.yml | Adds tag-triggered publishing and release jobs with separate permissions. |
Review effort: Balanced
Samy-O7
left a comment
There was a problem hiding this comment.
Thanks, this covers most of what we need for releases: minimal permissions with the GitHub Release in its own job, the tag-to-version and CHANGELOG checks, provenance, an SBOM and a SHA-256 checksum on each release, and only GitHub-owned actions. I also confirmed in npm's docs that trusted publishing needs the package to exist first, so a one-time bootstrap is unavoidable.
Requesting changes for a few things before the first release. spreedly/spreedly-mcp's release.yml already does most of them, so it's a good reference:
- Approval gate. Run the publish job in a
releaseenvironment that needs a reviewer's approval and only deploys fromv*tags, keep any npm token there as an environment secret, and bind the npm trusted publisher to that environment. Tag-triggered runs use the workflow file from the tagged commit, so checks inside the workflow can be edited along with the code. The environment approval is what stops an unreviewed commit from publishing. - Immutable tags. A tag ruleset that blocks updating and deleting
v*tags, without limiting who creates them, so a released version's tag can't be moved to different code. A check that the tagged commit is onmain, asspreedly-mcphas, still catches tagging the wrong commit. - Immutable releases. The repo setting is off.
gh release createalready creates a draft, uploads the assets and then publishes, so the release step works with it on. - Keep dependency code out of the OIDC job.
npm ciandnpm test(webpack, TypeScript and Vitest) run in the job that holdsid-token: write, so any dependency code that runs there could request the OIDC token that trusted publishing accepts. Building, testing and packing in a job withoutid-token, then publishing the checksum-verified tarball from the gated job, keeps third-party code out of that job. Since this is the template for the other SDKs, it matters more there: the client SDK's test tooling runs install scripts. - Bootstrap without a direct-publish token. Because the package doesn't exist yet, the token can only be limited to the
@spreedlyscope, which also holds@spreedly/spreedly-mcp. npm's staged publishing avoids that: a stage-only granular token can runnpm stage publish --provenance, which can create a new package, and a maintainer approves the staged version with 2FA, so the token can't publish anything directly. It needs npm CLI 11.15 or later. If you'd rather keep a direct-publish token, check first that@spreedly/spreedly-mcpis set to disallow tokens. - Scan the artifact before publishing, for example with
npm audit --omit=dev --audit-level=high. It's trivial for this package, which has no runtime dependencies, but the other SDKs inherit it.
Smaller items:
- After the bootstrap, drop
registry-urlandNODE_AUTH_TOKEN.spreedly-mcpnotes that with them, a failed OIDC exchange shows up as a misleading 404 instead of an auth error. - Add
concurrencyfor the tag, andpersist-credentials: falseon checkout. CHANGELOG.mdstill has## 1.0.0 (unreleased), which the section check accepts. Worth updating before tagging, or rejecting "unreleased" in the check.- Optional, from
spreedly-mcp: fail if the version is already on npm, and scan the packed tarball for secrets.
|
@Samy-O7 Thanks for the review. All of it is addressed in 9b85c4c:
Smaller items:
One side effect of staging to note: for a package that doesn't exist yet, npm first publishes a public |
Samy-O7
left a comment
There was a problem hiding this comment.
Approving f51abe1. Everything from my review is addressed, and I checked it against the workflow and the repo settings:
buildhas onlycontents: readand runs all the dependency code (install, build, tests, audit, pack, SBOM, checksums and the gitleaks scan).publishruns in thereleaseenvironment withid-token: write, runs no dependency code, verifies the tarball checksum and then stages or publishes it.releasecreates the GitHub Release as a draft when the version is staged.- The
Release tagsruleset blocks updates, deletions and force pushes onv*with no bypass, immutable releases are on, and thereleaseenvironment only acceptsv*tags with admin bypass off. - The pinned gitleaks archive matches the checksum in the official 8.30.1 release, and the
0.0.0-stageplaceholder is documented npm behavior.
A few notes, none blocking:
- Create
NPM_STAGE_TOKENas a secret on thereleaseenvironment rather than the repo, so only the gated job can read it. Repo-level secrets are also visible to jobs in that environment. npm stageneeds npm 11.15 or later (Node 24.18+). Today's ubuntu-24.04 image caches Node 24.21.0 with npm 11.19.0, so it works now;check-latest: trueon the publish job'ssetup-nodewould guard against an older cached version later.- After 1.0.0 is approved, deprecating
0.0.0-stageand checking thatlatestpoints to 1.0.0 would keep the version list clear.

Changes
publish.yml: on av*tag, abuildjob (noid-token) checks the tag againstpackage.json,src/version.ts,mainand a releasedCHANGELOG.mdsection, then installs, tests, audits runtime dependencies, packs, scans the tarball for secrets, and writes the SBOM and SHA-256 checksum.publish.yml: apublishjob in thereleaseenvironment verifies the tarball checksum and publishes it to npm with provenance. The first release is staged with a stage-only token for maintainer approval; later releases use trusted publishing (OIDC).publish.yml: areleasejob creates the GitHub Release with the CHANGELOG notes, tarball, checksum and SBOM.package.json: removed the unusedpinstpublish hooks and thepinstand commitlint devDependencies.