Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/CreateRelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ jobs:
environment: release
runs-on: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd"]
if: ${{ contains(github.ref, 'refs/heads/release/') }}
outputs:
version: ${{ steps.set-version.outputs.version }}

steps:
- uses: actions/checkout@v6
Expand All @@ -48,11 +50,13 @@ jobs:
id: crates-io-auth

- name: Set crate versions
id: set-version
run: |
git fetch --tags || true
version=$(echo "${{ github.ref }}" | sed -E 's#refs/heads/release/v##')
echo "Setting version to 'v$version'"
echo "HYPERLIGHT_JS_VERSION=v$version" >> $GITHUB_ENV
echo "version=$version" >> $GITHUB_OUTPUT

- name: Publish hyperlight-js
run: |
Expand Down Expand Up @@ -94,3 +98,13 @@ jobs:
benchmarks_Linux_hyperv3.tar.gz
env:
GH_TOKEN: ${{ github.token }}

publish-npm-packages:
needs: [publish-hyperlight-js-packages-and-create-release]
if: ${{ contains(github.ref, 'refs/heads/release/') }}
uses: ./.github/workflows/npm-publish.yml
with:
version: ${{ needs.publish-hyperlight-js-packages-and-create-release.outputs.version }}
dry-run: false
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
229 changes: 229 additions & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json

name: Publish npm packages

on:
workflow_dispatch:
inputs:
version:
description: 'Version to publish (e.g., 0.17.0)'
required: true
type: string
dry-run:
description: 'Dry run (skip actual publish)'
required: false
type: boolean
default: false
workflow_call:
inputs:
version:
description: 'Version to publish'
required: true
type: string
dry-run:
description: 'Dry run (skip actual publish)'
required: false
type: boolean
default: false
secrets:
NPM_TOKEN:
required: true

permissions:
contents: read
id-token: write

env:
WORKING_DIR: src/js-host-api

jobs:
build:
strategy:
fail-fast: true
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd"]
build_name: linux-x64-gnu
- target: x86_64-unknown-linux-musl
os: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd"]
build_name: linux-x64-musl
- target: x86_64-pc-windows-msvc
os: [self-hosted, Windows, X64, "1ES.Pool=hld-win2022-amd"]
build_name: win32-x64-msvc
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Hyperlight setup
uses: hyperlight-dev/ci-setup-workflow@v1.8.0
with:
rust-toolchain: "1.89"

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: 'src/js-host-api/package-lock.json'

- name: Install dependencies
working-directory: ${{ env.WORKING_DIR }}
run: npm ci --ignore-scripts --omit=optional

- name: Set package version
working-directory: ${{ env.WORKING_DIR }}
shell: bash
run: |
npm version ${{ inputs.version }} --no-git-tag-version --allow-same-version

- name: Install musl tools
if: contains(matrix.target, 'musl')
run: sudo apt-get update && sudo apt-get install -y musl-tools

- name: Add musl Rust target
if: contains(matrix.target, 'musl')
run: rustup target add x86_64-unknown-linux-musl

- name: Build native module
working-directory: ${{ env.WORKING_DIR }}
run: npm run build -- --target ${{ matrix.target }}

- name: Upload artifact
uses: actions/upload-artifact@v8
with:
name: bindings-${{ matrix.build_name }}
path: ${{ env.WORKING_DIR }}/*.node
if-no-files-found: error

publish:
needs: build
runs-on: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd"]
steps:
- uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
cache-dependency-path: 'src/js-host-api/package-lock.json'

- name: Install dependencies
working-directory: ${{ env.WORKING_DIR }}
run: npm ci --ignore-scripts --omit=optional

- name: Download Linux GNU artifact
uses: actions/download-artifact@v8
with:
name: bindings-linux-x64-gnu
path: ${{ env.WORKING_DIR }}/artifacts/linux-x64-gnu

- name: Download Linux musl artifact
uses: actions/download-artifact@v8
with:
name: bindings-linux-x64-musl
path: ${{ env.WORKING_DIR }}/artifacts/linux-x64-musl

- name: Download Windows artifact
uses: actions/download-artifact@v8
with:
name: bindings-win32-x64-msvc
path: ${{ env.WORKING_DIR }}/artifacts/win32-x64-msvc

- name: List artifacts
run: ls -la ${{ env.WORKING_DIR }}/artifacts/*/

