diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c494b94 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 }} diff --git a/.github/workflows/move-major-tag.yml b/.github/workflows/move-major-tag.yml new file mode 100644 index 0000000..d279f17 --- /dev/null +++ b/.github/workflows/move-major-tag.yml @@ -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})" diff --git a/.github/workflows/prebuild-all.yml b/.github/workflows/prebuild-all.yml index e3138e1..ccda34d 100644 --- a/.github/workflows/prebuild-all.yml +++ b/.github/workflows/prebuild-all.yml @@ -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 }} diff --git a/.github/workflows/prebuild.yml b/.github/workflows/prebuild.yml index 98ad3b4..f57bcf3 100644 --- a/.github/workflows/prebuild.yml +++ b/.github/workflows/prebuild.yml @@ -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 }} diff --git a/.github/workflows/test-android.yml b/.github/workflows/test-android.yml index 48c7872..ed36e96 100644 --- a/.github/workflows/test-android.yml +++ b/.github/workflows/test-android.yml @@ -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: | @@ -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: diff --git a/.github/workflows/test-ios.yml b/.github/workflows/test-ios.yml index 5c3e628..c816678 100644 --- a/.github/workflows/test-ios.yml +++ b/.github/workflows/test-ios.yml @@ -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: | @@ -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: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ce95272..c5521bb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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: @@ -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' }} @@ -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 }} diff --git a/README.md b/README.md index 069bd2a..6e62972 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 `@` 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 diff --git a/test-harness/android/run-test.sh b/test-harness/android/run-test.sh index a3ba333..27b4f4f 100755 --- a/test-harness/android/run-test.sh +++ b/test-harness/android/run-test.sh @@ -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__: 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__: 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). @@ -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