Repository navigation
2. Publish cyt-indexer-sdk to npm #159
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |