Skip to content

2. Publish cyt-indexer-sdk to npm #159

2. Publish cyt-indexer-sdk to npm

2. Publish cyt-indexer-sdk to npm #159

# Publish cyt-indexer-sdk to npm when a GitHub Release is published.
#
# Single-package layout: all platform .node binaries ship in cyt-indexer-sdk
# (no separate cyt-indexer-sdk-* platform packages on npm).
#
# One-time setup (repository maintainer with admin access):
#
# 1. npm — Trusted Publisher (https://www.npmjs.com/package/cyt-indexer-sdk/access):
# - Package: cyt-indexer-sdk (unscoped)
# - Owner: qdrddr
# - Repository: clear-your-tools
# - Workflow filename: publish-npm-sdk.yml (exact match, case-sensitive)
# - Environment: npm-sdk (must match the GitHub Environment name below exactly)
#
# 2. GitHub — Environment (Settings → Environments → New environment):
# - Name: npm-sdk (approval gate on the publish job + npm trusted publishing)
# - Optional: required reviewers, wait timer, deployment branch rule (main only)
# - No NPM_TOKEN secret needed when using npm trusted publishing (OIDC)
#
# 3. First publish must be manual (`npm login` + `npm publish --access public`).
# OIDC cannot create a brand-new package; it only works after the package exists.
#
# 4. Push tag vX.Y.Z to trigger publish-crates.yml; when that workflow succeeds,
# this workflow runs automatically (workflow_run) in parallel with PyPI SDK and
# C FFI GitHub Release publish. Polls crates.io briefly before building native bindings.
#
# Manual runs: workflow_dispatch (optional version input).
#
# Troubleshooting misleading E404 on publish:
# - Trusted publisher repo / workflow / environment mismatch → npm returns 404, not 403
# - npm CLI must be >= 11.5.1 (this workflow upgrades npm explicitly)
# - Cannot republish an existing version (bump semver before re-running CI)
name: 2. Publish cyt-indexer-sdk to npm
on:
workflow_run:
workflows: ["1. Publish cyt-indexer to crates.io"]
types: [completed]
workflow_dispatch:
inputs:
version:
description: Semver to wait for on crates.io (manual runs only)
required: false
permissions:
contents: read
actions: read
jobs:
resolve-version:
name: Resolve release version
if: |
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')
runs-on: ubuntu-latest
outputs:
version: ${{ steps.parse.outputs.version }}
steps:
- uses: actions/checkout@v6
- name: Resolve version
id: parse
env:
GH_TOKEN: ${{ github.token }}
INPUT_VERSION: ${{ github.event.inputs.version }}
PARENT_RUN_ID: ${{ github.event.workflow_run.id }}
run: sdk/e2e/scripts/resolve-version-from-parent.sh
wait-for-crate:
name: Wait for cyt-indexer on crates.io
needs: resolve-version
if: |
(github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')) &&
needs.resolve-version.outputs.version != ''
runs-on: ubuntu-latest
env:
CYT_RELEASE_VERSION: ${{ needs.resolve-version.outputs.version }}
steps:
- uses: actions/checkout@v6
- name: Wait for crates.io
run: sdk/e2e/scripts/wait-registry.sh crate
build-native:
name: Build native (${{ matrix.target }})
needs: [resolve-version, wait-for-crate]
if: |
(github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')) &&
needs.resolve-version.result == 'success' &&
(needs.wait-for-crate.result == 'success' || needs.wait-for-crate.result == 'skipped')
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
- os: macos-latest
target: aarch64-apple-darwin
- os: macos-latest
target: x86_64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: windows-11-arm
target: aarch64-pc-windows-msvc
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Prepare Cargo workspace for CI
run: bash scripts/ci/prepare-cargo-workspace.sh
- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
cache-dependency-path: sdk/typescript/package-lock.json
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Configure Cargo for Windows network reliability
if: runner.os == 'Windows'
shell: bash
run: |
mkdir -p "${HOME}/.cargo"
cat >> "${HOME}/.cargo/config.toml" <<'EOF'
[http]
multiplexing = false
EOF
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
workspaces: |
. -> target
cache-on-failure: true
- name: Install dependencies
working-directory: sdk/typescript
run: npm ci
- name: Set package version
if: needs.resolve-version.outputs.version != ''
working-directory: sdk/typescript
run: |
echo "Setting cyt-indexer-sdk version to ${{ needs.resolve-version.outputs.version }}"
npm version "${{ needs.resolve-version.outputs.version }}" --no-git-tag-version --allow-same-version
- name: Fetch Rust dependencies
shell: bash
env:
CARGO_NET_RETRY: 10
run: |
set -euo pipefail
for attempt in 1 2 3 4 5; do
if cargo fetch \
--manifest-path sdk/rust/cyt-indexer/Cargo.toml \
--target "${{ matrix.target }}"; then
exit 0
fi
echo "cargo fetch failed (attempt ${attempt}/5), retrying in $((attempt * 15))s..."
sleep $((attempt * 15))
done
echo "::error::cargo fetch failed after 5 attempts"
exit 1
- name: Build native binding
working-directory: sdk/typescript
shell: bash
env:
CARGO_NET_RETRY: 10
run: |
set -euo pipefail
for attempt in 1 2 3 4 5; do
if npx napi build --platform --release \
--manifest-path ../rust/cyt-indexer/Cargo.toml \
-p cyt-indexer \
--features node \
--no-default-features \
--output-dir . \
--js native.cjs \
--dts native.d.ts \
--target "${{ matrix.target }}"; then
exit 0
fi
echo "napi build failed (attempt ${attempt}/5), retrying in $((attempt * 15))s..."
sleep $((attempt * 15))
done
echo "::error::napi build failed after 5 attempts"
exit 1
- name: Stage native binding
shell: bash
run: |
shopt -s nullglob
files=(sdk/typescript/cyt-indexer-sdk.*.node sdk/rust/cyt-indexer/cyt-indexer-sdk.*.node)
if [ ${#files[@]} -eq 0 ]; then
echo "::error::No .node file produced by napi build"
ls -la sdk/typescript/
ls -la sdk/rust/cyt-indexer/ || true
exit 1
fi
mkdir -p "artifacts/bindings-${{ matrix.target }}"
cp "${files[@]}" "artifacts/bindings-${{ matrix.target }}/"
ls -la "artifacts/bindings-${{ matrix.target }}/"
- name: Verify Mach-O LINKEDIT alignment (macOS 27+)
if: contains(matrix.target, 'apple-darwin')
shell: bash
run: |
set -euo pipefail
shopt -s nullglob
files=(artifacts/bindings-${{ matrix.target }}/cyt-indexer-sdk.*.node)
if ((${#files[@]} == 0)); then
echo "::error::no .node binding found for ${{ matrix.target }}"
exit 1
fi
bash scripts/ci/verify-mach-o-linkedit.sh "${files[@]}"
- name: Upload native binding
uses: actions/upload-artifact@v7
with:
name: bindings-${{ matrix.target }}
path: artifacts/bindings-${{ matrix.target }}/
if-no-files-found: error
- name: Upload native loader stubs
if: matrix.target == 'x86_64-unknown-linux-gnu'
uses: actions/upload-artifact@v7
with:
name: native-loader-stubs
path: |
sdk/typescript/native.cjs
sdk/typescript/native.d.ts
if-no-files-found: error
publish:
name: Upload SDK to npm
needs: [build-native, resolve-version]
if: |
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')
runs-on: ubuntu-latest
environment:
name: npm-sdk
url: https://www.npmjs.com/package/cyt-indexer-sdk
permissions:
id-token: write
attestations: write
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version: "24"
registry-url: https://registry.npmjs.org
cache: npm
cache-dependency-path: sdk/typescript/package-lock.json
- name: Upgrade npm for trusted publishing
run: |
npm install -g npm@latest
npm --version
node --version
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install dependencies
working-directory: sdk/typescript
run: npm ci
- name: Set package version
if: needs.resolve-version.outputs.version != ''
working-directory: sdk/typescript
run: |
echo "Setting cyt-indexer-sdk version to ${{ needs.resolve-version.outputs.version }}"
npm version "${{ needs.resolve-version.outputs.version }}" --no-git-tag-version --allow-same-version
- name: Download native loader stubs
uses: actions/download-artifact@v7
with:
name: native-loader-stubs
path: sdk/typescript
- name: Download native bindings
uses: actions/download-artifact@v7
with:
pattern: bindings-*
merge-multiple: true
path: sdk/typescript/artifacts
- name: Stage all platform binaries in package root
working-directory: sdk/typescript
shell: bash
run: |
set -euo pipefail
shopt -s nullglob
while IFS= read -r -d '' file; do
mv -f "$file" .
done < <(find artifacts -name '*.node' -print0)
rm -rf artifacts
mapfile -t nodes < <(find . -maxdepth 1 -name 'cyt-indexer-sdk.*.node' -print | sort)
expected=6
if [ "${#nodes[@]}" -ne "${expected}" ]; then
echo "::error::Expected ${expected} platform .node files in package root, found ${#nodes[@]}"
printf '%s\n' "${nodes[@]}"
exit 1
fi
printf '%s\n' "${nodes[@]}"
ls -la cyt-indexer-sdk.*.node
- name: Build JavaScript package
working-directory: sdk/typescript
run: npm run build:js
- name: Publish to npm
working-directory: sdk/typescript
run: npm publish --access public