I recently worked on a repository that ships both JavaScript plugins and Rust plugins. Both plugin types need CI builds and npm publishing, but Rust adds one extra requirement: each native binding has to be built for several platforms before publishing.
This note records the GitHub Actions setup I used for that mixed plugin repository.
Rust plugins
Building Rust plugins
Rust plugins need platform-specific native artifacts. Before publishing to npm, the workflow builds each ABI target and uploads the compiled artifact.
name: Building Rust Binding And Upload Artifacts
on: workflow_call
jobs:
build:
name: Build and Upload Artifacts - ${{ matrix.settings.abi }}
runs-on: ${{ matrix.settings.os }}
strategy:
fail-fast: false
matrix:
settings:
- os: ubuntu-latest
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
abi: linux-x64-gnu
build: >-
git config --global --add safe.directory /build &&
set -e &&
unset CC_x86_64_unknown_linux_gnu &&
unset CC &&
pnpm --filter "{rust-plugins}[HEAD~1]" --sequential build --target x86_64-unknown-linux-gnu --abi linux-x64-gnu
- os: ubuntu-latest
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
abi: linux-x64-musl
build: >-
git config --global --add safe.directory /build &&
set -e &&
unset CC_x86_64_unknown_linux_musl &&
unset CC &&
pnpm --filter "{rust-plugins}[HEAD~1]" --sequential build --target x86_64-unknown-linux-musl --abi linux-x64-musl
- os: windows-latest
abi: win32-x64-msvc
- os: macos-latest
abi: darwin-arm64
- os: macos-13
abi: darwin-x64
# cross compile
# windows. Note swc plugins is not supported on ia32 and arm64
- os: windows-latest
abi: win32-ia32-msvc
target: i686-pc-windows-msvc
build: |
export CARGO_PROFILE_RELEASE_LTO=false
cargo install cargo-xwin --locked
pnpm --filter "{rust-plugins}[HEAD~1]" --sequential build --target i686-pc-windows-msvc --abi win32-ia32-msvc --cargo-flags="--no-default-features"
- os: windows-latest
abi: win32-arm64-msvc
target: aarch64-pc-windows-msvc
build: |
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=256
export CARGO_PROFILE_RELEASE_LTO=false
cargo install cargo-xwin --locked
pnpm --filter "{rust-plugins}[HEAD~1]" --sequential build --target aarch64-pc-windows-msvc --abi win32-arm64-msvc --cargo-flags="--no-default-features"
# linux
- os: ubuntu-latest
abi: linux-arm64-musl
target: aarch64-unknown-linux-musl
zig: true
- os: ubuntu-latest
abi: linux-arm64-gnu
target: aarch64-unknown-linux-gnu
zig: true
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
# - run: |
# git fetch --no-tags --prune --depth=1 origin +refs/heads/main:refs/remotes/HEAD~1
- name: Cache rust artifacts
uses: Swatinem/rust-cache@v2
with:
shared-key: rust-build-${{ matrix.settings.abi }}
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install Dependencies
run: npm config set registry https://registry.npmmirror.com && npm install -g [email protected] && pnpm i --frozen-lockfile
- run: rustup target add ${{ matrix.settings.target }}
if: ${{ matrix.settings.target }}
# Use the v1 of this action
- uses: mbround18/setup-osxcross@v1
if: ${{ matrix.settings.osxcross }}
# This builds executables & sets env variables for rust to consume.
with:
osx-version: '12.3'
- uses: goto-bus-stop/setup-zig@v2
if: ${{ matrix.settings.zig }}
- name: Build in docker
uses: addnab/docker-run-action@v3
if: ${{ matrix.settings.docker }}
with:
image: ${{ matrix.settings.docker }}
options: -v ${{ env.HOME }}/.cargo/git:/root/.cargo/git -v ${{ env.HOME }}/.cargo/registry:/root/.cargo/registry -v ${{ github.workspace }}:/build -w /build
run: ${{ matrix.settings.build }}
- name: Default Build
if: ${{ !matrix.settings.docker && !matrix.settings.build }}
run: |
pnpm --filter "{rust-plugins}[HEAD~1]" --sequential build --abi ${{ matrix.settings.abi }} ${{ matrix.settings.target && format('--target {0}', matrix.settings.target) || '' }} ${{ matrix.settings.zig && '--zig' || '' }}
shell: bash
- name: Build
if: ${{ !matrix.settings.docker && matrix.settings.build }}
run: ${{ matrix.settings.build }}
shell: bash
- name: Upload Plugin dsv
uses: actions/upload-artifact@v3
with:
name: ${{ github.sha }}-${{ matrix.settings.abi }}-dsv
path: ./rust-plugins/dsv/npm/${{ matrix.settings.abi }}/index.farm
if-no-files-found: ignore
# other packages upload
This workflow builds Rust plugins across platform targets. The key command is pnpm --filter "{rust-plugins}[HEAD~1]" , which limits the build to packages changed since the previous commit under rust-plugins . That keeps the matrix useful without rebuilding every plugin on every run.
Deploying Rust plugins
name: Publish packages and crates
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
call-rust-build:
if: contains(github.event.head_commit.message, 'rust-plugins') || contains(github.event.head_commit.message, 'all')
uses: ./.github/workflows/build.yaml
release:
name: Release
if: contains(github.event.head_commit.message, 'rust-plugins') || contains(github.event.head_commit.message, 'all')
needs: [call-rust-build]
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v3
with:
fetch-depth: 2
- run: |
git fetch --no-tags --prune --depth=1 origin +refs/heads/main:refs/remotes/HEAD~1
- name: Setup Node.js 18.x
uses: actions/setup-node@v3
with:
node-version: 18.x
# batch download artifacts
- uses: actions/download-artifact@v3
with:
path: /tmp/artifacts
- name: Move Artifacts
run: |
for abi in linux-x64-gnu linux-x64-musl darwin-x64 win32-x64-msvc linux-arm64-musl linux-arm64-gnu darwin-arm64 win32-ia32-msvc win32-arm64-msvc
do
for package in dsv react-components virtual yaml strip image url icons auto-import mdx
do
folder_path="/tmp/artifacts/${{github.sha}}-${abi}-${package}"
if [ -d "${folder_path}" ] && [ -n "$(ls -A $folder_path)" ]; then
mv /tmp/artifacts/${{ github.sha }}-${abi}-${package}/* ./packages/${package}/npm/${abi}
ls -R $folder_path
ls -R ./packages/${package}/npm/${abi}
test -f ./packages/${package}/npm/${abi}/index.farm
else
echo "${folder_path} is empty"
fi
done
done
- name: Install Dependencies
run: npm install -g [email protected] && pnpm i --frozen-lockfile
- name: Publish to npm
run: |
npm set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }} && npm config set access public && pnpm --filter "{rust-plugins}[HEAD~1]" publish --no-git-checks
The release workflow uses the commit message to decide whether to run. Commits containing rust-plugins or all trigger the Rust release path.
The release job downloads the artifacts from the build workflow, moves each ABI artifact into the corresponding package directory, and publishes the changed Rust plugin packages to npm.
JavaScript plugins
Building JavaScript plugins
name: PR build plugins
on: workflow_call
jobs:
build:
runs-on: ubuntu-latest
name: release
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
# - run: |
# git fetch --no-tags --prune --depth=1 origin +refs/heads/main:refs/remotes/HEAD~1
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 20
registry-url: https://registry.npmjs.org/
- name: Enable Corepack
id: pnpm-setup
run: |
corepack enable
- name: Initialize .npmrc
run: >
echo -e "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}\n$(cat .npmrc)" > .npmrc
&& cat -n .npmrc
- name: pnpm install
run: pnpm install --frozen-lockfile
- name: Build Packages
run: |
pnpm --filter "{js-plugins}[HEAD~1]" build
The JavaScript plugin workflow follows the same pattern. The build command uses pnpm --filter "{js-plugins}[HEAD~1]" build , so only changed JavaScript plugins are built.
Deploying JavaScript plugins
name: Release Packages
on:
push:
branches:
- main
jobs:
release:
runs-on: ubuntu-latest
if: contains(github.event.head_commit.message, 'js-plugins') || contains(github.event.head_commit.message, 'all')
name: release
steps:
- name: Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 20
registry-url: https://registry.npmjs.org/
- name: Enable Corepack
id: pnpm-setup
run: |
corepack enable
- name: Initialize .npmrc
run: >
echo -e "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}\n$(cat .npmrc)" > .npmrc
&& cat -n .npmrc
- name: pnpm install
run: pnpm install --frozen-lockfile
- name: Build Packages
run: |
pnpm --filter "{js-plugins}[HEAD~1]" build
- name: Release and Publish Packages
run: |
npm set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }} && npm config set access public && pnpm --filter "{js-plugins}[HEAD~1]" publish --no-git-checks
The JavaScript release workflow uses the same trigger strategy as the Rust release workflow. Commits containing js-plugins or all run the release job, then publish changed JavaScript plugin packages to npm.
Summary
- Use
pnpm --filter "{xx}[HEAD~1]"to build and publish only changed packages. - Use
contains(github.event.head_commit.message, ...)to route commits into the Rust, JavaScript, or full release workflow.