This repository contains shared workflows for building native modules maintained
by the Holepunch team. These modules are built using
bare-make.
- Introduction
- Usage
- Testing prebuilds on an emulator / simulator
- Patching the module before build
- Versioning and releasing
- Contributing
- License
The nodejs-mobile-bare-prebuilds repository provides a set of workflows and
tools to streamline the process of building native modules for Node.js mobile
applications. These workflows are designed to be used with the bare-make build
system.
The build input is always the npm tarball (npm pack <module>@<version>),
so the published prebuild corresponds exactly to what users get from
npm install. The test input is the upstream git repo at the commit the
tarball was published from — needed because the test/ folder is almost always
excluded from npm tarballs.
The most common entry point is prebuild-all.yml, which builds the standard
target set (three Android ABIs + iOS device + the arm64 iOS simulator),
optionally runs the module's own test suite on an emulator/simulator, and
publishes a GitHub Release with the artifacts.
name: Build, test, and release prebuilds
on:
workflow_dispatch:
inputs:
module_version:
description: "Exact version or dist-tag"
required: false
default: "latest"
type: string
jobs:
build:
uses: digidem/nodejs-mobile-bare-prebuilds/.github/workflows/prebuild-all.yml@v3
with:
module_name: sodium-native
module_version: ${{ inputs.module_version }}
test-android:
needs: build
uses: digidem/nodejs-mobile-bare-prebuilds/.github/workflows/test-android.yml@v3
with:
module_spec: ${{ needs.build.outputs.module_spec }}
test_runner: module
git_repo_slug: ${{ needs.build.outputs.git_repo_slug }}
git_ref: ${{ needs.build.outputs.git_ref }}
# Skip any tests that can't run in the emulator sandbox.
test_exclude: |
test-spawn.js
test-ios:
needs: build
uses: digidem/nodejs-mobile-bare-prebuilds/.github/workflows/test-ios.yml@v3
with:
module_spec: ${{ needs.build.outputs.module_spec }}
test_runner: module
git_repo_slug: ${{ needs.build.outputs.git_repo_slug }}
git_ref: ${{ needs.build.outputs.git_ref }}
release:
needs: [ build, test-android, test-ios ]
permissions:
contents: write
uses: digidem/nodejs-mobile-bare-prebuilds/.github/workflows/release.yml@v3
with:
module_spec: ${{ needs.build.outputs.module_spec }}prebuild-all.yml / prebuild.yml
| Input | Required | Default | Description |
|---|---|---|---|
module_name |
yes | — | npm module to build |
module_version |
no | latest |
Exact version or dist-tag. Resolved against npm before the matrix runs. |
patches_dir |
no | patches |
Directory in the caller repo holding <module>+<version>.patch files (see below) |
nodejs_mobile_version |
no | 24.19.0-0 |
digidem/nodejs-mobile release to link against, without the leading v |
test-android.yml / test-ios.yml
| Input | Required | Default | Description |
|---|---|---|---|
module_spec |
yes | — | <module>@<version>. Pass needs.build.outputs.module_spec. |
target |
no | android-x64 / ios-arm64 |
Runtime prebuilds/<target>/ subdir. Android also accepts android-arm64. |
artifact_name |
no | derived from module_spec + target | Override the default artifact name. |
test_runner |
no | smoke |
smoke / module / custom — see Testing prebuilds below. |
test_script |
no | — | Caller-repo path to a test script. Only used when test_runner: custom. |
test_exclude |
no | — | Newline-separated list of test file basenames to skip when test_runner: module. |
git_repo_slug |
no | — | owner/repo of the module's upstream GitHub repo. Pass needs.build.outputs.git_repo_slug. Required when test_runner: module. |
git_ref |
no | — | Commit SHA or tag to check out the upstream repo at. Pass needs.build.outputs.git_ref. Required when test_runner: module. |
nodejs_mobile_version |
no | v24.19.0-0 |
digidem/nodejs-mobile release tag the test app runs on. |
harness_version |
no | 1.0.0 |
Exact version of nodejs-mobile-test-harness to build and run the test app with. |
release.yml
| Input | Required | Default | Description |
|---|---|---|---|
module_spec |
yes | — | <module>@<version>. Pass needs.build.outputs.module_spec. The release tag is the version. |
prebuild-all.yml
| Output | Description |
|---|---|
module_version |
The exact version resolved and used for all builds |
module_spec |
<module>@<version> — pass to test-*.yml / release.yml |
git_repo_slug |
owner/repo of the upstream GitHub repo, or empty if not on GitHub / no repository.url |
git_ref |
Commit SHA (from gitHead) or v<version> tag, or empty if neither resolves |
prebuild.yml is a thin wrapper around the composite action for single-target
builds, triggerable from the Actions tab for debugging. test.yml chains
prebuild → test-* for end-to-end debugging on a branch.
Two reusable workflows run the built .node file inside nodejs-mobile on a
real emulator / simulator to catch runtime issues the prebuild step cannot
(wrong page size, missing symbols, require-addon not finding the binary,
etc.). The workflow writes a test.js next to the installed module and hands
the directory to
nodejs-mobile-test-harness,
which builds it into a minimal native app, runs it on the emulator /
simulator, streams the app's stdout into the workflow log, and passes/fails on
the Node exit code. Use that package directly to run your own tests on
nodejs-mobile outside these workflows.
Three test_runner modes, selected per test workflow:
smoke(default): generated test that justrequire()s the module. Enough to catch linker / load-time breakage without caller JS.module: runs the module's own test suite inside nodejs-mobile. See below.custom: uses the file attest_script(caller-repo path).
The module's tests are usually excluded from npm tarballs (via .npmignore
or the package's files field), so the test workflows check out the
upstream GitHub repo separately and overlay the tests into
node_modules/<module>/ before running. Two layouts are supported:
- directory style —
test/*.{js,mjs,cjs}(each fileimport()ed in sorted order) - single-file style —
test.{js,mjs,cjs}at the module root
The module's devDependencies are installed from the tarball's own
package.json into node_modules/<module>/node_modules/, so whatever test
runner the module declares (brittle, tape, tap, node:test, …) is available
when the test files require() / import it. npm ci is used when the
upstream ships a package-lock.json, npm install otherwise.
The git ref is resolved in prebuild-all.yml's resolve job:
gitHeadfrom npm metadata — recorded bynpm publishand usually reliable for modern publishes. This is a commit SHA;actions/checkoutaccepts bare SHAs even when the commit isn't reachable from a branch (e.g. after a force-push).v<version>tag — checked via the GitHub API (authenticated withGITHUB_TOKEN). Fallback for older tarballs with missinggitHead.- Empty — no git source available.
test_runner: modulewill fail loudly at test time; usesmokeinstead.
Pass both git_repo_slug and git_ref from prebuild-all.yml's outputs
into the test workflows when using test_runner: module. Non-GitHub
upstreams are not supported.
Skipping individual tests. Some suites include tests that can't run in
the emulator / simulator sandbox (process spawning, host FS paths, native
OS features). Pass a newline-separated list of basenames in test_exclude
to skip them:
test_exclude: |
test-spawn.js
test-ipc.jsExcluded files are listed as a TAP # excluded: … comment in the workflow
log for visibility.
Note: the build always uses the npm tarball as its source. The git
checkout is only used to populate the test files for the module test
runner.
Some modules need small patches to build for nodejs-mobile — e.g. fixing an
include path in CMakeLists.txt. Patches use the
patch-package filename convention:
<caller-repo>/
└── patches/
├── quickbit-native+2.4.1.patch
└── quickbit-native+2.4.2.patch
The prebuild step picks patches/<module_name>+<resolved_version>.patch and
applies it with patch -p1 --forward --no-backup-if-mismatch after npm pack
unpack and before npm install.
Failure modes:
- Patch applies cleanly → build proceeds.
- No
<module>+*.patchfiles exist → no-op, build proceeds. - A matching file is missing but siblings for other versions exist → fail
loudly. This catches the usual error of forgetting to rename the patch
after bumping
module_version. - Patch applies but with fuzz/rejects → fail loudly (
--forwardsuppresses the interactive prompt and surfaces rejects as a non-zero exit).
To use a different directory name, pass patches_dir: to
prebuild-all.yml / prebuild.yml.
Callers pin the reusable workflows to a floating major tag:
uses: digidem/nodejs-mobile-bare-prebuilds/.github/workflows/prebuild-all.yml@v3Internally, each reusable workflow checks this repo out at its own ref
(job.workflow_sha) and runs the composite actions from that checkout, so
workflow and actions always run at the same commit — whatever v3 (or a
branch, for dispatches) resolves to. Never reference a composite action with
an @<ref> from inside this repo; use the local path from that checkout. The
test harness is the one exception: it is an npm package, pinned to an exact
version by the test workflows' harness_version default.
| Tag | nodejs-mobile line |
|---|---|
v3 |
v24 only, from digidem/nodejs-mobile (tags vX.Y.Z-R) |
v2 |
last line supporting upstream nodejs-mobile v18.20.4 |
v3 also drops the ios-x64-simulator target, which the v24 line doesn't
ship.
-
Merge to
main. PRs touching.github/**run the real pipeline (ci.yml: prebuild → emulator test, including the floor-API emulator), at the PR's own commit — in-flight changes are what execute, so no manual validation dispatch is needed. -
Tag a semver release and push it:
git tag v3.1.0 && git push origin v3.1.0 # or, with release notes: gh release create v3.1.0 --generate-notes
-
The
move-major-tag.ymlworkflow force-movesv3to the new tag automatically. Callers on@v3pick the new version up on their next run — no changes needed in the calling repos.
Breaking changes (renamed/removed inputs, changed artifact layout, new
required permissions) get a new major: tag v4.0.0, which creates/moves
v4; existing @v3 callers are untouched until they opt in by editing
their uses: references.
To test unreleased changes end-to-end, dispatch test.yml from your branch
in the Actions tab — the workflows and actions run at the branch's commit.
We welcome contributions to this repository. If you have an idea for a new feature or have found a bug, please open an issue or submit a pull request.
This project is licensed under the MIT License. See the LICENSE file for more details.