Skip to content

ci: stop passing NPM_TOKEN to Speakeasy workflows for npm trusted publishing - #1267

Merged
alexb-openrouter merged 1 commit into
mainfrom
devin/1793075644-npm-oidc
Oct 27, 2026
Merged

alexb-openrouter merged 1 commit into
mainfrom
devin/1793075644-npm-oidc

Conversation

@alexb-openrouter

Copy link
Copy Markdown
Contributor

Summary

Removes npm_token: ${{ secrets.NPM_TOKEN }} from sdk_publish.yaml, sdk_generation.yaml and sdk_generation_for_spec_change.yaml, so that the long-lived NPM_TOKEN secret can be deleted. Publishing relies on npm trusted publishing (OIDC) only.

  • npm trusted publishing is already working for this package: @openrouter/sdk@1.5.11 shows _npmUser: GitHub Actions <npm-oidc-no-reply@github.com> and has SLSA provenance. The token is passed today but OIDC is what gets used, so nothing needs to change on npmjs.com.
  • The pinned sdk-publish.yaml@68f3bfe publish-npm job runs plain npm publish on a GitHub-hosted runner with Node 24.x, and it inherits the caller's id-token: write. npm_token is required: false in both reusable workflows. With NODE_AUTH_TOKEN empty, npm uses OIDC.
  • The two generation workflows run mode: pr and never publish. They only passed the token through.
  • .speakeasy/workflow.yaml keeps token: $npm_token. In sdk-gen-config the NPM schema marks token as required, and unlike PyPI there is no useTrustedPublishing option. It is only an env-var reference, and an unset variable resolves to empty.

Not verified until the first release after merge: that Speakeasy's release step accepts an empty $npm_token. If it fails, revert this PR.

After a successful release: on npmjs.com, turn on "Require two-factor authentication and disallow tokens" for @openrouter/sdk, revoke the npm token and delete the NPM_TOKEN repo secret.

Link to Devin session: https://openrouter.devinenterprise.com/sessions/ab9f8df32f544a1eae4412b551a5f4b4
Open in Devin Desktop: https://openrouter.devinenterprise.com/desktop/session/ab9f8df32f544a1eae4412b551a5f4b4?variant=devin
Requested by: @alexb-openrouter

@devin-ai-integration

Copy link
Copy Markdown
Contributor

I'll fix CI failures and address comments from users with write access that start with 'Devin'.

  • Disable automatic comment, CI, and merge conflict monitoring

Original prompt from Alex

SYSTEM:
<latest_message>
Alex Bujduveanu (U0BFFJD2XR8) [ts=1793074025.888729]: can this change be done entirely in a PR without git config chnages outside the PR?

publish tokens are long-lived. release.yaml publishes to npm with NPM_TOKEN, typescript-sdk uses NPM_TOKEN, python-sdk uses PYPI_TOKEN. litellm got hit in March because a token like this was readable from shared CI. fix: npm/PyPI trusted publishing (OIDC). ai-sdk-provider already does this.
</latest_message>

=== BEGIN THREAD HISTORY ===
Alex Bujduveanu (U0BFFJD2XR8) [ts=1793074025.888729]: can this change be done entirely in a PR without git config chnages outside the PR?

publish tokens are long-lived. release.yaml publishes to npm with NPM_TOKEN, typescript-sdk uses NPM_TOKEN, python-sdk uses PYPI_TOKEN. litellm got hit in March because a token like this was readable from shared CI. fix: npm/PyPI trusted publishing (OIDC). ai-sdk-provider already does this.
=== END THREAD HISTORY ===
Channel ID: D0BFEN3DLHK
Thread URL: https://openrouter.slack.com/archives/D0BFEN3DLHK/p1793074025888729?thread_ts=1793074025.888729&amp;cid=D0BFEN3DLHK

The <latest_message> is the message that you should use to guide your goals + task for this session, and you should use the rest of the slack thread as context.
A [ts=...] marker on a Slack message is that message's timestamp. To act on a specific message with the slack tool (e.g. adding an emoji reaction via the reaction command), pass that value as timestamp along with the Channel ID — no extra lookup call is needed.

@alexb-openrouter
alexb-openrouter marked this pull request as ready for review October 27, 2026 04:36
@alexb-openrouter
alexb-openrouter requested a review from a team October 27, 2026 04:37
@alexb-openrouter
alexb-openrouter merged commit 8032fec into main Oct 27, 2026
1 check passed
@alexb-openrouter
alexb-openrouter deleted the devin/1793075644-npm-oidc branch October 27, 2026 04:53

@perry-the-pr-reviewer perry-the-pr-reviewer Bot 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.

Perry's Review

Verdict: 💬 Comments / questions

APPROVE was downgraded to COMMENT: Perry's maintainer app is not installed on OpenRouterTeam, so it cannot cast an approving review here. The findings below are clean — this is ready for a human to approve.

Correct removal, and I was able to verify the one thing the description flags as unverified. One thing to document so this doesn't break later.

Verification of the premise

