From ae014c253c723e4d4126e87c7ea46dd03f21a8f9 Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Mon, 26 Oct 2026 19:43:52 +0000 Subject: [PATCH] ci(npm): publish via npm trusted publishing (GitHub OIDC) Drop NPM_TOKEN from publish-js.yml and publish-wasm.yml. Publish jobs keep id-token: write, ensure npm >= 11.5.1 for the OIDC token exchange, and no longer set setup-node registry-url (its .npmrc carries a placeholder token). Guard with a release-workflow test and document npmjs.com publisher config. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/publish-js.yml | 25 +++++++++++++++++++------ .github/workflows/publish-wasm.yml | 24 ++++++++++++++++++------ knowledge/operations/release-process.md | 19 ++++++++++++++----- knowledge/runtimes/browser-package.md | 2 +- scripts/tests/test_release_workflow.py | 17 +++++++++++++++++ 5 files changed, 69 insertions(+), 18 deletions(-) diff --git a/.github/workflows/publish-js.yml b/.github/workflows/publish-js.yml index fa83a171a..f3d0bfdd6 100644 --- a/.github/workflows/publish-js.yml +++ b/.github/workflows/publish-js.yml @@ -3,9 +3,13 @@ # Triggered alongside publish.yml on GitHub Release or manual dispatch. # Adapted from https://github.com/pydantic/monty CI JS-building pattern. # -# Prerequisites: -# - NPM_TOKEN secret with publish access (GitHub Settings > Secrets > Actions) -# - id-token: write permission for npm provenance (OIDC attestation) +# Auth: npm trusted publishing (GitHub OIDC), no NPM_TOKEN secret. +# - npmjs.com @everruns/bashkit > Settings > Trusted publisher: +# GitHub Actions, everruns/bashkit, workflow publish-js.yml, no environment +# - release-js needs id-token: write; npm >= 11.5.1 exchanges the OIDC token +# for a short-lived publish token and attaches provenance automatically +# - setup-node has no registry option: it would write an .npmrc carrying a +# placeholder auth token name: Publish JS @@ -509,7 +513,18 @@ jobs: node-version: "24.22.1" cache: pnpm cache-dependency-path: crates/bashkit-js/pnpm-lock.yaml - registry-url: "https://registry.npmjs.org" + + - name: Require npm with trusted publishing (>= 11.5.1) + # npm trusted publishing (GitHub OIDC -> short-lived publish token) + # landed in npm 11.5.1. Upgrade to a pinned release if the bundled + # npm is older, then assert. + run: | + npm_ok() { + node -e 'const [a,b,c]=process.argv[1].split(".").map(Number);process.exit(a>11||(a==11&&(b>5||(b==5&&c>=1)))?0:1)' "$(npm --version)" + } + npm_ok || npm install -g npm@11.6.2 + npm_ok || { echo "::error::npm $(npm --version) lacks trusted publishing"; exit 1; } + echo "npm $(npm --version)" - name: Install dependencies run: pnpm install --frozen-lockfile @@ -558,8 +573,6 @@ jobs: echo "Publishing with dist-tag: $TAG" python3 "$GITHUB_WORKSPACE/scripts/publish_npm.py" --tag "$TAG" working-directory: crates/bashkit-js - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Verify npm publish run: | diff --git a/.github/workflows/publish-wasm.yml b/.github/workflows/publish-wasm.yml index 2d549d854..72bc6b681 100644 --- a/.github/workflows/publish-wasm.yml +++ b/.github/workflows/publish-wasm.yml @@ -3,8 +3,11 @@ # publishes to npm. Triggered alongside publish.yml on GitHub Release. # # Prerequisites: -# - NPM_TOKEN secret with publish access -# - id-token: write permission for npm provenance (OIDC attestation) +# - npm trusted publishing (GitHub OIDC), no NPM_TOKEN secret: +# npmjs.com @everruns/bashkit-wasm > Settings > Trusted publisher: +# GitHub Actions, everruns/bashkit, workflow publish-wasm.yml, no environment +# - id-token: write on publish-wasm; npm >= 11.5.1 (OIDC token exchange + +# automatic provenance) # # Derive the bindgen CLI from Cargo.lock; never duplicate its schema version. @@ -110,7 +113,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - id-token: write # npm provenance + id-token: write # npm trusted publishing + provenance steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -129,7 +132,18 @@ jobs: uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6 with: node-version: "24.22.1" - registry-url: "https://registry.npmjs.org" + + - name: Require npm with trusted publishing (>= 11.5.1) + # npm trusted publishing (GitHub OIDC -> short-lived publish token) + # landed in npm 11.5.1. Upgrade to a pinned release if the bundled + # npm is older, then assert. + run: | + npm_ok() { + node -e 'const [a,b,c]=process.argv[1].split(".").map(Number);process.exit(a>11||(a==11&&(b>5||(b==5&&c>=1)))?0:1)' "$(npm --version)" + } + npm_ok || npm install -g npm@11.6.2 + npm_ok || { echo "::error::npm $(npm --version) lacks trusted publishing"; exit 1; } + echo "npm $(npm --version)" - name: Download pkg uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8 @@ -161,5 +175,3 @@ jobs: TAG="${{ steps.dist-tag.outputs.tag }}" echo "Publishing @everruns/bashkit-wasm with dist-tag: $TAG" python3 "$GITHUB_WORKSPACE/scripts/publish_npm.py" --tag "$TAG" - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/knowledge/operations/release-process.md b/knowledge/operations/release-process.md index 1c3313145..893165f21 100644 --- a/knowledge/operations/release-process.md +++ b/knowledge/operations/release-process.md @@ -303,9 +303,8 @@ Trigger: Release published (parallel). Builds native NAPI-RS bindings (macOS x86_64/aarch64, Linux x86_64/aarch64, Windows x86_64, wasm32-wasip1-threads), tests on Node 20/22/24 (Node 20 runs the runtime-compat suite, not the ava suites, since ava >= 8 needs Node 22.20+), -publishes to npm. Secret: -`NPM_TOKEN` (Automation token); provenance via `id-token: write` OIDC + -`--provenance`, same pattern as everruns/sdk. JS package version is synced +publishes to npm via npm trusted publishing (GitHub OIDC, `id-token: write`, +npm >= 11.5.1, provenance automatic); no `NPM_TOKEN`. JS package version is synced from the workspace `Cargo.toml` by `build.rs` updating `package.json`. Only a run whose ref is the release tag (`refs/tags/vX.Y.Z`, version matching @@ -337,7 +336,7 @@ one invariant, since each is a hand-maintained copy of the same set. Dispatched by `release.yml` from the verified release tag. Builds the single-threaded `wasm32-unknown-unknown` browser bundle, runs the headless Node integration suite, and publishes `@everruns/bashkit-wasm` to npm with -provenance. Uses the same `NPM_TOKEN` as `publish-js.yml`; the package version +provenance via npm trusted publishing, same as `publish-js.yml`; the package version is synced manually from the workspace version during release preparation. CI and publication both use the `wasm-release` profile and run the package's @@ -365,7 +364,17 @@ from `main`; build checkout remains pinned to the tag. - `CARGO_REGISTRY_TOKEN` (crates.io token, publish scopes) in GitHub Actions secrets. - PyPI trusted publishing: configure publisher at pypi.org for repo `everruns/bashkit`, workflow `publish-python.yml`, environment `release-python`. -- `NPM_TOKEN` (Automation token) in GitHub Actions secrets; no separate GitHub environment required. +- npm trusted publishing (GitHub OIDC), no npm token secret. On npmjs.com, each + package's Settings > Trusted publisher: GitHub Actions, organization `everruns`, + repository `bashkit`, workflow `publish-js.yml` (`@everruns/bashkit`) or + `publish-wasm.yml` (`@everruns/bashkit-wasm`), environment empty. Publish jobs + need `id-token: write` and npm >= 11.5.1 (the step upgrades to a pinned npm if + Node's bundled one is older). Do not set setup-node `registry-url`: its `.npmrc` + carries a placeholder `NODE_AUTH_TOKEN`. The long-lived `NPM_TOKEN` was rejected + before 0.20.1 and npm reported the auth failure as a 404 on + `@everruns/bashkit-wasm`; an npm publish 404 on an existing package means auth, + check the trusted publisher config first. Guarded by + `scripts/tests/test_release_workflow.py`. ## Post-Release Verification diff --git a/knowledge/runtimes/browser-package.md b/knowledge/runtimes/browser-package.md index fc128d5a3..d3551ed0d 100644 --- a/knowledge/runtimes/browser-package.md +++ b/knowledge/runtimes/browser-package.md @@ -299,7 +299,7 @@ not evidence of a divergent published feature selection. Version tracks the workspace `Cargo.toml` (currently synced by the release prepare step, same as the other packages). `publish-wasm.yml` triggers on release published, builds `pkg/`, runs the smoke test, and `npm publish`es -`@everruns/bashkit-wasm` with provenance (`NPM_TOKEN`, `id-token: write`), same +`@everruns/bashkit-wasm` via npm trusted publishing (GitHub OIDC, `id-token: write`), same pattern as `publish-js.yml`. Browser example smoke testing writes a file under `/home/user`, reloads the page, and verifies `browserLocal` restores it from `localStorage`. The text-only example adapter commits only a complete traversal; diff --git a/scripts/tests/test_release_workflow.py b/scripts/tests/test_release_workflow.py index 970ae4bbe..24415fbf3 100644 --- a/scripts/tests/test_release_workflow.py +++ b/scripts/tests/test_release_workflow.py @@ -159,6 +159,23 @@ def test_npm_publish_lanes_share_exact_archive_verification(self) -> None: with self.subTest(workflow=name): self.assertIn('python3 "$GITHUB_WORKSPACE/scripts/publish_npm.py" --tag "$TAG"', workflow) + def test_npm_publish_lanes_use_trusted_publishing(self) -> None: + """npm auth is GitHub OIDC trusted publishing, never a long-lived token. + + A rejected NPM_TOKEN made npm answer the 0.20.1 wasm publish with a + misleading 404. setup-node's registry-url writes an .npmrc carrying a + placeholder NODE_AUTH_TOKEN, so it stays off too. + """ + for name, job in (("publish-js.yml", "release-js"), ("publish-wasm.yml", "publish-wasm")): + workflow = (ROOT / ".github/workflows" / name).read_text() + with self.subTest(workflow=name): + self.assertNotIn("secrets.NPM_TOKEN", workflow) + self.assertNotIn("NODE_AUTH_TOKEN", workflow) + self.assertNotIn("registry-url:", workflow) + body = workflow.split(f"\n {job}:\n", 1)[1] + self.assertIn("id-token: write", body.split("\n steps:\n", 1)[0]) + self.assertIn("npm install -g npm@11.6.2", body) + def test_web_ci_exercises_release_wasm_optimization(self) -> None: build_script = (ROOT / "crates/bashkit-wasm/scripts/build.sh").read_text() ci_workflow = (ROOT / ".github/workflows/ci.yml").read_text()