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 + two iOS simulator slices),
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@v2
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@v2
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@v2
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@v2
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) |
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. |
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 bundles a test.js and the installed module into a
minimal native app, installs it on an emulator / simulator, streams the app's
stdout into the workflow log, and passes/fails on the Node exit code.
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@v2Internally, nothing else is pinned: each reusable workflow checks this repo
out at its own ref (job.workflow_sha) and runs the composite action and
test harness from that checkout. Workflow, action, and harness therefore
always run at the same commit — whatever v2 (or a branch, for dispatches)
resolves to. Never reference the composite action with an @<ref> from
inside this repo; use the local path from the harness checkout.
-
Merge to
main. PRs touching.github/**ortest-harness/**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 v2.2.0 && git push origin v2.2.0 # or, with release notes: gh release create v2.2.0 --generate-notes
-
The
move-major-tag.ymlworkflow force-movesv2to the new tag automatically. Callers on@v2pick 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 v3.0.0, which creates/moves
v3; existing @v2 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 whole pipeline (workflows, action, harness) runs 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.