Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: CI (pipeline self-test)

# Runs the real prebuild -> emulator-test pipeline on every PR that touches
# the workflows, the composite actions, or the test harness. Because the
# reusable workflows check out the action and harness at their own
# `job.workflow_sha`, the PR's in-flight changes are what actually run —
# no need for temporary action-ref pins or manual `test.yml` dispatches.
#
# Android-only on purpose: it's the cheap leg (~15 min on ubuntu runners,
# including the floor-API emulator) and where toolchain regressions like
# the RELR Android < 9 crash live. Run `test.yml` manually for an iOS leg.

on:
pull_request:
paths:
- ".github/**"
- "test-harness/**"

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
build-android:
uses: ./.github/workflows/prebuild.yml
with:
# Small, fast module that still exercises the C++ stdlib include
# injection path in the prebuild action. Pinned so a breaking
# upstream release can't fail CI for unrelated PRs.
module_name: quickbit-native
platform: android
arch: x64
module_version: "2.4.8"

test-android:
needs: build-android
uses: ./.github/workflows/test-android.yml
with:
module_spec: ${{ needs.build-android.outputs.module_spec }}
36 changes: 36 additions & 0 deletions .github/workflows/move-major-tag.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Move major version tag

# Keeps the floating major tag callers pin (`uses: ...@v2`) pointed at the
# latest release of that major. Pushing a semver tag (directly or via
# `gh release create`) force-moves the corresponding major tag — callers
# pick the new version up on their next run, with workflow, composite
# action, and test harness guaranteed in sync (the reusable workflows
# check themselves out at `job.workflow_sha`).

on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"

permissions:
contents: write

jobs:
move-major-tag:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v6

- name: Force-move major tag
run: |
set -eu
major="${GITHUB_REF_NAME%%.*}"
# Peel through annotated tag objects to the commit — GITHUB_SHA
# is the tag object for annotated tags, not the commit.
commit=$(git rev-parse "${GITHUB_REF_NAME}^{commit}")
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag --force --annotate "$major" --message "Point ${major} at ${GITHUB_REF_NAME}" "$commit"
git push origin "refs/tags/${major}" --force
echo "Moved ${major} -> ${GITHUB_REF_NAME} (${commit})"
11 changes: 10 additions & 1 deletion .github/workflows/prebuild-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,16 @@ jobs:
# resolves against the workspace root.
- uses: actions/checkout@v6

- uses: digidem/nodejs-mobile-bare-prebuilds/.github/actions/prebuild@main
# Same-ref harness checkout — see the comment in prebuild.yml.
- name: Checkout prebuild harness
uses: actions/checkout@v6
with:
repository: ${{ job.workflow_repository }}
ref: ${{ job.workflow_sha }}
path: .nodejs-mobile-bare-prebuilds
persist-credentials: false

- uses: ./.nodejs-mobile-bare-prebuilds/.github/actions/prebuild
with:
module_name: ${{ inputs.module_name }}
platform: ${{ matrix.platform }}
Expand Down
15 changes: 14 additions & 1 deletion .github/workflows/prebuild.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,20 @@ jobs:
# resolves against the workspace root.
- uses: actions/checkout@v6

- uses: digidem/nodejs-mobile-bare-prebuilds/.github/actions/prebuild@main
# Check out this repo at the SAME ref this reusable workflow is
# running from (`job.workflow_sha` resolves the caller's @v2/@main/
# branch ref), so the composite action below always matches the
# workflow version — no @main pin that can drift from tagged
# callers, and branch dispatches test in-flight action changes.
- name: Checkout prebuild harness
uses: actions/checkout@v6
with:
repository: ${{ job.workflow_repository }}
ref: ${{ job.workflow_sha }}
path: .nodejs-mobile-bare-prebuilds
persist-credentials: false

