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
2 changes: 1 addition & 1 deletion .github/actions/materialize/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ runs:
set -euo pipefail
UP="$RUNNER_TEMP/upstream-$BASE_TAG"
if [ ! -d "$UP/.git" ]; then
git clone --depth 1 --branch "$BASE_TAG" https://github.com/nodejs/node.git "$UP"
scripts/clone-upstream.sh https://github.com/nodejs/node.git "$BASE_TAG" "$UP"
fi
# file:// so git treats the shallow cache as a proper transport.
UPSTREAM_REPO="file://$UP" scripts/prepare.sh "$RUNNER_TEMP/matsrc"
Expand Down
26 changes: 26 additions & 0 deletions .github/actions/prepare-android-emulator/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Prepare the verified Android Emulator
description: >
Install a fixed, checksum-verified Google emulator archive and avoid redundant
stable updates. All other SDK installs still use the original SDK Manager.
runs:
using: composite
steps:
- name: Install emulator runtime dependency
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y libpulse0
- name: Read emulator archive pin
id: pin
shell: bash
run: |
source "$GITHUB_ACTION_PATH/pin.sh"
echo "archive=$RUNNER_TEMP/emulator-linux_x64-${EMULATOR_BUILD_ID}.zip" >> "$GITHUB_OUTPUT"
echo "key=android-emulator-linux-${EMULATOR_BUILD_ID}-${EMULATOR_SHA256}" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v6
with:
path: ${{ steps.pin.outputs.archive }}
key: ${{ steps.pin.outputs.key }}
- name: Install verified emulator and prepare SDK Manager
shell: bash
run: bash "$GITHUB_ACTION_PATH/install.sh"
70 changes: 70 additions & 0 deletions .github/actions/prepare-android-emulator/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
set -euo pipefail

: "${ANDROID_HOME:?ANDROID_HOME must identify the runner Android SDK}"
: "${RUNNER_TEMP:?RUNNER_TEMP must identify the runner temporary directory}"
ACTION_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
source "$ACTION_DIR/pin.sh"
SDK_MANAGER="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager"
ORIGINAL="$SDK_MANAGER.nodejs-mobile-original"
EMULATOR="$ANDROID_HOME/emulator/emulator"
ARCHIVE="$RUNNER_TEMP/emulator-linux_x64-${EMULATOR_BUILD_ID}.zip"
URL="https://dl.google.com/android/repository/emulator-linux_x64-${EMULATOR_BUILD_ID}.zip"
WORK=$(mktemp -d "$RUNNER_TEMP/prepare-emulator.XXXXXX")
trap 'rm -rf "$WORK"' EXIT

[ -x "$SDK_MANAGER" ] || { echo "::error::SDK Manager is missing" >&2; exit 1; }

if [ -e "$ORIGINAL" ]; then
cmp -s "$ACTION_DIR/sdkmanager-wrapper.sh" "$SDK_MANAGER" || {
echo "::error::SDK Manager was changed after emulator preparation" >&2
exit 1
}
fi

valid_archive() {
[ -f "$1" ] && [ "$(wc -c < "$1" | tr -d '[:space:]')" = "$EMULATOR_SIZE" ] &&
printf '%s %s\n' "$EMULATOR_SHA256" "$1" | sha256sum --check --status
}

# A truncated or poisoned cache is never installed. Downloads have bounded
# transport retries plus size/digest validation before extraction.
if ! valid_archive "$ARCHIVE"; then
for attempt in 1 2 3; do
echo "Downloading Android Emulator $EMULATOR_VERSION ($EMULATOR_BUILD_ID), attempt $attempt"
if curl --fail --location --silent --show-error --retry 2 --retry-all-errors \
--connect-timeout 20 --max-time 180 "$URL" -o "$WORK/emulator.zip" &&
valid_archive "$WORK/emulator.zip"; then
mv "$WORK/emulator.zip" "$ARCHIVE"
break
fi
echo "::warning::Emulator archive transfer or size/checksum verification failed"
if [ "$attempt" -lt 3 ]; then sleep 5; fi
done
fi
valid_archive "$ARCHIVE" || { echo "::error::No verified emulator archive available" >&2; exit 1; }
unzip -tq "$ARCHIVE" >/dev/null
unzip -q "$ARCHIVE" -d "$WORK"
grep -Fx "Pkg.Revision=$EMULATOR_VERSION" "$WORK/emulator/source.properties"
grep -Fx "Pkg.BuildId=$EMULATOR_BUILD_ID" "$WORK/emulator/source.properties"
VERSION=$("$WORK/emulator/emulator" -version 2>&1) || {
echo "::error::The verified Android Emulator cannot run: $VERSION" >&2
exit 1
}
[[ "$VERSION" == *"Android emulator version $EMULATOR_VERSION."* &&
"$VERSION" == *"build_id $EMULATOR_BUILD_ID"* ]] || {
echo "::error::The emulator binary does not match its reviewed version/build" >&2
exit 1
}
printf '%s\n' "$VERSION"
if [ -e "$ANDROID_HOME/emulator" ]; then
mv "$ANDROID_HOME/emulator" "$WORK/previous-emulator"
fi
mv "$WORK/emulator" "$ANDROID_HOME/emulator"

