Skip to content

consolidate npm publishing for trusted publishing (OIDC) - #26

Merged
iamalwaysuncomfortable merged 8 commits into
mainfrom
npm-trusted-publishing
Jul 29, 2026
Merged

consolidate npm publishing for trusted publishing (OIDC)#26
iamalwaysuncomfortable merged 8 commits into
mainfrom
npm-trusted-publishing

Conversation

@iamalwaysuncomfortable

@iamalwaysuncomfortable iamalwaysuncomfortable commented Jul 29, 2026

Copy link
Copy Markdown
Member

Motivation

Groundwork for moving npm publishing from a long-lived NPM_TOKEN secret to npm trusted publishing (OIDC), which replaces the token with short-lived per-publish credentials and adds automatic provenance attestation.

npm's trusted publisher config allows one publisher per package, matched by workflow filename — so all publishing must live in a single workflow file. This PR makes npm-publish.yml that file and tightens the release flow:

  • release.yml no longer publishes to npm inline; after the GitHub release is published it dispatches npm-publish.yml, then watches the dispatched run to completion and propagates its result — a failed npm publish fails the release run instead of going unnoticed.
  • npm-publish.yml checks out the release tag instead of the default branch: the tag's tree is what gets published. Consequence: it only works for tags that contain scripts/build-npm.mjs, i.e. releases after this PR.
  • Newest stable tag only: the workflow rejects any tag that is not exactly vX.Y.Z and the repository's newest such tag — prereleases, malformed tags, and older releases must be published manually. release.yml skips the npm dispatch for non-stable tags so they still get a GitHub release.
  • Per-package retry with no backpublishing: already-published packages are skipped and each version must be strictly newer than that package's current npm latest, so retrying the newest tag publishes exactly the packages a partial earlier run missed, and the workflow can never move latest backwards or create cross-package version gaps.

Publishing still uses NPM_TOKEN for now — the packages must exist on npm before trusted publishers can be configured for them. A follow-up PR switches npm-publish.yml to OIDC (id-token: write, npm ≥ 11.5.1, drop NODE_AUTH_TOKEN) once the bootstrap publish has happened and the trusted publisher config is in place on npmjs.com.

Test Plan

  • Both workflows pass YAML validation.
  • The tag-format guard was exercised against accepted (v0.2.1, v10.20.30) and rejected (v0.3.0-rc.1, v0.3, v1.2.3.4, 0.2.1, v0.2.1x) shapes.
  • The publish-or-skip guard's semver comparison was exercised against the edge cases: first-ever publish (→ publish), normal new release (→ publish), older or equal version (→ skip), numeric vs lexicographic ordering (0.2.10 > 0.2.9), and a manually published prerelease latest followed by its stable release (0.3.0-rc.10.3.0 publishes).
  • The dispatch-and-watch chaining relies on workflow_dispatch being one of the event types GITHUB_TOKEN is permitted to trigger; end-to-end verification happens with the next release tag.

Related PRs

release.yml no longer publishes to npm directly; after the GitHub release
is created it dispatches npm-publish.yml, which publishes from the release
assets. A single publishing workflow file is a prerequisite for npm trusted
publishing, whose per-package publisher config matches on workflow filename.
…y safely

- release.yml now watches the dispatched npm-publish run to completion and
  propagates its result, so a failed npm publish fails the release run.
- npm-publish.yml checks out the release tag instead of the default branch:
  the tag's tree is what gets published. This requires the tag to contain
  the npm packaging scripts.
- Retrying a version older than the current npm latest publishes under a
  temporary dist-tag so it cannot steal the "latest" dist-tag from a newer
  release.

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.

🟡 Not ready to approve

The release workflow’s dispatch polling window is short enough to be flaky under queue/API propagation delays, which can incorrectly fail releases even when the publish workflow was successfully dispatched.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

This PR consolidates npm publishing into a single workflow (npm-publish.yml) to support npm Trusted Publishing (OIDC) constraints, and updates the release workflow to dispatch-and-watch that publishing workflow so npm publish failures correctly fail the release pipeline.