- uses: ./.nodejs-mobile-bare-prebuilds/.github/actions/prebuild
id: prebuild
with:
module_name: ${{ inputs.module_name }}
Expand Down
13 changes: 9 additions & 4 deletions .github/workflows/test-android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,13 @@ on:
default: 24
harness_ref:
type: string
description: "Ref of digidem/nodejs-mobile-bare-prebuilds to pull the harness from"
description: |
Optional override for the harness checkout ref. Defaults to the
ref this reusable workflow itself is running from
(`job.workflow_sha`), so the harness always matches the workflow
version.
required: false
default: "main"
default: ""
git_repo_slug:
type: string
description: |
Expand Down Expand Up @@ -137,9 +141,10 @@ jobs:
- name: Checkout test harness
uses: actions/checkout@v6
with:
repository: digidem/nodejs-mobile-bare-prebuilds
ref: ${{ inputs.harness_ref }}
repository: ${{ job.workflow_repository }}
ref: ${{ inputs.harness_ref || job.workflow_sha }}
path: .nodejs-mobile-bare-prebuilds
persist-credentials: false

- uses: actions/setup-node@v6
with:
Expand Down
13 changes: 9 additions & 4 deletions .github/workflows/test-ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,13 @@ on:
default: "iPhone 15"
harness_ref:
type: string
description: "Ref of digidem/nodejs-mobile-bare-prebuilds to pull the harness from"
description: |
Optional override for the harness checkout ref. Defaults to the
ref this reusable workflow itself is running from
(`job.workflow_sha`), so the harness always matches the workflow
version.
required: false
default: "main"
default: ""
git_repo_slug:
type: string
description: |
Expand Down Expand Up @@ -100,9 +104,10 @@ jobs:
- name: Checkout test harness
uses: actions/checkout@v6
with:
repository: digidem/nodejs-mobile-bare-prebuilds
ref: ${{ inputs.harness_ref }}
repository: ${{ job.workflow_repository }}
ref: ${{ inputs.harness_ref || job.workflow_sha }}
path: .nodejs-mobile-bare-prebuilds
persist-credentials: false

- uses: actions/setup-node@v6
with:
Expand Down
13 changes: 8 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ name: End-to-end test (manual dispatch)
# whole pipeline can be exercised from the Actions tab without needing to call
# these reusable workflows from another repo.
#
# The harness is always checked out at the same commit as this dispatch run
# (`harness_ref: ${{ github.sha }}`), so in-flight changes to the harness or
# the composite action are tested as-is on the triggering branch.
# Everything (workflows, composite action, test harness) runs at the commit
# of this dispatch: the reusable workflows check out the action and harness
# at their own `job.workflow_sha`, so in-flight changes on the triggering
# branch are tested as-is.

on:
workflow_dispatch:
Expand Down Expand Up @@ -58,8 +59,10 @@ jobs:
uses: ./.github/workflows/test-android.yml
with:
module_spec: ${{ needs.build-android.outputs.module_spec }}
# test_script is only consulted when test_runner is 'custom' —
# derive it so a provided script can't be silently ignored.
test_runner: ${{ inputs.test_script != '' && 'custom' || 'smoke' }}
test_script: ${{ inputs.test_script }}
harness_ref: ${{ github.sha }}

