ci: publish to npm via trusted publishing instead of a stored token #1190
Workflow file for this run
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
| name: Nightly release | |
| on: | |
| push: | |
| branches: | |
| - "**" | |
| tags: | |
| - "!**" | |
| pull_request: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| inputs: | |
| skip_build: | |
| description: "Skip the build-napi job. Requires napi_run_id." | |
| type: boolean | |
| default: false | |
| skip_release: | |
| description: "Skip the release (publish) job; only build NAPI artifacts." | |
| type: boolean | |
| default: false | |
| napi_run_id: | |
| description: "Existing run ID to download NAPI artifacts from (only used when skip_build is true)." | |
| type: string | |
| default: "" | |
| permissions: | |
| contents: read | |
| actions: read | |
| env: | |
| CARGO_TERM_COLOR: always | |
| LINUX_GLIBC_VERSION: "2.17" | |
| jobs: | |
| build-napi: | |
| name: Build NAPI - ${{ matrix.target }} | |
| if: github.event_name != 'workflow_dispatch' || inputs.skip_build != true | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: macos-latest | |
| target: x86_64-apple-darwin | |
| platform: darwin-x64 | |
| - os: macos-latest | |
| target: aarch64-apple-darwin | |
| platform: darwin-arm64 | |
| - os: ubuntu-22.04 | |
| target: x86_64-unknown-linux-gnu | |
| platform: linux-x64-gnu | |
| - os: ubuntu-22.04 | |
| target: x86_64-unknown-linux-musl | |
| platform: linux-x64-musl | |
| - os: blacksmith-32vcpu-ubuntu-2404 | |
| target: aarch64-unknown-linux-gnu | |
| platform: linux-arm64-gnu | |
| - os: blacksmith-32vcpu-ubuntu-2404 | |
| target: aarch64-unknown-linux-musl | |
| platform: linux-arm64-musl | |
| - os: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| platform: win32-x64-msvc | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Vite+ | |
| uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb # v1.15.0 | |
| with: | |
| node-version-file: ".node-version" | |
| cache: true | |
| run-install: true | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - name: Mount sticky cache | |
| if: startsWith(matrix.os, 'blacksmith-') | |
| uses: ./.github/actions/mount-sticky-cache | |
| with: | |
| key-prefix: ${{ github.repository }}-nightly-${{ github.job }}-${{ matrix.target }} | |
| cargo: "true" | |
| target: target | |
| - name: Cache cargo build | |
| if: ${{ !startsWith(matrix.os, 'blacksmith-') }} | |
| # zizmor: ignore[cache-poisoning] save-if restricts writes to main pushes, so PR runs cannot poison release builds. | |
| uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1 | |
| with: | |
| shared-key: nightly-${{ matrix.target }} | |
| # Only save cache on main pushes to prevent PR runs from poisoning the cache used by release builds. | |
| save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} | |
| - name: Install zig (Linux) | |
| if: contains(matrix.target, 'unknown-linux') | |
| run: | | |
| curl -fsSL https://ziglang.org/download/0.13.0/zig-linux-x86_64-0.13.0.tar.xz -o zig.tar.xz | |
| tar -xf zig.tar.xz | |
| echo "$PWD/zig-linux-x86_64-0.13.0" >> "$GITHUB_PATH" | |
| - name: Install cargo-zigbuild (Linux) | |
| if: contains(matrix.target, 'unknown-linux') | |
| run: cargo install cargo-zigbuild | |
| - name: Build NAPI (Linux) | |
| if: contains(matrix.target, 'unknown-linux') | |
| working-directory: crates/ox_content_napi | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| build_target="${{ matrix.target }}" | |
| if [[ "${{ matrix.target }}" == *-unknown-linux-gnu ]]; then | |
| build_target="${build_target}.${LINUX_GLIBC_VERSION}" | |
| else | |
| export RUSTFLAGS="${RUSTFLAGS:-} -C target-feature=-crt-static" | |
| fi | |
| cargo zigbuild --release --target "$build_target" | |
| src="../../target/${{ matrix.target }}/release/libox_content_napi.so" | |
| if [[ ! -f "$src" ]]; then | |
| src="../../target/${build_target}/release/libox_content_napi.so" | |
| fi | |
| cp "$src" "ox-content.${{ matrix.platform }}.node" | |
| - name: Verify Linux native dependencies | |
| if: contains(matrix.target, 'unknown-linux') | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| node_file="crates/ox_content_napi/ox-content.${{ matrix.platform }}.node" | |
| if [[ "${{ matrix.target }}" == *-unknown-linux-gnu ]]; then | |
| max_glibc="$( | |
| readelf --version-info "$node_file" | | |
| sed -n 's/.*Name: GLIBC_\([0-9.]*\).*/\1/p' | | |
| sort -V | | |
| tail -1 | |
| )" | |
| echo "max GLIBC version: ${max_glibc:-none}" | |
| if [[ -n "$max_glibc" ]] && | |
| [[ "$(printf '%s\n' "$LINUX_GLIBC_VERSION" "$max_glibc" | sort -V | tail -1)" != "$LINUX_GLIBC_VERSION" ]]; then | |
| echo "::error::${node_file} requires GLIBC_${max_glibc}, expected <= GLIBC_${LINUX_GLIBC_VERSION}" | |
| exit 1 | |
| fi | |
| else | |
| if readelf --version-info "$node_file" | grep -q 'GLIBC_'; then | |
| echo "::error::${node_file} unexpectedly references GLIBC symbols" | |
| exit 1 | |
| fi | |
| fi | |
| - name: Build NAPI | |
| if: ${{ !contains(matrix.target, 'unknown-linux') }} | |
| working-directory: crates/ox_content_napi | |
| run: vp exec -- napi build --release --target ${{ matrix.target }} | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: napi-${{ matrix.target }} | |
| path: crates/ox_content_napi/*.node | |
| if-no-files-found: error | |
| release: | |
| name: Publish preview via pkg-pr-new | |
| runs-on: blacksmith-32vcpu-ubuntu-2404 | |
| needs: build-napi | |
| # Skip when explicitly disabled via workflow_dispatch. | |
| # Use always() so this still runs when build-napi was skipped (skip_build mode). | |
| if: | | |
| always() && | |
| (github.event_name != 'workflow_dispatch' || inputs.skip_release != true) && | |
| (needs.build-napi.result == 'success' || | |
| (github.event_name == 'workflow_dispatch' && inputs.skip_build == true)) | |
| steps: | |
| - name: Validate napi_run_id when skip_build is true | |
| if: github.event_name == 'workflow_dispatch' && inputs.skip_build == true | |
| env: | |
| NAPI_RUN_ID: ${{ inputs.napi_run_id }} | |
| run: | | |
| if [ -z "$NAPI_RUN_ID" ]; then | |
| echo "::error::napi_run_id input is required when skip_build is true." | |
| exit 1 | |
| fi | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Vite+ | |
| uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb # v1.15.0 | |
| with: | |
| node-version-file: ".node-version" | |
| cache: true | |
| run-install: true | |
| - name: Download artifacts (same run) | |
| if: github.event_name != 'workflow_dispatch' || inputs.skip_build != true | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: napi-* | |
| path: crates/ox_content_napi/artifacts | |
| - name: Download artifacts (from previous run) | |
| if: github.event_name == 'workflow_dispatch' && inputs.skip_build == true | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: napi-* | |
| path: crates/ox_content_napi/artifacts | |
| run-id: ${{ inputs.napi_run_id }} | |
| github-token: ${{ github.token }} | |
| # napi-cli 3.8 rejects managed paths that escape the boundary it discovers | |
| # from its cwd, so point --cwd at the workspace root and keep every path | |
| # relative to it. `binding-packages` then resolves inside the pnpm | |
| # workspace instead of escaping the crate directory. | |
| - name: Create npm sub-package directories | |
| working-directory: crates/ox_content_napi | |
| run: >- | |
| vp exec -- napi create-npm-dirs --cwd ../.. | |
| --package-json-path crates/ox_content_napi/package.json | |
| --npm-dir binding-packages | |
| - name: Move artifacts to sub-packages | |
| working-directory: crates/ox_content_napi | |
| run: | | |
| declare -A target_map=( | |
| ["x86_64-apple-darwin"]="darwin-x64" | |
| ["aarch64-apple-darwin"]="darwin-arm64" | |
| ["x86_64-unknown-linux-gnu"]="linux-x64-gnu" | |
| ["x86_64-unknown-linux-musl"]="linux-x64-musl" | |
| ["aarch64-unknown-linux-gnu"]="linux-arm64-gnu" | |
| ["aarch64-unknown-linux-musl"]="linux-arm64-musl" | |
| ["x86_64-pc-windows-msvc"]="win32-x64-msvc" | |
| ) | |
| for dir in artifacts/napi-*; do | |
| target="${dir##*/napi-}" | |
| platform="${target_map[$target]}" | |
| if [ -n "$platform" ]; then | |
| for f in "$dir"/*.node; do | |
| cp "$f" "../../binding-packages/${platform}/ox-content.${platform}.node" | |
| done | |
| fi | |
| done | |
| echo "=== Verify binding packages ===" | |
| for pkg in ../../binding-packages/*/; do | |
| ls -la "$pkg"*.node 2>/dev/null || echo "WARNING: No .node file in $pkg" | |
| done | |
| # Same step as publish.yml: rewrite the main package's | |
| # optionalDependencies to reference the binding sub-packages. | |
| # Without this, consumers installing @ox-content/napi cannot | |
| # resolve the matching binding. --skip-optional-publish tells | |
| # napi-cli not to publish the optional sub-packages itself | |
| # (pkg-pr-new handles that downstream). | |
| - name: Update optionalDependencies (napi pre-publish) | |
| working-directory: crates/ox_content_napi | |
| run: >- | |
| vp exec -- napi pre-publish -t npm --cwd ../.. | |
| --package-json-path crates/ox_content_napi/package.json | |
| --npm-dir binding-packages --no-gh-release --skip-optional-publish | |
| # `pnpm build` recompiles @ox-content/napi via `napi build --release`, | |
| # so the job needs a Rust toolchain. Pin stable instead of relying on the | |
| # runner image's preinstalled rustc: the Blacksmith image ships an older | |
| # rustc than the oxc dependencies' MSRV (1.93). | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| - name: Mount sticky cache | |
| uses: ./.github/actions/mount-sticky-cache | |
| with: | |
| key-prefix: ${{ github.repository }}-nightly-${{ github.job }} | |
| cargo: "true" | |
| target: target | |
| - name: Build TS packages | |
| run: vp exec -- pnpm --filter "@ox-content/*" build | |
| - name: Publish to pkg-pr-new | |
| id: publish-pkg-pr-new | |
| continue-on-error: ${{ github.event_name == 'pull_request' }} | |
| run: | | |
| vp exec -- pnpx pkg-pr-new publish --compact --pnpm \ | |
| ./crates/ox_content_napi \ | |
| ./binding-packages/darwin-x64 \ | |
| ./binding-packages/darwin-arm64 \ | |
| ./binding-packages/linux-x64-gnu \ | |
| ./binding-packages/linux-x64-musl \ | |
| ./binding-packages/linux-arm64-gnu \ | |
| ./binding-packages/linux-arm64-musl \ | |
| ./binding-packages/win32-x64-msvc \ | |
| ./npm/ox-content-islands \ | |
| ./npm/unplugin-ox-content \ | |
| ./npm/vite-plugin-ox-content \ | |
| ./npm/vite-plugin-ox-content-react \ | |
| ./npm/vite-plugin-ox-content-solid \ | |
| ./npm/vite-plugin-ox-content-svelte \ | |
| ./npm/vite-plugin-ox-content-vue | |
| - name: Warn when preview publish is unavailable | |
| if: github.event_name == 'pull_request' && steps.publish-pkg-pr-new.outcome == 'failure' | |
| run: echo "::warning::pkg-pr-new preview publish failed; keeping pull request CI non-blocking for the external preview service." |