- name: Move artifacts to npm directories
working-directory: ${{ env.WORKING_DIR }}
run: |
# Rename artifacts to match napi-rs naming convention
mv artifacts/linux-x64-gnu/*.node npm/linux-x64-gnu/js-host-api.linux-x64-gnu.node
mv artifacts/linux-x64-musl/*.node npm/linux-x64-musl/js-host-api.linux-x64-musl.node
mv artifacts/win32-x64-msvc/*.node npm/win32-x64-msvc/js-host-api.win32-x64-msvc.node
ls -la npm/linux-x64-gnu/
ls -la npm/linux-x64-musl/
ls -la npm/win32-x64-msvc/

- name: Set package versions
working-directory: ${{ env.WORKING_DIR }}
run: |
# Update main package version
npm version ${{ inputs.version }} --no-git-tag-version --allow-same-version

# Update platform package versions
cd npm/linux-x64-gnu && npm version ${{ inputs.version }} --no-git-tag-version --allow-same-version
cd ../linux-x64-musl && npm version ${{ inputs.version }} --no-git-tag-version --allow-same-version
cd ../win32-x64-msvc && npm version ${{ inputs.version }} --no-git-tag-version --allow-same-version

- name: Update optionalDependencies versions
working-directory: ${{ env.WORKING_DIR }}
run: |
# Update only @hyperlight platform package versions (not other optionalDeps)
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
for (const dep of Object.keys(pkg.optionalDependencies || {})) {
if (dep.startsWith('@hyperlight/js-host-api-')) {
pkg.optionalDependencies[dep] = '${{ inputs.version }}';
}
}
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
"
cat package.json

- name: Generate JS bindings (index.js and index.d.ts)
working-directory: ${{ env.WORKING_DIR }}
run: |
# napi prepublish generates index.js and index.d.ts from the .node artifacts
npx napi prepublish -t npm --skip-gh-release
ls -la index.js index.d.ts

- name: Publish Linux GNU package
if: ${{ !inputs.dry-run }}
working-directory: ${{ env.WORKING_DIR }}/npm/linux-x64-gnu
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Publish Linux musl package
if: ${{ !inputs.dry-run }}
working-directory: ${{ env.WORKING_DIR }}/npm/linux-x64-musl
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Publish Windows package
if: ${{ !inputs.dry-run }}
working-directory: ${{ env.WORKING_DIR }}/npm/win32-x64-msvc
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Publish main package
if: ${{ !inputs.dry-run }}
working-directory: ${{ env.WORKING_DIR }}
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Dry run - show what would be published
if: ${{ inputs.dry-run }}
working-directory: ${{ env.WORKING_DIR }}
run: |
echo "=== DRY RUN - Would publish the following packages ==="
echo ""
echo "--- @hyperlight/js-host-api-linux-x64-gnu ---"
npm pack ./npm/linux-x64-gnu --dry-run
echo ""
echo "--- @hyperlight/js-host-api-linux-x64-musl ---"
npm pack ./npm/linux-x64-musl --dry-run
echo ""
echo "--- @hyperlight/js-host-api-win32-x64-msvc ---"
npm pack ./npm/win32-x64-msvc --dry-run
echo ""
echo "--- @hyperlight/js-host-api ---"
npm pack --dry-run
32 changes: 32 additions & 0 deletions src/js-host-api/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# npm publish ignores - override .gitignore for package publishing
# Include generated files that are gitignored but needed in the package
!index.js
!index.d.ts

# Exclude development files
node_modules/
target/
Cargo.lock
*.tgz
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.DS_Store

# Exclude package-lock from published package
package-lock.json

# Exclude test and dev files
tests/
examples/
*.config.js
*.config.mjs
.prettierrc
TYPE_NAMING.md
build.rs
src/
Cargo.toml
test-examples.sh

# Exclude artifacts directory (only used during CI)
artifacts/
50 changes: 50 additions & 0 deletions src/js-host-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -600,3 +600,53 @@ just test-js-host-api release
just build-all
just test-all release
```

## Publishing to npm

The package is published to npmjs.com as `@hyperlight/js-host-api` with platform-specific binary packages.

### Automated Release

Publishing happens automatically when a release is created via the `CreateRelease` workflow on a `release/vX.Y.Z` branch.

### Manual Publishing

You can also trigger the npm publish workflow manually:

1. Go to **Actions** → **Publish npm packages**
2. Click **Run workflow**
3. Enter the version (e.g., `0.17.0`)
4. Optionally enable **dry-run** to test without publishing

### Setup Requirements

The following secret must be configured in the repository:

| Secret | Description |
|--------|-------------|
| `NPM_TOKEN` | npm access token with publish permissions for the `@hyperlight` scope |

To create an npm token:
1. Log in to [npmjs.com](https://www.npmjs.com/)
2. Go to **Access Tokens** → **Generate New Token**
3. Select **Automation** token type (for CI/CD)
4. Add the token as a repository secret named `NPM_TOKEN`

### Package Structure

The npm release consists of the following packages:

| Package | Description |
|---------|-------------|
| `@hyperlight/js-host-api` | Main package (installs correct binary automatically) |
| `@hyperlight/js-host-api-linux-x64-gnu` | Linux x86_64 (glibc) native binary |
| `@hyperlight/js-host-api-linux-x64-musl` | Linux x86_64 (musl/Alpine) native binary |
| `@hyperlight/js-host-api-win32-x64-msvc` | Windows x86_64 native binary |

### How Platform Selection Works

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.

**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.

**At runtime:** The napi-rs generated `index.js` detects the platform (including glibc vs musl on Linux) and loads the correct `.node` binary.
30 changes: 30 additions & 0 deletions src/js-host-api/npm/linux-x64-gnu/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@hyperlight/js-host-api-linux-x64-gnu",
"version": "0.17.0",
"os": [
"linux"
],
"cpu": [
"x64"
],
"main": "js-host-api.linux-x64-gnu.node",
"files": [
"js-host-api.linux-x64-gnu.node"
],
"description": "Node.js API bindings for Hyperlight JS - Linux x64 gnu",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/hyperlight-dev/hyperlight-js.git"
},
"homepage": "https://github.com/hyperlight-dev/hyperlight-js#readme",
"bugs": {
"url": "https://github.com/hyperlight-dev/hyperlight-js/issues"
},
"engines": {
"node": ">= 18"
},
"libc": [
"glibc"
]
}
Loading
Loading