I checked each claim in the description against the pinned action source and the registry rather than taking it on trust:

  • npm OIDC is live for this package. Versions 1.5.9, 1.5.10 and 1.5.11 of the package all carry _npmUser: GitHub Actions <npm-oidc-no-reply@github.com> with trustedPublisher.id: github and dist.attestations, so the last several releases already published over OIDC, not the token. The token was genuinely unused.
  • npm_token is optional in both reusable workflows. The pinned revisions of sdk-publish.yaml and workflow-executor.yaml in sdk-generation-action declare it required: false, so dropping the secrets: entry does not fail workflow validation.
  • The release step never reads npm_token. Its Go implementation only calls addPublishOutputs → AddTargetPublishOutputs, which decides whether to publish from target.IsPublished(). Nothing in the release path touches the npm token, so the "not verified until the first release" caveat in the description does not apply — there is no code path that could reject an empty $npm_token.
  • Empty NODE_AUTH_TOKEN still leaves OIDC reachable. In the npm CLI's publish command, oidc() runs before the code treats a missing credential as ENEEDAUTH; the exchanged token is installed via config.set(authTokenKey, token, 'cli'), which outranks the _authToken line setup-node writes into the npmrc. An empty env-var-derived value is simply overridden. Publishing keeps working.
  • id-token: write reaches the callee. sdk_publish.yaml grants it at workflow level and the pinned reusable workflow declares no permissions: block, so it cannot reduce it. The 1.5.11 provenance attestation is the empirical proof.
The one thing worth documenting

The Speakeasy workflow file keeps token: $npm_token, and that line is now load-bearing even though the variable it names no longer exists.

workflow.Load in the sdk-gen-config library never expands environment references — it leaves Token as the literal string "$npm_token". Because that string is non-empty, Publishing.IsPublished() returns true, AddTargetPublishOutputs emits publish_typescript=true, and the publish-npm job runs. That is the only reason npm publishing still happens.

So if someone later cleans up that workflow file and deletes the token: $npm_token line — a very natural follow-up, since the secret is being retired — IsPublished() flips to false, the publish-npm job is skipped on the next release, and the Publish workflow still reports success. The only signal would be the agent-bump dispatch failing ~20 minutes later because the new version is not on npm. Nothing in the file says that line must stay.

I left an inline comment asking for a short comment there. Not a blocker, and not something this PR has to solve before merging — but it is the kind of silent failure that costs an afternoon later.

Risk

Risk: 🟡 Medium

Estimated impact: If this change is wrong, the next Publish run fails to publish to npm and blocks the SDK release chain (and the agent bump that follows it). Fully recoverable by re-adding the NPM_TOKEN secret and reverting this diff; detected within one release cycle.

Dimension Severity Risk Reasoning
Implementation risk 🟨🟨 Medium The diff is three deletions and each is verified against the pinned action source; the residual risk is the undocumented dependency on the orphaned token: line.
Premise risk 🟩 Low Every claim was independently confirmed: OIDC user on three published versions, required: false on both reusable workflows, and no npm_token read anywhere in the release path.
Estimated impact 🟨🟨 Medium Worst case is a blocked npm release, not a bad publish — nothing reaches consumers, and a revert plus secret restore recovers it.
Risk Factor Severity Risk Reasoning
Reversibility 🟩 Low Reverting the diff and restoring the secret returns the previous state completely.
Detectability 🟨🟨 Medium The Publish workflow can succeed while skipping npm publish, so the failure surfaces only when the downstream version check times out.
Blast radius 🟨🟨 Medium All consumers of the SDK are affected if publishing stops, but no published artifact can be corrupted.
Data integrity None No persisted state is touched; a failed publish writes nothing to the registry.
Financial exposure None No billing or revenue path is involved.
Security and privacy exposure 🟩 Low This removes a long-lived credential from CI and narrows to short-lived OIDC — a net reduction in exposure.
Propagation 🟨🟨 Medium A missed publish propagates into the dependent agent repo and the monorepo bump via the dispatch workflow.
Availability 🟨🟨 Medium Release availability depends on the publish path staying enabled.
Recovery cost 🟩 Low One revert commit plus re-adding a secret; no reconciliation or backfill.
Time to correct 🟩 Low Correctable on the next workflow run once the revert lands.

Comment thread .github/workflows/sdk_publish.yaml Resolved
alexb-openrouter added a commit that referenced this pull request Nov 5, 2026
…der (#1268)

## Summary

Follow-up to #1267, from Perry's review there. Adds a comment only.
Nothing changes at runtime.

`targets.openrouter.publish.npm.token: $npm_token` now looks dead,
because `NPM_TOKEN` is no longer passed. But it is what turns publishing
on. sdk-gen-config does not expand `$npm_token`, and
`Publishing.IsPublished("typescript")` returns true only when `NPM.Token
!= ""`. That makes the Speakeasy release step emit
`publish_typescript=true`, which is what runs `publish-npm`. If the line
is deleted, npm publishing is skipped silently and the `Publish`
workflow still reports success. npm itself authenticates through trusted
publishing (OIDC).

Unverified: whether a later Speakeasy CLI rewrite of `workflow.yaml`
(for example a `speakeasyVersion` bump) keeps YAML comments.

Searched existing PRs; none matched.

Link to Devin session:
https://openrouter.devinenterprise.com/sessions/ab9f8df32f544a1eae4412b551a5f4b4
Open in Devin Desktop:
https://openrouter.devinenterprise.com/desktop/session/ab9f8df32f544a1eae4412b551a5f4b4?variant=devin
Requested by: @alexb-openrouter

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
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.

2 participants