Skip to content

Commit ad92d57

Browse files
feat(js-host-api): add npm publishing workflow (#36)
* feat(js-host-api): add npm publishing workflow - Add npm-publish.yml workflow for cross-platform builds (Linux/Windows x64) - Add platform-specific npm packages for native binaries - Integrate npm publish into CreateRelease workflow - Add .npmignore to control published files - Update README with publishing documentation Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> * Review Feedabck Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> * Review Feedabck Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> * Update version number to 0.2.0 Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> * Add a npm validation script/step to publish Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> * Fixes from copilot review Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> * Update .github/workflows/npm-publish.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent c28a4a9 commit ad92d57

10 files changed

Lines changed: 632 additions & 5 deletions

File tree

.github/workflows/CreateRelease.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ jobs:
3030
environment: release
3131
runs-on: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd", "JobId=publish-hyperlight-js-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}"]
3232
if: ${{ contains(github.ref, 'refs/heads/release/') }}
33+
outputs:
34+
version: ${{ steps.set-version.outputs.version }}
3335

3436
steps:
3537
- uses: actions/checkout@v6
@@ -48,11 +50,13 @@ jobs:
4850
id: crates-io-auth
4951

5052
- name: Set crate versions
53+
id: set-version
5154
run: |
5255
git fetch --tags || true
5356
version=$(echo "${{ github.ref }}" | sed -E 's#refs/heads/release/v##')
5457
echo "Setting version to 'v$version'"
5558
echo "HYPERLIGHT_JS_VERSION=v$version" >> $GITHUB_ENV
59+
echo "version=$version" >> $GITHUB_OUTPUT
5660
5761
- name: Publish hyperlight-js
5862
run: |
@@ -94,3 +98,13 @@ jobs:
9498
benchmarks_Linux_hyperv3.tar.gz
9599
env:
96100
GH_TOKEN: ${{ github.token }}
101+
102+
publish-npm-packages:
103+
needs: [publish-hyperlight-js-packages-and-create-release]
104+
if: ${{ contains(github.ref, 'refs/heads/release/') }}
105+
uses: ./.github/workflows/npm-publish.yml
106+
with:
107+
version: ${{ needs.publish-hyperlight-js-packages-and-create-release.outputs.version }}
108+
dry-run: false
109+
secrets:
110+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

