Skip to content

ci: publish to npm on version tags - #3

Merged
drkjc merged 6 commits into
mainfrom
add-npm-publish-workflow
Oct 21, 2026
Merged

drkjc merged 6 commits into
mainfrom
add-npm-publish-workflow

Conversation

@drkjc

@drkjc drkjc commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Changes

  • publish.yml: on a v* tag, a build job (no id-token) checks the tag against package.json, src/version.ts, main and a released CHANGELOG.md section, then installs, tests, audits runtime dependencies, packs, scans the tarball for secrets, and writes the SBOM and SHA-256 checksum.
  • publish.yml: a publish job in the release environment 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: a release job creates the GitHub Release with the CHANGELOG notes, tarball, checksum and SBOM.
  • package.json: removed the unused pinst publish hooks and the pinst and commitlint devDependencies.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 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 Samy-O7 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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:

  1. Approval gate. Run the publish job in a release environment that needs a reviewer's approval and only deploys from v* 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.
  2. 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 on main, as spreedly-mcp has, still catches tagging the wrong commit.
  3. Immutable releases. The repo setting is off. gh release create already creates a draft, uploads the assets and then publishes, so the release step works with it on.
  4. Keep dependency code out of the OIDC job. npm ci and npm test (webpack, TypeScript and Vitest) run in the job that holds id-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 without id-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.
  5. Bootstrap without a direct-publish token. Because the package doesn't exist yet, the token can only be limited to the @spreedly scope, which also holds @spreedly/spreedly-mcp. npm's staged publishing avoids that: a stage-only granular token can run npm 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-mcp is set to disallow tokens.
  6. 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-url and NODE_AUTH_TOKEN. spreedly-mcp notes that with them, a failed OIDC exchange shows up as a misleading 404 instead of an auth error.
  • Add concurrency for the tag, and persist-credentials: false on checkout.
  • CHANGELOG.md still 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.

@drkjc

drkjc commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

@Samy-O7 Thanks for the review. All of it is addressed in 9b85c4c:

  1. Approval gate. The publish job runs in a release environment with required reviewers that only deploys from v* tags. The bootstrap token is an environment secret, and the npm trusted publisher will be bound to release.
  2. Immutable tags. A tag ruleset on v* blocks updates, deletions and force pushes, with no bypass and creation unrestricted. The build job also fails if the tagged commit isn't on main.
  3. Immutable releases. Enabled. The release job uses gh release create with the assets, so they're attached before the release is published.
  4. No dependency code in the OIDC job. build (no id-token) runs the checks, npm ci, tests, audit, pack, SBOM, checksums and the secret scan. publish only downloads the artifact, runs sha256sum -c, and publishes that tarball.
  5. Bootstrap. 1.0.0 uses npm stage publish --provenance with a stage-only token stored in the release environment, and a maintainer approves the staged version with 2FA. A local dry run confirmed npm stage publish accepts the packed tarball. After 1.0.0, a follow-up PR removes the stage step and the token, leaving OIDC only.
  6. Artifact scan. npm audit --omit=dev --audit-level=high in build.

Smaller items:

  • No registry-url, and no NODE_AUTH_TOKEN outside the bootstrap step.
  • concurrency per tag (no cancelling in progress), and persist-credentials: false.
  • The CHANGELOG check rejects a heading containing "unreleased".
  • Optional items, both added: the publish job fails if the version is already on npm, and build scans the unpacked tarball with gitleaks 8.30.1 (pinned by SHA-256, --redact --verbose). No SARIF upload; the failed step blocks the release.

One side effect of staging to note: for a package that doesn't exist yet, npm first publishes a public 0.0.0-stage placeholder version, which stays in the version list.

@drkjc
drkjc requested review from Samy-O7 and a balanced review from Copilot October 20, 2026 18:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Public GitHub Release creation must wait for npm publication or remain a draft pending approval.

1 open issue

Review effort: Balanced

Comment thread .github/workflows/publish.yml Outdated

@Samy-O7 Samy-O7 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approving f51abe1. Everything from my review is addressed, and I checked it against the workflow and the repo settings:

  • build has only contents: read and runs all the dependency code (install, build, tests, audit, pack, SBOM, checksums and the gitleaks scan). publish runs in the release environment with id-token: write, runs no dependency code, verifies the tarball checksum and then stages or publishes it. release creates the GitHub Release as a draft when the version is staged.
  • The Release tags ruleset blocks updates, deletions and force pushes on v* with no bypass, immutable releases are on, and the release environment only accepts v* tags with admin bypass off.
  • The pinned gitleaks archive matches the checksum in the official 8.30.1 release, and the 0.0.0-stage placeholder is documented npm behavior.

A few notes, none blocking:

  • Create NPM_STAGE_TOKEN as a secret on the release environment rather than the repo, so only the gated job can read it. Repo-level secrets are also visible to jobs in that environment.
  • npm stage needs 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: true on the publish job's setup-node would guard against an older cached version later.
  • After 1.0.0 is approved, deprecating 0.0.0-stage and checking that latest points to 1.0.0 would keep the version list clear.

@drkjc
drkjc merged commit f20bac9 into main Oct 21, 2026
5 checks passed
@drkjc
drkjc deleted the add-npm-publish-workflow branch October 21, 2026 17:01
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.

3 participants