# The emulator action prepends this exact SDK bin directory to PATH, so a
# separate PATH shim would be shadowed. Keep the original beside its wrapper;
# its APP_HOME still resolves to the same command-line tools installation.
if [ ! -e "$ORIGINAL" ]; then mv "$SDK_MANAGER" "$ORIGINAL"; fi
cp "$ACTION_DIR/sdkmanager-wrapper.sh" "$SDK_MANAGER"
chmod +x "$SDK_MANAGER"
7 changes: 7 additions & 0 deletions .github/actions/prepare-android-emulator/pin.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Google repository2-3.xml stable Linux x64 archive, verified on 2026-10-13.
# Official size: 349654171; SHA-1: cd7362ea55dfb86a418958138dc396e74165dd01.
# SHA-256 below was computed after checking both the official size and SHA-1.
EMULATOR_VERSION=37.2.12
EMULATOR_BUILD_ID=16428233
EMULATOR_SIZE=349654171
EMULATOR_SHA256=b08fc43d8608d2955f607f1b287beb525041e730086bdca3af152accac3af9c1
24 changes: 24 additions & 0 deletions .github/actions/prepare-android-emulator/sdkmanager-wrapper.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail

ORIGINAL="${BASH_SOURCE[0]}.nodejs-mobile-original"

# Only intercept the emulator action's single stable-emulator update. Licenses,
# system images, build tools, NDK and mixed installs are forwarded unchanged.
if [ "$#" -eq 3 ] && [ "$1" = --install ] &&
[ "$2" = emulator ] && [ "$3" = --channel=0 ]; then
: "${ANDROID_HOME:?ANDROID_HOME must identify the runner Android SDK}"
VERSION=$("$ANDROID_HOME/emulator/emulator" -version 2>&1) || {
echo "::error::The prepared Android Emulator cannot run: $VERSION" >&2
exit 1
}
[[ "$VERSION" == *'Android emulator version '* ]] || {
echo "::error::The prepared Android Emulator did not report its version" >&2
exit 1
}
echo "::notice::Reusing checksum-verified Android Emulator; skipping redundant update download"
printf '%s\n' "$VERSION"
exit 0
fi

exec "$ORIGINAL" "$@"
46 changes: 46 additions & 0 deletions .github/actions/upload-test-log/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Upload diagnostic test logs
description: >
Upload test logs with one retry for transient artifact-service failures.
Logs are best effort; the test step remains the required pass/fail gate.
inputs:
name:
description: Artifact name
required: true
path:
description: Log paths, one per line
required: true
runs:
using: composite
steps:
- name: Upload logs
id: upload
continue-on-error: true
uses: actions/upload-artifact@v7
with:
name: ${{ inputs.name }}
path: ${{ inputs.path }}
if-no-files-found: ignore

- name: Wait before retrying the artifact service
if: always() && steps.upload.outcome == 'failure'
shell: bash
run: sleep 15

- name: Retry log upload
id: retry
if: always() && steps.upload.outcome == 'failure'
continue-on-error: true
uses: actions/upload-artifact@v7
with:
# A failed response can still leave an artifact on the service. A
# distinct name avoids conflicts without deleting the first upload.
name: ${{ inputs.name }}-retry
path: ${{ inputs.path }}
if-no-files-found: ignore