.github/workflows/npm-publish.yml

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
2+
3+
name: Publish npm packages
4+
5+
on:
6+
workflow_dispatch:
7+
inputs:
8+
version:
9+
description: 'Version to publish (e.g., 0.2.0)'
10+
required: true
11+
type: string
12+
dry-run:
13+
description: 'Dry run (skip actual publish)'
14+
required: false
15+
type: boolean
16+
default: false
17+
workflow_call:
18+
inputs:
19+
version:
20+
description: 'Version to publish'
21+
required: true
22+
type: string
23+
dry-run:
24+
description: 'Dry run (skip actual publish)'
25+
required: false
26+
type: boolean
27+
default: false
28+
secrets:
29+
NPM_TOKEN:
30+
required: true
31+
32+
permissions:
33+
contents: read
34+
id-token: write
35+
36+
concurrency:
37+
group: npm-publish-${{ inputs.version }}
38+
cancel-in-progress: false
39+
40+
env:
41+
WORKING_DIR: src/js-host-api
42+
43+
jobs:
44+
build:
45+
strategy:
46+
fail-fast: true
47+
matrix:
48+
include:
49+
- target: x86_64-unknown-linux-gnu
50+
os: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd"]
51+
build_name: linux-x64-gnu
52+
- target: x86_64-unknown-linux-musl
53+
os: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd"]
54+
build_name: linux-x64-musl
55+
- target: x86_64-pc-windows-msvc
56+
os: [self-hosted, Windows, X64, "1ES.Pool=hld-win2022-amd"]
57+
build_name: win32-x64-msvc
58+
runs-on: ${{ matrix.os }}
59+
steps:
60+
- uses: actions/checkout@v6
61+
with:
62+
fetch-depth: 0
63+
64+
- name: Hyperlight setup
65+
uses: hyperlight-dev/ci-setup-workflow@v1.9.0
66+
with:
67+
rust-toolchain: "1.89"
68+
69+
- name: Setup Node.js
70+
uses: actions/setup-node@v6
71+
with:
72+
node-version: '22'
73+
cache: 'npm'
74+
cache-dependency-path: 'src/js-host-api/package-lock.json'
75+
76+
- name: Install dependencies
77+
working-directory: ${{ env.WORKING_DIR }}
78+
run: npm ci --ignore-scripts --omit=optional
79+
80+
- name: Set package version
81+
working-directory: ${{ env.WORKING_DIR }}
82+
run: npm version "$VERSION" --no-git-tag-version --allow-same-version
83+
env:
84+
VERSION: ${{ inputs.version }}
85+
86+
- name: Install musl tools
87+
if: contains(matrix.target, 'musl')
88+
run: sudo apt-get update && sudo apt-get install -y musl-tools
89+
90+
- name: Add musl Rust target
91+
if: contains(matrix.target, 'musl')
92+
run: rustup target add x86_64-unknown-linux-musl
93+
94+
- name: Build native module
95+
working-directory: ${{ env.WORKING_DIR }}
96+
run: npm run build -- --target ${{ matrix.target }}
97+
98+
- name: Upload artifact
99+
uses: actions/upload-artifact@v8
100+
with:
101+
name: bindings-${{ matrix.build_name }}
102+
path: ${{ env.WORKING_DIR }}/*.node
103+
if-no-files-found: error
104+
105+
publish:
106+
needs: build
107+
runs-on: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd"]
108+
steps:
109+
- uses: actions/checkout@v6
110+
111+
- name: Setup Node.js
112+
uses: actions/setup-node@v6
113+
with:
114+
node-version: '22'
115+
registry-url: 'https://registry.npmjs.org'
116+
cache: 'npm'
117+
cache-dependency-path: 'src/js-host-api/package-lock.json'
118+
119+
- name: Validate version format
120+
run: npx --yes semver "$VERSION" >/dev/null 2>&1 || { echo "Invalid version: $VERSION"; exit 1; }
121+
env:
122+
VERSION: ${{ inputs.version }}
123+
124+
- name: Install dependencies
125+
working-directory: ${{ env.WORKING_DIR }}
126+
run: npm ci --ignore-scripts --omit=optional
127+
128+
- name: Download Linux GNU artifact
129+
uses: actions/download-artifact@v8
130+
with:
131+
name: bindings-linux-x64-gnu
132+
path: ${{ env.WORKING_DIR }}/artifacts/linux-x64-gnu
133+
134+
- name: Download Linux musl artifact
135+
uses: actions/download-artifact@v8
136+
with:
137+
name: bindings-linux-x64-musl
138+
path: ${{ env.WORKING_DIR }}/artifacts/linux-x64-musl
139+
140+
- name: Download Windows artifact
141+
uses: actions/download-artifact@v8
142+
with:
143+
name: bindings-win32-x64-msvc
144+
path: ${{ env.WORKING_DIR }}/artifacts/win32-x64-msvc
145+
146+
- name: List artifacts
147+
run: ls -la ${{ env.WORKING_DIR }}/artifacts/*/
148+
149+
- name: Move artifacts to npm directories
150+
working-directory: ${{ env.WORKING_DIR }}
151+
run: |
152+
# Rename artifacts to match napi-rs naming convention
153+
mv artifacts/linux-x64-gnu/*.node npm/linux-x64-gnu/js-host-api.linux-x64-gnu.node
154+
mv artifacts/linux-x64-musl/*.node npm/linux-x64-musl/js-host-api.linux-x64-musl.node
155+
mv artifacts/win32-x64-msvc/*.node npm/win32-x64-msvc/js-host-api.win32-x64-msvc.node
156+
ls -la npm/linux-x64-gnu/
157+
ls -la npm/linux-x64-musl/
158+
ls -la npm/win32-x64-msvc/
159+
160+
- name: Set package versions
161+
working-directory: ${{ env.WORKING_DIR }}
162+
run: |
163+
# Update main package version
164+
npm version "$VERSION" --no-git-tag-version --allow-same-version
165+
166+
# Update platform package versions
167+
cd npm/linux-x64-gnu && npm version "$VERSION" --no-git-tag-version --allow-same-version
168+
cd ../linux-x64-musl && npm version "$VERSION" --no-git-tag-version --allow-same-version
169+
cd ../win32-x64-msvc && npm version "$VERSION" --no-git-tag-version --allow-same-version
170+
env:
171+
VERSION: ${{ inputs.version }}
172+
173+
- name: Update optionalDependencies versions
174+
working-directory: ${{ env.WORKING_DIR }}
175+
run: |
176+
# Update only @hyperlight platform package versions (not other optionalDeps)
177+
node -e "
178+
const fs = require('fs');
179+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
180+
for (const dep of Object.keys(pkg.optionalDependencies || {})) {
181+
if (dep.startsWith('@hyperlight/js-host-api-')) {
182+
pkg.optionalDependencies[dep] = process.env.VERSION;
183+
}
184+
}
185+
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
186+
"
187+
cat package.json
188+
env:
189+
VERSION: ${{ inputs.version }}
190+
191+
- name: Generate JS bindings (index.js and index.d.ts)
192+
working-directory: ${{ env.WORKING_DIR }}
193+
run: |
194+
# napi prepublish generates index.js and index.d.ts from the .node artifacts
195+
npx --no-install napi prepublish -t npm --skip-gh-release
196+
ls -la index.js index.d.ts
197+
198+
- name: Validate packages
199+
working-directory: ${{ env.WORKING_DIR }}
200+
run: ./test-pack.sh
201+
202+
- name: Publish Linux GNU package
203+
if: ${{ !inputs['dry-run'] }}
204+
working-directory: ${{ env.WORKING_DIR }}/npm/linux-x64-gnu
205+
run: npm publish --access public --ignore-scripts
206+
env:
207+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
208+
209+
- name: Publish Linux musl package
210+
if: ${{ !inputs['dry-run'] }}
211+
working-directory: ${{ env.WORKING_DIR }}/npm/linux-x64-musl
212+
run: npm publish --access public --ignore-scripts
213+
env:
214+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
215+
216+
- name: Publish Windows package
217+
if: ${{ !inputs['dry-run'] }}
218+
working-directory: ${{ env.WORKING_DIR }}/npm/win32-x64-msvc
219+
run: npm publish --access public --ignore-scripts
220+
env:
221+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
222+
223+
- name: Publish main package
224+
if: ${{ !inputs['dry-run'] }}
225+
working-directory: ${{ env.WORKING_DIR }}
226+
run: npm publish --access public --ignore-scripts
227+
env:
228+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
229+
230+
- name: Dry run - show what would be published
231+
if: ${{ inputs['dry-run'] }}
232+
working-directory: ${{ env.WORKING_DIR }}
233+
run: |
234+
echo "=== DRY RUN - Would publish the following packages ==="
235+
echo ""
236+
echo "--- @hyperlight/js-host-api-linux-x64-gnu ---"
237+
npm pack ./npm/linux-x64-gnu --dry-run
238+
echo ""
239+
echo "--- @hyperlight/js-host-api-linux-x64-musl ---"
240+
npm pack ./npm/linux-x64-musl --dry-run
241+
echo ""
242+
echo "--- @hyperlight/js-host-api-win32-x64-msvc ---"
243+
npm pack ./npm/win32-x64-msvc --dry-run
244+
echo ""
245+
echo "--- @hyperlight/js-host-api ---"
246+
npm pack --dry-run

src/js-host-api/.npmignore

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# npm publish ignores - override .gitignore for package publishing
2+
# Include generated files that are gitignored but needed in the package
3+
!index.js
4+
!index.d.ts
5+
6+
# Exclude development files
7+
node_modules/
8+
target/
9+
Cargo.lock
10+
*.tgz
11+
*.node
12+
npm-debug.log*
13+
yarn-debug.log*
14+
yarn-error.log*
15+
.DS_Store
16+
17+
# Exclude package-lock from published package
18+
package-lock.json
19+
20+
# Exclude test and dev files
21+
tests/
22+
examples/
23+
*.config.js
24+
*.config.mjs
25+
.prettierrc
26+
TYPE_NAMING.md
27+
build.rs
28+
src/
29+
Cargo.toml
30+
test-examples.sh
31+
test-pack.sh
32+
33+
# Exclude artifacts directory (only used during CI)
34+
artifacts/
35+
36+
# Exclude platform sub-packages (published separately as optionalDependencies)
37+
npm/

src/js-host-api/README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,3 +630,53 @@ just test-js-host-api release
630630
just build-all
631631
just test-all release
632632
```
633+
634+
## Publishing to npm
635+
636+
The package is published to npmjs.com as `@hyperlight/js-host-api` with platform-specific binary packages.
637+
638+
### Automated Release
639+
640+
Publishing happens automatically when a release is created via the `CreateRelease` workflow on a `release/vX.Y.Z` branch.
641+
642+
### Manual Publishing
643+
644+
You can also trigger the npm publish workflow manually:
645+
646+
1. Go to **Actions****Publish npm packages**
647+
2. Click **Run workflow**
648+
3. Enter the version (e.g., `0.2.0`)
649+
4. Optionally enable **dry-run** to test without publishing
650+
651+
### Setup Requirements
652+
653+
The following secret must be configured in the repository:
654+
655+
| Secret | Description |
656+
|--------|-------------|
657+
| `NPM_TOKEN` | npm access token with publish permissions for the `@hyperlight` scope |
658+
659+
To create an npm token:
660+
1. Log in to [npmjs.com](https://www.npmjs.com/)
661+
2. Go to **Access Tokens****Generate New Token**
662+
3. Select **Automation** token type (for CI/CD)
663+
4. Add the token as a repository secret named `NPM_TOKEN`
664+
665+
### Package Structure
666+
667+
The npm release consists of the following packages:
668+
669+
| Package | Description |
670+
|---------|-------------|
671+
| `@hyperlight/js-host-api` | Main package (installs correct binary automatically) |
672+
| `@hyperlight/js-host-api-linux-x64-gnu` | Linux x86_64 (glibc) native binary |
673+
| `@hyperlight/js-host-api-linux-x64-musl` | Linux x86_64 (musl/Alpine) native binary |
674+
| `@hyperlight/js-host-api-win32-x64-msvc` | Windows x86_64 native binary |
675+
676+
### How Platform Selection Works
677+
678+
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.
679+
680+
**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.
681+
682+
**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)