Skip to content

npm publish

npm publish #18

Workflow file for this run

# Publish the `docling.rs` npm package (Node.js / Bun native bindings) for a
# chosen release. The package is a native N-API addon, so it can't be a one-line
# `npm publish`: it's built on a matrix of native runners (one per OS/arch),
# each producing a platform `.node`, then a publish job assembles the main
# package plus per-platform `optionalDependencies` (docling.rs-<triple>) via
# napi-rs and publishes them all.
#
# Trigger: manual only (workflow_dispatch). By default it builds the latest
# master (the version in the workspace Cargo.toml); optionally pass a release
# tag to build that instead. Either way the workflow checks out the chosen ref
# and publishes, so npm releases stay decoupled from the crates.io release on
# every master push. Run it from the Actions tab (or
# `gh workflow run npm-publish.yml`, optionally `-f tag=v0.7.0`).
#
# Requires one repository secret: NPM_TOKEN — an npm automation token with
# publish rights to `docling.rs` and the `docling.rs-*` platform packages
# (including `docling.rs-cuda` when the cuda input is used).
#
# The optional `cuda` input additionally publishes `docling.rs-cuda`
# (issue #74, Linux x64): the CUDA addon + ONNX Runtime provider libraries are
# far past npm's practical size limits, so the binaries go to a GitHub release
# (`npm-cuda-v<version>`) and the npm package is a small JS shim whose
# postinstall downloads + sha256-verifies them — the same
# fetch-at-install-time model onnxruntime-node uses for its CUDA binaries.
name: npm publish
on:
workflow_dispatch:
inputs:
tag:
description: "Release tag to build (e.g. v0.7.0). Blank = latest master."
required: false
default: ""
version:
description: "Override the npm version (e.g. 0.7.0). Blank = tag, or workspace version on master."
required: false
default: ""
cuda:
description: "Also publish docling.rs-cuda (Linux x64: binaries to a GitHub release, npm shim downloads on install)."
required: false
type: boolean
default: false
concurrency:
group: npm-publish-${{ inputs.tag || github.ref }}
cancel-in-progress: false
defaults:
run:
working-directory: crates/docling-node
jobs:
build:
name: build ${{ matrix.target }}
runs-on: ${{ matrix.host }}
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
host: ubuntu-22.04
# GitHub-hosted ARM64 Linux runner (native — avoids cross-compiling the
# ONNX/ort build). Requires the arm64 runner image to be available.
- target: aarch64-unknown-linux-gnu
host: ubuntu-24.04-arm
# macOS (darwin) prebuilds are omitted: GitHub-hosted macOS runners are
# blocked in this environment, and darwin can't be cross-compiled on
# Linux (needs the Apple SDK). macOS users build from source. To re-add
# when macOS runners are available, add the darwin targets back here AND
# to `napi.triples.additional` in package.json:
# - { target: aarch64-apple-darwin, host: macos-14 }
# - { target: x86_64-apple-darwin, host: macos-14 } # cross via --target
- target: x86_64-pc-windows-msvc
host: windows-latest
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.tag || github.ref }}
# rustup is preinstalled on GitHub-hosted runners (Linux + Windows); use it
# directly — the docling-project org allowlists only GitHub-owned / vetted
# actions, not marketplace toolchain actions.
- name: Install Rust (stable)
shell: bash
run: |
rustup toolchain install stable --profile minimal
rustup default stable
rustup target add ${{ matrix.target }}
# One workspace, key the cache per target so the 5 jobs don't collide.
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
target
key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-${{ matrix.target }}-
- uses: actions/setup-node@v6
with:
node-version: 20
- name: Install napi CLI
run: npm install
# Native addon + the JS loader / d.ts. `--strip` keeps the (ONNX-linked)
# binary as small as possible; strip isn't available under MSVC, so the
# Windows build omits it.
- name: Build (unix)
if: runner.os != 'Windows'
run: npx napi build --platform --release --strip --target ${{ matrix.target }} --js native.js --dts native.d.ts
- name: Build (windows)
if: runner.os == 'Windows'
run: npx napi build --platform --release --target ${{ matrix.target }} --js native.js --dts native.d.ts
- name: Upload prebuilt binary
uses: actions/upload-artifact@v7
with:
name: bindings-${{ matrix.target }}
path: crates/docling-node/docling-rs.*.node
if-no-files-found: error
# The JS loader + types are platform-agnostic; upload them once (from the
# linux-x64 build) for the publish job to include in the main package.
- name: Upload JS binding
if: matrix.target == 'x86_64-unknown-linux-gnu'
uses: actions/upload-artifact@v7
with:
name: js-binding
path: |
crates/docling-node/native.js
crates/docling-node/native.d.ts
if-no-files-found: error
publish:
name: publish to npm
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.tag || github.ref }}
- uses: actions/setup-node@v6
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
- name: Install napi CLI
run: npm install
# Prebuilt binaries → artifacts/<name>/docling-rs.<triple>.node
- name: Download prebuilt binaries
uses: actions/download-artifact@v8
with:
pattern: bindings-*
path: crates/docling-node/artifacts
# The JS loader + types → the package root.
- name: Download JS binding
uses: actions/download-artifact@v8
with:
name: js-binding
path: crates/docling-node
# Version = the explicit override, else the selected tag (sans leading `v`),
# else the workspace version from the root Cargo.toml (latest-master run).
- name: Resolve version
id: ver
run: |
v="${{ inputs.version }}"
if [ -z "$v" ]; then
if [ -n "${{ inputs.tag }}" ]; then
v="${{ inputs.tag }}"; v="${v#v}"
else
v="$(grep -m1 '^version = ' ../../Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/')"
fi
fi
echo "version=$v" >> "$GITHUB_OUTPUT"
echo "Publishing docling.rs@$v (ref: ${{ inputs.tag || github.ref }})"
# Skip cleanly if this version is already on npm (idempotent re-runs).
- name: Check if already published
id: check
run: |
v="${{ steps.ver.outputs.version }}"
if npm view "docling.rs@$v" version >/dev/null 2>&1; then
echo "published=true" >> "$GITHUB_OUTPUT"
echo "docling.rs@$v is already on npm — skipping."
else
echo "published=false" >> "$GITHUB_OUTPUT"
fi
- name: Set package version
if: steps.check.outputs.published == 'false'
run: npm version "${{ steps.ver.outputs.version }}" --no-git-tag-version --allow-same-version
# Create the per-platform package dirs and move each prebuilt .node into
# its dir. `npm publish` then runs `prepublishOnly` (napi prepublish), which
# publishes the docling.rs-<triple> packages and wires them into the main
# package's optionalDependencies before the main package is published.
- name: Assemble platform packages
if: steps.check.outputs.published == 'false'
run: |
npx napi create-npm-dir -t .
npx napi artifacts --dir artifacts
- name: Publish
if: steps.check.outputs.published == 'false'
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# docling.rs-cuda (issue #74): CUDA addon binaries → GitHub release, small
# npm shim with a postinstall downloader → npm. Self-contained (doesn't need
# the CPU matrix): build with the cuda feature, upload the gzipped .node +
# ONNX Runtime provider libraries + sha256 manifest to the npm-cuda-v<ver>
# release, then validate the packed shim by actually installing it (its
# postinstall pulls from that very release) and converting on the (GPU-less)
# runner — the auto→CPU fallback keeps that meaningful — before publishing.
#
# ubuntu-24.04, not the manylinux container: the CUDA ONNX Runtime static
# binaries carry glibc-2.38 symbols (same floor as the docling-rs-cuda
# Python wheel — see pypi-publish.yml's wheel-cuda job).
publish-cuda:
name: publish docling.rs-cuda
if: inputs.cuda
runs-on: ubuntu-24.04
permissions:
contents: write # gh release create/upload
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.tag || github.ref }}
- name: Install Rust (stable)
run: |
rustup toolchain install stable --profile minimal
rustup default stable
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
target
key: ${{ runner.os }}-cargo-cuda-node-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-cuda-node-
- uses: actions/setup-node@v6
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
- name: Install napi CLI
run: npm install
# Same resolution as the publish job — the two packages must agree.
- name: Resolve version
id: ver
run: |
v="${{ inputs.version }}"
if [ -z "$v" ]; then
if [ -n "${{ inputs.tag }}" ]; then
v="${{ inputs.tag }}"; v="${v#v}"
else
v="$(grep -m1 '^version = ' ../../Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/')"
fi
fi
echo "version=$v" >> "$GITHUB_OUTPUT"
echo "Publishing docling.rs-cuda@$v (ref: ${{ inputs.tag || github.ref }})"
- name: Check if already published
id: check
run: |
v="${{ steps.ver.outputs.version }}"
if npm view "docling.rs-cuda@$v" version >/dev/null 2>&1; then
echo "published=true" >> "$GITHUB_OUTPUT"
echo "docling.rs-cuda@$v is already on npm — skipping."
else
echo "published=false" >> "$GITHUB_OUTPUT"
fi
- name: Set package version
if: steps.check.outputs.published == 'false'
run: npm version "${{ steps.ver.outputs.version }}" --no-git-tag-version --allow-same-version
# $ORIGIN rpath: ONNX Runtime dlopens the provider libraries by bare
# name; the rpath makes it look next to the .node, where postinstall
# puts them (same mechanism as the Python CUDA wheel).
- name: Build CUDA addon
if: steps.check.outputs.published == 'false'
run: RUSTFLAGS='-C link-arg=-Wl,-rpath,$ORIGIN' npx napi build --platform --release --strip --features cuda --js native.js --dts native.d.ts
# manifest.json carries sha256 of the UNCOMPRESSED files — postinstall
# verifies after gunzip, catching truncated downloads and bad mirrors.
# `cp -L`: ort's copy-dylibs leaves symlinks into the download cache.
- name: Package release assets
if: steps.check.outputs.published == 'false'
run: |
mkdir -p cuda/release-assets
cp -L ../../target/release/libonnxruntime_providers_shared.so \
../../target/release/libonnxruntime_providers_cuda.so \
cuda/release-assets/
cp docling-rs.linux-x64-gnu.node cuda/release-assets/
cd cuda/release-assets
node -e '
const fs = require("fs"), crypto = require("crypto");
const m = {};
for (const f of fs.readdirSync(".")) {
m[f] = crypto.createHash("sha256").update(fs.readFileSync(f)).digest("hex");
}
fs.writeFileSync("manifest.json", JSON.stringify(m, null, 2));
'
gzip -9 docling-rs.linux-x64-gnu.node libonnxruntime_providers_shared.so libonnxruntime_providers_cuda.so
ls -la
- name: Upload to GitHub release
if: steps.check.outputs.published == 'false'
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="npm-cuda-v${{ steps.ver.outputs.version }}"
if gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then
gh release upload "$TAG" cuda/release-assets/* --repo "${{ github.repository }}" --clobber
else
gh release create "$TAG" cuda/release-assets/* \
--repo "${{ github.repository }}" \
--title "docling.rs-cuda ${{ steps.ver.outputs.version }} (npm binaries)" \
--notes "CUDA addon binaries for the docling.rs-cuda npm package. Downloaded by its postinstall — not for direct use."
fi
- name: Assemble npm package
if: steps.check.outputs.published == 'false'
run: node cuda/make-package.mjs
# End-to-end validation before publish: install the packed tarball in a
# scratch dir (npm runs postinstall → downloads from the release just
# uploaded, hash-checked) and convert something. Catches a broken
# loader, a bad manifest, or an unpublishable release *before* npm has
# an unusable version.
- name: Validate install from release
if: steps.check.outputs.published == 'false'
run: |
(cd cuda/pkg && npm pack >/dev/null)
PKG_TGZ="$(ls "$PWD"/cuda/pkg/*.tgz)"
mkdir -p /tmp/cuda-install-test && cd /tmp/cuda-install-test
npm init -y >/dev/null
npm install "$PKG_TGZ"
node -e '
const m = require("docling.rs-cuda");
const r = m.convert({ data: Buffer.from("# GPU shim smoke"), name: "x.md" });
if (r.status !== "success" || !r.content.startsWith("# GPU shim smoke")) {
throw new Error("unexpected result: " + JSON.stringify(r));
}
console.log("docling.rs-cuda install validated:", JSON.stringify(r.content));
'
- name: Publish
if: steps.check.outputs.published == 'false'
run: cd cuda/pkg && npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# docling.rs-wasm: the browser/WebAssembly package (no native matrix — one
# Linux runner compiles wasm32-unknown-unknown and publishes). Same
# ref-selection and idempotency as the main package: builds the checked-out
# workspace version, skips cleanly when that version is already on npm.
publish-wasm:
name: publish docling.rs-wasm
runs-on: ubuntu-latest
defaults:
run:
working-directory: .
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.tag || github.ref }}
- name: Install Rust (stable + wasm32 target)
run: |
rustup toolchain install stable --profile minimal
rustup default stable
rustup target add wasm32-unknown-unknown
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
~/.cargo/bin
target
key: ${{ runner.os }}-cargo-v2-wasm-npm-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-v2-wasm-npm-
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: "https://registry.npmjs.org"
- name: Install wasm-bindgen-cli (pinned to Cargo.lock)
run: |
# A CLI/crate version mismatch produces glue the module rejects at
# load time — pin to the workspace's wasm-bindgen (same as pages.yml).
version=$(cargo metadata --format-version 1 --locked \
| python3 -c 'import json,sys; print(next(p["version"] for p in json.load(sys.stdin)["packages"] if p["name"]=="wasm-bindgen"))')
echo "wasm-bindgen $version"
command -v wasm-bindgen >/dev/null && [ "$(wasm-bindgen --version)" = "wasm-bindgen $version" ] \
|| cargo install wasm-bindgen-cli --version "$version" --locked
- name: Build the package
run: bash scripts/ci/build_wasm_pkg.sh target/npm-wasm
# Skip cleanly if this version is already on npm (idempotent re-runs).
- name: Check if already published
id: wcheck
run: |
v="$(grep -m1 '^version = ' Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/')"
echo "version=$v" >> "$GITHUB_OUTPUT"
if npm view "docling.rs-wasm@$v" version >/dev/null 2>&1; then
echo "published=true" >> "$GITHUB_OUTPUT"
echo "docling.rs-wasm@$v is already on npm — skipping."
else
echo "published=false" >> "$GITHUB_OUTPUT"
fi
# Load the module in Node off the packed tarball (exactly what npm will
# serve) and convert a real file before publishing anything.
- name: Validate the packed package
if: steps.wcheck.outputs.published == 'false'
run: |
(cd target/npm-wasm && npm pack >/dev/null)
mkdir -p /tmp/wasm-check && tar -xzf target/npm-wasm/*.tgz -C /tmp/wasm-check --strip-components=1
node --input-type=module -e '
import { readFileSync } from "node:fs";
const m = await import("/tmp/wasm-check/web/docling_wasm.js");
await m.default({ module_or_path: readFileSync("/tmp/wasm-check/web/docling_wasm_bg.wasm") });
const html = new TextEncoder().encode("<html><body><h1>T</h1><p>ok</p></body></html>");
const md = m.convert(html, "t.html", "md");
if (!md.includes("# T")) throw new Error("bad output: " + md);
console.log("docling.rs-wasm@" + m.version() + " converts: " + JSON.stringify(md));
'
- name: Publish
if: steps.wcheck.outputs.published == 'false'
run: cd target/npm-wasm && npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}