Skip to content

Commit 67d240a

Browse files
authored
feat: switch npm publishing to OIDC trusted publishing (#107)
- Move publish job to ubuntu-latest (OIDC requires GitHub-hosted runners) - Add --provenance flag for production releases (conditional on event type) - Pin npm to v11 (trusted publishing requires 11.5.1+) - Add NPM_TOKEN fallback for manual workflow_dispatch - Add post-publish verification step for all 4 packages - Update release docs with trusted publisher setup and manual publishing procedure - Remove NPM_TOKEN secret from CreateRelease.yml workflow_call Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
1 parent b48706d commit 67d240a

6 files changed

Lines changed: 220 additions & 100 deletions

File tree

.github/workflows/CreateRelease.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,5 +106,3 @@ jobs:
106106
with:
107107
version: ${{ needs.publish-hyperlight-js-packages-and-create-release.outputs.version }}
108108
dry-run: false
109-
secrets:
110-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

.github/workflows/npm-publish.yml

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ on:
1414
required: false
1515
type: boolean
1616
default: false
17+
# IMPORTANT: Trusted publishing (OIDC) is configured on npmjs.com with
18+
# workflow filename 'CreateRelease.yml'. npm checks the *calling* workflow
19+
# for workflow_call, not the reusable workflow that runs npm publish.
20+
# Calling this workflow from a different parent workflow will fail OIDC auth.
21+
# See: https://docs.npmjs.com/trusted-publishers#troubleshooting
1722
workflow_call:
1823
inputs:
1924
version:
@@ -25,9 +30,6 @@ on:
2530
required: false
2631
type: boolean
2732
default: false
28-
secrets:
29-
NPM_TOKEN:
30-
required: true
3133

3234
permissions:
3335
contents: read
@@ -114,7 +116,10 @@ jobs:
114116

115117
publish:
116118
needs: build
117-
runs-on: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd"]
119+
# Trusted publishing (OIDC) requires GitHub-hosted runners.
120+
# The publish job only downloads pre-built artifacts and runs npm publish,
121+
# so it does not need self-hosted runners with KVM/Hyperlight.
122+
runs-on: ubuntu-latest
118123
steps:
119124
- uses: actions/checkout@v6
120125

@@ -126,6 +131,10 @@ jobs:
126131
cache: 'npm'
127132
cache-dependency-path: 'src/js-host-api/package-lock.json'
128133

134+
# Trusted publishing requires npm 11.5.1+ (Node 22 ships with npm 10.x)
135+
- name: Upgrade npm for trusted publishing
136+
run: npm install -g npm@11 && npm --version
137+
129138
- name: Validate version format
130139
run: npx --yes semver "$VERSION"
131140
env:
@@ -218,33 +227,91 @@ jobs:
218227
working-directory: ${{ env.WORKING_DIR }}
219228
run: ./test-pack.sh
220229

230+
# ── Authentication strategy ────────────────────────────────────
231+
# Production releases via CreateRelease.yml use trusted publishing
232+
# (OIDC) — npm auto-detects the id-token and exchanges it for a
233+
# short-lived publish credential. Provenance attestations are
234+
# generated automatically. The --provenance flag is added explicitly
235+
# so the build fails loudly if OIDC is misconfigured.
236+
#
237+
# Manual workflow_dispatch requires an NPM_TOKEN repo secret because
238+
# trusted publishing is configured for CreateRelease.yml only.
239+
# Provenance is NOT available for token-based publishing.
240+
# You should almost never need to publish manually — if you do,
241+
# see docs/release.md for the full (deliberately painful) steps.
242+
- name: Validate NPM_TOKEN for manual dispatch
243+
if: ${{ github.event_name == 'workflow_dispatch' && !inputs['dry-run'] }}
244+
run: |
245+
if [ -z "$NPM_TOKEN" ]; then
246+
echo "::error::NPM_TOKEN repo secret is required for manual workflow_dispatch publishing."
247+
echo "::error::See docs/release.md 'Manual npm publishing (emergency only)' for instructions."
248+
exit 1
249+
fi
250+
env:
251+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
252+
253+
# Set provenance flag once — OIDC path gets --provenance (fails loud
254+
# if misconfigured), token path skips it (not supported).
255+
- name: Set publish flags
256+
id: publish-flags
257+
run: |
258+
if [ "${{ github.event_name }}" != "workflow_dispatch" ]; then
259+
echo "provenance=--provenance" >> "$GITHUB_OUTPUT"
260+
else
261+
echo "provenance=" >> "$GITHUB_OUTPUT"
262+
fi
263+
221264
- name: Publish Linux GNU package
222265
if: ${{ !inputs['dry-run'] }}
223266
working-directory: ${{ env.WORKING_DIR }}/npm/linux-x64-gnu
224-
run: npm publish --access public --ignore-scripts
267+
run: npm publish --access public --ignore-scripts ${{ steps.publish-flags.outputs.provenance }}
225268
env:
226-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
269+
NODE_AUTH_TOKEN: ${{ github.event_name == 'workflow_dispatch' && secrets.NPM_TOKEN || '' }}
227270

228271
- name: Publish Linux musl package
229272
if: ${{ !inputs['dry-run'] }}
230273
working-directory: ${{ env.WORKING_DIR }}/npm/linux-x64-musl
231-
run: npm publish --access public --ignore-scripts
274+
run: npm publish --access public --ignore-scripts ${{ steps.publish-flags.outputs.provenance }}
232275
env:
233-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
276+
NODE_AUTH_TOKEN: ${{ github.event_name == 'workflow_dispatch' && secrets.NPM_TOKEN || '' }}
234277

235278
- name: Publish Windows package
236279
if: ${{ !inputs['dry-run'] }}
237280
working-directory: ${{ env.WORKING_DIR }}/npm/win32-x64-msvc
238-
run: npm publish --access public --ignore-scripts
281+
run: npm publish --access public --ignore-scripts ${{ steps.publish-flags.outputs.provenance }}
239282
env:
240-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
283+
NODE_AUTH_TOKEN: ${{ github.event_name == 'workflow_dispatch' && secrets.NPM_TOKEN || '' }}
241284

242285
- name: Publish main package
243286
if: ${{ !inputs['dry-run'] }}
244287
working-directory: ${{ env.WORKING_DIR }}
245-
run: npm publish --access public --ignore-scripts
288+
run: npm publish --access public --ignore-scripts ${{ steps.publish-flags.outputs.provenance }}
289+
env:
290+
NODE_AUTH_TOKEN: ${{ github.event_name == 'workflow_dispatch' && secrets.NPM_TOKEN || '' }}
291+
292+
- name: Verify all packages published
293+
if: ${{ !inputs['dry-run'] }}
294+
run: |
295+
echo "Waiting for registry propagation..."
296+
sleep 15
297+
FAILED=0
298+
for pkg in "@hyperlight-dev/js-host-api" \
299+
"@hyperlight-dev/js-host-api-linux-x64-gnu" \
300+
"@hyperlight-dev/js-host-api-linux-x64-musl" \
301+
"@hyperlight-dev/js-host-api-win32-x64-msvc"; do
302+
if npm view "$pkg@$VERSION" version > /dev/null 2>&1; then
303+
echo "✅ $pkg@$VERSION published"
304+
else
305+
echo "::error::❌ $pkg@$VERSION NOT found on registry!"
306+
FAILED=1
307+
fi
308+
done
309+
if [ "$FAILED" -eq 1 ]; then
310+
echo "::error::Some packages failed to publish. Check above for details."
311+
exit 1
312+
fi
246313
env:
247-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
314+
VERSION: ${{ inputs.version }}
248315

249316
- name: Dry run - show what would be published
250317
if: ${{ inputs['dry-run'] }}

docs/release.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,67 @@ This release contains the benchmark results and the source code for the release
5353

5454
In addition the hyperlight-js crates will be published to crates.io. You can verify this by going to the [hyperlight-js page on crates.io](https://crates.io/crates/hyperlight-js) and checking that the new version is listed.
5555

56+
The npm packages (`@hyperlight-dev/js-host-api` and platform-specific binaries) are also published automatically as part of this workflow. Publishing uses [npm trusted publishing (OIDC)](https://docs.npmjs.com/trusted-publishers) — no `NPM_TOKEN` secret is needed for the `CreateRelease` workflow. Provenance attestations are generated automatically.
57+
58+
You can verify the npm publish by checking the [npmjs.com package page](https://www.npmjs.com/package/@hyperlight-dev/js-host-api).
59+
60+
### npm trusted publishing setup
61+
62+
Trusted publishing is configured on [npmjs.com](https://www.npmjs.com/) for each package. If you add a new platform package, you must configure its trusted publisher:
63+
64+
1. Go to the package on npmjs.com → Settings → Trusted Publisher
65+
2. Select **GitHub Actions**
66+
3. Set **Organization**: `hyperlight-dev`, **Repository**: `hyperlight-js`, **Workflow**: `CreateRelease.yml`
67+
4. Save
68+
69+
This must be done for all 4 packages:
70+
- `@hyperlight-dev/js-host-api`
71+
- `@hyperlight-dev/js-host-api-linux-x64-gnu`
72+
- `@hyperlight-dev/js-host-api-linux-x64-musl`
73+
- `@hyperlight-dev/js-host-api-win32-x64-msvc`
74+
75+
> **Note:** Trusted publishing only works via `CreateRelease.yml` (the production release path). Manual publishing is deliberately discouraged — see [Manual npm publishing (emergency only)](#manual-npm-publishing-emergency-only) below.
76+
5677
## Patching a release
5778

5879
If you need to update a previously released version of hyperlight-js then you should open a Pull Request against the release branch you want to patch, for example if you wish to patch the release `v0.18.0` then you should open a PR against the `release/v0.18.0` branch.
5980

6081
Once the PR is merged, then you should follow the instructions above. In this instance the version number of the tag should be a patch version, for example if you are patching the `release/v0.18.0` branch and this is the first patch release to that branch then the tag should be `v0.18.1`. If you are patching a patch release then the tag should be `v0.18.2` and the target branch should be `release/v0.18.1` and so on.
82+
83+
## Manual npm publishing (emergency only)
84+
85+
> ⚠️ **Do not use this for regular releases.** Use the `CreateRelease` workflow instead. Manual publishing bypasses OIDC trusted publishing and will **not** generate provenance attestations — meaning publish will show up without the "Published via trusted publishing" badge on npmjs.com. Only use this if the automated release pipeline is broken and you need to ship an urgent fix.
86+
87+
If you need to publish npm packages manually via `workflow_dispatch`, you'll need to:
88+
89+
1. **Temporarily allow token-based publishing on npmjs.com**
90+
- Go to each package on [npmjs.com](https://www.npmjs.com/) → Settings → Publishing access
91+
- Change from "Require two-factor authentication and disallow tokens" to "Require two-factor authentication or automation tokens"
92+
- Do this for all 4 packages:
93+
- `@hyperlight-dev/js-host-api`
94+
- `@hyperlight-dev/js-host-api-linux-x64-gnu`
95+
- `@hyperlight-dev/js-host-api-linux-x64-musl`
96+
- `@hyperlight-dev/js-host-api-win32-x64-msvc`
97+
98+
2. **Create an npm automation token**
99+
- Go to [npmjs.com](https://www.npmjs.com/) → Access Tokens → Generate New Token → Granular Access Token
100+
- Set scope to the `@hyperlight-dev` org, packages: read and write, expiry: shortest available
101+
- Copy the token
102+
103+
3. **Add the token as a repo secret**
104+
- Go to the repo on GitHub → Settings → Secrets and variables → Actions → New repository secret
105+
- Name: `NPM_TOKEN`
106+
- Value: paste the token from the previous step
107+
- Save
108+
109+
4. **Run the workflow**
110+
- Go to Actions → "Publish npm packages" → Run workflow
111+
- Select the correct branch
112+
- Enter the version (e.g. `0.2.1`)
113+
- Set `dry-run` to `false`
114+
115+
5. **Clean up immediately after publishing**
116+
- Delete the `NPM_TOKEN` repo secret on GitHub → Settings → Secrets and variables → Actions
117+
- Revoke the npm token on npmjs.com → Access Tokens
118+
- Re-enable "Require two-factor authentication and disallow tokens" on all 4 packages
119+
- Verify the packages published correctly: `npm view @hyperlight-dev/js-host-api versions`

src/js-host-api/.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ examples/
2424
*.config.mjs
2525
.prettierrc
2626
TYPE_NAMING.md
27+
DEVELOPMENT.md
2728
build.rs
2829
src/
2930
Cargo.toml

src/js-host-api/DEVELOPMENT.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Hyperlight JS Host API — Development Guide
2+
3+
Development, build, and publishing instructions for contributors.
4+
5+
## Requirements
6+
7+
- **Node.js** >= 18
8+
- **Rust** toolchain (see `rust-toolchain.toml` in repository root)
9+
- **just** (build automation) — install via `cargo install just` or your package manager
10+
11+
## Building from Source
12+
13+
### Build Commands
14+
15+
```bash
16+
# Install dependencies
17+
npm install
18+
19+
# Release builds (optimized)
20+
npm run build
21+
22+
# Debug builds (with symbols)
23+
npm run build:debug
24+
25+
# Run tests
26+
npm test
27+
```
28+
29+
### Using Just (Build Automation)
30+
31+
From the repository root:
32+
33+
```bash
34+
# Build js-host-api
35+
just build-js-host-api release
36+
37+
# Build with debug symbols
38+
just build-js-host-api debug
39+
40+
# Run js-host-api examples
41+
just run-js-host-api-examples release
42+
43+
# Run js-host-api tests
44+
just test-js-host-api release
45+
46+
# Build and test everything (all runtimes and targets)
47+
just build-all
48+
just test-all release
49+
```
50+
51+
## Publishing to npm
52+
53+
The package is published to npmjs.com as `@hyperlight-dev/js-host-api` with platform-specific binary packages using [npm trusted publishing (OIDC)](https://docs.npmjs.com/trusted-publishers).
54+
55+
> **Note:** You cannot `npm publish` from your local machine. Publishing is handled by CI via the `CreateRelease` workflow, which uses OIDC for authentication. Trusted publishing must be configured on npmjs.com — see [docs/release.md](../../docs/release.md) for full setup and release instructions.
56+
57+
**For release instructions, see [docs/release.md](../../docs/release.md).**
58+
59+
### Package Structure
60+
61+
The npm release consists of the following packages:
62+
63+
| Package | Description |
64+
|---------|-------------|
65+
| `@hyperlight-dev/js-host-api` | Main package (installs correct binary automatically) |
66+
| `@hyperlight-dev/js-host-api-linux-x64-gnu` | Linux x86_64 (glibc) native binary |
67+
| `@hyperlight-dev/js-host-api-linux-x64-musl` | Linux x86_64 (musl/Alpine) native binary |
68+
| `@hyperlight-dev/js-host-api-win32-x64-msvc` | Windows x86_64 native binary |
69+
70+
### How Platform Selection Works
71+
72+
This project uses the [napi-rs](https://napi.rs/docs/deep-dive/release#3-the-native-addon-for-different-platforms-is-distributed-through-different-npm-packages) approach for distributing native addons across platforms. Each platform-specific binary is published as a separate npm package and listed as an `optionalDependency` of the main package.
73+
74+
**At install time:** npm uses the `os`, `cpu`, and `libc` fields in each platform sub-package's `package.json` to determine which optional dependency to install. Packages that don't match the user's platform are silently skipped. The main package itself does **not** have `os`/`cpu` fields because it contains only JavaScript — restricting it would prevent installation on unsupported platforms even for type-checking or development purposes.
75+
76+
**At runtime:** The napi-rs generated `index.js` detects the platform (including glibc vs musl on Linux) and loads the correct `.node` binary.

0 commit comments

Comments
 (0)