From 027df5bf15fd3257ac68cc156c44d13376621bdb Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sat, 30 May 2026 10:39:07 -0400 Subject: [PATCH 1/3] ci: fix linux-arm64-musl release build The runner can't exec JS actions inside Alpine containers on arm64 hosts (actions/runner#801), so both musl targets now build via docker run on native-arch runners instead of job containers. - install rust from a version-pinned, sha256-verified rustup-init instead of curl | sh (replaces dtolnay/rust-toolchain in the alpine builds) - override napi's assumed aarch64-linux-musl-gcc cross linker with the native musl gcc - run the musl smoke tests inside the container --- .github/workflows/release.yml | 54 +++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e477dcb..cbbf278 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,6 @@ jobs: permissions: contents: read runs-on: ${{ matrix.os }} - container: ${{ matrix.container }} strategy: fail-fast: false matrix: @@ -65,11 +64,17 @@ jobs: build: yarn napi build --platform --release --esm --output-dir . --target aarch64-unknown-linux-gnu --js binding.js --dts binding.d.ts artifact: index.linux-arm64-gnu.node - # ── Linux musl (Alpine container on native-arch runner) ─────────── + # ── Linux musl (Alpine via docker run on native-arch runners) ───── + # Job containers can't be used here: the runner refuses to exec JS + # actions (checkout, upload-artifact, ...) inside Alpine containers + # on arm64 hosts (actions/runner#801), so the "Build (docker)" step + # runs the whole alpine build via `docker run` instead. Rust comes + # from a version-pinned, checksum-verified rustup-init. - name: linux-x64-musl os: ubuntu-latest - container: node:22-alpine@sha256:968df39aedcea65eeb078fb336ed7191baf48f972b4479711397108be0966920 - setup: apk add --no-cache bash curl build-base cmake python3 git + docker: node:22-alpine@sha256:968df39aedcea65eeb078fb336ed7191baf48f972b4479711397108be0966920 + rustup_url: https://static.rust-lang.org/rustup/archive/1.29.0/x86_64-unknown-linux-musl/rustup-init + rustup_sha256: 9cd3fda5fd293890e36ab271af6a786ee22084b5f6c2b83fd8323cec6f0992c1 build: | rustup target add x86_64-unknown-linux-musl yarn napi build --platform --release --esm --output-dir . --target x86_64-unknown-linux-musl --js binding.js --dts binding.d.ts @@ -77,9 +82,13 @@ jobs: - name: linux-arm64-musl os: ubuntu-24.04-arm - container: node:22-alpine@sha256:968df39aedcea65eeb078fb336ed7191baf48f972b4479711397108be0966920 - setup: apk add --no-cache bash curl build-base cmake python3 git + docker: node:22-alpine@sha256:968df39aedcea65eeb078fb336ed7191baf48f972b4479711397108be0966920 + rustup_url: https://static.rust-lang.org/rustup/archive/1.29.0/aarch64-unknown-linux-musl/rustup-init + rustup_sha256: 88761caacddb92cd79b0b1f939f3990ba1997d701a38b3e8dd6746a562f2a759 build: | + # napi assumes aarch64-musl is cross-compiled and would default the + # linker to aarch64-linux-musl-gcc; here gcc IS the native musl gcc. + export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=gcc rustup target add aarch64-unknown-linux-musl yarn napi build --platform --release --esm --output-dir . --target aarch64-unknown-linux-musl --js binding.js --dts binding.d.ts artifact: index.linux-arm64-musl.node @@ -89,19 +98,13 @@ jobs: with: persist-credentials: false - - name: Container setup - if: matrix.setup - run: ${{ matrix.setup }} - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - if: ${{ !matrix.container }} with: node-version-file: .nvmrc cache: yarn - # The alpine containers ship no Rust; hosted runners have stable preinstalled. - - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable # zizmor: ignore[superfluous-actions] - if: matrix.container + # Rust (stable) is preinstalled on the hosted runner images; the alpine + # docker builds install their own pinned rustup in "Build (docker)". - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: key: ${{ matrix.name }} @@ -109,14 +112,35 @@ jobs: - run: yarn install --immutable - name: Build + if: ${{ !matrix.docker }} shell: bash run: ${{ matrix.build }} + # Everything that the other entries do across separate steps — alpine + # deps, Rust, yarn install, build, strip, smoke test — has to happen + # inside the container here, since the host can't run musl binaries. + - name: Build (docker) + if: matrix.docker + run: | + docker run --rm -v "$PWD:/build" -w /build "${{ matrix.docker }}" sh -ec ' + apk add --no-cache bash curl build-base cmake python3 git + curl --proto "=https" --tlsv1.2 -fsSL -o /tmp/rustup-init "${{ matrix.rustup_url }}" + echo "${{ matrix.rustup_sha256 }} /tmp/rustup-init" | sha256sum -c - + chmod +x /tmp/rustup-init + /tmp/rustup-init -y --profile minimal --default-toolchain stable + export PATH="$HOME/.cargo/bin:$PATH" + yarn install --immutable + ${{ matrix.build }} + node scripts/strip-binding-fallbacks.js + yarn test + ' + sudo chown -R "$(id -u):$(id -g)" . + - name: Strip binding.js package fallbacks run: node scripts/strip-binding-fallbacks.js - name: Smoke test (native targets only) - if: ${{ !contains(matrix.name, 'arm64') || contains(matrix.os, 'arm') || matrix.os == 'macos-latest' }} + if: ${{ !matrix.docker && (!contains(matrix.name, 'arm64') || contains(matrix.os, 'arm') || matrix.os == 'macos-latest') }} run: yarn test - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 From f89569099f9c3d6f0b7aabb3fb68540608057c9c Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sat, 30 May 2026 10:42:42 -0400 Subject: [PATCH 2/3] ci: run all prebuild targets on pull requests Move the 7-target build matrix from the release workflow into ci.yml so every PR proves all targets still build (and smoke-tests them where the host can run them). The release workflow reuses these jobs via workflow_call and publishes the artifacts they upload. --- .github/workflows/ci.yml | 144 +++++++++++++++++++++++++++++++ .github/workflows/release.yml | 157 ++-------------------------------- 2 files changed, 152 insertions(+), 149 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0a9fca..6d4fa7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + CARGO_TERM_COLOR: always + jobs: test: name: ${{ matrix.os }} @@ -44,3 +47,144 @@ jobs: - uses: rustsec/audit-check@69366f33c96575abad1ee0dba8212993eecbe998 # v2.0.0 with: token: ${{ secrets.GITHUB_TOKEN }} + + # Build every prebuild target the released package ships. Running this on + # PRs (not just on main) means a broken target is caught before merge; the + # Release workflow reuses these same jobs (via workflow_call) and publishes + # the artifacts they upload. + build: + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + # ── macOS: build both arches, lipo to universal ─────────────────── + - name: darwin-universal + os: macos-latest + build: | + rustup target add x86_64-apple-darwin + yarn napi build --platform --release --esm --output-dir . --target aarch64-apple-darwin --js binding.js --dts binding.d.ts + yarn napi build --platform --release --esm --output-dir . --target x86_64-apple-darwin --js binding.js --dts binding.d.ts + yarn napi universalize --output-dir . + artifact: index.darwin-universal.node + + # ── Windows ─────────────────────────────────────────────────────── + - name: win32-x64 + os: windows-latest + build: yarn napi build --platform --release --esm --output-dir . --target x86_64-pc-windows-msvc --js binding.js --dts binding.d.ts + artifact: index.win32-x64-msvc.node + + - name: win32-arm64 + os: windows-latest + build: | + rustup target add aarch64-pc-windows-msvc + yarn napi build --platform --release --esm --output-dir . --target aarch64-pc-windows-msvc --js binding.js --dts binding.d.ts + artifact: index.win32-arm64-msvc.node + + # ── Linux glibc (native runners for each arch) ──────────────────── + - name: linux-x64-gnu + os: ubuntu-latest + build: yarn napi build --platform --release --esm --output-dir . --target x86_64-unknown-linux-gnu --js binding.js --dts binding.d.ts + artifact: index.linux-x64-gnu.node + + - name: linux-arm64-gnu + os: ubuntu-24.04-arm + build: yarn napi build --platform --release --esm --output-dir . --target aarch64-unknown-linux-gnu --js binding.js --dts binding.d.ts + artifact: index.linux-arm64-gnu.node + + # ── Linux musl (Alpine via docker run on native-arch runners) ───── + # Job containers can't be used here: the runner refuses to exec JS + # actions (checkout, upload-artifact, ...) inside Alpine containers + # on arm64 hosts (actions/runner#801), so the "Build (docker)" step + # runs the whole alpine build via `docker run` instead. Rust comes + # from a version-pinned, checksum-verified rustup-init. + - name: linux-x64-musl + os: ubuntu-latest + docker: node:22-alpine@sha256:968df39aedcea65eeb078fb336ed7191baf48f972b4479711397108be0966920 + rustup_url: https://static.rust-lang.org/rustup/archive/1.29.0/x86_64-unknown-linux-musl/rustup-init + rustup_sha256: 9cd3fda5fd293890e36ab271af6a786ee22084b5f6c2b83fd8323cec6f0992c1 + build: | + rustup target add x86_64-unknown-linux-musl + yarn napi build --platform --release --esm --output-dir . --target x86_64-unknown-linux-musl --js binding.js --dts binding.d.ts + artifact: index.linux-x64-musl.node + + - name: linux-arm64-musl + os: ubuntu-24.04-arm + docker: node:22-alpine@sha256:968df39aedcea65eeb078fb336ed7191baf48f972b4479711397108be0966920 + rustup_url: https://static.rust-lang.org/rustup/archive/1.29.0/aarch64-unknown-linux-musl/rustup-init + rustup_sha256: 88761caacddb92cd79b0b1f939f3990ba1997d701a38b3e8dd6746a562f2a759 + build: | + # napi assumes aarch64-musl is cross-compiled and would default the + # linker to aarch64-linux-musl-gcc; here gcc IS the native musl gcc. + export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=gcc + rustup target add aarch64-unknown-linux-musl + yarn napi build --platform --release --esm --output-dir . --target aarch64-unknown-linux-musl --js binding.js --dts binding.d.ts + artifact: index.linux-arm64-musl.node + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: .nvmrc + cache: yarn + + # Rust (stable) is preinstalled on the hosted runner images; the alpine + # docker builds install their own pinned rustup in "Build (docker)". + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + key: ${{ matrix.name }} + + - run: yarn install --immutable + + - name: Build + if: ${{ !matrix.docker }} + shell: bash + run: ${{ matrix.build }} + + # Everything that the other entries do across separate steps — alpine + # deps, Rust, yarn install, build, strip, smoke test — has to happen + # inside the container here, since the host can't run musl binaries. + - name: Build (docker) + if: matrix.docker + run: | + docker run --rm -v "$PWD:/build" -w /build "${{ matrix.docker }}" sh -ec ' + apk add --no-cache bash curl build-base cmake python3 git + curl --proto "=https" --tlsv1.2 -fsSL -o /tmp/rustup-init "${{ matrix.rustup_url }}" + echo "${{ matrix.rustup_sha256 }} /tmp/rustup-init" | sha256sum -c - + chmod +x /tmp/rustup-init + /tmp/rustup-init -y --profile minimal --default-toolchain stable + export PATH="$HOME/.cargo/bin:$PATH" + yarn install --immutable + ${{ matrix.build }} + node scripts/strip-binding-fallbacks.js + yarn test + ' + sudo chown -R "$(id -u):$(id -g)" . + + - name: Strip binding.js package fallbacks + run: node scripts/strip-binding-fallbacks.js + + - name: Smoke test (native targets only) + if: ${{ !matrix.docker && (!contains(matrix.name, 'arm64') || contains(matrix.os, 'arm') || matrix.os == 'macos-latest') }} + run: yarn test + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ matrix.name }} + path: ${{ matrix.artifact }} + if-no-files-found: error + + # binding.js / binding.d.ts are gitignored; ship one canonical copy from + # this job for the release step to pick up. + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: matrix.name == 'linux-x64-gnu' + with: + name: binding + path: | + binding.js + binding.d.ts + if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cbbf278..244e684 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,8 +1,9 @@ name: Release -# On every push to main: run CI, build all 7 native targets, then let -# semantic-release decide the version from the conventional commits, publish the -# fat package to npm, and cut a GitHub release. No-op if no releasable commits. +# On every push to main: run the full CI suite (tests, audit, and all 7 native +# target builds — the same jobs PRs run), then let semantic-release decide the +# version from the conventional commits, publish the fat package to npm, and +# cut a GitHub release. No-op if no releasable commits. on: push: @@ -10,159 +11,17 @@ on: permissions: {} -env: - CARGO_TERM_COLOR: always - jobs: - # Reuse the full PR test suite as a release gate. - test: + # Reuse the full PR suite as the release gate; the build jobs in it upload + # the prebuild artifacts that the release job below packages. + ci: permissions: contents: read uses: ./.github/workflows/ci.yml - build: - name: ${{ matrix.name }} - needs: test - permissions: - contents: read - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - include: - # ── macOS: build both arches, lipo to universal ─────────────────── - - name: darwin-universal - os: macos-latest - build: | - rustup target add x86_64-apple-darwin - yarn napi build --platform --release --esm --output-dir . --target aarch64-apple-darwin --js binding.js --dts binding.d.ts - yarn napi build --platform --release --esm --output-dir . --target x86_64-apple-darwin --js binding.js --dts binding.d.ts - yarn napi universalize --output-dir . - artifact: index.darwin-universal.node - - # ── Windows ─────────────────────────────────────────────────────── - - name: win32-x64 - os: windows-latest - build: yarn napi build --platform --release --esm --output-dir . --target x86_64-pc-windows-msvc --js binding.js --dts binding.d.ts - artifact: index.win32-x64-msvc.node - - - name: win32-arm64 - os: windows-latest - build: | - rustup target add aarch64-pc-windows-msvc - yarn napi build --platform --release --esm --output-dir . --target aarch64-pc-windows-msvc --js binding.js --dts binding.d.ts - artifact: index.win32-arm64-msvc.node - - # ── Linux glibc (native runners for each arch) ──────────────────── - - name: linux-x64-gnu - os: ubuntu-latest - build: yarn napi build --platform --release --esm --output-dir . --target x86_64-unknown-linux-gnu --js binding.js --dts binding.d.ts - artifact: index.linux-x64-gnu.node - - - name: linux-arm64-gnu - os: ubuntu-24.04-arm - build: yarn napi build --platform --release --esm --output-dir . --target aarch64-unknown-linux-gnu --js binding.js --dts binding.d.ts - artifact: index.linux-arm64-gnu.node - - # ── Linux musl (Alpine via docker run on native-arch runners) ───── - # Job containers can't be used here: the runner refuses to exec JS - # actions (checkout, upload-artifact, ...) inside Alpine containers - # on arm64 hosts (actions/runner#801), so the "Build (docker)" step - # runs the whole alpine build via `docker run` instead. Rust comes - # from a version-pinned, checksum-verified rustup-init. - - name: linux-x64-musl - os: ubuntu-latest - docker: node:22-alpine@sha256:968df39aedcea65eeb078fb336ed7191baf48f972b4479711397108be0966920 - rustup_url: https://static.rust-lang.org/rustup/archive/1.29.0/x86_64-unknown-linux-musl/rustup-init - rustup_sha256: 9cd3fda5fd293890e36ab271af6a786ee22084b5f6c2b83fd8323cec6f0992c1 - build: | - rustup target add x86_64-unknown-linux-musl - yarn napi build --platform --release --esm --output-dir . --target x86_64-unknown-linux-musl --js binding.js --dts binding.d.ts - artifact: index.linux-x64-musl.node - - - name: linux-arm64-musl - os: ubuntu-24.04-arm - docker: node:22-alpine@sha256:968df39aedcea65eeb078fb336ed7191baf48f972b4479711397108be0966920 - rustup_url: https://static.rust-lang.org/rustup/archive/1.29.0/aarch64-unknown-linux-musl/rustup-init - rustup_sha256: 88761caacddb92cd79b0b1f939f3990ba1997d701a38b3e8dd6746a562f2a759 - build: | - # napi assumes aarch64-musl is cross-compiled and would default the - # linker to aarch64-linux-musl-gcc; here gcc IS the native musl gcc. - export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=gcc - rustup target add aarch64-unknown-linux-musl - yarn napi build --platform --release --esm --output-dir . --target aarch64-unknown-linux-musl --js binding.js --dts binding.d.ts - artifact: index.linux-arm64-musl.node - - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: .nvmrc - cache: yarn - - # Rust (stable) is preinstalled on the hosted runner images; the alpine - # docker builds install their own pinned rustup in "Build (docker)". - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - key: ${{ matrix.name }} - - - run: yarn install --immutable - - - name: Build - if: ${{ !matrix.docker }} - shell: bash - run: ${{ matrix.build }} - - # Everything that the other entries do across separate steps — alpine - # deps, Rust, yarn install, build, strip, smoke test — has to happen - # inside the container here, since the host can't run musl binaries. - - name: Build (docker) - if: matrix.docker - run: | - docker run --rm -v "$PWD:/build" -w /build "${{ matrix.docker }}" sh -ec ' - apk add --no-cache bash curl build-base cmake python3 git - curl --proto "=https" --tlsv1.2 -fsSL -o /tmp/rustup-init "${{ matrix.rustup_url }}" - echo "${{ matrix.rustup_sha256 }} /tmp/rustup-init" | sha256sum -c - - chmod +x /tmp/rustup-init - /tmp/rustup-init -y --profile minimal --default-toolchain stable - export PATH="$HOME/.cargo/bin:$PATH" - yarn install --immutable - ${{ matrix.build }} - node scripts/strip-binding-fallbacks.js - yarn test - ' - sudo chown -R "$(id -u):$(id -g)" . - - - name: Strip binding.js package fallbacks - run: node scripts/strip-binding-fallbacks.js - - - name: Smoke test (native targets only) - if: ${{ !matrix.docker && (!contains(matrix.name, 'arm64') || contains(matrix.os, 'arm') || matrix.os == 'macos-latest') }} - run: yarn test - - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ${{ matrix.name }} - path: ${{ matrix.artifact }} - if-no-files-found: error - - # binding.js / binding.d.ts are gitignored; ship one canonical copy from - # this job for the release step to pick up. - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - if: matrix.name == 'linux-x64-gnu' - with: - name: binding - path: | - binding.js - binding.d.ts - if-no-files-found: error - release: name: Release - needs: build + needs: ci runs-on: ubuntu-latest environment: npm-trusted-publisher permissions: From a856f16ad92817e9c9abc6dd90af3791a9e990e4 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Mon, 1 Jun 2026 14:24:50 -0700 Subject: [PATCH 3/3] Update .github/workflows/ci.yml Co-authored-by: David Sanders --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d4fa7d..3954f1e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,6 +131,7 @@ jobs: with: node-version-file: .nvmrc cache: yarn + package-manager-cache: ${{ github.ref != 'refs/heads/main' }} # Rust (stable) is preinstalled on the hosted runner images; the alpine # docker builds install their own pinned rustup in "Build (docker)".