Changes:

  • Move npm publishing out of release.yml into a dispatched npm-publish.yml run, and propagate its success/failure back to the release workflow.
  • Update npm-publish.yml to publish from the release tag’s tree (reproducible republish) and add a dist-tag guard to prevent older retries from stealing latest.
File summaries
File Description
.github/workflows/release.yml Replaces inline npm publish steps with dispatch + polling + watch of npm-publish.yml, so npm failures fail the release workflow.
.github/workflows/npm-publish.yml Switches checkout to the release tag and adds a backfill dist-tag strategy when republishing versions older than current npm latest.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Low

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread .github/workflows/release.yml Outdated
…lation

- Backdate the dispatch timestamp by a minute so clock skew between the
  runner and GitHub cannot hide the dispatched run from the --created
  filter, which would fail the release while the publish proceeds.
- Compare versions with real semver (npx semver) instead of sort -V,
  which ranks prereleases above their release (0.3.0-rc.1 > 0.3.0) and
  would let an RC keep the "latest" dist-tag.
- Pass the tag and repository into the dispatch script via env instead
  of expression interpolation; tag names may contain shell
  metacharacters.
…essing

npm-publish.yml accepts an optional dispatch_id input and embeds it in
its run name; release.yml passes its own run id and polls for the run
whose name carries it. Discovery of the dispatched run is now exact:
immune to clock skew and to concurrent releases matching each other's
runs. The discovery window is also widened from 1 to 2 minutes to ride
out API propagation delays.

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.

🟡 Human review recommended

It changes production release/publishing automation in ways that can only be fully validated by an end-to-end release run (dispatch + permissions + registry behavior).

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Low

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

- npm-publish.yml fails immediately for any tag that is not exactly
  vX.Y.Z; prerelease and other tag shapes must be published manually.
  release.yml skips the dispatch for such tags so they still get a
  GitHub release without failing the run.
- Per package, publish only when the version is strictly newer than the
  package's current npm latest; skip otherwise. This replaces the
  temporary "backfill" dist-tag mechanism: the workflow can now never
  move `latest` backwards, at the cost that filling version gaps older
  than latest is a manual operation.
The per-package guard compared against each package's own npm latest,
so retrying an old tag could still "forward-publish" a package that had
missed several releases, leaving cross-package version gaps (a launcher
whose exact-pinned platform packages do not exist). The workflow now
refuses any tag that is not the repository's newest stable vX.Y.Z tag:
retrying the newest tag fills in exactly the packages a partial run
missed, and anything older is a manual operation.

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.

🟡 Not ready to approve

The publish guard uses an unpinned npx semver invocation, which can introduce nondeterministic release behavior if the semver CLI changes.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Comments suppressed due to low confidence (1)

.github/workflows/npm-publish.yml:111

  • npx --yes semver is unpinned, so a future semver CLI release could change behavior and make publishing nondeterministic or break unexpectedly. Pin at least the major version (or an exact version) to keep the workflow stable.
            if [ -n "$latest" ] && ! npx --yes semver -r ">${latest}" "$VERSION" >/dev/null 2>&1; then
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Low

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

- Install the semver CLI in its own step instead of npx-on-demand in the
  publish loop: a transient fetch failure there was indistinguishable
  from "version not newer" and would silently skip publishing a package
  while the run stayed green. As a dedicated step, a fetch failure fails
  the run loudly.
- Match the dispatched run by "dispatch <id>)" including the closing
  paren, so a run id that is a string-prefix of a concurrent release's
  run id cannot match the wrong run.
The publish job is the sole holder of npm publish rights for the
@ProvableHQ packages, and semver is the only third-party code it
fetches at run time. Pinning to an exact, immutable version closes off
behavioral drift and package-takeover via a future semver release.
@iamalwaysuncomfortable
iamalwaysuncomfortable merged commit ce0630c into main Jul 29, 2026
1 check passed
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