build-ios:
if: ${{ inputs.platforms == 'both' || inputs.platforms == 'ios' }}
Expand All @@ -78,5 +81,5 @@ jobs:
uses: ./.github/workflows/test-ios.yml
with:
module_spec: ${{ needs.build-ios.outputs.module_spec }}
test_runner: ${{ inputs.test_script != '' && 'custom' || 'smoke' }}
test_script: ${{ inputs.test_script }}
harness_ref: ${{ github.sha }}
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ by the Holepunch team. These modules are built using
- [Testing prebuilds on an emulator / simulator](#testing-prebuilds-on-an-emulator--simulator)
- [Running the module's own test suite](#running-the-modules-own-test-suite)
- [Patching the module before build](#patching-the-module-before-build)
- [Versioning and releasing](#versioning-and-releasing)
- [Contributing](#contributing)
- [License](#license)

Expand Down Expand Up @@ -228,6 +229,48 @@ unpack and before `npm install`.
To use a different directory name, pass `patches_dir:` to
`prebuild-all.yml` / `prebuild.yml`.

## Versioning and releasing

Callers pin the reusable workflows to a **floating major tag**:

```yaml
uses: digidem/nodejs-mobile-bare-prebuilds/.github/workflows/prebuild-all.yml@v2
```

Internally, 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.

### Cutting a release

1. Merge to `main`. PRs touching `.github/**` or `test-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.
2. Tag a semver release and push it:

```sh
git tag v2.2.0 && git push origin v2.2.0
# or, with release notes:
gh release create v2.2.0 --generate-notes
```

3. The `move-major-tag.yml` workflow force-moves `v2` to the new tag
automatically. Callers on `@v2` pick 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.

## Contributing

We welcome contributions to this repository. If you have an idea for a new
Expand Down
39 changes: 28 additions & 11 deletions test-harness/android/run-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,21 @@ adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do slee
adb install -r -g "$APK"
adb logcat -c

# Launch the activity. The app pumps node's stdout/stderr to logcat tag
# NODEJS-MOBILE and emits __NODE_EXIT__:<code> when done.
adb shell am start -W -n "$APP_ID/.TestActivity"

# Run logcat as a coprocess so we can kill it explicitly once the sentinel
# is seen. (A plain `adb logcat | awk` pipeline hangs because adb only
# notices the pipe has closed when it next tries to write, and no further
# lines are coming once the app has exited.)
# lines are coming once the app has exited.) Started BEFORE the launch so
# no early NODEJS-MOBILE output is missed.
coproc LOGCAT { adb logcat -v raw -s NODEJS-MOBILE:V; }

# Launch the activity. The app pumps node's stdout/stderr to logcat tag
# NODEJS-MOBILE and emits __NODE_EXIT__:<code> when done. Fire-and-forget,
# `timeout`-bounded: `am start -W` blocks until the launch completes, which
# hangs forever on a wedged emulator (seen on flaky API 24 images) — and
# that hang is BEFORE the watch loop, so neither the loop's per-read
# timeout nor its SECONDS budget can bound it. Drop -W and cap the dispatch
# so a stuck launch falls through to the loop, which fails on the timeout.
timeout 60 adb shell am start -n "$APP_ID/.TestActivity" || true
# Snapshot the coproc's PID and read FD: bash UNSETS LOGCAT_PID and
# LOGCAT[0] as soon as it reaps the terminated coprocess, so reading
# them later can trip `set -u` (observed when the app exits quickly).
Expand All @@ -44,12 +50,23 @@ while (( SECONDS < TIMEOUT_SECONDS )); do
break
;;
esac
elif [ -z "$(adb shell pidof -s "$APP_ID" 2>/dev/null | tr -d '[:space:]')" ]; then
# Logcat went quiet AND the app process is gone: it crashed without
# emitting the sentinel (e.g. a native SIGSEGV in an addon). Fail
# now instead of waiting out the full TIMEOUT_SECONDS.
APP_DIED=1
break
else
# Logcat went quiet. Distinguish "app crashed" from "emulator wedged".
# `timeout` bounds the adb call so a hung emulator can't freeze this
# loop — without it the SECONDS budget (only re-checked at the loop
# top) never fires and the job stalls to its 45-min wall clock.
# `timeout` exits 124 specifically when it kills a hung adb; any other
# exit means adb answered (pidof itself exits non-zero when the
# process is absent, so we key off timeout's code, not adb's). The
# `|| rc=$?` form keeps `set -e` from exiting on that non-zero.
pid=$(timeout 15 adb shell pidof -s "$APP_ID" 2>/dev/null) && rc=0 || rc=$?
pid=${pid//[$' \t\r\n']/}
if [ "$rc" -ne 124 ] && [ -z "$pid" ]; then
# App process is gone without emitting the sentinel (e.g. a native
# SIGSEGV in an addon). Fail now instead of waiting out the timeout.
APP_DIED=1
break
fi
fi
done

Expand Down