- name: Report unavailable log artifact
if: always() && steps.upload.outcome == 'failure' && steps.retry.outcome == 'failure'
shell: bash
run: |
echo '::warning::Test log upload failed twice; use the live job log and test summary. The test verdict is unchanged.'
echo 'Test log artifact unavailable after two upload attempts. See the live job log and the test summary above.' >> "$GITHUB_STEP_SUMMARY"
2 changes: 2 additions & 0 deletions .github/workflows/android-emulator-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ jobs:
# Boot the emulator, build + install + prime the testnode app, then run the
# curated subset through the proxy. Everything runs in one emulator-runner
# script so the device stays alive for the whole session.
- uses: ./.github/actions/prepare-android-emulator

- name: Run curated JS subset on emulator
uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0
with:
Expand Down
45 changes: 39 additions & 6 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,28 @@ permissions:
contents: read

jobs:
package-preview:
if: (github.ref == 'refs/heads/recipe' || (github.event_name == 'pull_request' && needs.upgrade-check.outputs.upgrade == 'true')) && needs.release-check.outputs.cold != 'true'
needs: [release-check, upgrade-check, ci-required, curated-tests-android, curated-tests-ios]
runs-on: ubuntu-24.04
permissions:
contents: read
actions: read
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with:
pattern: nodejs-mobile-*
path: preview-input
- name: Package versioned previews
run: python3 scripts/package-preview.py preview-input preview-packages
- uses: actions/upload-artifact@v7
with:
name: nodejs-mobile-${{ needs.release-check.outputs.version }}-preview
path: preview-packages
retention-days: 90
if-no-files-found: error

# Flavor matrices are inline expressions, not a helper job (a full VM boot
# to echo two constants earns nothing). Android always runs both flavors
# (ubuntu+KVM is 1x-billed and plentiful); iOS drops to full-only on
Expand Down Expand Up @@ -286,11 +308,11 @@ jobs:
path: out_android

build-ios:
needs: release-check
needs: [release-check, upgrade-check]
strategy:
fail-fast: false
matrix:
flavor: ${{ github.event_name == 'pull_request' && fromJSON('["full"]') || fromJSON('["full","lite"]') }}
flavor: ${{ github.event_name == 'pull_request' && needs.upgrade-check.outputs.upgrade != 'true' && fromJSON('["full"]') || fromJSON('["full","lite"]') }}
config:
# x86_64 (Intel Mac) simulator support was dropped for v24 — Intel
# Macs are EOL.
Expand Down Expand Up @@ -509,9 +531,9 @@ jobs:
strategy:
fail-fast: false
matrix:
flavor: ${{ github.event_name == 'pull_request' && fromJSON('["full"]') || fromJSON('["full","lite"]') }}
flavor: ${{ github.event_name == 'pull_request' && needs.upgrade-check.outputs.upgrade != 'true' && fromJSON('["full"]') || fromJSON('["full","lite"]') }}
runs-on: macos-26
needs: [build-ios]
needs: [build-ios, upgrade-check]
env:
NODEJS_MOBILE_FLAVOR: ${{ matrix.flavor }}
FLAVOR_SUFFIX: ${{ matrix.flavor == 'lite' && '-lite' || '' }}
Expand Down Expand Up @@ -609,6 +631,8 @@ jobs:
cp "$NDK/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/libc++_shared.so" \
"$PWD/libc++_shared.so"

- uses: ./.github/actions/prepare-android-emulator

- name: Run smoke on emulator
uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0
with:
Expand All @@ -627,11 +651,11 @@ jobs:
# Boot smoke for iOS: install the testnode app on a booted simulator and run a
# console.log via the freshly built NodeMobile.xcframework.
smoke-ios:
needs: [combine-ios]
needs: [combine-ios, upgrade-check]
strategy:
fail-fast: false
matrix:
flavor: ${{ github.event_name == 'pull_request' && fromJSON('["full"]') || fromJSON('["full","lite"]') }}
flavor: ${{ github.event_name == 'pull_request' && needs.upgrade-check.outputs.upgrade != 'true' && fromJSON('["full"]') || fromJSON('["full","lite"]') }}
runs-on: macos-26
env:
NODEJS_MOBILE_FLAVOR: ${{ matrix.flavor }}
Expand Down Expand Up @@ -1128,13 +1152,22 @@ jobs:
fetch-depth: 1

- name: Check the pinned upstream base is on this fork
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
# || true: grep exits 1 on a comments-only file, which under pipefail
# would kill the step before the message below ever prints.
BASE=$(grep -Ev '^\s*(#|$)' upstream-base.txt | head -n1 | tr -d '[:space:]' || true)
[ -n "$BASE" ] || { echo "::error::no base in upstream-base.txt"; exit 1; }

if [[ "$BASE" =~ ^[0-9a-f]{40}$ ]]; then
WANT=$(gh api "repos/nodejs/node/commits/${BASE}" --jq .sha)
[ "$WANT" = "$BASE" ] || { echo "::error::upstream snapshot SHA mismatch"; exit 1; }
echo "Verified upstream PR snapshot ${BASE}; no release ancestry branch required"
exit 0
fi

UPSTREAM=https://github.com/nodejs/node.git
# ^{} peels the annotated tag to the commit it points at; upstream
# release tags are annotated, so the unpeeled line is the tag object.
Expand Down
33 changes: 22 additions & 11 deletions .github/workflows/full-device-suite.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# The full device suite: everything test/parallel + test/sequential that
# .status does not skip, on an Android emulator and an iOS simulator, in 4
# round-robin shards per platform (test.py --run=n,4).
# .status does not skip, on an Android emulator and an iOS simulator, in
# round-robin shards (16 Android, 4 iOS).
#
# The curated gate (build.yml) is the fast PR allow-list; this job is the
# complement: a test upstream adds is picked up automatically, and excluding
Expand Down Expand Up @@ -97,10 +97,15 @@ jobs:
if: needs.precheck.outputs.skip != 'true' && inputs.platform != 'ios'
strategy:
fail-fast: false
# Keep in step with the ,4 in --run below: a matrix of a different
# length would drop or double-run tests rather than fail.
# Two Node 26.11 runs lost runners after ~470-560 app launches, with
# QEMU hangs and no failing test. Shorter emulator sessions retain all
# test coverage; bound concurrency to the previous four Android jobs.
max-parallel: 4
matrix:
shard: [0, 1, 2, 3]
shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
env:
# The denominator and matrix length are checked by the integrity gate.
ANDROID_SUITE_SHARDS: 16
runs-on: ubuntu-24.04
timeout-minutes: 90
steps:
Expand Down Expand Up @@ -143,31 +148,38 @@ jobs:
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- uses: ./.github/actions/prepare-android-emulator

- name: Run shard ${{ matrix.shard }} on emulator
uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0
with:
api-level: ${{ env.EMULATOR_API_LEVEL }}
target: default
arch: x86_64
ram-size: 2048M
heap-size: 256M
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: |
adb wait-for-device
./tools/mobile-test/android/prepare-android-test.sh
bash -o pipefail -c "./tools/test.py -j 1 --flaky-tests=skip --timeout=300 --arch android --run=${{ matrix.shard }},4 parallel sequential 2>&1 | tee shard.log"
./tools/mobile-test/android/run-suite-shard.sh ${{ matrix.shard }} "$ANDROID_SUITE_SHARDS"

- name: Summarize shard ${{ matrix.shard }}
if: always()
run: |
./tools/mobile-test/summarize-suite-run.sh shard.log "android shard ${{ matrix.shard }}"

- uses: actions/upload-artifact@v7
# Log transport must not change the device-test verdict. Binary uploads
# in Build remain required; only these diagnostic logs are best effort.
- uses: ./.github/actions/upload-test-log
if: always()
with:
name: full-suite-android-shard-${{ matrix.shard }}
path: shard.log
if-no-files-found: ignore
path: |
shard.log
shard-resources.log

ios:
needs: precheck
Expand Down Expand Up @@ -225,12 +237,11 @@ jobs:
run: |
./tools/mobile-test/summarize-suite-run.sh shard.log "ios shard ${{ matrix.shard }}"

- uses: actions/upload-artifact@v7
- uses: ./.github/actions/upload-test-log
if: always()
with:
name: full-suite-ios-shard-${{ matrix.shard }}
path: shard.log
if-no-files-found: ignore

- name: Shutdown simulator
if: always()
Expand Down
Loading
Loading