diff --git a/.github/actions/materialize/action.yml b/.github/actions/materialize/action.yml index 0ef4aef685..e7c3f2abf1 100644 --- a/.github/actions/materialize/action.yml +++ b/.github/actions/materialize/action.yml @@ -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" diff --git a/.github/actions/prepare-android-emulator/action.yml b/.github/actions/prepare-android-emulator/action.yml new file mode 100644 index 0000000000..6ac66f3503 --- /dev/null +++ b/.github/actions/prepare-android-emulator/action.yml @@ -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" diff --git a/.github/actions/prepare-android-emulator/install.sh b/.github/actions/prepare-android-emulator/install.sh new file mode 100644 index 0000000000..7f7e3023a3 --- /dev/null +++ b/.github/actions/prepare-android-emulator/install.sh @@ -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" diff --git a/.github/actions/prepare-android-emulator/pin.sh b/.github/actions/prepare-android-emulator/pin.sh new file mode 100644 index 0000000000..acdbe8d79e --- /dev/null +++ b/.github/actions/prepare-android-emulator/pin.sh @@ -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 diff --git a/.github/actions/prepare-android-emulator/sdkmanager-wrapper.sh b/.github/actions/prepare-android-emulator/sdkmanager-wrapper.sh new file mode 100644 index 0000000000..d5fee20717 --- /dev/null +++ b/.github/actions/prepare-android-emulator/sdkmanager-wrapper.sh @@ -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" "$@" diff --git a/.github/actions/upload-test-log/action.yml b/.github/actions/upload-test-log/action.yml new file mode 100644 index 0000000000..6415551702 --- /dev/null +++ b/.github/actions/upload-test-log/action.yml @@ -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" diff --git a/.github/workflows/android-emulator-tests.yml b/.github/workflows/android-emulator-tests.yml index da41073b18..e317b18954 100644 --- a/.github/workflows/android-emulator-tests.yml +++ b/.github/workflows/android-emulator-tests.yml @@ -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: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bd9ab3a8a0..61c7f1d778 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 @@ -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. @@ -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' || '' }} @@ -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: @@ -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 }} @@ -1128,6 +1152,8 @@ 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 @@ -1135,6 +1161,13 @@ jobs: 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. diff --git a/.github/workflows/full-device-suite.yml b/.github/workflows/full-device-suite.yml index 4650b83f9f..acc38984c7 100644 --- a/.github/workflows/full-device-suite.yml +++ b/.github/workflows/full-device-suite.yml @@ -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 @@ -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: @@ -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 @@ -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() diff --git a/.github/workflows/verify-patches.yml b/.github/workflows/verify-patches.yml index ea5128172b..0e54d1999e 100644 --- a/.github/workflows/verify-patches.yml +++ b/.github/workflows/verify-patches.yml @@ -84,6 +84,9 @@ jobs: - name: Reconstruct + verify run: scripts/prepare.sh out + - name: Check immutable upstream clones and cache transport + run: python3 scripts/test-upstream-clone.py + - uses: actions/setup-node@v6 with: node-version: '24' @@ -102,6 +105,23 @@ jobs: - name: Check manual prerelease version selection and guards run: python3 scripts/test-release-control.py + - name: Check Android full-suite coverage and failure propagation + run: python3 scripts/test-android-suite.py out + + - name: Check Intl assertions stay runnable without mobile subprocesses + run: python3 scripts/test-device-compat.py out + + - name: Check verified emulator preparation and SDK failure propagation + run: | + python3 -m pip install PyYAML + python3 scripts/test-emulator-preparation.py + + - name: Check iOS smoke launches and verdict failure propagation + run: python3 scripts/test-ios-smoke.py + + - name: Check Android startup recovery and authoritative test failures + run: python3 scripts/test-android-proxy.py + # The curated device-test list is the functional gate; validate it against # the materialized tree so an upstream bump can't silently shrink it. # tools/test.py ignores a path that matches nothing, so a deleted or @@ -278,7 +298,7 @@ jobs: run: | set -euo pipefail BASE=$(grep -Ev '^\s*(#|$)' upstream-base.txt | head -n1 | tr -d '[:space:]') - git clone --depth 1 --branch "$BASE" https://github.com/nodejs/node.git src + scripts/clone-upstream.sh https://github.com/nodejs/node.git "$BASE" src git -C src config user.name ci git -C src config user.email ci@invalid while IFS= read -r p; do diff --git a/README.md b/README.md index 109428ecdb..23948cffcd 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,18 @@ in-app, with npm modules and N-API native addons. | Android | `libnode.so` + headers | `arm64-v8a`, `armeabi-v7a`, `x86_64` (16 KB page size) | | iOS | `NodeMobile.xcframework` | `arm64` device, `arm64` simulator | -Current line: **Node.js 24** (`v24.21.0`). `process.version` reports the +Current line: **Node.js 24 LTS**, previewing the `v24.22.0` release proposal +[#66667](https://github.com/nodejs/node/pull/66667) at a fixed commit. The +official upstream tag is not published yet. `process.version` reports the upstream version unchanged, so version-parsing tools keep working; the -mobile build is identified by `process.versions.mobile` (e.g. `"24.21.0-0"`). +mobile build is identified by `process.versions.mobile` (`"24.22.0-0"`). +See [the 24.22 preview notes](docs/UPSTREAM-24.22.md) for source provenance. ## Maintained version lines | Branch | Node.js base | Purpose | | --- | --- | --- | -| [`recipe`](https://github.com/fogtape/nodejs-mobile/tree/recipe) | 24.21.0 | Stable Node 24 maintenance and releases | +| [`recipe`](https://github.com/fogtape/nodejs-mobile/tree/recipe) | 24.22.0 proposal | Node 24 LTS maintenance and CI previews | | [`recipe-v26`](https://github.com/fogtape/nodejs-mobile/tree/recipe-v26) | 26.1.0 | Independent Node 26 port and CI previews | Each branch owns its upstream tag, patches, source-tree checksum and toolchain. @@ -55,7 +58,7 @@ for building one — about 1.5 MB instead of a 1 GB fork: | | | |---|---| -| `upstream-base.txt` | the pinned upstream release tag (`v24.21.0`) | +| `upstream-base.txt` | the pinned upstream release tag or full proposal commit SHA | | `patches/` | per-concern patches to upstream files, plus `series` (apply order) and `files.map` (which patch owns which file) | | `mobile-src/` | files that have no upstream counterpart — build scripts, the iOS framework project, test apps and harness | | `expected-tree.txt` | the git tree hash the reconstruction must produce | diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 77d0c8a79f..7ee4dc0041 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,7 @@ +24.22.0-0 (preview)
24.21.0-0
24.20.0-0
24.19.0-2
@@ -39,8 +40,22 @@ + +## Unreleased, Version 24.22.0-0 (Current) + +### Notable Changes + +- **Engine:** preview Node.js 24.22.0 LTS release proposal [nodejs/node#66667](https://github.com/nodejs/node/pull/66667), pinned to `a5d0eb95dd03b90986ad1a7c0cd3f2de1a0f383c`. This is an upstream proposal snapshot, not an official Node.js release tag. +- **Lite:** retain the reviewed danmu ICU data profile, feature omissions, Android 64-bit pointer compression and ABI-preserving SDK symbol stripping. Both flavors keep the full ICU API; iOS keeps jitless V8 and polywasm without pointer compression. +- **Mobile compatibility:** use upstream's new `V8_ENABLE_WEBASSEMBLY` guard for JSPI and preserve the remaining platform patches. +- **Tests/builds:** add 26 in-process upstream regressions to the curated gate, triage new child-process/permission cases, and build both iOS flavors on upgrade PRs. Reuse the bounded Android emulator shards already validated on the Node 26 line. +- **Artifacts:** successful upgrade builds package Android/iOS full/lite previews with source metadata and `SHA256SUMS`. Ordinary builds do not publish a release. +- **Device regressions:** preserve Intl and SQLite backup in-process coverage while skipping only unsupported child probes; use the observed mobile filesystem error and exclude a native WASM flag test only on iOS. These three runnable regressions also join the curated gate. + +This source upgrade does not arm publication. + -## 2026-09-14, Version 24.21.0-0 (Current) +## 2026-09-14, Version 24.21.0-0 ### Notable Changes diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md index 6bec9ef6c2..bb4b17ea89 100644 --- a/docs/UPGRADING.md +++ b/docs/UPGRADING.md @@ -7,6 +7,24 @@ the patches are files, and the upgrade is an ordinary reviewable PR. Expect a handful of small conflicts resolvable in minutes — plus, occasionally, one that only the compile catches (see the warning below). +## Preview an upstream release proposal + +To build a release PR before its official tag exists, pin its full +40-character commit SHA in `upstream-base.txt`. `scripts/clone-upstream.sh` +supports both tags and immutable commits, including the CI source cache and +per-patch configure checks. It records the exact baseline as local Git +metadata for the test-edit audit without creating an upstream release tag. +Never follow a moving proposal branch. + +Update the mobile version header and CHANGELOG for the preview, regenerate +the patches and tree checksum, and open an upgrade PR against `recipe`. +The upgrade builds both Android/iOS flavors, runs the full device gates and +packages versioned CI previews. Cut release and publication require an +official upstream tag on this line. Once the tag exists, compare it against +the pinned proposal, update the pin, regenerate and rerun the gates. +See [the Node 24.22 proposal](UPSTREAM-24.22.md) and the current +[manual release procedure](RELEASING.md). + ## Re-base the series ```sh diff --git a/docs/UPSTREAM-24.22.md b/docs/UPSTREAM-24.22.md new file mode 100644 index 0000000000..359ca0b769 --- /dev/null +++ b/docs/UPSTREAM-24.22.md @@ -0,0 +1,204 @@ +# Node 24.22 LTS upstream PR preview + +This recipe previews the official Node.js release proposal +[#66667](https://github.com/nodejs/node/pull/66667), titled +`2026-10-14, Version 24.22.0 'Krypton' (LTS)`. On 2026-10-13 it is still a +draft PR, with no official `v24.22.0` tag. + +The reviewed baseline is pinned in `upstream-base.txt`: +`a5d0eb95dd03b90986ad1a7c0cd3f2de1a0f383c`. +The upstream header identifies Node 24.22.0 and the mobile header identifies +24.22.0-0. Those version strings identify the proposed source; they do not +mean that the upstream release has been published. The immutable commit and +`expected-tree.txt` identify the build if the proposal branch changes. + +## Lite configuration + +The upgrade retains the latest [danmu runtime contract](DANMU-LITE.md), +introduced on this line by PR #6. Both flavors use the full ICU API. +Lite uses the reviewed `danmu-lite.json` data profile with all Node web +encoding converters, selected English/Chinese/Cantonese/Japanese/Korean +locale families, Unicode normalization/IDNA, time zones and segmentation. +The profile and ICU source data are unchanged by this upstream upgrade. + +Lite still omits Amaro, Inspector, SQLite/Web Storage, SEA and native V8 +object-print helpers. Android keeps JIT/native WebAssembly and pointer +compression on 64-bit targets. Its SDK copy is stripped only after confirming +the dynamic exports remain identical. iOS keeps jitless V8 and polywasm, +without pointer compression. Node 26 feature choices and toolchains are +maintained separately. + +## Compatibility and build gates + +Upstream now defines `V8_ENABLE_WEBASSEMBLY` for Node targets and guards the +JSPI flag itself. The equivalent mobile-only guard is retired. Existing +mobile patches are rebased onto the proposal, and two new upstream workflow +files are removed from the generated product tree. + +Twenty-six new in-process crypto, histogram, filesystem, HTTP/TLS, utility, +SQLite and compression regressions enter the curated gate. The new +benchmark, WPT-helper and provider-snapshot tests launch a separate Node +process and are skipped on both mobile platforms. The new permission test +cannot write the harness verdict and is also skipped. The new glob test keeps +its in-process assertions and guards only the separate permission-model +child on mobile. These decisions follow the test code; the device suites +must confirm them. + +Existing Android debugger CLI tests and sequential watch CLI tests use the +same unsupported child-process path. Their exclusions carry forward the +corrections already validated on the Node 26 line. The emulator suite also +uses that line's 16 deterministic shards, four concurrent jobs, bounded +Gradle/emulator memory settings and failure-preserving log wrapper. These +changes preserve complete runnable-test coverage. + +The upgrade PR compiles all ten targets: Android arm/arm64/x86_64 and iOS +arm64 device/simulator, each in full/lite. Boot smokes verify the danmu ICU +contract. Curated and full device suites run on emulators/simulators. +Successful builds produce four `*-24.22.0-0-preview.zip` packages with +`BUILD-INFO.json` and `SHA256SUMS` in the Actions artifacts. This is a CI +preview and does not create a release tag or GitHub release. + +Local verification passed 34 regression tests: immutable clones/cache/CI +baseline checks (5), flavor/ICU/preview metadata (5), native symbol stripping +(1), manual release controls (17), and Android shard coverage/failure +propagation (6). The actual upstream test selector covered all 3,771 runnable +Android cases exactly once in 16 shards of 235–236 tests. All 236 curated +entries exist, patch ownership is consistent, the generated tree has no +upstream workflows, and the test-edit audit passed. + +A fresh shallow fetch applied all 20 regenerated patches and reconstructed +`b1927713596da90cceebd71afada3a6af42764eb`; regeneration from that fresh tree +preserved every patch and mobile source byte. Native cross-compilation and +device execution are verified separately by CI. + +All six Android GYP graphs also generated successfully (arm/arm64/x64, +full/lite), with assertions for the ICU API, WebAssembly, feature omissions +and Android-only 64-bit lite pointer compression. This configure-only check +uses Termux Clang and does not substitute for an NDK cross-compile. + +## First complete device sweep and corrections + +[Build run 38280763394](https://github.com/fogtape/nodejs-mobile/actions/runs/38280763394) +passed all ten platform/flavor builds, host checks, full/lite boot smokes and +Android full/lite plus iOS full curated tests. The full suites finished with +three failing Android tests and two failing iOS tests, representing four +unique cases. Every failing shard completed; there were no hung, crashed or +missing-verdict tests in those shard summaries. + +- Android `test-intl` spawned `process.execPath`, which is `app_process64` + in the embedder, and the child aborted with dalvik-cache permission errors. + Both mobile platforms now skip only its two child startup probes. All + in-process assertions remain; iOS's previous whole-test exclusion is removed. +- Android `test-sqlite-backup` gained a child-process subtest upstream. Only + that subtest is skipped on both mobile platforms; the other backup and + source-lifetime/GC regressions still run. The iOS simulator happened to allow + the spawn, but an iOS app cannot rely on it. +- Both platforms' `test-fs-rmSync-special-char-additional-error` returned + `ENOTEMPTY` rather than `EACCES` when removing a read-only directory. The + mobile branch now expects the observed error while retaining every path, + syscall, message and cleanup assertion. The unmodified failure and corrected + pass were reproduced on Termux's Android filesystem too. +- iOS `test-internal-webidl-buffer-source` failed before executing JS because + `--experimental-wasm-rab-integration` is compiled out of its jitless V8. + It is now listed in the existing iOS native-WebAssembly exclusion section; + Android and desktop still run it. + +Intl, SQLite backup and the filesystem error test also enter the curated gate, +which now contains 239 cases. An Intl execution regression verifies that both +mobile branches retain all 19 in-process assertions and launch no child Node, +while the desktop branch retains both locale-startup probes. The real status +parser verifies only the iOS WASM case is excluded and the other three tests +remain runnable on every platform. No runtime/build/ICU input changed, so +compiled-library cache keys remain identical to the completed build. + +The corrected source tree is +`3264ab4c31529014c7b4c5de198fdada43978027`. The next complete CI run verifies +these corrections on devices before the preview packages can be produced. + +## Emulator workflow preparation + +[Build run 38321165710](https://github.com/fogtape/nodejs-mobile/actions/runs/38321165710) +passed all ten builds, host checks, platform/flavor boot smokes, every curated +suite, all four iOS full-suite shards and 15 of the 16 Android shards. The +previous device-test failures were eliminated. Android shard 8 failed before +running any test: SDK Manager could not unpack its Emulator update +(`Error on ZipFile unknown archive`). Retrying that job reproduced the same +package-install failure, so the remaining red gates were infrastructure failures. + +All three Android emulator entry points now use +`.github/actions/prepare-android-emulator`. It installs the exact successful +Emulator 37.2.12 build 16428233 archive from Google's repository. The reviewed +archive size (349,654,171 bytes) and SHA-1 were verified against Google's +`repository2-3.xml`; SHA-256 is pinned alongside the build ID. Cached and newly +downloaded archives must match the size/digest and pass ZIP validation before +installation. Transfer/verification retries are bounded. The extracted binary +must report the pinned version/build before the test action can start. + +A narrowly scoped SDK Manager wrapper bypasses only the emulator action's +redundant `--install emulator --channel=0` call. Licenses, system images, +build/platform tools, NDK and other install requests are forwarded unchanged, +including their failure exit codes. The fixed archive is cached by its build +and digest, and a corrupt cache is rejected. Real test failures are not retried +or ignored. + +Seven local regressions cover a verified cache, corrupt downloads, exhausted +retries, unchanged SDK arguments/failures, repeated preparation, broken emulator +binaries and all three workflow entry points. This is a CI-only change: the +reviewed product tree, compiled-library keys and danmu lite recipe are unchanged. + +The preparation action explicitly installs `libpulse0` before validating the +emulator binary. Hosted runner images differ in their installed libraries; +the first CI validation found `libpulse.so.0` missing even though the pinned +archive's size, digest, ZIP contents and package metadata were correct. + +## iOS smoke launch transport + +[Build run 38334766961](https://github.com/fogtape/nodejs-mobile/actions/runs/38334766961) +passed all sixteen Android full-suite shards, both Android boot smokes and +curated flavors with the verified emulator preparation. The remaining iOS full +smoke wrote no output or verdict on three attempts using `simctl --console`; +iOS lite smoke and the same full framework's curated device suite passed. +The full iOS sweep was skipped because its smoke prerequisite failed. + +The smoke launcher now follows the already validated curated proxy: launch +without a console FIFO, read the app PID, poll the per-token verdict and read +stdout from the app's sandbox file. A real `FAIL` still fails immediately; +a missing verdict never passes and retries remain bounded. Four host regressions +exercise PASS, immediate FAIL, recovery after no verdict and exhausted retries. +No Node runtime, ICU or compilation input changed. The test harness update is +included in source tree `7dd2d6b6eaf7937b6a050e715754920dcbe4c8c3`. + +## Android per-test launch acknowledgement + +[Build run 38337972601](https://github.com/fogtape/nodejs-mobile/actions/runs/38337972601) +passed all ten builds, both platforms' full/lite boot smokes and curated gates, +all four iOS full-suite shards and fifteen Android shards. Android shard 15 +reported 234 passes and one timeout in `test-https-simple`. Its output contained +messages from an earlier TLS renegotiation test and an `am start` failure, +without a verdict from the requested test. The previous proxy ignored errors +when force-stopping the old activity, clearing logs and launching its replacement, +and discarded the launch command's diagnostic output. + +The proxy now bounds each ADB call, requires the old process to be stopped and +logs to be cleared before launch, and retains failed-launch diagnostics. +MainActivity writes a per-token startup acknowledgement before starting Node. +A launch may be retried only when a successful force-stop, process query and +sandbox query prove that Node never started. Missing/failed queries cannot +authorize a retry. An acknowledged JS failure, hang or native crash is never +retried; PASS still requires the authoritative per-token verdict file. + +Ten regressions exercise device targeting/output, failed force-stop, stale +activities, failed log clearing, recovery before Node starts, a lost launch +reply after Node starts, real FAIL/hang/crash, exhausted attempts and an unknown +or late startup acknowledgement. The full runnable suite and all its tests are +retained. The runtime, ICU profile and full/lite build configuration are unchanged. +The updated harness is included in source tree +`786da7b5d33a6cabb31668f3089731b3c0a62713`. + +## Move to the official release + +After upstream publishes `v24.22.0`, compare its commit with this baseline, +review any differences, update `upstream-base.txt`, regenerate the recipe and +checksum, and rerun platform/device validation. Follow +[RELEASING.md](RELEASING.md) for reviewed version selection and explicit +publication. A source upgrade or ordinary build does not publish by itself. diff --git a/expected-tree.txt b/expected-tree.txt index 5f15a93f81..1c24087e77 100644 --- a/expected-tree.txt +++ b/expected-tree.txt @@ -14,4 +14,4 @@ # run prepare.sh (or regenerate-patches.py, which prints the hash) and # update the value below in the same commit. If you forget, CI fails and # prints the hash it got. -35d63881b69e554edcf8ce74032671a8aaf805fb +786da7b5d33a6cabb31668f3089731b3c0a62713 diff --git a/mobile-src/src/node_mobile_version.h b/mobile-src/src/node_mobile_version.h index 7d8653b5ec..4c9c6a69fe 100644 --- a/mobile-src/src/node_mobile_version.h +++ b/mobile-src/src/node_mobile_version.h @@ -4,7 +4,7 @@ #include "node_version.h" #define NODE_MOBILE_MAJOR_VERSION 24 -#define NODE_MOBILE_MINOR_VERSION 21 +#define NODE_MOBILE_MINOR_VERSION 22 #define NODE_MOBILE_PATCH_VERSION 0 // Mobile rebuild revision of the same upstream Node version: the -R in the diff --git a/mobile-src/tools/mobile-test/android/node-android-proxy.sh b/mobile-src/tools/mobile-test/android/node-android-proxy.sh index f59515d033..047f0fa321 100755 --- a/mobile-src/tools/mobile-test/android/node-android-proxy.sh +++ b/mobile-src/tools/mobile-test/android/node-android-proxy.sh @@ -14,20 +14,24 @@ fi # Seconds to wait for a test to report a RESULT before treating it as failed, # so a hung test can't block the whole run. TIMEOUT="${NODE_ANDROID_PROXY_TIMEOUT:-120}" +START_TIMEOUT="${NODE_ANDROID_START_TIMEOUT:-10}" +LAUNCH_ATTEMPTS="${NODE_ANDROID_LAUNCH_ATTEMPTS:-3}" -# Kill the test app if it's running, clear the log so we only read this run, and -# drop any verdict files left by previous launches. The per-launch token already -# makes a stale file un-readable (different name), but clearing them keeps the -# sandbox from accumulating files across a 151-test run and removes any doubt. -# None of this is the verdict channel, so a transient adb hiccup here must not -# become a test FAIL via set -e: tolerate failure and let the verdict poll below -# be the only thing that decides. -# -# The force-stop and the cleanup share one round trip. Each adb invocation -# spawns a client and costs ~30 ms, which is pure overhead on every one of -# ~3,300 tests. `logcat -c` is a different adb subcommand, so it stays separate. -adb $TARGET shell "am force-stop nodejsmobile.test.testnode; run-as nodejsmobile.test.testnode sh -c 'rm -f files/result-*.txt'" 2>/dev/null || true -adb $TARGET logcat -c || true +# Bound individual transport calls too: a blocked ADB launch must not consume +# the upstream runner's entire per-test timeout before verdict polling starts. +adb_call() { timeout "${NODE_ANDROID_ADB_TIMEOUT:-15}" adb $TARGET "$@"; } + +emit_output() { + adb_call shell 'logcat -d -b main -v raw -s TestNode:E -s TestNode:I -s nodejs' \ + | sed '/^--------- beginning of/d' +} + +# A failed ADB query is unknown, never proof that the app is stopped or that +# JS has not started. pidof is provided by the Android images used by the suite. +app_state() { + adb_call shell 'pids=$(pidof nodejsmobile.test.testnode); rc=$?; if [ "$rc" = 0 ] && [ -n "$pids" ]; then echo RUNNING; elif [ "$rc" = 1 ]; then echo STOPPED; else exit 2; fi' \ + 2>/dev/null | tr -d '\r\n' +} TEST_PATH="$( cd "$( dirname "$0" )" && cd .. && cd .. && cd test && pwd )" @@ -42,28 +46,70 @@ ARGS="$*" RUN_TOKEN="$(LC_ALL=C tr -dc 'a-f0-9' /dev/null | head -c 16)" [ -n "$RUN_TOKEN" ] || RUN_TOKEN="${RANDOM}${RANDOM}$$" -# Start the test app, passing the test filename, the host dir to substitute, and -# the per-launch token (its own extra so it never enters the test's process.argv). -# -W makes `am start` block until the activity is idle, so the launch can't -# coalesce into the just-force-stopped process still tearing down (which would -# leave node un-started -> a spurious no-verdict timeout). +# MainActivity creates this file BEFORE starting the Node thread. Its absence +# can authorize another launch only after a confirmed force-stop and a +# successful sandbox query. A lost ADB response cannot authorize a test retry. +started_state() { + adb_call shell "run-as nodejsmobile.test.testnode sh -c 'if [ -f files/started-${RUN_TOKEN}.txt ]; then echo STARTED; else echo NOT_STARTED; fi'" \ + 2>/dev/null | tr -d '\r\n' +} + ADB_START_COMMAND='am start -W -n nodejsmobile.test.testnode/nodejsmobile.test.testnode.MainActivity -e "nodeargs" "'$ARGS'" -e "substitutedir" "'$TEST_PATH'" -e "runtoken" "'$RUN_TOKEN'" ' -adb $TARGET shell "$ADB_START_COMMAND" > /dev/null \ - || echo "::warning::node-android-proxy: am start failed for: $ARGS" >&2 +started="" +for ((attempt=1; attempt<=LAUNCH_ATTEMPTS; attempt++)); do + # Never ignore a failed force-stop or log clear: that previously allowed a + # stale activity/output to stand in for a new test and burn the verdict wait. + if ! preparation=$(adb_call shell "am force-stop nodejsmobile.test.testnode && run-as nodejsmobile.test.testnode sh -c 'rm -f files/result-*.txt files/started-*.txt'" 2>&1) || + [ "$(app_state || true)" != STOPPED ] || + ! preparation=$(adb_call logcat -c 2>&1); then + echo "::warning::node-android-proxy: preparation failed (attempt $attempt/$LAUNCH_ATTEMPTS) for: $ARGS" >&2 + echo "$preparation" >&2 + sleep 0.1 + continue + fi + + # Preserve the actual launch diagnostics, including stdout from am. ADB can + # lose its reply after the app has started; the token, rather than the command + # exit status, decides whether this test has begun. + if ! launch_output=$(adb_call shell "$ADB_START_COMMAND" 2>&1); then + echo "::warning::node-android-proxy: am start failed (attempt $attempt/$LAUNCH_ATTEMPTS) for: $ARGS" >&2 + echo "$launch_output" >&2 + fi + start_deadline=$((SECONDS + START_TIMEOUT)) + while (( SECONDS < start_deadline )); do + if [ "$(started_state || true)" = STARTED ]; then + started=1 + break + fi + sleep 0.1 + done + [ -z "$started" ] || break + + echo "::warning::node-android-proxy: no startup acknowledgement (attempt $attempt/$LAUNCH_ATTEMPTS) for: $ARGS" >&2 + echo "$launch_output" >&2 + adb_call shell 'logcat -d -b main -v brief -s AndroidRuntime:E -s ActivityManager:W' >&2 || true + # Stop first to close the race with an app creating its marker after our last + # poll. If it ever acknowledged this launch, JS may have run: fail, no retry. + if ! adb_call shell 'am force-stop nodejsmobile.test.testnode' >&2 || + [ "$(app_state || true)" != STOPPED ] || + [ "$(started_state || true)" != NOT_STARTED ]; then + echo "::error::node-android-proxy: cannot confirm test never started; refusing relaunch for: $ARGS" >&2 + emit_output || true + exit 1 + fi +done +if [ -z "$started" ]; then + echo "::error::node-android-proxy: app did not start test after $LAUNCH_ATTEMPTS attempts for: $ARGS" >&2 + emit_output || true + exit 1 +fi # Is the app process still up? Empty `pidof` output = gone. The app never exits # on its own (MainActivity stays after the node thread returns), so "gone" # means the process died: a native crash, an abort, or a system kill. app_alive() { - [ -n "$(adb $TARGET shell 'pidof nodejsmobile.test.testnode' 2>/dev/null | tr -d '\r\n' || true)" ] + [ "$(app_state || true)" != STOPPED ] } -# Calibrate once, now, while `am start -W` guarantees the process is up: if -# pidof can't see it here (missing on an older device, or a renamed process), -# the probe is unusable and the poll below must not read its empty output as a -# dead process — that would FAIL every test instantly. Fall back to the plain -# timeout poll instead. -liveness=1 -app_alive || liveness="" # The app writes the test's real exit code to a per-launch file in its sandbox # (filesDir/result-.txt). Read it via run-as — a durable, private channel @@ -89,7 +135,7 @@ waited=0 ticks=0 gone="" while :; do - verdict=$(adb $TARGET shell "run-as nodejsmobile.test.testnode cat ${RESULT_REL} 2>/dev/null" 2>/dev/null | tr -d '\r\n' || true) + verdict=$(adb_call shell "run-as nodejsmobile.test.testnode cat ${RESULT_REL} 2>/dev/null" 2>/dev/null | tr -d '\r\n' || true) case "$verdict" in PASS) RESULT=0; break ;; FAIL) RESULT=1; break ;; @@ -97,7 +143,7 @@ while :; do # Re-read once after the process died — it may have written the verdict and # exited between the two probes — then stop. if [ -n "$gone" ]; then break; fi - if [ -n "$liveness" ] && [ $((ticks % POLL_HZ)) -eq 0 ] && ! app_alive; then + if [ $((ticks % POLL_HZ)) -eq 0 ] && ! app_alive; then gone=1 continue fi @@ -119,7 +165,6 @@ esac # Echo the raw stdout/stderr for test.py's .out comparison. The verdict no longer # rides this stream, so there is nothing to strip. -adb $TARGET shell 'logcat -d -b main -v raw -s TestNode:E -s TestNode:I -s nodejs' \ - | sed '/^--------- beginning of/d' +emit_output exit $RESULT diff --git a/mobile-src/tools/mobile-test/android/prepare-android-test.sh b/mobile-src/tools/mobile-test/android/prepare-android-test.sh index 121188c075..14c9b75f8c 100755 --- a/mobile-src/tools/mobile-test/android/prepare-android-test.sh +++ b/mobile-src/tools/mobile-test/android/prepare-android-test.sh @@ -23,8 +23,9 @@ unlink "$NODEJS_BASE_DIR/test/fixtures/wasi/subdir/loop2" unlink "$NODEJS_BASE_DIR/test/fixtures/wasi/subdir/outside.txt" set -e -# Build the Android test app -( cd "$TEST_APP_BASE_DIR" && ./gradlew assembleDebug ) +# Build once without retaining a Gradle JVM during the emulator test sweep. +# The daemon can reserve 1.5 GB after APK assembly, alongside QEMU and the app. +( cd "$TEST_APP_BASE_DIR" && ./gradlew --no-daemon --max-workers=2 assembleDebug ) # Copy the Android proxy to the target directory. cp "$SCRIPT_BASE_DIR/node-android-proxy.sh" "$TEST_PROXY_TARGETDIR/node" diff --git a/mobile-src/tools/mobile-test/android/run-suite-shard.sh b/mobile-src/tools/mobile-test/android/run-suite-shard.sh new file mode 100755 index 0000000000..6057a83fec --- /dev/null +++ b/mobile-src/tools/mobile-test/android/run-suite-shard.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Keep the test verdict authoritative and publish runner resource diagnostics +# while the suite is running: a runner shutdown cannot upload post-step logs. +set -euo pipefail + +SHARD="${1:?usage: run-suite-shard.sh }" +SHARDS="${2:?usage: run-suite-shard.sh }" +if ! [[ "$SHARD" =~ ^[0-9]+$ && "$SHARDS" =~ ^[1-9][0-9]*$ ]] || + (( SHARD >= SHARDS )); then + echo "invalid Android shard $SHARD/$SHARDS" >&2 + exit 2 +fi + +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +cd "$ROOT" + +resource_snapshot() { + echo "Android shard $SHARD/$SHARDS resources at $(date -u +%FT%TZ)" + free -m || true + df -h . "${ANDROID_AVD_HOME:-${HOME}/.android/avd}" 2>/dev/null || true + ps -eo pid,comm,rss --sort=-rss | head -n 12 || true + for metric in /proc/pressure/memory /sys/fs/cgroup/memory.events; do + if [ -r "$metric" ]; then + echo "$metric" + cat "$metric" || true + fi + done + timeout 5 adb shell 'cat /proc/meminfo; df -h /data' 2>/dev/null || true +} + +report_resources() { + resource_snapshot | tee -a shard-resources.log +} + +MONITOR_PID="" +cleanup() { + if [ -n "$MONITOR_PID" ]; then + # Stop both the loop and its pending sleep/adb query. + pkill -P "$MONITOR_PID" 2>/dev/null || true + kill "$MONITOR_PID" 2>/dev/null || true + wait "$MONITOR_PID" 2>/dev/null || true + fi + report_resources +} +trap cleanup EXIT + +report_resources +( while sleep 60; do report_resources; done ) & +MONITOR_PID=$! + +# pipefail preserves test.py's failure; metrics and tee cannot turn a failed +# test into a successful job. Each deterministic shard runs once, no retries. +./tools/test.py -j 1 --flaky-tests=skip --timeout=300 --arch android \ + "--run=$SHARD,$SHARDS" parallel sequential 2>&1 | tee shard.log diff --git a/mobile-src/tools/mobile-test/android/testnode/app/src/main/java/nodejsmobile/test/testnode/MainActivity.java b/mobile-src/tools/mobile-test/android/testnode/app/src/main/java/nodejsmobile/test/testnode/MainActivity.java index 4ba984b31c..e63ed3fd0e 100644 --- a/mobile-src/tools/mobile-test/android/testnode/app/src/main/java/nodejsmobile/test/testnode/MainActivity.java +++ b/mobile-src/tools/mobile-test/android/testnode/app/src/main/java/nodejsmobile/test/testnode/MainActivity.java @@ -110,6 +110,14 @@ public void uncaughtException(Thread t, Throwable e) { } catch (IOException ignored) {} } }); + // The proxy may retry a failed launch only if this per-token marker + // is absent after force-stop. Persist it before any Node code can run, + // so a JS failure, native crash or hang is never retried as transport. + try { + new File(testFolderPath + "/started-" + runToken + ".txt").createNewFile(); + } catch (IOException e) { + throw new IllegalStateException("Cannot acknowledge Node test startup", e); + } mainNodeThread.start(); } diff --git a/mobile-src/tools/mobile-test/audit-test-edits.sh b/mobile-src/tools/mobile-test/audit-test-edits.sh index 2924db7e25..0145caff43 100755 --- a/mobile-src/tools/mobile-test/audit-test-edits.sh +++ b/mobile-src/tools/mobile-test/audit-test-edits.sh @@ -18,6 +18,9 @@ SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) REPO_ROOT=$(git -C "${SCRIPT_DIR}" rev-parse --show-toplevel) cd "${REPO_ROOT}" +# prepare.sh records the exact upstream commit as local git metadata, so +# unreleased PR snapshots can be audited without a fabricated release tag. +# For older materializations, derive the tag from src/node_version.h. # The upstream base is derived from the tree itself: src/node_version.h is # upstream's own version header and is not patched by this project, so it # states exactly which nodejs/node release this tree was built from. That @@ -28,6 +31,9 @@ MA=$(grep -oE '#define NODE_MAJOR_VERSION [0-9]+' src/node_version.h | awk '{pri MI=$(grep -oE '#define NODE_MINOR_VERSION [0-9]+' src/node_version.h | awk '{print $3}') PA=$(grep -oE '#define NODE_PATCH_VERSION [0-9]+' src/node_version.h | awk '{print $3}') BASE="v${MA}.${MI}.${PA}" +if git rev-parse --verify --quiet 'refs/nodejs-mobile/upstream-base^{commit}' >/dev/null; then + BASE=refs/nodejs-mobile/upstream-base +fi if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then echo "error: upstream base '${BASE}' (from src/node_version.h) is not a commit here." >&2 echo " This audit needs the upstream tag present, as it is in a tree" >&2 diff --git a/mobile-src/tools/mobile-test/curated-device-tests.txt b/mobile-src/tools/mobile-test/curated-device-tests.txt index 9ac0b14000..3a50901839 100644 --- a/mobile-src/tools/mobile-test/curated-device-tests.txt +++ b/mobile-src/tools/mobile-test/curated-device-tests.txt @@ -27,6 +27,9 @@ parallel/test-compression-decompression-stream parallel/test-crypto parallel/test-crypto-argon2-unsupported parallel/test-crypto-boringssl-evp-list +parallel/test-crypto-cipherbase-options-fast-path +parallel/test-crypto-cipheriv-cbc-cts +parallel/test-crypto-cipheriv-xts parallel/test-crypto-default-shake-lengths parallel/test-crypto-dh-constructor parallel/test-crypto-dh-group-setters @@ -35,7 +38,18 @@ parallel/test-crypto-domain parallel/test-crypto-eddsa-variants parallel/test-crypto-gcm-explicit-short-tag parallel/test-crypto-hash-stream-pipe +parallel/test-crypto-jwk-raw-validation +parallel/test-crypto-key-encoding-provider-cipher +parallel/test-crypto-key-objects-ec-jwk-private parallel/test-crypto-key-objects-messageport +parallel/test-crypto-keygen-ec-group-names +parallel/test-crypto-mac +parallel/test-crypto-mac-errors +parallel/test-crypto-mac-vectors +parallel/test-crypto-pkcs12 +parallel/test-crypto-rsa-oaep-mgf1 +parallel/test-crypto-rsa-pss-parameters +parallel/test-crypto-x509-clone-transfer parallel/test-dgram-abort-closed parallel/test-dgram-bind-default-address parallel/test-dgram-bind-socket-close-before-lookup @@ -78,9 +92,12 @@ parallel/test-fs-cp-sync-copy-symlink-over-file-error parallel/test-fs-cp-sync-dest-name-prefix-match parallel/test-fs-cp-sync-mode-flags parallel/test-fs-cp-sync-preserve-timestamps +parallel/test-fs-glob-no-follow-symlink-globstar +parallel/test-fs-rmSync-special-char-additional-error parallel/test-http parallel/test-http-agent-keep-alive-timeout-buffer parallel/test-http-bind-twice +parallel/test-http-client-abort-completed-keepalive parallel/test-http-client-abort-keep-alive-queued-unix-socket parallel/test-http-client-abort-unix-socket parallel/test-http-client-agent @@ -90,6 +107,7 @@ parallel/test-http-conn-reset parallel/test-http-unix-socket parallel/test-http-unix-socket-keep-alive parallel/test-http2-allow-http1 +parallel/test-http2-bidirectional-write-deadlock parallel/test-http2-client-port-80 parallel/test-http2-client-upload-reject parallel/test-http2-compat-serverrequest-trailers @@ -102,6 +120,8 @@ parallel/test-https-client-checkServerIdentity parallel/test-https-close parallel/test-https-host-headers parallel/test-https-unix-socket-self-signed +parallel/test-intl +parallel/test-mark-promise-handled parallel/test-mobile-credentials parallel/test-mobile-fetch parallel/test-mobile-node-path @@ -137,6 +157,8 @@ parallel/test-os-process-priority parallel/test-path parallel/test-path-glob parallel/test-path-normalize +parallel/test-perf-hooks-histogram-qrde +parallel/test-perf-hooks-sliding-window-histogram parallel/test-pipe-address parallel/test-pipe-stream parallel/test-pipe-writev @@ -155,6 +177,8 @@ parallel/test-process-env-windows-error-reset parallel/test-process-initgroups parallel/test-querystring parallel/test-querystring-maxKeys-non-finite +parallel/test-sqlite-backup +parallel/test-sqlite-udf-close parallel/test-stream-add-abort-signal parallel/test-stream-construct parallel/test-stream-duplex-readable-end @@ -162,6 +186,7 @@ parallel/test-stream-finished parallel/test-stream-pipe-await-drain parallel/test-stream-pipe-manual-resume parallel/test-stream-pipe-unpipe-streams +parallel/test-stream-readable-unobserved-eof-readable parallel/test-string-decoder parallel/test-string-decoder-end parallel/test-timers @@ -188,12 +213,15 @@ parallel/test-tls-destroy-stream parallel/test-tls-error-servername parallel/test-tls-generic-stream parallel/test-tls-getcipher +parallel/test-tls-peer-certificate-repeated-reads parallel/test-url-domain-ascii-unicode parallel/test-url-format-invalid-input parallel/test-url-is-url-internal parallel/test-util +parallel/test-util-debounce parallel/test-util-getcallsites-preparestacktrace parallel/test-util-inspect-proxy +parallel/test-util-throttle parallel/test-vm-access-process-env parallel/test-vm-create-and-run-in-context parallel/test-vm-deleting-property @@ -220,3 +248,4 @@ parallel/test-zlib-convenience-methods parallel/test-zlib-destroy-pipe parallel/test-zlib-flush-drain parallel/test-zlib-invalid-arg-value-brotli-compress +parallel/test-zlib-zstd-reset diff --git a/mobile-src/tools/mobile-test/smoke/run-ios-smoke.sh b/mobile-src/tools/mobile-test/smoke/run-ios-smoke.sh index 091bf3e05f..db41b9d381 100755 --- a/mobile-src/tools/mobile-test/smoke/run-ios-smoke.sh +++ b/mobile-src/tools/mobile-test/smoke/run-ios-smoke.sh @@ -37,9 +37,9 @@ console.log('${MARKER} ' + process.version + ' ' + process.platform + ' ' + proc # Launch once; echo the app output to stderr (for log visibility) and print the # verdict (PASS | FAIL | "") to stdout for the caller to capture. run_once() { - local log token docs result lp verdict="" i + local log token docs result stdout app_pid verdict="" i log="$(mktemp)" - token="$(/usr/bin/uuidgen | tr 'A-F' 'a-f' | tr -d '-')" + token="$(uuidgen | tr 'A-F' 'a-f' | tr -d '-')" xcrun simctl install "$UDID" "$APP" >/dev/null 2>&1 # Decide pass/fail from the durable verdict file NodeRunner writes @@ -55,26 +55,34 @@ run_once() { return 0 fi result="$docs/Documents/result-${token}.txt" - rm -f "$result" + stdout="$docs/Documents/stdout-${token}.txt" + rm -f "$result" "$stdout" - xcrun simctl launch --console --terminate-running-process "$UDID" "$BUNDLE_ID" \ - --run-token "$token" -e "$EXPR" >| "$log" 2>&1 & - lp=$! + # The app already redirects stdout/stderr to its per-token sandbox file. + # Use the same launch path as the curated proxy: simctl returns the app PID + # without establishing a console FIFO that can stall before Node starts. + xcrun simctl launch --terminate-running-process "$UDID" "$BUNDLE_ID" \ + --run-token "$token" -e "$EXPR" >| "$log" 2>&1 + app_pid="$(sed -nE 's/^.*: ([0-9]+)$/\1/p' "$log" | tail -1)" for ((i=0; i/dev/null || { [ -f "$result" ] && verdict=$(tr -d '\r\n' < "$result"); break; } + if [ -n "$app_pid" ] && ! kill -0 "$app_pid" 2>/dev/null; then + [ -f "$result" ] && verdict=$(tr -d '\r\n' < "$result") + break + fi + if [ -z "$app_pid" ]; then break; fi sleep 1 done - kill "$lp" 2>/dev/null || true - wait "$lp" 2>/dev/null || true + xcrun simctl terminate "$UDID" "$BUNDLE_ID" >/dev/null 2>&1 || true echo "----- app output -----" >&2 + [ -f "$stdout" ] && cat "$stdout" >&2 cat "$log" >&2 echo "----------------------" >&2 - rm -f "$result" "$log" + rm -f "$result" "$stdout" "$log" printf '%s' "$verdict" } diff --git a/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch b/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch index a0ac776fd1..1e7a571f4a 100644 --- a/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch +++ b/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch @@ -138,10 +138,10 @@ index 5cea0393f4..6430bee810 100644 - os.system("./configure --dest-cpu=" + DEST_CPU + " --dest-os=android --openssl-no-asm --cross-compiling") + os.system("./configure --dest-cpu=" + DEST_CPU + " --dest-os=android --openssl-no-asm --with-intl=" + intl + extra_flags + " --cross-compiling --shared") diff --git a/configure.py b/configure.py -index 3c3b5a3492..aef22790bc 100755 +index b826e24e3f..65e3255d10 100755 --- a/configure.py +++ b/configure.py -@@ -1146,6 +1146,11 @@ parser.add_argument('-C', +@@ -1119,6 +1119,11 @@ parser.add_argument('-C', default=None, help=argparse.SUPPRESS) @@ -153,7 +153,7 @@ index 3c3b5a3492..aef22790bc 100755 parser.add_argument('--clang-cl', action='store', dest='clang_cl', -@@ -1655,6 +1660,9 @@ def configure_node_cctest_sources(o): +@@ -1628,6 +1633,9 @@ def configure_node_cctest_sources(o): SearchFiles('test/cctest', 'h') def configure_node(o): diff --git a/patches/build-mobile-platform-settings-in-common.gypi.patch b/patches/build-mobile-platform-settings-in-common.gypi.patch index a3db68104d..7fbc34d3b8 100644 --- a/patches/build-mobile-platform-settings-in-common.gypi.patch +++ b/patches/build-mobile-platform-settings-in-common.gypi.patch @@ -23,10 +23,10 @@ Co-authored-by: João Matos 1 file changed, 118 insertions(+), 7 deletions(-) diff --git a/common.gypi b/common.gypi -index 83e0691d4a..dc33231a0d 100644 +index bc690955d3..b3a74cb587 100644 --- a/common.gypi +++ b/common.gypi -@@ -112,7 +112,7 @@ +@@ -115,7 +115,7 @@ 'obj_dir%': '<(PRODUCT_DIR)/obj.target', 'v8_base': '<(PRODUCT_DIR)/obj.target/tools/v8_gypfiles/libv8_snapshot.a', }], @@ -35,7 +35,7 @@ index 83e0691d4a..dc33231a0d 100644 'obj_dir%': '<(PRODUCT_DIR)/obj.target', 'v8_base': '<(PRODUCT_DIR)/libv8_snapshot.a', }], -@@ -223,6 +223,23 @@ +@@ -226,6 +226,23 @@ }], ], }], @@ -59,7 +59,7 @@ index 83e0691d4a..dc33231a0d 100644 ['OS=="solaris"', { # pull in V8's postmortem metadata 'ldflags': [ '-Wl,-z,allextract' ] -@@ -231,7 +248,7 @@ +@@ -234,7 +251,7 @@ # increase performance, number from experimentation 'cflags': [ '-qINLINE=::150:100000' ] }], @@ -68,7 +68,7 @@ index 83e0691d4a..dc33231a0d 100644 # -fno-omit-frame-pointer is necessary for the --perf_basic_prof # flag to work correctly. perf(1) gets confused about JS stack # frames otherwise, even with --call-graph dwarf. -@@ -450,7 +467,7 @@ +@@ -453,7 +470,7 @@ [ 'target_arch=="arm64"', { 'msvs_configuration_platform': 'arm64', }], @@ -77,7 +77,7 @@ index 83e0691d4a..dc33231a0d 100644 'cflags+': [ '-fno-omit-frame-pointer', '-fsanitize=address', -@@ -478,7 +495,7 @@ +@@ -481,7 +498,7 @@ }], ], }], @@ -86,7 +86,7 @@ index 83e0691d4a..dc33231a0d 100644 'cflags+': [ '-fno-omit-frame-pointer', '-fsanitize=undefined', -@@ -487,7 +504,7 @@ +@@ -490,7 +507,7 @@ 'cflags!': [ '-fno-omit-frame-pointer' ], 'ldflags': [ '-fsanitize=undefined' ], }], @@ -95,7 +95,7 @@ index 83e0691d4a..dc33231a0d 100644 'xcode_settings': { 'OTHER_CFLAGS+': [ '-fno-omit-frame-pointer', -@@ -595,11 +612,13 @@ +@@ -601,11 +618,13 @@ }], ['_toolset=="host"', { 'conditions': [ @@ -111,7 +111,7 @@ index 83e0691d4a..dc33231a0d 100644 'cflags': [ '-m64' ], 'ldflags': [ '-m64' ], }], -@@ -759,6 +778,98 @@ +@@ -773,6 +792,98 @@ }], ], }], diff --git a/patches/build-mobile-targets-in-node.gyp-node.gypi.patch b/patches/build-mobile-targets-in-node.gyp-node.gypi.patch index 3e377b67ab..15a9c503d0 100644 --- a/patches/build-mobile-targets-in-node.gyp-node.gypi.patch +++ b/patches/build-mobile-targets-in-node.gyp-node.gypi.patch @@ -21,7 +21,7 @@ Co-authored-by: Julia Samól 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/node.gyp b/node.gyp -index 988838e858..66fea75c62 100644 +index 62c748b130..b6f06f9d44 100644 --- a/node.gyp +++ b/node.gyp @@ -68,6 +68,11 @@ @@ -46,7 +46,7 @@ index 988838e858..66fea75c62 100644 'target_arch=="x64" and ' 'node_target_type=="executable"', { 'defines': [ 'NODE_ENABLE_LARGE_CODE_PAGES=1' ], -@@ -1407,8 +1413,9 @@ +@@ -1422,8 +1428,9 @@ ['OS=="solaris"', { 'ldflags': [ '-I<(SHARED_INTERMEDIATE_DIR)' ] }], @@ -58,7 +58,7 @@ index 988838e858..66fea75c62 100644 'type': 'none', }], [ 'node_shared=="true"', { -@@ -1629,7 +1636,8 @@ +@@ -1581,7 +1588,8 @@ [ 'node_shared_libuv=="false"', { 'dependencies': [ 'deps/uv/uv.gyp:libuv#host' ], }], diff --git a/patches/build-v8-mobile-host-target-settings-in-v8-gypfiles.patch b/patches/build-v8-mobile-host-target-settings-in-v8-gypfiles.patch index 92c0c6032b..fd43fd6b46 100644 --- a/patches/build-v8-mobile-host-target-settings-in-v8-gypfiles.patch +++ b/patches/build-v8-mobile-host-target-settings-in-v8-gypfiles.patch @@ -46,7 +46,7 @@ index e614c39a6b..711428f898 100644 'configurations': { 'Debug': { diff --git a/tools/v8_gypfiles/v8.gyp b/tools/v8_gypfiles/v8.gyp -index f90a29d102..b814fee544 100644 +index 370001847e..3f1d3ee860 100644 --- a/tools/v8_gypfiles/v8.gyp +++ b/tools/v8_gypfiles/v8.gyp @@ -868,6 +868,15 @@ diff --git a/patches/ci-remove-upstream-only-workflows-and-config.patch b/patches/ci-remove-upstream-only-workflows-and-config.patch index 0a8d9b2113..df304c708b 100644 --- a/patches/ci-remove-upstream-only-workflows-and-config.patch +++ b/patches/ci-remove-upstream-only-workflows-and-config.patch @@ -23,14 +23,15 @@ is read only from the default branch and is left untouched. .github/ISSUE_TEMPLATE/config.yml | 14 - .github/SUPPORT.md | 27 -- .github/label-pr-config.yml | 224 ------------ + .github/workflows/author-ready-conflicts.yml | 48 --- .github/workflows/auto-start-ci.yml | 78 ----- .github/workflows/build-tarball.yml | 96 ------ .github/workflows/close-stalled.yml | 36 -- - .github/workflows/codeql.yml | 42 --- + .github/workflows/codeql.yml | 44 --- .github/workflows/comment-labeled.yml | 57 ---- - .../commit-lint-problem-matcher.json | 13 - - .github/workflows/commit-lint.yml | 35 -- - .github/workflows/commit-queue.yml | 173 ---------- + .github/workflows/commit-lint.yml | 56 --- + .github/workflows/commit-queue.yml | 201 ----------- + .github/workflows/contributor-guidance.yml | 141 -------- .../workflows/coverage-linux-without-intl.yml | 84 ----- .github/workflows/coverage-linux.yml | 84 ----- .github/workflows/coverage-windows.yml | 98 ------ @@ -46,7 +47,7 @@ is read only from the default branch and is left untouched. .github/workflows/lint-release-proposal.yml | 119 ------- .github/workflows/linters.yml | 250 -------------- .github/workflows/major-release.yml | 49 --- - .github/workflows/notify-on-push.yml | 61 ---- + .github/workflows/notify-on-push.yml | 64 ---- .github/workflows/notify-on-review-wanted.yml | 45 --- .github/workflows/post-release.yml | 33 -- .../remark-lint-problem-matcher.json | 22 -- @@ -62,18 +63,19 @@ is read only from the default branch and is left untouched. .github/workflows/update-openssl.yml | 61 ---- .github/workflows/update-v8.yml | 57 ---- .github/workflows/update-wpt.yml | 95 ------ - 42 files changed, 3364 deletions(-) + 43 files changed, 3594 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/SUPPORT.md delete mode 100644 .github/label-pr-config.yml + delete mode 100644 .github/workflows/author-ready-conflicts.yml delete mode 100644 .github/workflows/auto-start-ci.yml delete mode 100644 .github/workflows/build-tarball.yml delete mode 100644 .github/workflows/close-stalled.yml delete mode 100644 .github/workflows/codeql.yml delete mode 100644 .github/workflows/comment-labeled.yml - delete mode 100644 .github/workflows/commit-lint-problem-matcher.json delete mode 100644 .github/workflows/commit-lint.yml delete mode 100644 .github/workflows/commit-queue.yml + delete mode 100644 .github/workflows/contributor-guidance.yml delete mode 100644 .github/workflows/coverage-linux-without-intl.yml delete mode 100644 .github/workflows/coverage-linux.yml delete mode 100644 .github/workflows/coverage-windows.yml @@ -389,9 +391,63 @@ index 8b36066473..0000000000 - - wasi - - worker - - zlib +diff --git a/.github/workflows/author-ready-conflicts.yml b/.github/workflows/author-ready-conflicts.yml +deleted file mode 100644 +index f447c6b80a..0000000000 +--- a/.github/workflows/author-ready-conflicts.yml ++++ /dev/null +@@ -1,48 +0,0 @@ +-name: Unlabel not-ready pull requests +- +-on: +- schedule: +- - cron: 15 3 * * * +- +-permissions: +- contents: read +- +-jobs: +- remove-label: +- name: Remove author ready +- if: github.repository == 'nodejs/node' +- runs-on: ubuntu-slim +- permissions: +- pull-requests: write +- steps: +- - name: Remove the label and comment +- env: +- GH_TOKEN: ${{ github.token }} +- MESSAGE: >- +- This pull request has conflicts with its base branch, removing the +- `author ready` label. +- +- Please +- [rebase](https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#step-5-rebase) +- your branch onto the latest base branch, resolve the conflicts +- locally, and force-push. +- +- Afterwards the pull request needs a fresh collaborator approval, +- and a collaborator will add the label back once it is +- [author ready](https://github.com/nodejs/node/blob/HEAD/doc/contributing/collaborator-guide.md#author-ready-pull-requests) +- again. +- run: | +- # Requesting the mergeable field makes GitHub compute the merge state, +- # pull requests still reported as UNKNOWN are picked up by a later run. +- gh pr list \ +- --repo "$GITHUB_REPOSITORY" \ +- --label 'author ready' \ +- --state open \ +- --limit 100 \ +- --json number,mergeable \ +- --jq '.[] | select(.mergeable == "CONFLICTING") | .number' | +- while read -r number; do +- gh pr edit "$number" --repo "$GITHUB_REPOSITORY" --remove-label 'author ready' +- printf '%s\n' "$MESSAGE" | +- gh pr comment "$number" --repo "$GITHUB_REPOSITORY" --body-file - +- done diff --git a/.github/workflows/auto-start-ci.yml b/.github/workflows/auto-start-ci.yml deleted file mode 100644 -index 0a69ff636f..0000000000 +index 8c01b0592c..0000000000 --- a/.github/workflows/auto-start-ci.yml +++ /dev/null @@ -1,78 +0,0 @@ @@ -406,7 +462,7 @@ index 0a69ff636f..0000000000 - # optimistic, it can take longer to run. - # To understand why `schedule` is used instead of other events, refer to - # ./doc/contributing/commit-queue.md -- - cron: '*/5 * * * *' +- - cron: 3/5 * * * * - -concurrency: ${{ github.workflow }} - @@ -448,7 +504,7 @@ index 0a69ff636f..0000000000 - runs-on: ubuntu-slim - steps: - - name: Install Node.js -- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ env.NODE_VERSION }} - @@ -475,7 +531,7 @@ index 0a69ff636f..0000000000 - GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/build-tarball.yml b/.github/workflows/build-tarball.yml deleted file mode 100644 -index 4bb9702797..0000000000 +index 8b306b59fe..0000000000 --- a/.github/workflows/build-tarball.yml +++ /dev/null @@ -1,96 +0,0 @@ @@ -522,7 +578,7 @@ index 4bb9702797..0000000000 - if: github.event.pull_request.draft == false - runs-on: ubuntu-slim - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Set up Python ${{ env.PYTHON_VERSION }} @@ -549,7 +605,7 @@ index 4bb9702797..0000000000 - SCCACHE_GHA_ENABLED: ${{ github.base_ref == 'main' || github.ref_name == 'main' }} - SCCACHE_IDLE_TIMEOUT: '0' - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - sparse-checkout: .github/actions/install-clang @@ -619,10 +675,10 @@ index 3c7ab04339..0000000000 - days-before-stale: -1 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 -index 23084c68ac..0000000000 +index 1b3179ef4a..0000000000 --- a/.github/workflows/codeql.yml +++ /dev/null -@@ -1,42 +0,0 @@ +@@ -1,44 +0,0 @@ -name: Run CodeQL - -on: @@ -649,20 +705,22 @@ index 23084c68ac..0000000000 - - steps: - - name: Checkout repository -- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 +- with: +- persist-credentials: false - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL -- uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 +- uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 - with: - languages: ${{ matrix.language }} - config-file: ./.github/codeql-config.yml - - - name: Autobuild -- uses: github/codeql-action/autobuild@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 +- uses: github/codeql-action/autobuild@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 - - - name: Perform CodeQL Analysis -- uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 +- uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 - with: - category: /language:${{matrix.language}} diff --git a/.github/workflows/comment-labeled.yml b/.github/workflows/comment-labeled.yml @@ -728,35 +786,16 @@ index 5e6f936d38..0000000000 - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NUMBER: ${{ github.event.pull_request.number }} - run: gh pr comment "$NUMBER" --repo "$GITHUB_REPOSITORY" --body "$NOTABLE_CHANGE_MESSAGE" -diff --git a/.github/workflows/commit-lint-problem-matcher.json b/.github/workflows/commit-lint-problem-matcher.json -deleted file mode 100644 -index 72dd13b9e0..0000000000 ---- a/.github/workflows/commit-lint-problem-matcher.json -+++ /dev/null -@@ -1,13 +0,0 @@ --{ -- "problemMatcher": [ -- { -- "owner": "core-validate-commit", -- "pattern": [ -- { -- "regexp": "^not ok \\d+ (.*)$", -- "message": 1 -- } -- ] -- } -- ] --} diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml deleted file mode 100644 -index 896340ba3a..0000000000 +index 6b173d8bc9..0000000000 --- a/.github/workflows/commit-lint.yml +++ /dev/null -@@ -1,35 +0,0 @@ +@@ -1,56 +0,0 @@ -name: First commit message adheres to guidelines - -on: -- pull_request: +- pull_request_target: - branches: - - main - @@ -764,36 +803,57 @@ index 896340ba3a..0000000000 - NODE_VERSION: lts/* - -permissions: -- contents: read +- pull-requests: read +- +-concurrency: +- group: ${{ github.workflow }}-${{ github.event.pull_request.number }} +- cancel-in-progress: true - -jobs: - lint-commit-message: +- name: lint-commit-message - runs-on: ubuntu-slim - steps: -- - name: Compute number of commits in the PR -- id: nb-of-commits -- run: | -- echo "plusOne=$((${{ github.event.pull_request.commits }} + 1))" >> $GITHUB_OUTPUT -- echo "minusOne=$((${{ github.event.pull_request.commits }} - 1))" >> $GITHUB_OUTPUT -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 -- with: -- fetch-depth: ${{ steps.nb-of-commits.outputs.plusOne }} -- persist-credentials: false -- - run: git reset HEAD^2 - - name: Install Node.js -- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ env.NODE_VERSION }} -- - name: Validate commit message -- run: | -- echo "::add-matcher::.github/workflows/commit-lint-problem-matcher.json" -- git rev-parse HEAD~${{ steps.nb-of-commits.outputs.minusOne }} | xargs npx -q core-validate-commit --no-validate-metadata --tap +- - name: Validate first commit message +- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +- with: +- script: | +- const { data: [commit] } = await github.rest.pulls.listCommits({ +- ...context.repo, +- pull_number: context.issue.number, +- per_page: 1, +- }); +- if (!commit) { +- throw new Error('No commits found in pull request'); +- } +- const { exitCode, stdout, stderr } = await exec.getExecOutput('npx', [ +- '-q', '--yes', '--ignore-scripts', 'core-validate-commit@6.0.0', +- '--no-validate-metadata', '--tap', '-', +- ], { +- cwd: process.env.RUNNER_TEMP, +- input: Buffer.from(JSON.stringify([{ +- id: commit.sha, +- message: commit.commit.message, +- author: commit.commit.author, +- }])), +- silent: true, +- ignoreReturnCode: true, +- }); +- if (exitCode !== 0) { +- core.setFailed(stdout + stderr || 'Commit message validation failed'); +- } else { +- core.info('First commit message passes validation'); +- } diff --git a/.github/workflows/commit-queue.yml b/.github/workflows/commit-queue.yml deleted file mode 100644 -index a1d691e9e1..0000000000 +index 8f29a390c6..0000000000 --- a/.github/workflows/commit-queue.yml +++ /dev/null -@@ -1,173 +0,0 @@ +@@ -1,201 +0,0 @@ -# This action requires the following secrets to be set on the repository: -# GH_USER_TOKEN: GitHub user token, to be used by ncu and to push changes -# JENKINS_USER: GitHub user whose Jenkins token is defined below @@ -807,7 +867,7 @@ index a1d691e9e1..0000000000 - # be read-only, and the Action won't have access to any other repository - # secrets, which it needs to access Jenkins API. - schedule: -- - cron: '*/5 * * * *' +- - cron: 3/5 * * * * - -concurrency: ${{ github.workflow }} - @@ -824,6 +884,7 @@ index a1d691e9e1..0000000000 - if: github.repository == 'nodejs/node' - runs-on: ubuntu-slim - outputs: +- aged_prs: ${{ steps.get_candidate_prs.outputs.aged_prs }} - candidates: ${{ steps.get_candidate_prs.outputs.candidates }} - steps: - - name: Get Pull Request Candidates @@ -846,6 +907,7 @@ index a1d691e9e1..0000000000 - --search "-label:blocked") - candidates=$(printf '%s %s\n' "$fast_track_prs" "$aged_prs" | - jq -r -s 'reduce .[] as $pr ([]; if index($pr) then . else . + [$pr] end) | join(" ")') +- echo "aged_prs=$aged_prs" >> "$GITHUB_OUTPUT" - echo "candidates=$candidates" >> "$GITHUB_OUTPUT" - env: - GH_TOKEN: ${{ github.token }} @@ -860,7 +922,7 @@ index a1d691e9e1..0000000000 - runs-on: ubuntu-slim - steps: - - name: Install Node.js -- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ env.NODE_VERSION }} - @@ -889,6 +951,7 @@ index a1d691e9e1..0000000000 - curl -fsSLo "$readme" "https://github.com/${GITHUB_REPOSITORY}/raw/${GITHUB_SHA}/README.md" - - numbers= +- lacks_second_approval_prs= - # shellcheck disable=SC2086 - for pr in $CANDIDATES; do - metadata="${RUNNER_TEMP}/metadata-${pr}.json" @@ -935,6 +998,14 @@ index a1d691e9e1..0000000000 - if [ "$metadata_status" -ge 20 ] && [ "$metadata_status" -le 29 ]; then - echo "pr ${pr} skipped, not ready to land" - echo "reason codes: ${metadata_reason_codes}" +- if jq -e ' +- (.reasonCodes | index("wait-time")) and +- (.pullRequest.labels | index("lacks-second-approval") | not) +- ' "$metadata" > /dev/null; then +- case " $AGED_PRS " in +- *" $pr "*) lacks_second_approval_prs="$lacks_second_approval_prs $pr" ;; +- esac +- fi - continue - fi - @@ -944,12 +1015,28 @@ index a1d691e9e1..0000000000 - done - - numbers=$(echo "$numbers" | xargs) +- lacks_second_approval_prs=$(echo "$lacks_second_approval_prs" | xargs) - echo "numbers=$numbers" >> "$GITHUB_OUTPUT" +- echo "lacks_second_approval_prs=$lacks_second_approval_prs" >> "$GITHUB_OUTPUT" - env: +- AGED_PRS: ${{ needs.get_candidate_prs.outputs.aged_prs }} - CANDIDATES: ${{ needs.get_candidate_prs.outputs.candidates }} - GH_TOKEN: ${{ github.token }} - -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - name: Label Pull Requests Lacking a Second Approval +- if: steps.get_mergeable_prs.outputs.lacks_second_approval_prs != '' +- run: | +- # shellcheck disable=SC2086 +- for pr in $PULL_REQUESTS; do +- if ! gh -R "$GITHUB_REPOSITORY" pr edit "$pr" --add-label 'lacks-second-approval'; then +- echo "::warning::Failed to add lacks-second-approval to PR ${pr}" +- fi +- done +- env: +- GH_TOKEN: ${{ secrets.GH_USER_TOKEN }} +- PULL_REQUESTS: ${{ steps.get_mergeable_prs.outputs.lacks_second_approval_prs }} +- +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - if: steps.get_mergeable_prs.outputs.numbers != '' - with: - # A personal token is required because pushing with GITHUB_TOKEN will @@ -957,6 +1044,7 @@ index a1d691e9e1..0000000000 - # to be set here because `checkout` configures GitHub authentication - # for push as well. - token: ${{ secrets.GH_USER_TOKEN }} +- persist-credentials: true - - - name: Start the Commit Queue - if: steps.get_mergeable_prs.outputs.numbers != '' @@ -967,9 +1055,156 @@ index a1d691e9e1..0000000000 - ./tools/actions/commit-queue.sh ${{ steps.get_mergeable_prs.outputs.numbers }} - env: - GH_TOKEN: ${{ secrets.GH_USER_TOKEN }} +diff --git a/.github/workflows/contributor-guidance.yml b/.github/workflows/contributor-guidance.yml +deleted file mode 100644 +index 4035ea9320..0000000000 +--- a/.github/workflows/contributor-guidance.yml ++++ /dev/null +@@ -1,141 +0,0 @@ +-# This action requires the following secrets to be set on the repository: +-# GH_USER_TOKEN: GitHub user token, used because FIRST_TIMER and +-# FIRST_TIME_CONTRIBUTOR associations are only returned for +-# privileged API calls +- +-name: Contributor guidance +- +-on: +- pull_request_target: +- types: [opened] +- +-permissions: {} +- +-jobs: +- contributor: +- name: Resolve contributor status +- # Avoid allocating a runner unless a welcome or scan is a plausible outcome. +- if: >- +- github.run_attempt == 1 && +- github.repository == 'nodejs/node' && +- github.event.pull_request.user.type != 'Bot' && +- (github.event.pull_request.author_association == 'CONTRIBUTOR' || +- github.event.pull_request.author_association == 'FIRST_TIMER' || +- github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' || +- github.event.pull_request.author_association == 'NONE') +- runs-on: ubuntu-slim +- outputs: +- is_first_time: ${{ steps.resolve.outputs.is_first_time }} +- # The event association may not expose private organization membership, +- # so gate the guidance job on the privileged API result. +- should_scan: ${{ steps.resolve.outputs.should_scan }} +- steps: +- - name: Check author association +- id: resolve +- env: +- EVENT_ASSOCIATION: ${{ github.event.pull_request.author_association }} +- GH_TOKEN: ${{ secrets.GH_USER_TOKEN }} +- NUMBER: ${{ github.event.pull_request.number }} +- run: | +- api_association=$(gh api "/repos/$GITHUB_REPOSITORY/pulls/$NUMBER" \ +- --jq '.author_association') +- echo "API author association: $api_association" +- echo "Event author association: $EVENT_ASSOCIATION" +- +- case "$api_association" in +- FIRST_TIMER|FIRST_TIME_CONTRIBUTOR) +- echo 'is_first_time=true' >> "$GITHUB_OUTPUT" +- echo 'should_scan=true' >> "$GITHUB_OUTPUT" +- ;; +- CONTRIBUTOR|NONE) +- echo 'is_first_time=false' >> "$GITHUB_OUTPUT" +- echo 'should_scan=true' >> "$GITHUB_OUTPUT" +- ;; +- COLLABORATOR|MANNEQUIN|MEMBER|OWNER) +- echo 'is_first_time=false' >> "$GITHUB_OUTPUT" +- echo 'should_scan=false' >> "$GITHUB_OUTPUT" +- ;; +- *) +- echo "Unexpected author association: $api_association" >&2 +- exit 1 +- ;; +- esac +- +- guidance: +- name: Apply contributor guidance +- needs: contributor +- if: needs.contributor.outputs.should_scan == 'true' +- runs-on: ubuntu-slim +- permissions: +- contents: read +- pull-requests: write +- steps: +- - name: Scan contributor activity +- id: agentscan +- # The welcome should still be posted if this advisory scan fails. +- continue-on-error: true +- uses: MatteoGabriele/agentscan-action@8112fb79b33fafb8506159df20129a34209ac410 # v2.5.0 +- with: +- github-token: ${{ github.token }} +- mode: labels +- scan-pull-requests: true +- scan-issues: false +- auto-close: false +- honeypot: false +- # The privileged association check skips trusted authors before this job, +- # so `trusted-author-associations` would be redundant here. +- # trusted-author-associations: collaborator,member,owner +- +- - name: Comment with contributor guidance +- if: env.IS_FIRST_TIME == 'true' || env.ADD_CAUTION == 'true' +- env: +- ADD_CAUTION: >- +- ${{ +- steps.agentscan.outputs.classification == 'mixed' || +- steps.agentscan.outputs.classification == 'automation' || +- steps.agentscan.outputs['community-flagged'] == 'true' +- }} +- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +- IS_FIRST_TIME: ${{ needs.contributor.outputs.is_first_time }} +- NUMBER: ${{ github.event.pull_request.number }} +- WELCOME_MESSAGE: >2- +- Welcome to Node.js, and thank you for your first contribution! +- +- Before review, please take a moment to read: +- +- * the [guide for first-time contributors](https://github.com/nodejs/node/blob/HEAD/doc/contributing/first-contributions.md) +- * the [contribution and automation policies](https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md) +- * the [pull request guide](https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md) +- * the [AI use policy](https://github.com/nodejs/node/blob/HEAD/doc/contributing/ai-guidelines.md) +- * the [Code of Conduct](https://github.com/nodejs/admin/blob/HEAD/CODE_OF_CONDUCT.md) +- +- Please make sure every commit is +- [signed off](https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#step-4-commit). +- For a first pull request, GitHub Actions require collaborator +- approval and Jenkins CI must be started by a collaborator or triager, +- so an initial wait is normal. +- CAUTION_MESSAGE: >- +- > [!CAUTION] +- +- > AgentScan found account activity patterns that may be consistent with +- automation. This is a heuristic, not proof that this pull request was +- opened by an agent or violates policy. AI-assisted contributions are +- permitted, but automated tooling must not open pull requests without +- advance approval, and contributors must personally understand, test, +- verify, and take responsibility for every submitted change. See the +- [AgentScan analysis](https://agentscan.tools/user/${{ github.event.pull_request.user.login }}), +- [AI use policy](https://github.com/nodejs/node/blob/HEAD/doc/contributing/ai-guidelines.md), +- and +- [automation policy](https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md#automation-and-bots) +- for additional context. +- run: | +- if [[ "$IS_FIRST_TIME" == "true" && "$ADD_CAUTION" == "true" ]]; then +- body="$WELCOME_MESSAGE"$'\n\n'"$CAUTION_MESSAGE" +- elif [[ "$IS_FIRST_TIME" == "true" ]]; then +- body="$WELCOME_MESSAGE" +- else +- body="$CAUTION_MESSAGE" +- fi +- +- printf '%s\n' "$body" | +- gh pr comment "$NUMBER" --repo "$GITHUB_REPOSITORY" --body-file - diff --git a/.github/workflows/coverage-linux-without-intl.yml b/.github/workflows/coverage-linux-without-intl.yml deleted file mode 100644 -index 19e03391f1..0000000000 +index 61309d3d6a..0000000000 --- a/.github/workflows/coverage-linux-without-intl.yml +++ /dev/null @@ -1,84 +0,0 @@ @@ -1024,7 +1259,7 @@ index 19e03391f1..0000000000 - if: github.event.pull_request.draft == false - runs-on: ubuntu-24.04 - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Set up Python ${{ env.PYTHON_VERSION }} @@ -1059,7 +1294,7 @@ index 19e03391f1..0000000000 - directory: ./coverage diff --git a/.github/workflows/coverage-linux.yml b/.github/workflows/coverage-linux.yml deleted file mode 100644 -index c2c4b22c57..0000000000 +index 4d350ebd2e..0000000000 --- a/.github/workflows/coverage-linux.yml +++ /dev/null @@ -1,84 +0,0 @@ @@ -1114,7 +1349,7 @@ index c2c4b22c57..0000000000 - if: github.event.pull_request.draft == false - runs-on: ubuntu-24.04-arm - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Set up Python ${{ env.PYTHON_VERSION }} @@ -1149,7 +1384,7 @@ index c2c4b22c57..0000000000 - directory: ./coverage diff --git a/.github/workflows/coverage-windows.yml b/.github/workflows/coverage-windows.yml deleted file mode 100644 -index cbb5a776cb..0000000000 +index d735ea551c..0000000000 --- a/.github/workflows/coverage-windows.yml +++ /dev/null @@ -1,98 +0,0 @@ @@ -1224,7 +1459,7 @@ index cbb5a776cb..0000000000 - if: github.event.pull_request.draft == false - runs-on: windows-2025 - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Set up Python ${{ env.PYTHON_VERSION }} @@ -1253,7 +1488,7 @@ index cbb5a776cb..0000000000 - directory: ./coverage diff --git a/.github/workflows/create-release-proposal.yml b/.github/workflows/create-release-proposal.yml deleted file mode 100644 -index 883bb48b0d..0000000000 +index 735b15ffd1..0000000000 --- a/.github/workflows/create-release-proposal.yml +++ /dev/null @@ -1,81 +0,0 @@ @@ -1292,14 +1527,14 @@ index 883bb48b0d..0000000000 - RELEASE_LINE: ${{ inputs.release-line }} - runs-on: ubuntu-slim - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ env.STAGING_BRANCH }} - persist-credentials: false - - # Install dependencies - - name: Install Node.js -- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ env.NODE_VERSION }} - @@ -1340,7 +1575,7 @@ index 883bb48b0d..0000000000 - BOT_TOKEN: ${{ secrets.GH_USER_TOKEN }} diff --git a/.github/workflows/daily-wpt-fyi.yml b/.github/workflows/daily-wpt-fyi.yml deleted file mode 100644 -index 0895f1353e..0000000000 +index 1078899499..0000000000 --- a/.github/workflows/daily-wpt-fyi.yml +++ /dev/null @@ -1,177 +0,0 @@ @@ -1395,7 +1630,7 @@ index 0895f1353e..0000000000 - run: echo "NIGHTLY=$(curl -s https://nodejs.org/download/nightly/index.json | jq -r '[.[] | select(.files[] | contains("linux-arm64"))][0].version')" >> $GITHUB_ENV - - name: Install Node.js - id: setup-node -- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ env.NIGHTLY || matrix.node-version }} - check-latest: true @@ -1407,7 +1642,7 @@ index 0895f1353e..0000000000 - SHORT_SHA=$(node -p 'process.version.split(/-nightly\d{8}/)[1]') - echo "NIGHTLY_REF=$(gh api /repos/nodejs/node/commits/$SHORT_SHA --jq '.sha')" >> $GITHUB_ENV - - name: Checkout ${{ steps.setup-node.outputs.node-version }} -- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - ref: ${{ env.NIGHTLY_REF || steps.setup-node.outputs.node-version }} @@ -1423,7 +1658,7 @@ index 0895f1353e..0000000000 - run: rm -rf wpt - working-directory: test/fixtures - - name: Checkout epochs/daily WPT -- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - repository: web-platform-tests/wpt - persist-credentials: false @@ -1454,7 +1689,7 @@ index 0895f1353e..0000000000 - # version-specific checkout above overwrites .github/actions/ - - name: Checkout undici WPT actions - if: ${{ env.WPT_REPORT != '' }} -- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - sparse-checkout: | - .github/actions/undici-wpt-current @@ -1523,7 +1758,7 @@ index 0895f1353e..0000000000 - done diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml deleted file mode 100644 -index 6c4b2ec9a4..0000000000 +index b7a616e2df..0000000000 --- a/.github/workflows/daily.yml +++ /dev/null @@ -1,30 +0,0 @@ @@ -1545,11 +1780,11 @@ index 6c4b2ec9a4..0000000000 - if: github.repository == 'nodejs/node' || github.event_name == 'workflow_dispatch' - runs-on: ubuntu-24.04-arm - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Use Node.js ${{ env.NODE_VERSION }} -- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ env.NODE_VERSION }} - - name: Build lto @@ -1559,7 +1794,7 @@ index 6c4b2ec9a4..0000000000 - ninja -C out/Release diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml deleted file mode 100644 -index 6e5dfe7326..0000000000 +index 693d9ad3dd..0000000000 --- a/.github/workflows/doc.yml +++ /dev/null @@ -1,41 +0,0 @@ @@ -1589,11 +1824,11 @@ index 6e5dfe7326..0000000000 - if: github.event.pull_request.draft == false - runs-on: ubuntu-slim - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Use Node.js ${{ env.NODE_VERSION }} -- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ env.NODE_VERSION }} - - name: Build @@ -1606,7 +1841,7 @@ index 6e5dfe7326..0000000000 - run: NODE=$(command -v node) make test-doc-ci TEST_CI_ARGS="-p actions --measure-flakiness 9" diff --git a/.github/workflows/find-inactive-collaborators.yml b/.github/workflows/find-inactive-collaborators.yml deleted file mode 100644 -index e6e0c7f54f..0000000000 +index 8c74fda224..0000000000 --- a/.github/workflows/find-inactive-collaborators.yml +++ /dev/null @@ -1,50 +0,0 @@ @@ -1631,13 +1866,13 @@ index e6e0c7f54f..0000000000 - runs-on: ubuntu-slim - - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Use Node.js ${{ env.NODE_VERSION }} -- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ env.NODE_VERSION }} - @@ -1662,7 +1897,7 @@ index e6e0c7f54f..0000000000 - title: 'meta: move one or more collaborators to emeritus' diff --git a/.github/workflows/find-inactive-tsc.yml b/.github/workflows/find-inactive-tsc.yml deleted file mode 100644 -index c1804b78d3..0000000000 +index d59f26b73b..0000000000 --- a/.github/workflows/find-inactive-tsc.yml +++ /dev/null @@ -1,62 +0,0 @@ @@ -1688,13 +1923,13 @@ index c1804b78d3..0000000000 - - steps: - - name: Checkout the repo -- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Clone nodejs/TSC repository -- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - path: .tmp @@ -1702,7 +1937,7 @@ index c1804b78d3..0000000000 - repository: nodejs/TSC - - - name: Use Node.js ${{ env.NODE_VERSION }} -- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ env.NODE_VERSION }} - @@ -1813,7 +2048,7 @@ index 03aa1fa9a8..0000000000 - configuration-path: .github/label-pr-config.yml diff --git a/.github/workflows/license-builder.yml b/.github/workflows/license-builder.yml deleted file mode 100644 -index a95ee13c45..0000000000 +index 3bdf3fc31d..0000000000 --- a/.github/workflows/license-builder.yml +++ /dev/null @@ -1,38 +0,0 @@ @@ -1836,7 +2071,7 @@ index a95ee13c45..0000000000 - if: github.repository == 'nodejs/node' - runs-on: ubuntu-slim - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - run: ./tools/license-builder.sh # Run the license builder tool @@ -1857,7 +2092,7 @@ index a95ee13c45..0000000000 - labels: meta diff --git a/.github/workflows/lint-release-proposal.yml b/.github/workflows/lint-release-proposal.yml deleted file mode 100644 -index e8ffab1784..0000000000 +index 027aee336a..0000000000 --- a/.github/workflows/lint-release-proposal.yml +++ /dev/null @@ -1,119 +0,0 @@ @@ -1886,7 +2121,7 @@ index e8ffab1784..0000000000 - contents: read - pull-requests: read - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - fetch-depth: 2 @@ -1982,7 +2217,7 @@ index e8ffab1784..0000000000 - GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml deleted file mode 100644 -index 9a9177e01c..0000000000 +index 38980ecf6d..0000000000 --- a/.github/workflows/linters.yml +++ /dev/null @@ -1,250 +0,0 @@ @@ -2013,11 +2248,11 @@ index 9a9177e01c..0000000000 - if: github.event.pull_request.draft == false - runs-on: ubuntu-slim - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Use Node.js ${{ env.NODE_VERSION }} -- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ env.NODE_VERSION }} - - name: Lint addon docs @@ -2026,7 +2261,7 @@ index 9a9177e01c..0000000000 - if: github.event.pull_request.draft == false - runs-on: ubuntu-slim - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Set up Python ${{ env.PYTHON_VERSION }} @@ -2039,12 +2274,12 @@ index 9a9177e01c..0000000000 - if: ${{ github.event.pull_request && github.event.pull_request.draft == false && github.base_ref == github.event.repository.default_branch }} - runs-on: ubuntu-slim - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - - name: Use Node.js ${{ env.NODE_VERSION }} -- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ env.NODE_VERSION }} - - name: Set up Python ${{ env.PYTHON_VERSION }} @@ -2075,11 +2310,11 @@ index 9a9177e01c..0000000000 - if: github.event.pull_request.draft == false - runs-on: ubuntu-slim - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Use Node.js ${{ env.NODE_VERSION }} -- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ env.NODE_VERSION }} - - name: Lint JavaScript files @@ -2122,7 +2357,7 @@ index 9a9177e01c..0000000000 - if: github.event.pull_request.draft == false - runs-on: ubuntu-slim - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - sparse-checkout: | @@ -2147,7 +2382,7 @@ index 9a9177e01c..0000000000 - if: github.event.pull_request.draft == false - runs-on: ubuntu-slim - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - sparse-checkout: | @@ -2169,7 +2404,7 @@ index 9a9177e01c..0000000000 - if: github.event.pull_request.draft == false - runs-on: ubuntu-slim - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - sparse-checkout: | @@ -2185,7 +2420,7 @@ index 9a9177e01c..0000000000 - # cannot use ubuntu-24.04-arm here because the docker image is x86 only - runs-on: ubuntu-latest - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - uses: mszostok/codeowners-validator@7f3f5e28c6d7b8dfae5731e54ce2272ca384592f @@ -2195,7 +2430,7 @@ index 9a9177e01c..0000000000 - if: ${{ github.event.pull_request }} - runs-on: ubuntu-slim - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 2 - persist-credentials: false @@ -2210,7 +2445,7 @@ index 9a9177e01c..0000000000 - lint-readme: - runs-on: ubuntu-slim - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - sparse-checkout: | @@ -2293,10 +2528,10 @@ index b65917f89e..0000000000 - GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/notify-on-push.yml b/.github/workflows/notify-on-push.yml deleted file mode 100644 -index e709df93a8..0000000000 +index 25421b8447..0000000000 --- a/.github/workflows/notify-on-push.yml +++ /dev/null -@@ -1,61 +0,0 @@ +@@ -1,64 +0,0 @@ -on: - push: - branches: @@ -2317,7 +2552,7 @@ index e709df93a8..0000000000 - runs-on: ubuntu-24.04-arm - steps: - - name: Slack Notification -- uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # 2.4.0 +- uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # v2.4.0 - env: - SLACK_COLOR: '#DE512A' - SLACK_ICON: https://github.com/nodejs.png?size=48 @@ -2331,12 +2566,15 @@ index e709df93a8..0000000000 - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - - validateCommitMessage: +- # If a commit is force-pushed out of the main branch, the `commits` array is +- # empty and there are no checks to run. +- if: github.event.commits[0] - name: Notify on Push on `main` with invalid message - # cannot use ubuntu-slim here because rtCamp/action-slack-notify is dockerized - runs-on: ubuntu-24.04-arm - steps: - - name: Install Node.js -- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ env.NODE_VERSION }} - - name: Validate commits @@ -2346,7 +2584,7 @@ index e709df93a8..0000000000 - COMMITS: ${{ toJSON(github.event.commits) }} - - name: Slack Notification - if: ${{ failure() && steps.commit-check.conclusion == 'failure' && github.repository == 'nodejs/node' }} -- uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # 2.4.0 +- uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # v2.4.0 - env: - SLACK_COLOR: '#DE512A' - SLACK_ICON: https://github.com/nodejs.png?size=48 @@ -2360,7 +2598,7 @@ index e709df93a8..0000000000 - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/notify-on-review-wanted.yml b/.github/workflows/notify-on-review-wanted.yml deleted file mode 100644 -index 2f1f3af813..0000000000 +index effc6c209e..0000000000 --- a/.github/workflows/notify-on-review-wanted.yml +++ /dev/null @@ -1,45 +0,0 @@ @@ -2400,7 +2638,7 @@ index 2f1f3af813..0000000000 - fi - - - name: Slack Notification -- uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # 2.4.0 +- uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # v2.4.0 - env: - MSG_MINIMAL: actions url - SLACK_COLOR: '#3d85c6' @@ -2478,7 +2716,7 @@ index cfb281310a..0000000000 -} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml deleted file mode 100644 -index f84c91e1a3..0000000000 +index 4a7415bc90..0000000000 --- a/.github/workflows/scorecard.yml +++ /dev/null @@ -1,81 +0,0 @@ @@ -2520,12 +2758,12 @@ index f84c91e1a3..0000000000 - - steps: - - name: Harden Runner -- uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 +- uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 - with: - egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - - - name: Checkout code -- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - @@ -2560,7 +2798,7 @@ index f84c91e1a3..0000000000 - - # Upload the results to GitHub's code scanning dashboard. - - name: Upload to code-scanning -- uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 +- uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 - with: - sarif_file: results.sarif diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml @@ -2622,7 +2860,7 @@ index eb5921edb9..0000000000 - remove-stale-when-updated: true diff --git a/.github/workflows/stress-test.yml b/.github/workflows/stress-test.yml deleted file mode 100644 -index 10f61e8f92..0000000000 +index af1df77fe5..0000000000 --- a/.github/workflows/stress-test.yml +++ /dev/null @@ -1,114 +0,0 @@ @@ -2682,7 +2920,7 @@ index 10f61e8f92..0000000000 - SCCACHE_GHA_ENABLED: 'true' - SCCACHE_IDLE_TIMEOUT: '0' - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - path: node @@ -2742,7 +2980,7 @@ index 10f61e8f92..0000000000 - "$TEST_PATH" diff --git a/.github/workflows/test-internet.yml b/.github/workflows/test-internet.yml deleted file mode 100644 -index 2bc7a1f029..0000000000 +index ee3a85d10f..0000000000 --- a/.github/workflows/test-internet.yml +++ /dev/null @@ -1,64 +0,0 @@ @@ -2794,7 +3032,7 @@ index 2bc7a1f029..0000000000 - if: (github.event_name == 'schedule' && github.repository == 'nodejs/node') || (github.event.pull_request && github.event.pull_request.draft == false) - runs-on: ubuntu-24.04-arm - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Set up Python ${{ env.PYTHON_VERSION }} @@ -2812,7 +3050,7 @@ index 2bc7a1f029..0000000000 - run: make test-internet -j4 V=1; diff --git a/.github/workflows/test-linux-quic.yml b/.github/workflows/test-linux-quic.yml deleted file mode 100644 -index 9916b7993d..0000000000 +index c1a57509c0..0000000000 --- a/.github/workflows/test-linux-quic.yml +++ /dev/null @@ -1,69 +0,0 @@ @@ -2863,7 +3101,7 @@ index 9916b7993d..0000000000 - if: github.event.pull_request.draft == false - runs-on: ubuntu-24.04-arm - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - path: node @@ -2887,7 +3125,7 @@ index 9916b7993d..0000000000 - run: git add -A && git diff --name-only --exit-code --staged diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml deleted file mode 100644 -index e154ee866d..0000000000 +index 03f349fbed..0000000000 --- a/.github/workflows/test-linux.yml +++ /dev/null @@ -1,85 +0,0 @@ @@ -2947,7 +3185,7 @@ index e154ee866d..0000000000 - matrix: - os: [ubuntu-24.04, ubuntu-24.04-arm] - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - path: node @@ -2978,7 +3216,7 @@ index e154ee866d..0000000000 - DIR: dir%20with $unusual"chars?'åß∂ƒ©∆¬…` diff --git a/.github/workflows/test-macos.yml b/.github/workflows/test-macos.yml deleted file mode 100644 -index 943ea4cb6c..0000000000 +index 29b9d4b0b9..0000000000 --- a/.github/workflows/test-macos.yml +++ /dev/null @@ -1,102 +0,0 @@ @@ -3034,7 +3272,7 @@ index 943ea4cb6c..0000000000 - SCCACHE_GHA_ENABLED: ${{ github.base_ref == 'main' || github.ref_name == 'main' }} - SCCACHE_IDLE_TIMEOUT: '0' - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - path: node @@ -3086,7 +3324,7 @@ index 943ea4cb6c..0000000000 - DIR: dir%20with $unusual"chars?'åß∂ƒ©∆¬…` diff --git a/.github/workflows/timezone-update.yml b/.github/workflows/timezone-update.yml deleted file mode 100644 -index 94961f8ae6..0000000000 +index 481c20da4e..0000000000 --- a/.github/workflows/timezone-update.yml +++ /dev/null @@ -1,74 +0,0 @@ @@ -3113,12 +3351,12 @@ index 94961f8ae6..0000000000 - - steps: - - name: Checkout nodejs/node -- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Checkout unicode-org/icu-data -- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - path: icu-data - persist-credentials: false @@ -3166,7 +3404,7 @@ index 94961f8ae6..0000000000 - update-pull-request-title-and-body: true diff --git a/.github/workflows/tools.yml b/.github/workflows/tools.yml deleted file mode 100644 -index 900cf3ae00..0000000000 +index 89ed96f31d..0000000000 --- a/.github/workflows/tools.yml +++ /dev/null @@ -1,321 +0,0 @@ @@ -3458,7 +3696,7 @@ index 900cf3ae00..0000000000 - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - if: github.event_name == 'schedule' || inputs.id == 'all' || inputs.id == matrix.id - with: - persist-credentials: false @@ -3483,7 +3721,7 @@ index 900cf3ae00..0000000000 - # no-op if the base branch is already up-to-date. - with: - token: ${{ secrets.GH_USER_TOKEN }} -- branch: actions/${{ github.ref_name == 'main' || format('{0}/', github.ref_name) }}tools-update-${{ matrix.id }} # Custom branch *just* for this Action. +- branch: actions/${{ github.ref_name != 'main' && format('{0}/', github.ref_name) || '' }}tools-update-${{ matrix.id }} # Custom branch *just* for this Action. - delete-branch: true - commit-message: ${{ env.COMMIT_MSG }} - labels: ${{ matrix.label }} @@ -3493,7 +3731,7 @@ index 900cf3ae00..0000000000 - author: Node.js GitHub Bot diff --git a/.github/workflows/update-openssl.yml b/.github/workflows/update-openssl.yml deleted file mode 100644 -index 49f0006526..0000000000 +index 10eb3289a1..0000000000 --- a/.github/workflows/update-openssl.yml +++ /dev/null @@ -1,61 +0,0 @@ @@ -3514,7 +3752,7 @@ index 49f0006526..0000000000 - # Cannot use ubuntu-slim here because the update script requires Docker - runs-on: ubuntu-latest - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Check and download new OpenSSL version @@ -3560,7 +3798,7 @@ index 49f0006526..0000000000 - path: deps/openssl diff --git a/.github/workflows/update-v8.yml b/.github/workflows/update-v8.yml deleted file mode 100644 -index e365cf7701..0000000000 +index 8e71e56389..0000000000 --- a/.github/workflows/update-v8.yml +++ /dev/null @@ -1,57 +0,0 @@ @@ -3582,7 +3820,7 @@ index e365cf7701..0000000000 - if: github.repository == 'nodejs/node' - runs-on: ubuntu-slim - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Cache node modules and update-v8 @@ -3596,7 +3834,7 @@ index e365cf7701..0000000000 - ~/.npm - key: ${{ runner.os }}-build-${{ env.cache-name }} - - name: Install Node.js -- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ env.NODE_VERSION }} - - name: Install @node-core/utils @@ -3623,7 +3861,7 @@ index e365cf7701..0000000000 - labels: dependencies, v8 engine diff --git a/.github/workflows/update-wpt.yml b/.github/workflows/update-wpt.yml deleted file mode 100644 -index 4f752cc189..0000000000 +index 7b59a2f137..0000000000 --- a/.github/workflows/update-wpt.yml +++ /dev/null @@ -1,95 +0,0 @@ @@ -3656,12 +3894,12 @@ index 4f752cc189..0000000000 - subsystem: ${{ fromJSON(github.event.inputs.subsystems || '["url", "urlpattern", "WebCryptoAPI"]') }} - - steps: -- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Install Node.js -- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ env.NODE_VERSION }} - diff --git a/patches/doc-build-mobile-notes-and-ignores.patch b/patches/doc-build-mobile-notes-and-ignores.patch index b1c6d3faef..7ef7091f77 100644 --- a/patches/doc-build-mobile-notes-and-ignores.patch +++ b/patches/doc-build-mobile-notes-and-ignores.patch @@ -12,8 +12,8 @@ git operations in a built tree never sweep them up. Co-authored-by: Andre Staltz --- .gitignore | 5 + - README.md | 917 ++--------------------------------------------------- - 2 files changed, 32 insertions(+), 890 deletions(-) + README.md | 920 ++--------------------------------------------------- + 2 files changed, 32 insertions(+), 893 deletions(-) diff --git a/.gitignore b/.gitignore index 74eb99c815..ce1c201ce9 100644 @@ -39,10 +39,10 @@ index 74eb99c815..ce1c201ce9 100644 /deps/openssl/openssl.props /deps/openssl/openssl.targets diff --git a/README.md b/README.md -index 3827556a44..1d3ab7e321 100644 +index d90887e0f5..1d3ab7e321 100644 --- a/README.md +++ b/README.md -@@ -1,912 +1,49 @@ +@@ -1,915 +1,49 @@ -# Node.js +# Node.js for Mobile Apps @@ -356,6 +356,8 @@ index 3827556a44..1d3ab7e321 100644 - **Antoine du Hamel** <> (he/him) - [Support me](https://github.com/sponsors/aduh95) -* [anonrig](https://github.com/anonrig) - - **Yagiz Nizipli** <> (he/him) - [Support me](https://github.com/sponsors/anonrig) +-* [araujogui](https://github.com/araujogui) - +- **Guilherme Araújo** <> (he/him) -* [atlowChemi](https://github.com/atlowChemi) - - **Chemi Atlow** <> (he/him) -* [avivkeller](https://github.com/avivkeller) - @@ -832,6 +834,8 @@ index 3827556a44..1d3ab7e321 100644 - **Wiyeong Seo** <> -* [iam-frankqiu](https://github.com/iam-frankqiu) - - **Frank Qiu** <> (he/him) +-* [inoway46](https://github.com/inoway46) - +- **Yuya Inoue** <> (he/him) -* [milesguicent](https://github.com/milesguicent) - - **Miles Guicent** <> (he/him) -* [preveen-stack](https://github.com/preveen-stack) - @@ -871,7 +875,6 @@ index 3827556a44..1d3ab7e321 100644 -gpg --keyserver hkps://keys.openpgp.org --recv-keys 5BE8A3F6C8A5C01D106C0AD820B1A390B168D356 # Antoine du Hamel -gpg --keyserver hkps://keys.openpgp.org --recv-keys DD792F5973C6DE52C432CBDAC77ABFA00DDBF2B7 # Juan José Arboleda -gpg --keyserver hkps://keys.openpgp.org --recv-keys CC68F5A3106FF448322E48ED27F5E38D5B0A215F # Marco Ippolito --gpg --keyserver hkps://keys.openpgp.org --recv-keys 8FCCA13FEF1D0C2E91008E09770F7A9A5AE15600 # Michaël Zasso -gpg --keyserver hkps://keys.openpgp.org --recv-keys 890C08DB8579162FEE0DF9DB8BEAB4DFCF555EF4 # Rafael Gonzaga -gpg --keyserver hkps://keys.openpgp.org --recv-keys C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C # Richard Lau -gpg --keyserver hkps://keys.openpgp.org --recv-keys 108F52B48DB57BB0CC439B2997B01419BD92F80A # Ruy Adorno diff --git a/patches/files.map b/patches/files.map index 05207b8e07..62dc68ad71 100644 --- a/patches/files.map +++ b/patches/files.map @@ -37,6 +37,7 @@ test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-dg test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-dgram-membership.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-dgram-socket-buffer-size.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-copyfile.js +test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-glob-no-follow-symlink-globstar.mjs test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-lchmod.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-mkdir.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-open-flags.js @@ -46,12 +47,14 @@ test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-readdir-ucs2.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-realpath-native.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-realpath.js +test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-rmSync-special-char-additional-error.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-rmdir-recursive.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-stat.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-watch.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-watchfile.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-http-response-setheaders.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-http2-respond-file-error-dir.js +test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-intl.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-net-connect-options-fd.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-os.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-parse-args.mjs @@ -63,6 +66,7 @@ test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-pr test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-repl-options.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-runner-concurrency.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-signal-handler.js +test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-sqlite-backup.mjs test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-watch-mode-files_watcher.mjs test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/sequential/sequential.status test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/sequential/test-async-wrap-getasyncid.js @@ -72,14 +76,15 @@ doc-build-mobile-notes-and-ignores.patch README.md ci-remove-upstream-only-workflows-and-config.patch .github/ISSUE_TEMPLATE/config.yml ci-remove-upstream-only-workflows-and-config.patch .github/SUPPORT.md ci-remove-upstream-only-workflows-and-config.patch .github/label-pr-config.yml +ci-remove-upstream-only-workflows-and-config.patch .github/workflows/author-ready-conflicts.yml ci-remove-upstream-only-workflows-and-config.patch .github/workflows/auto-start-ci.yml ci-remove-upstream-only-workflows-and-config.patch .github/workflows/build-tarball.yml ci-remove-upstream-only-workflows-and-config.patch .github/workflows/close-stalled.yml ci-remove-upstream-only-workflows-and-config.patch .github/workflows/codeql.yml ci-remove-upstream-only-workflows-and-config.patch .github/workflows/comment-labeled.yml -ci-remove-upstream-only-workflows-and-config.patch .github/workflows/commit-lint-problem-matcher.json ci-remove-upstream-only-workflows-and-config.patch .github/workflows/commit-lint.yml ci-remove-upstream-only-workflows-and-config.patch .github/workflows/commit-queue.yml +ci-remove-upstream-only-workflows-and-config.patch .github/workflows/contributor-guidance.yml ci-remove-upstream-only-workflows-and-config.patch .github/workflows/coverage-linux-without-intl.yml ci-remove-upstream-only-workflows-and-config.patch .github/workflows/coverage-linux.yml ci-remove-upstream-only-workflows-and-config.patch .github/workflows/coverage-windows.yml diff --git a/patches/lib-install-a-WebAssembly-polyfill-when-the-engine-h.patch b/patches/lib-install-a-WebAssembly-polyfill-when-the-engine-h.patch index 40800ddf6a..1aaed7f904 100644 --- a/patches/lib-install-a-WebAssembly-polyfill-when-the-engine-h.patch +++ b/patches/lib-install-a-WebAssembly-polyfill-when-the-engine-h.patch @@ -46,7 +46,7 @@ index 9cc3315dd3..b876545603 100644 + SOFTWARE. + """ diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js -index a2790695e0..ed2b287e09 100644 +index d17da5d977..3f54af0958 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -9,6 +9,7 @@ const { diff --git a/patches/src-crypto-mobile-adjustment-in-crypto_context.cc.patch b/patches/src-crypto-mobile-adjustment-in-crypto_context.cc.patch index 098d80ba8d..66fd2c1c8c 100644 --- a/patches/src-crypto-mobile-adjustment-in-crypto_context.cc.patch +++ b/patches/src-crypto-mobile-adjustment-in-crypto_context.cc.patch @@ -19,10 +19,10 @@ Co-authored-by: Julia Samól 1 file changed, 17 insertions(+) diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc -index af8d52fbe6..ea1cce665a 100644 +index 74ea622dad..575cbb51ff 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc -@@ -335,6 +335,9 @@ bool isSelfIssued(X509* cert) { +@@ -336,6 +336,9 @@ bool isSelfIssued(X509* cert) { #ifdef __APPLE__ TrustStatus IsTrustDictionaryTrustedForPolicy(CFDictionaryRef trust_dict, bool is_self_issued) { @@ -32,7 +32,7 @@ index af8d52fbe6..ea1cce665a 100644 // Trust settings may be scoped to a single application // skip as this is not supported if (CFDictionaryContainsKey(trust_dict, kSecTrustSettingsApplication)) { -@@ -426,6 +429,10 @@ TrustStatus IsTrustDictionaryTrustedForPolicy(CFDictionaryRef trust_dict, +@@ -427,6 +430,10 @@ TrustStatus IsTrustDictionaryTrustedForPolicy(CFDictionaryRef trust_dict, return (trust_settings_result == kSecTrustSettingsResultTrustAsRoot) ? TrustStatus::TRUSTED : TrustStatus::UNSPECIFIED; @@ -43,7 +43,7 @@ index af8d52fbe6..ea1cce665a 100644 } TrustStatus IsTrustSettingsTrustedForPolicy(CFArrayRef trust_settings, -@@ -497,6 +504,8 @@ bool IsCertificateTrustedForPolicy(X509* cert, SecCertificateRef ref) { +@@ -498,6 +505,8 @@ bool IsCertificateTrustedForPolicy(X509* cert, SecCertificateRef ref) { bool trust_evaluated = false; bool is_self_issued = isSelfIssued(cert); @@ -52,7 +52,7 @@ index af8d52fbe6..ea1cce665a 100644 // Evaluate user trust domain, then admin. User settings can override // admin (and both override the system domain, but we don't check that). for (const auto& trust_domain : -@@ -535,6 +544,14 @@ bool IsCertificateTrustedForPolicy(X509* cert, SecCertificateRef ref) { +@@ -536,6 +545,14 @@ bool IsCertificateTrustedForPolicy(X509* cert, SecCertificateRef ref) { CFRelease(trust_settings); } } diff --git a/patches/src-mobile-platform-guards-in-node.cc-node_internals.patch b/patches/src-mobile-platform-guards-in-node.cc-node_internals.patch index 73d40add6a..e904fb0ab8 100644 --- a/patches/src-mobile-platform-guards-in-node.cc-node_internals.patch +++ b/patches/src-mobile-platform-guards-in-node.cc-node_internals.patch @@ -8,24 +8,18 @@ for process-level facilities mobile OSes forbid or lack (tty tcsetattr teardown on iOS; NODE_IMPLEMENTS_POSIX_CREDENTIALS turned on for Android API >= 21, which upstream leaves off). -Also drops --experimental-wasm-jspi from the V8 argument list on iOS, -which upstream appends unconditionally as of v24.20.0. iOS builds V8 in -lite mode, so V8_ENABLE_WEBASSEMBLY is off and the whole ---experimental-wasm-* flag family does not exist; V8 leaves the flag -unconsumed and node turns that into "bad option" and exits before -running any JS. The guard is on the platform rather than on -V8_ENABLE_WEBASSEMBLY because node's own targets never receive that -define -- it is set only on V8's, and testing it here would silently -drop the flag on Android too, where WebAssembly is enabled. +The iOS --experimental-wasm-jspi guard is now upstream: v24.22.0 supplies +V8_ENABLE_WEBASSEMBLY to Node's targets and gates the flag on that define. +The mobile-only guard was retired rather than duplicating upstream logic. Co-authored-by: Jaime Bernardo --- - src/node.cc | 9 +++++++++ + src/node.cc | 3 +++ src/node_internals.h | 2 +- - 2 files changed, 10 insertions(+), 1 deletion(-) + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/node.cc b/src/node.cc -index 838106acff..1bc0d957ae 100644 +index 92c5fd8cb6..5bd029b5a2 100644 --- a/src/node.cc +++ b/src/node.cc @@ -698,6 +698,8 @@ void ResetStdio() { @@ -45,22 +39,8 @@ index 838106acff..1bc0d957ae 100644 } #endif // __POSIX__ } -@@ -787,7 +790,13 @@ static ExitCode ProcessGlobalArgsInternal(std::vector* args, - } - - // WebAssembly JS Promise Integration -+ // nodejs-mobile patch: iOS builds V8 in lite mode, which compiles out -+ // WebAssembly and every --experimental-wasm-* flag with it. V8 leaves the -+ // unrecognized flag in v8_args and node then fails with "bad option" before -+ // running any JS, so the flag must not be added there. -+#if !(defined(__APPLE__) && TARGET_OS_IPHONE) - v8_args.emplace_back("--experimental-wasm-jspi"); -+#endif // !(defined(__APPLE__) && TARGET_OS_IPHONE) - - #ifdef __POSIX__ - // Block SIGPROF signals when sleeping in epoll_wait/kevent/etc. Avoids the diff --git a/src/node_internals.h b/src/node_internals.h -index 8e930a6fec..b279bf9999 100644 +index dc228f8e1e..c0ef621060 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -326,7 +326,7 @@ class ThreadPoolWork { diff --git a/patches/test-mobile-test-harness.patch b/patches/test-mobile-test-harness.patch index 8f3a42165d..938b82ff51 100644 --- a/patches/test-mobile-test-harness.patch +++ b/patches/test-mobile-test-harness.patch @@ -24,7 +24,7 @@ Co-authored-by: Andre Staltz diff --git a/test/common/index.js b/test/common/index.js old mode 100755 new mode 100644 -index 9d15b57bf6..70d5fefae0 +index 0f2d695bbc..4ac724e14c --- a/test/common/index.js +++ b/test/common/index.js @@ -161,6 +161,8 @@ const isFreeBSD = process.platform === 'freebsd'; @@ -36,7 +36,7 @@ index 9d15b57bf6..70d5fefae0 const isASan = process.config.variables.asan === 1; const isRiscv64 = process.arch === 'riscv64'; const isDebug = process.features.debug; -@@ -976,8 +978,10 @@ const common = { +@@ -977,8 +979,10 @@ const common = { invalidArgTypeHelper, isAlive, isASan, @@ -153,10 +153,10 @@ index 5470fd2bd9..4db0a9d5c6 100644 return result diff --git a/tools/test.py b/tools/test.py -index 0bdfa5de79..97c9503393 100755 +index 1495182fcc..8aa12d755a 100755 --- a/tools/test.py +++ b/tools/test.py -@@ -1745,6 +1745,12 @@ def Main(): +@@ -1746,6 +1746,12 @@ def Main(): 'asan': get_asan_state(vm, context), 'pointer_compression': get_pointer_compression_state(vm, context), } diff --git a/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch b/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch index 882db2701a..ae8231074d 100644 --- a/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch +++ b/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch @@ -16,11 +16,12 @@ series itself. Co-authored-by: Jaime Bernardo Co-authored-by: Andre Staltz --- - test/parallel/parallel.status | 1498 +++++++++++++++++ + test/parallel/parallel.status | 1511 +++++++++++++++++ test/parallel/test-dgram-bind-fd.js | 2 +- test/parallel/test-dgram-membership.js | 10 +- .../parallel/test-dgram-socket-buffer-size.js | 2 +- test/parallel/test-fs-copyfile.js | 3 +- + ...est-fs-glob-no-follow-symlink-globstar.mjs | 2 +- test/parallel/test-fs-lchmod.js | 4 +- test/parallel/test-fs-mkdir.js | 5 +- test/parallel/test-fs-open-flags.js | 3 +- @@ -30,12 +31,14 @@ Co-authored-by: Andre Staltz test/parallel/test-fs-readdir-ucs2.js | 2 +- test/parallel/test-fs-realpath-native.js | 7 + test/parallel/test-fs-realpath.js | 8 +- + ...fs-rmSync-special-char-additional-error.js | 3 +- test/parallel/test-fs-rmdir-recursive.js | 6 + test/parallel/test-fs-stat.js | 7 + test/parallel/test-fs-watch.js | 10 +- test/parallel/test-fs-watchfile.js | 4 +- .../parallel/test-http-response-setheaders.js | 6 + .../test-http2-respond-file-error-dir.js | 7 + + test/parallel/test-intl.js | 4 +- test/parallel/test-net-connect-options-fd.js | 2 +- test/parallel/test-os.js | 25 +- test/parallel/test-parse-args.mjs | 2 + @@ -47,17 +50,18 @@ Co-authored-by: Andre Staltz test/parallel/test-repl-options.js | 6 +- test/parallel/test-runner-concurrency.js | 3 + test/parallel/test-signal-handler.js | 12 +- + test/parallel/test-sqlite-backup.mjs | 6 +- .../test-watch-mode-files_watcher.mjs | 3 + - test/sequential/sequential.status | 129 ++ + test/sequential/sequential.status | 132 ++ test/sequential/test-async-wrap-getasyncid.js | 15 +- test/sequential/test-fs-watch.js | 4 + - 35 files changed, 1808 insertions(+), 46 deletions(-) + 39 files changed, 1834 insertions(+), 51 deletions(-) diff --git a/test/parallel/parallel.status b/test/parallel/parallel.status -index d13efc8ac1..f52cc544b2 100644 +index d13efc8ac1..fccf6d71a2 100644 --- a/test/parallel/parallel.status +++ b/test/parallel/parallel.status -@@ -160,6 +160,1504 @@ test-tls-set-default-ca-certificates-append-fetch: SKIP +@@ -160,6 +160,1517 @@ test-tls-set-default-ca-certificates-append-fetch: SKIP test-tls-set-default-ca-certificates-reset-fetch: SKIP test-inspector-invalid-protocol: SKIP @@ -103,35 +107,43 @@ index d13efc8ac1..f52cc544b2 100644 +test-async-hooks-fatal-error: SKIP +test-async-wrap-pop-id-during-load: SKIP +test-bash-completion: SKIP ++test-benchmark-compare: SKIP +test-buffer-constructor-node-modules-paths: SKIP +test-child-process-*: SKIP +test-cli-*: SKIP +test-cluster-*: SKIP -+test-common: SKIP +test-common-countdown: SKIP +test-common-expect-warning: SKIP ++test-common-wpt-script-modifier: SKIP ++test-common: SKIP +test-coverage-with-inspector-disabled: SKIP +test-crypto-fips: SKIP ++test-crypto-provider-cache-snapshot: SKIP +test-crypto-secure-heap: SKIP +test-cwd-enoent*: SKIP +test-debug-usage: SKIP +test-debugger-address: SKIP +test-debugger-auto-resume: SKIP ++test-debugger-backtrace: SKIP +test-debugger-break: SKIP +test-debugger-breakpoint-exists: SKIP +test-debugger-clear-breakpoints: SKIP +test-debugger-exceptions: SKIP +test-debugger-exec-scope: SKIP ++test-debugger-exec: SKIP +test-debugger-extract-function-name: SKIP +test-debugger-heap-profiler: SKIP +test-debugger-help: SKIP +test-debugger-invalid-json: SKIP +test-debugger-list: SKIP ++test-debugger-low-level: SKIP +test-debugger-object-type-remote-object: SKIP -+test-debugger-profile: SKIP +test-debugger-profile-command: SKIP -+test-debugger-random-port: SKIP ++test-debugger-profile: SKIP +test-debugger-random-port-with-inspect-port: SKIP ++test-debugger-random-port: SKIP ++test-debugger-repeat-last: SKIP ++test-debugger-restart-message: SKIP +test-debugger-run-after-quit-restart: SKIP +test-debugger-sb-before-load: SKIP +test-debugger-scripts: SKIP @@ -193,8 +205,8 @@ index d13efc8ac1..f52cc544b2 100644 +test-internal-util-decorate-error-stack: SKIP +test-kill-segfault-freebsd: SKIP +test-listen-fd-cluster: SKIP -+test-listen-fd-detached: SKIP +test-listen-fd-detached-inherit: SKIP ++test-listen-fd-detached: SKIP +test-listen-fd-server: SKIP +test-loaders-unknown-builtin-module: SKIP +test-macos-signed-deps: SKIP @@ -228,31 +240,31 @@ index d13efc8ac1..f52cc544b2 100644 +test-pipe-unref: SKIP +test-policy-crypto-default-encoding: SKIP +test-policy-dependencies: SKIP -+test-policy-integrity: SKIP +test-policy-integrity-flag: SKIP ++test-policy-integrity: SKIP +test-policy-manifest: SKIP +test-policy-parse-integrity: SKIP +test-policy-process-binding: SKIP +test-policy-scopes: SKIP -+test-preload: SKIP +test-preload-print-process-argv: SKIP +test-preload-self-referential: SKIP +test-preload-worker: SKIP ++test-preload: SKIP +test-process-argv-0: SKIP +test-process-env: SKIP +test-process-exec-argv: SKIP +test-process-execpath: SKIP +test-process-exit-after-fetch-throw: SKIP +test-process-exit-code: SKIP -+test-process-external-stdio-close: SKIP +test-process-external-stdio-close-spawn: SKIP ++test-process-external-stdio-close: SKIP +test-process-getgroups: SKIP +test-process-kill-null: SKIP +test-process-ppid: SKIP +test-process-raw-debug: SKIP +test-process-really-exit: SKIP -+test-process-redirect-warnings: SKIP +test-process-redirect-warnings-env: SKIP ++test-process-redirect-warnings: SKIP +test-process-remove-all-signal-listeners: SKIP +test-process-title: SKIP +test-process-uncaught-exception-monitor: SKIP @@ -272,8 +284,8 @@ index d13efc8ac1..f52cc544b2 100644 +test-repl-require-context: SKIP +test-repl-require-self-referential: SKIP +test-repl-setprompt: SKIP -+test-repl-sigint: SKIP +test-repl-sigint-nested-eval: SKIP ++test-repl-sigint: SKIP +test-repl-strict-mode-previews: SKIP +test-repl-syntax-error-handling: SKIP +test-repl-tab-complete-nested-repls: SKIP @@ -309,10 +321,10 @@ index d13efc8ac1..f52cc544b2 100644 +test-snapshot-basic: SKIP +test-snapshot-cjs-main: SKIP +test-snapshot-console: SKIP -+test-snapshot-dns-lookup-localhost: SKIP +test-snapshot-dns-lookup-localhost-promise: SKIP -+test-snapshot-dns-resolve-localhost: SKIP ++test-snapshot-dns-lookup-localhost: SKIP +test-snapshot-dns-resolve-localhost-promise: SKIP ++test-snapshot-dns-resolve-localhost: SKIP +test-snapshot-error: SKIP +test-snapshot-eval: SKIP +test-snapshot-gzip: SKIP @@ -323,16 +335,16 @@ index d13efc8ac1..f52cc544b2 100644 +test-snapshot-warning: SKIP +test-snapshot-weak-reference: SKIP +test-source-map-enable: SKIP -+test-startup-large-pages: SKIP -+test-startup-empty-regexp-statics: SKIP +test-stack-size-limit: SKIP ++test-startup-empty-regexp-statics: SKIP ++test-startup-large-pages: SKIP +test-stdin-child-proc: SKIP +test-stdin-from-file: SKIP +test-stdin-pipe-large: SKIP +test-stdin-pipe-resume: SKIP +test-stdin-resume-pause: SKIP -+test-stdin-script-child: SKIP +test-stdin-script-child-option: SKIP ++test-stdin-script-child: SKIP +test-stdio-closed: SKIP +test-stdio-pipe-redirect: SKIP +test-stdio-pipe-stderr: SKIP @@ -356,12 +368,12 @@ index d13efc8ac1..f52cc544b2 100644 +test-tls-cipher-list: SKIP +test-tls-cli-min-max-conflict: SKIP +test-tls-enable-keylog-cli: SKIP -+test-tls-enable-trace: SKIP +test-tls-enable-trace-cli: SKIP ++test-tls-enable-trace: SKIP +test-tls-env-bad-extra-ca: SKIP -+test-tls-env-extra-ca: SKIP +test-tls-env-extra-ca-file-load: SKIP +test-tls-env-extra-ca-no-crypto: SKIP ++test-tls-env-extra-ca: SKIP +test-tls-handshake-exception: SKIP +test-tls-ticket-cluster: SKIP +test-tls-wrap-econnreset-pipe: SKIP @@ -373,8 +385,8 @@ index d13efc8ac1..f52cc544b2 100644 +test-trace-events-console: SKIP +test-trace-events-environment: SKIP +test-trace-events-file-pattern: SKIP -+test-trace-events-fs-sync: SKIP +test-trace-events-fs-async: SKIP ++test-trace-events-fs-sync: SKIP +test-trace-events-http: SKIP +test-trace-events-metadata: SKIP +test-trace-events-net: SKIP @@ -385,8 +397,8 @@ index d13efc8ac1..f52cc544b2 100644 +test-trace-events-threadpool: SKIP +test-trace-events-v8: SKIP +test-trace-events-vm: SKIP -+test-trace-events-worker-metadata: SKIP +test-trace-events-worker-metadata-with-name: SKIP ++test-trace-events-worker-metadata: SKIP +test-trace-exit: SKIP +test-tracing-no-crash: SKIP +test-unhandled-exception-rethrow-error: SKIP @@ -396,13 +408,13 @@ index d13efc8ac1..f52cc544b2 100644 +test-util-getcallsites-sourcemap: SKIP +test-v8-coverage: SKIP +test-v8-stop-coverage: SKIP -+test-v8-take-coverage: SKIP +test-v8-take-coverage-noop: SKIP ++test-v8-take-coverage: SKIP +test-v8-untrusted-code-mitigations: SKIP +test-vm-api-handles-getter-errors: SKIP +test-vm-cached-data: SKIP -+test-vm-sigint: SKIP +test-vm-sigint-existing-handler: SKIP ++test-vm-sigint: SKIP +test-vm-syntax-error-message: SKIP +test-vm-syntax-error-stderr: SKIP +test-worker-init-failure: SKIP @@ -681,6 +693,7 @@ index d13efc8ac1..f52cc544b2 100644 +test-permission-fs-symlink-target-write: SKIP +test-permission-fs-traversal-path: SKIP +test-permission-fs-wildcard: SKIP ++test-permission-fs-filehandle-sync: SKIP +test-permission-fs-write: SKIP +test-permission-fs-write-report: SKIP +test-permission-inspector: SKIP @@ -764,16 +777,19 @@ index d13efc8ac1..f52cc544b2 100644 +test-async-hooks-fatal-error: SKIP +test-async-wrap-pop-id-during-load: SKIP +test-bash-completion: SKIP ++test-benchmark-compare: SKIP +test-buffer-constructor-node-modules-paths: SKIP +test-child-process-*: SKIP +test-cli-*: SKIP +test-cluster-*: SKIP -+test-common: SKIP +test-common-countdown: SKIP +test-common-expect-warning: SKIP ++test-common-wpt-script-modifier: SKIP ++test-common: SKIP +test-coverage-with-inspector-disabled: SKIP +test-crypto-fips: SKIP +test-crypto-no-algorithm: SKIP ++test-crypto-provider-cache-snapshot: SKIP +test-crypto-secure-heap: SKIP +test-cwd-enoent*: SKIP +test-debug-usage: SKIP @@ -784,8 +800,8 @@ index d13efc8ac1..f52cc544b2 100644 +test-debugger-breakpoint-exists: SKIP +test-debugger-clear-breakpoints: SKIP +test-debugger-exceptions: SKIP -+test-debugger-exec: SKIP +test-debugger-exec-scope: SKIP ++test-debugger-exec: SKIP +test-debugger-extract-function-name: SKIP +test-debugger-heap-profiler: SKIP +test-debugger-help: SKIP @@ -794,15 +810,15 @@ index d13efc8ac1..f52cc544b2 100644 +test-debugger-low-level: SKIP +test-debugger-object-type-remote-object: SKIP +test-debugger-preserve-breaks: SKIP -+test-debugger-probe-cond: SKIP +test-debugger-probe-cond-invalid: SKIP +test-debugger-probe-cond-max-hit: SKIP +test-debugger-probe-cond-shared: SKIP +test-debugger-probe-cond-throws: SKIP -+test-debugger-profile: SKIP ++test-debugger-probe-cond: SKIP +test-debugger-profile-command: SKIP -+test-debugger-random-port: SKIP ++test-debugger-profile: SKIP +test-debugger-random-port-with-inspect-port: SKIP ++test-debugger-random-port: SKIP +test-debugger-repeat-last: SKIP +test-debugger-restart-message: SKIP +test-debugger-run-after-quit-restart: SKIP @@ -836,8 +852,8 @@ index d13efc8ac1..f52cc544b2 100644 +test-fs-syncwritestream: SKIP +test-fs-write-sigxfsz: SKIP +test-fs-write-stream-patch-open: SKIP -+test-handle-wrap-isrefed: SKIP +test-handle-wrap-hasref: SKIP ++test-handle-wrap-isrefed: SKIP +test-heap-prof-basic: SKIP +test-heap-prof-dir-absolute: SKIP +test-heap-prof-dir-name: SKIP @@ -868,8 +884,8 @@ index d13efc8ac1..f52cc544b2 100644 +test-internal-util-decorate-error-stack: SKIP +test-kill-segfault-freebsd: SKIP +test-listen-fd-cluster: SKIP -+test-listen-fd-detached: SKIP +test-listen-fd-detached-inherit: SKIP ++test-listen-fd-detached: SKIP +test-listen-fd-server: SKIP +test-macos-signed-deps: SKIP +test-math-random: SKIP @@ -902,31 +918,31 @@ index d13efc8ac1..f52cc544b2 100644 +test-pipe-unref: SKIP +test-policy-crypto-default-encoding: SKIP +test-policy-dependencies: SKIP -+test-policy-integrity: SKIP +test-policy-integrity-flag: SKIP ++test-policy-integrity: SKIP +test-policy-manifest: SKIP +test-policy-parse-integrity: SKIP +test-policy-process-binding: SKIP +test-policy-scopes: SKIP -+test-preload: SKIP +test-preload-print-process-argv: SKIP +test-preload-self-referential: SKIP +test-preload-worker: SKIP ++test-preload: SKIP +test-process-argv-0: SKIP +test-process-env: SKIP +test-process-exec-argv: SKIP +test-process-execpath: SKIP +test-process-exit-after-fetch-throw: SKIP +test-process-exit-code: SKIP -+test-process-external-stdio-close: SKIP +test-process-external-stdio-close-spawn: SKIP ++test-process-external-stdio-close: SKIP +test-process-getgroups: SKIP +test-process-kill-null: SKIP +test-process-ppid: SKIP +test-process-raw-debug: SKIP +test-process-really-exit: SKIP -+test-process-redirect-warnings: SKIP +test-process-redirect-warnings-env: SKIP ++test-process-redirect-warnings: SKIP +test-process-remove-all-signal-listeners: SKIP +test-process-title: SKIP +test-process-uncaught-exception-monitor: SKIP @@ -946,8 +962,8 @@ index d13efc8ac1..f52cc544b2 100644 +test-repl-require-context: SKIP +test-repl-require-self-referential: SKIP +test-repl-setprompt: SKIP -+test-repl-sigint: SKIP +test-repl-sigint-nested-eval: SKIP ++test-repl-sigint: SKIP +test-repl-strict-mode-previews: SKIP +test-repl-syntax-error-handling: SKIP +test-repl-tab-complete-nested-repls: SKIP @@ -980,10 +996,10 @@ index d13efc8ac1..f52cc544b2 100644 +test-snapshot-basic: SKIP +test-snapshot-cjs-main: SKIP +test-snapshot-console: SKIP -+test-snapshot-dns-lookup-localhost: SKIP +test-snapshot-dns-lookup-localhost-promise: SKIP -+test-snapshot-dns-resolve-localhost: SKIP ++test-snapshot-dns-lookup-localhost: SKIP +test-snapshot-dns-resolve-localhost-promise: SKIP ++test-snapshot-dns-resolve-localhost: SKIP +test-snapshot-error: SKIP +test-snapshot-eval: SKIP +test-snapshot-gzip: SKIP @@ -994,16 +1010,16 @@ index d13efc8ac1..f52cc544b2 100644 +test-snapshot-warning: SKIP +test-snapshot-weak-reference: SKIP +test-source-map-enable: SKIP -+test-startup-large-pages: SKIP -+test-startup-empty-regexp-statics: SKIP +test-stack-size-limit: SKIP ++test-startup-empty-regexp-statics: SKIP ++test-startup-large-pages: SKIP +test-stdin-child-proc: SKIP +test-stdin-from-file: SKIP +test-stdin-pipe-large: SKIP +test-stdin-pipe-resume: SKIP +test-stdin-resume-pause: SKIP -+test-stdin-script-child: SKIP +test-stdin-script-child-option: SKIP ++test-stdin-script-child: SKIP +test-stdio-closed: SKIP +test-stdio-pipe-redirect: SKIP +test-stdio-pipe-stderr: SKIP @@ -1027,12 +1043,12 @@ index d13efc8ac1..f52cc544b2 100644 +test-tls-cipher-list: SKIP +test-tls-cli-min-max-conflict: SKIP +test-tls-enable-keylog-cli: SKIP -+test-tls-enable-trace: SKIP +test-tls-enable-trace-cli: SKIP ++test-tls-enable-trace: SKIP +test-tls-env-bad-extra-ca: SKIP -+test-tls-env-extra-ca: SKIP +test-tls-env-extra-ca-file-load: SKIP +test-tls-env-extra-ca-no-crypto: SKIP ++test-tls-env-extra-ca: SKIP +test-tls-handshake-exception: SKIP +test-tls-ticket-cluster: SKIP +test-tls-wrap-econnreset-pipe: SKIP @@ -1044,8 +1060,8 @@ index d13efc8ac1..f52cc544b2 100644 +test-trace-events-console: SKIP +test-trace-events-environment: SKIP +test-trace-events-file-pattern: SKIP -+test-trace-events-fs-sync: SKIP +test-trace-events-fs-async: SKIP ++test-trace-events-fs-sync: SKIP +test-trace-events-http: SKIP +test-trace-events-metadata: SKIP +test-trace-events-net: SKIP @@ -1056,8 +1072,8 @@ index d13efc8ac1..f52cc544b2 100644 +test-trace-events-threadpool: SKIP +test-trace-events-v8: SKIP +test-trace-events-vm: SKIP -+test-trace-events-worker-metadata: SKIP +test-trace-events-worker-metadata-with-name: SKIP ++test-trace-events-worker-metadata: SKIP +test-trace-exit: SKIP +test-tracing-no-crash: SKIP +test-unhandled-exception-rethrow-error: SKIP @@ -1066,13 +1082,13 @@ index d13efc8ac1..f52cc544b2 100644 +test-util-callbackify: SKIP +test-v8-coverage: SKIP +test-v8-stop-coverage: SKIP -+test-v8-take-coverage: SKIP +test-v8-take-coverage-noop: SKIP ++test-v8-take-coverage: SKIP +test-v8-untrusted-code-mitigations: SKIP +test-vm-api-handles-getter-errors: SKIP +test-vm-cached-data: SKIP -+test-vm-sigint: SKIP +test-vm-sigint-existing-handler: SKIP ++test-vm-sigint: SKIP +test-vm-syntax-error-message: SKIP +test-vm-syntax-error-stderr: SKIP +test-worker-init-failure: SKIP @@ -1132,6 +1148,7 @@ index d13efc8ac1..f52cc544b2 100644 +# jitless configuration on the host instead (host-smoke workflow). +# +test-fetch-disabled: SKIP ++test-internal-webidl-buffer-source: SKIP +test-vm-codegen: SKIP +test-wasm: SKIP +test-wasm-simple: SKIP @@ -1257,6 +1274,7 @@ index d13efc8ac1..f52cc544b2 100644 +test-permission-drop-fs-scope: SKIP +test-permission-drop-fs-write: SKIP +test-permission-drop-worker: SKIP ++test-permission-fs-filehandle-sync: SKIP +test-permission-fs-write: SKIP +test-permission-fs-windows-path: SKIP +# @@ -1442,7 +1460,6 @@ index d13efc8ac1..f52cc544b2 100644 +test-http-server-drop-connections-in-cluster: SKIP +test-http2-respond-file-with-pipe: SKIP +test-http2-respond-with-file-connection-abort: SKIP -+test-intl: SKIP +test-is-internal-thread: SKIP +test-macos-app-sandbox: SKIP +test-module-loading-error: SKIP @@ -1630,6 +1647,19 @@ index 51d7153de0..9b1395f99b 100644 assert.strictEqual(err.path, src); assert.strictEqual(err.dest, dest); } +diff --git a/test/parallel/test-fs-glob-no-follow-symlink-globstar.mjs b/test/parallel/test-fs-glob-no-follow-symlink-globstar.mjs +index b1b3cda8d0..958b2cb300 100644 +--- a/test/parallel/test-fs-glob-no-follow-symlink-globstar.mjs ++++ b/test/parallel/test-fs-glob-no-follow-symlink-globstar.mjs +@@ -54,7 +54,7 @@ assert.deepStrictEqual(callbackMatches.sort(), expected); + + // Under the permission model, `**/*` must not expose entries from a symlink + // target that is outside the granted read set. +-if (common.hasCrypto) { ++if (common.hasCrypto && !common.isAndroid && !common.isIOS) { // nodejs-mobile patch + const child = ` + const assert = require('node:assert'); + const fs = require('node:fs'); diff --git a/test/parallel/test-fs-lchmod.js b/test/parallel/test-fs-lchmod.js index d439710291..d048680b95 100644 --- a/test/parallel/test-fs-lchmod.js @@ -1787,6 +1817,20 @@ index 6ad074397e..c27aee2759 100644 try { fs.unlinkSync(link); } catch { // Continue regardless of error. } +diff --git a/test/parallel/test-fs-rmSync-special-char-additional-error.js b/test/parallel/test-fs-rmSync-special-char-additional-error.js +index 9b4de3c84d..7b2ebf1290 100644 +--- a/test/parallel/test-fs-rmSync-special-char-additional-error.js ++++ b/test/parallel/test-fs-rmSync-special-char-additional-error.js +@@ -43,7 +43,8 @@ try { + }, (err) => { + // Verify that the error is due to permission restrictions + let expectedCode = 'EACCES'; +- if (common.isMacOS) { ++ // nodejs-mobile patch: both device suites return ENOTEMPTY for this case. ++ if (common.isMacOS || common.isAndroid || common.isIOS) { + expectedCode = 'ENOTEMPTY'; + } else if (common.isWindows) { + expectedCode = 'EPERM'; diff --git a/test/parallel/test-fs-rmdir-recursive.js b/test/parallel/test-fs-rmdir-recursive.js index 77c205794b..4c52904b43 100644 --- a/test/parallel/test-fs-rmdir-recursive.js @@ -1929,6 +1973,28 @@ index e5d0cfe880..ff428d3db2 100644 const server = http2.createServer(); server.on('stream', (stream) => { +diff --git a/test/parallel/test-intl.js b/test/parallel/test-intl.js +index 7d1742f2c7..28df1d924a 100644 +--- a/test/parallel/test-intl.js ++++ b/test/parallel/test-intl.js +@@ -140,7 +140,7 @@ if (!common.hasIntl) { + // `ffi` ligature (contraction) + assert.strictEqual(coll.compare('\ufb03', 'ffi'), 0); + +- { ++ if (!common.isAndroid && !common.isIOS) { // nodejs-mobile patch: child Node is unavailable. + // Regression test for https://github.com/nodejs/node/issues/27379 + const env = { ...process.env, LC_ALL: 'ja' }; + execFile( +@@ -150,7 +150,7 @@ if (!common.hasIntl) { + ); + } + +- { ++ if (!common.isAndroid && !common.isIOS) { // nodejs-mobile patch: child Node is unavailable. + // Regression test for https://github.com/nodejs/node/issues/27418 + const env = { ...process.env, LC_ALL: 'fr@EURO' }; + execFile( diff --git a/test/parallel/test-net-connect-options-fd.js b/test/parallel/test-net-connect-options-fd.js index bd761cf072..c913299ef7 100644 --- a/test/parallel/test-net-connect-options-fd.js @@ -2067,7 +2133,7 @@ index a2df7d580d..eeb6c1de91 100644 + +assert.deepStrictEqual(process.getActiveResourcesInfo(), expected); diff --git a/test/parallel/test-process-versions.js b/test/parallel/test-process-versions.js -index b670ff37ac..68724daf7f 100644 +index 7f54beeea6..14f222d299 100644 --- a/test/parallel/test-process-versions.js +++ b/test/parallel/test-process-versions.js @@ -2,8 +2,15 @@ @@ -2188,6 +2254,29 @@ index b84d2063a2..4411854e0b 100644 } }, 1); +diff --git a/test/parallel/test-sqlite-backup.mjs b/test/parallel/test-sqlite-backup.mjs +index ee5f54fd90..cad9dc5e90 100644 +--- a/test/parallel/test-sqlite-backup.mjs ++++ b/test/parallel/test-sqlite-backup.mjs +@@ -1,5 +1,7 @@ + // Flags: --expose-gc + import { ++ isAndroid, ++ isIOS, + isWindows, + skipIfSQLiteMissing, + spawnPromisified, +@@ -380,7 +382,9 @@ test('source database is kept alive while a backup is in flight', async (t) => { + t.assert.strictEqual(rows.n, 500); + }); + +-test('backup promise settles when the backup is the last active request', async (t) => { ++test('backup promise settles when the backup is the last active request', { ++ skip: isAndroid || isIOS, // nodejs-mobile patch: requires a separate Node process. ++}, async (t) => { + const { code, signal, stderr } = await spawnPromisified(process.execPath, [ + fixtures.path('sqlite', 'backup-last-request.mjs'), + nextDb(), diff --git a/test/parallel/test-watch-mode-files_watcher.mjs b/test/parallel/test-watch-mode-files_watcher.mjs index e1595350cd..f5d84f7480 100644 --- a/test/parallel/test-watch-mode-files_watcher.mjs @@ -2208,10 +2297,10 @@ index e1595350cd..f5d84f7480 100644 + */ }); diff --git a/test/sequential/sequential.status b/test/sequential/sequential.status -index 01a021130c..059f682010 100644 +index 6b5714500b..9041485ad6 100644 --- a/test/sequential/sequential.status +++ b/test/sequential/sequential.status -@@ -39,6 +39,135 @@ test-perf-hooks: SKIP +@@ -43,6 +43,138 @@ test-perf-hooks: SKIP # https://github.com/nodejs/node/issues/39683 test-tls-psk-client: PASS, FLAKY @@ -2241,6 +2330,8 @@ index 01a021130c..059f682010 100644 +test-stream2-stderr-sync: SKIP +test-util-debug: SKIP +test-vm-timeout-rethrow: SKIP ++test-watch-mode-inspect: SKIP ++test-watch-mode: SKIP +test-worker-prof: SKIP +# +# Tests that require the inspector, not included in the build @@ -2298,6 +2389,7 @@ index 01a021130c..059f682010 100644 +test-stream2-stderr-sync: SKIP +test-util-debug: SKIP +test-vm-timeout-rethrow: SKIP ++test-watch-mode-inspect: SKIP +test-worker-prof: SKIP +# +# Tests that require the inspector, not included in the build @@ -2348,7 +2440,7 @@ index 01a021130c..059f682010 100644 # https://github.com/nodejs/node/issues/49933 test-watch-mode-inspect: SKIP diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js -index 5db4a77631..dba092fd3f 100644 +index ef48f457a8..529cc62b12 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -12,6 +12,8 @@ const providers = { ...internalBinding('async_wrap').Providers }; @@ -2373,7 +2465,7 @@ index 5db4a77631..dba092fd3f 100644 const objKeys = Object.keys(providers); if (objKeys.length > 0) process._rawDebug(objKeys); -@@ -196,7 +204,8 @@ if (common.hasCrypto) { // eslint-disable-line node-core/crypto-check +@@ -198,7 +206,8 @@ if (common.hasCrypto) { // eslint-disable-line node-core/crypto-check testInitialized(handle, 'Pipe'); } @@ -2383,7 +2475,7 @@ index 5db4a77631..dba092fd3f 100644 tmpdir.refresh(); const server = net.createServer(common.mustCall((socket) => { -@@ -328,6 +337,8 @@ if (common.hasCrypto) { // eslint-disable-line node-core/crypto-check +@@ -330,6 +339,8 @@ if (common.hasCrypto) { // eslint-disable-line node-core/crypto-check // DIRHANDLE { const dirBinding = internalBinding('fs_dir'); diff --git a/scripts/clone-upstream.sh b/scripts/clone-upstream.sh new file mode 100755 index 0000000000..95c1f0d6d7 --- /dev/null +++ b/scripts/clone-upstream.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Clone an upstream tag/branch or an immutable PR commit, without inventing tags. +# Usage: scripts/clone-upstream.sh +set -euo pipefail + +REPO=$1 +REF=$2 +OUT=$3 +[ ! -e "$OUT" ] || { echo "error: $OUT already exists" >&2; exit 1; } + +if [[ "$REF" =~ ^[0-9a-f]{40}$ ]]; then + git init -q "$OUT" + git -C "$OUT" remote add origin "$REPO" + git -C "$OUT" fetch --depth 1 origin "$REF" + git -C "$OUT" checkout -q --detach FETCH_HEAD + GOT=$(git -C "$OUT" rev-parse HEAD) + [ "$GOT" = "$REF" ] || { echo "error: upstream commit mismatch: $GOT != $REF" >&2; exit 1; } +else + git clone --depth 1 --branch "$REF" "$REPO" "$OUT" +fi +# Local metadata for the in-tree test audit, including unreleased PR snapshots. +git -C "$OUT" update-ref refs/nodejs-mobile/upstream-base HEAD diff --git a/scripts/package-preview.py b/scripts/package-preview.py new file mode 100755 index 0000000000..2130f5ac5e --- /dev/null +++ b/scripts/package-preview.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Package this run's tested mobile artifacts without publishing a release.""" + +import hashlib +import json +import os +from pathlib import Path +import re +import subprocess +import sys +import zipfile + + +def main(): + source, output = map(Path, sys.argv[1:]) + header = Path('mobile-src/src/node_mobile_version.h').read_text() + values = [re.search(rf'^#define {key} (\d+)$', header, re.M).group(1) + for key in ('NODE_MOBILE_MAJOR_VERSION', 'NODE_MOBILE_MINOR_VERSION', + 'NODE_MOBILE_PATCH_VERSION', 'NODE_MOBILE_REVISION')] + version = '.'.join(values[:3]) + '-' + values[3] + base = next(line.strip() for line in Path('upstream-base.txt').read_text().splitlines() + if line.strip() and not line.startswith('#')) + tree = next(line for line in Path('expected-tree.txt').read_text().splitlines() + if re.fullmatch(r'[0-9a-f]{40}', line)) + sha = subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip() + output.mkdir(parents=True, exist_ok=True) + checksums = [] + for platform in ('android', 'ios'): + for flavor in ('full', 'lite'): + suffix = '-lite' if flavor == 'lite' else '' + name = f'nodejs-mobile-{platform}{suffix}' + artifact = source / name + required = ['include/node/node_version.h'] + if platform == 'android': + required += [f'bin/{abi}/libnode.so' + for abi in ('arm64-v8a', 'armeabi-v7a', 'x86_64')] + else: + required += ['NodeMobile.xcframework/Info.plist'] + for path in required: + if not (artifact / path).is_file(): + raise SystemExit(f'Missing {name}/{path}') + info = dict(version=version, upstream=base, platform=platform, + flavor=flavor, recipe_commit=sha, source_tree=tree, + branch=os.environ.get('GITHUB_REF_NAME', ''), + run_url=f"https://github.com/{os.environ['GITHUB_REPOSITORY']}/actions/runs/{os.environ['GITHUB_RUN_ID']}", + preview=True, icu_api_mode='full-icu', + icu_data_profile='complete' if flavor == 'full' else 'danmu-lite', + android_symbols='stripped' if platform == 'android' and flavor == 'lite' else 'default', + enabled_features=((['full-icu'] + (['node:ffi', 'Temporal'] if int(values[0]) >= 26 else [])) + if flavor == 'full' else ['icu', 'danmu-icu-profile']), + disabled_features=[] if flavor == 'full' else [ + 'node:sqlite', 'Web Storage', + 'Inspector', 'TypeScript type-stripping', 'SEA', + 'V8 native debugger object print'] + (['node:ffi', 'Temporal'] if int(values[0]) >= 26 else [])) + (artifact / 'BUILD-INFO.json').write_text(json.dumps(info, indent=2) + '\n') + package = output / f'{name}-{version}-preview.zip' + with zipfile.ZipFile(package, 'w', zipfile.ZIP_DEFLATED) as archive: + for file in sorted(artifact.rglob('*')): + if file.is_file(): + archive.write(file, file.relative_to(artifact)) + with package.open('rb') as package_file: + digest = hashlib.file_digest(package_file, 'sha256').hexdigest() + checksums.append(f'{digest} {package.name}') + print(package) + (output / 'SHA256SUMS').write_text('\n'.join(checksums) + '\n') + + +if __name__ == '__main__': + main() diff --git a/scripts/prepare.sh b/scripts/prepare.sh index cc47969d35..ae4c589c4a 100755 --- a/scripts/prepare.sh +++ b/scripts/prepare.sh @@ -9,7 +9,7 @@ # # Env overrides (mainly for CI/offline use): # UPSTREAM_REPO git URL or local path (default https://github.com/nodejs/node.git) -# UPSTREAM_REF tag/branch to clone (default: first line of upstream-base.txt) +# UPSTREAM_REF tag/branch/full SHA (default: first line of upstream-base.txt) # SKIP_VERIFY=1 don't fail on an expected-tree mismatch (prints both hashes) set -euo pipefail @@ -27,7 +27,7 @@ if [ -e "${OUT}" ]; then fi echo "1/4 Cloning ${UPSTREAM_REPO} @ ${UPSTREAM_REF} (shallow)..." -git clone --depth 1 --branch "${UPSTREAM_REF}" "${UPSTREAM_REPO}" "${OUT}" +"${HERE}/scripts/clone-upstream.sh" "${UPSTREAM_REPO}" "${UPSTREAM_REF}" "${OUT}" git -C "${OUT}" config user.name "${GIT_AUTHOR_NAME:-nodejs-mobile}" git -C "${OUT}" config user.email "${GIT_AUTHOR_EMAIL:-nodejs-mobile@invalid}" diff --git a/scripts/release-control.py b/scripts/release-control.py index 8f7fce9a70..d3c5c838ba 100644 --- a/scripts/release-control.py +++ b/scripts/release-control.py @@ -38,7 +38,10 @@ def upstream(root): lines = [line.strip() for line in (root / 'upstream-base.txt').read_text().splitlines() if line.strip() and not line.lstrip().startswith('#')] if len(lines) != 1: - raise ReleaseError('upstream-base.txt must pin exactly one upstream release tag') + raise ReleaseError('upstream-base.txt must pin exactly one upstream ref') + if re.fullmatch(r'[0-9a-f]{40}', lines[0]): + raise ReleaseError('This source pins an unreleased upstream PR commit. ' + 'Build a CI preview; pin the official release tag before Cut release or publication.') return parse_version(lines[0]) diff --git a/scripts/test-android-proxy.py b/scripts/test-android-proxy.py new file mode 100644 index 0000000000..d506b8c1fd --- /dev/null +++ b/scripts/test-android-proxy.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""Exercise Android launch recovery without rerunning an acknowledged test.""" +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parent.parent +PROXY = ROOT / 'out/tools/mobile-test/android/node-android-proxy.sh' + +ADB = r'''#!/usr/bin/env python3 +import json, os, pathlib, sys +root = pathlib.Path(os.environ['MOCK_ROOT']) +config = json.loads(os.environ['MOCK_CONFIG']) +state_path = root / 'state.json' +state = json.loads(state_path.read_text()) if state_path.exists() else { + 'launches': 0, 'stops': 0, 'clears': 0, 'alive': True, 'started': False, + 'verdict': '', 'old_output': True, 'force_failed': False} +args = sys.argv[1:] +with (root / 'commands.jsonl').open('a') as log: + log.write(json.dumps(args) + '\n') +if args[:2] == ['-s', 'fixture-device']: args = args[2:] +cmd = args[1] if args[:1] == ['shell'] else '' +code = 0 +if 'am force-stop' in cmd: + state['stops'] += 1 + if state['stops'] <= config.get('stop_failures', 0): + state['force_failed'] = True + print('error: closed', file=sys.stderr) + code = 1 + elif state['stops'] > config.get('stop_keeps_alive', 0): + state['alive'] = False + if code == 0 and 'rm -f' in cmd: + # A late acknowledgement during force-stop must prevent a relaunch. + state['started'] = False +elif args == ['logcat', '-c']: + state['clears'] += 1 + if state['clears'] <= config.get('clear_failures', 0): + print('logcat transport failed', file=sys.stderr) + code = 1 + else: state['old_output'] = False +elif 'am start -W' in cmd: + state['launches'] += 1 + assert not state['alive'], 'launched over stale activity' + assert not state['old_output'], 'launched with stale logs' + launches = config.get('launches', ['PASS']) + launch = launches[min(state['launches'] - 1, len(launches) - 1)] + state['started'] = launch not in ('NO_START', 'QUERY_ERROR', 'LATE_START') + state['query_error'] = launch == 'QUERY_ERROR' + state['late_start'] = launch == 'LATE_START' + state['alive'] = state['started'] and launch != 'CRASH' + state['verdict'] = launch if launch in ('PASS', 'FAIL') else '' + if launch == 'LOST_REPLY': + state['verdict'] = 'PASS' + if launch in ('NO_START', 'LOST_REPLY'): + print('am transport failed', file=sys.stderr) + code = 1 + else: print('Status: ok') +elif 'pids=$(pidof' in cmd: + print('RUNNING' if state['alive'] else 'STOPPED') +elif 'if [ -f files/started-' in cmd: + if state.get('query_error'): + print('sandbox query transport failed', file=sys.stderr) + code = 1 + else: + if state.get('late_start') and not state['alive'] and state['stops'] > 1: + state['started'] = True + print('STARTED' if state['started'] else 'NOT_STARTED') +elif 'cat files/result-' in cmd: + print(state['verdict']) +elif 'logcat -d' in cmd: + if '-v raw' in cmd: print('current test output') +else: + print('unexpected adb call: ' + repr(args), file=sys.stderr) + code = 91 +state_path.write_text(json.dumps(state)) +sys.exit(code) +''' + + +class AndroidProxy(unittest.TestCase): + def run_proxy(self, **config): + with tempfile.TemporaryDirectory(prefix='android-proxy-') as folder: + root = Path(folder) + proxy = root / 'tools/mobile-test/android/node-android-proxy.sh' + proxy.parent.mkdir(parents=True) + (root / 'tools/test').mkdir() + shutil.copy2(PROXY, proxy) + tools = root / 'bin' + tools.mkdir() + adb = tools / 'adb' + adb.write_text(ADB) + adb.chmod(0o755) + env = dict(os.environ, PATH=str(tools) + os.pathsep + os.environ['PATH'], + MOCK_ROOT=str(root), MOCK_CONFIG=json.dumps(config), + DEVICE_ID='fixture-device', NODE_ANDROID_PROXY_TIMEOUT='1', + NODE_ANDROID_START_TIMEOUT='1', NODE_ANDROID_LAUNCH_ATTEMPTS='3') + result = subprocess.run(['bash', str(proxy), '/host/test/parallel/test-https-simple.js'], + env=env, text=True, capture_output=True, timeout=20) + state = json.loads((root / 'state.json').read_text()) + commands = [json.loads(line) for line in (root / 'commands.jsonl').read_text().splitlines()] + self.assertTrue(all(args[:2] == ['-s', 'fixture-device'] for args in commands)) + return result, state + + def test_pass_uses_current_output_and_device(self): + result, state = self.run_proxy() + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, 'current test output\n') + self.assertEqual(state['launches'], 1) + + def test_failed_stop_is_recovered_before_launch(self): + result, state = self.run_proxy(stop_failures=1) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(state['launches'], 1) + self.assertEqual(state['stops'], 2) + self.assertIn('error: closed', result.stderr) + + def test_old_activity_must_be_gone_before_launch(self): + result, state = self.run_proxy(stop_keeps_alive=1) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(state['launches'], 1) + self.assertEqual(state['stops'], 2) + + def test_failed_log_clear_is_recovered_before_launch(self): + result, state = self.run_proxy(clear_failures=1) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(state['launches'], 1) + self.assertEqual(state['clears'], 2) + + def test_failed_launch_before_node_starts_can_recover(self): + result, state = self.run_proxy(launches=['NO_START', 'PASS']) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(state['launches'], 2) + self.assertIn('am transport failed', result.stderr) + + def test_lost_adb_reply_does_not_rerun_started_test(self): + result, state = self.run_proxy(launches=['LOST_REPLY', 'FAIL']) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(state['launches'], 1) + + def test_started_failure_hang_and_crash_are_never_retried(self): + for kind in ('FAIL', 'HANG', 'CRASH'): + with self.subTest(kind=kind): + result, state = self.run_proxy(launches=[kind, 'PASS']) + self.assertEqual(result.returncode, 1, result.stderr) + self.assertEqual(state['launches'], 1) + if kind != 'FAIL': + self.assertIn('hung' if kind == 'HANG' else 'crashed', result.stderr) + + def test_exhausted_launches_cannot_succeed(self): + result, state = self.run_proxy(launches=['NO_START']) + self.assertEqual(result.returncode, 1) + self.assertEqual(state['launches'], 3) + self.assertIn('after 3 attempts', result.stderr) + + def test_exhausted_preparation_cannot_launch(self): + result, state = self.run_proxy(stop_failures=3) + self.assertEqual(result.returncode, 1) + self.assertEqual(state['launches'], 0) + + def test_unknown_or_late_start_cannot_authorize_retry(self): + for kind in ('QUERY_ERROR', 'LATE_START'): + with self.subTest(kind=kind): + result, state = self.run_proxy(launches=[kind, 'PASS']) + self.assertEqual(result.returncode, 1, result.stderr) + self.assertEqual(state['launches'], 1) + self.assertIn('refusing relaunch', result.stderr) + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/test-android-suite.py b/scripts/test-android-suite.py new file mode 100644 index 0000000000..68a44d7f98 --- /dev/null +++ b/scripts/test-android-suite.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Check Android shard coverage and preserve failures through the log pipeline.""" +import ast +import contextlib +import importlib.util +import io +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +import tempfile +from types import SimpleNamespace +import unittest +import warnings + +RECIPE = Path(__file__).resolve().parent.parent +SOURCE = Path(sys.argv.pop(1)).resolve() if len(sys.argv) > 1 else RECIPE / 'out' +RUNNER = SOURCE / 'tools/mobile-test/android/run-suite-shard.sh' + + +def load_module(name, path): + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +class QuietOutput(io.StringIO): + def reconfigure(self, **_): + pass + + +class AndroidSuite(unittest.TestCase): + def setUp(self): + workflow = (RECIPE / '.github/workflows/full-device-suite.yml').read_text() + android = workflow.split('\n android:\n', 1)[1].split('\n ios:\n', 1)[0] + self.shards = ast.literal_eval(re.search(r'^ shard: (.+)$', android, re.M)[1]) + self.count = int(re.search(r'^ ANDROID_SUITE_SHARDS: (\d+)$', android, re.M)[1]) + + def test_matrix_matches_denominator(self): + self.assertEqual(self.shards, list(range(self.count))) + + def test_watch_cli_exclusions_preserve_in_process_device_tests(self): + sys.path.insert(0, str(SOURCE / 'tools')) + self.addCleanup(sys.path.remove, str(SOURCE / 'tools')) + runner = load_module('mobile_watch_status_runner', SOURCE / 'tools/test.py') + for suite, names in { + 'sequential': ['test-watch-mode', 'test-watch-mode-inspect', 'test-tls-connect'], + 'parallel': ['test-debugger-run-restart-init', 'test-debugger-wait-for-debugger', + 'test-fs-watch', 'test-fs-watch-persistent'], + }.items(): + sections, defs = [], {} + with warnings.catch_warnings(): + warnings.simplefilter('ignore', ResourceWarning) + runner.ReadConfigurationInto(str(SOURCE / 'test' / suite / (suite + '.status')), + sections, defs) + config = runner.Configuration(sections, defs) + for platform in ('android', 'ios', 'linux'): + with self.subTest(suite=suite, platform=platform): + cases = [SimpleNamespace(path=[suite, name]) for name in names] + env = dict(system=platform, arch='x64', mode='release', type='simple', + asan='off', pointer_compression='false') + classified, _ = config.ClassifyTests(cases, env) + skipped = {case.path[-1] for case in classified if runner.SKIP in case.outcomes} + expected = set(names[:2]) if suite == 'sequential' and platform != 'linux' else set() + self.assertEqual(skipped, expected) + + def test_native_wasm_flag_exclusion_keeps_other_regressions_runnable(self): + sys.path.insert(0, str(SOURCE / 'tools')) + self.addCleanup(sys.path.remove, str(SOURCE / 'tools')) + runner = load_module('mobile_buffer_status_runner', SOURCE / 'tools/test.py') + sections, defs = [], {} + with warnings.catch_warnings(): + warnings.simplefilter('ignore', ResourceWarning) + runner.ReadConfigurationInto(str(SOURCE / 'test/parallel/parallel.status'), sections, defs) + config = runner.Configuration(sections, defs) + names = ['test-internal-webidl-buffer-source', 'test-intl', 'test-sqlite-backup', + 'test-fs-rmSync-special-char-additional-error'] + for platform in ('android', 'ios', 'linux'): + with self.subTest(platform=platform): + cases = [SimpleNamespace(path=['parallel', name]) for name in names] + env = dict(system=platform, arch='x64', mode='release', type='simple', + asan='off', pointer_compression='false') + classified, _ = config.ClassifyTests(cases, env) + skipped = {case.path[-1] for case in classified if runner.SKIP in case.outcomes} + self.assertEqual(skipped, {names[0]} if platform == 'ios' else set()) + + def test_real_test_selector_covers_every_runnable_case_once(self): + # Use upstream's actual classification and --run selector, replacing + # only device execution. This detects missing/duplicate coverage when + # the workflow matrix or a future upstream test runner changes. + sys.path.insert(0, str(SOURCE / 'tools')) + self.addCleanup(sys.path.remove, str(SOURCE / 'tools')) + runner = load_module('android_suite_test_runner', SOURCE / 'tools/test.py') + coverage = load_module('android_suite_coverage', + SOURCE / 'tools/mobile-test/coverage-manifest.py') + selected = [] + + def collect(cases, *_): + selected.extend((case.arch, case.mode, case.file, tuple(case.path)) + for case in cases) + return {'allPassed': True, 'failed': []} + + runner.RunTestCases = collect + saved_cwd, saved_argv = os.getcwd(), sys.argv + try: + os.chdir(SOURCE) + with tempfile.TemporaryDirectory() as folder, coverage.host_binary_present(): + stub = str(Path(folder) / 'stub-node') + coverage.write_stub(stub) + + def select(shard=None): + selected.clear() + sys.argv = ['tools/test.py', '-j', '1', '--flaky-tests=skip', + '--arch', 'android', '--shell', stub] + if shard is not None: + sys.argv.append(f'--run={shard},{self.count}') + sys.argv.extend(['parallel', 'sequential']) + with contextlib.redirect_stdout(QuietOutput()), warnings.catch_warnings(): + # Upstream Execute reads temporary files without an + # explicit close. This host-only selection check calls + # it repeatedly; keep those warnings out of CI output. + warnings.simplefilter('ignore', ResourceWarning) + self.assertEqual(runner.Main(), 0) + return list(selected) + + expected = select() + actual = [] + sizes = [] + for shard in self.shards: + cases = select(shard) + sizes.append(len(cases)) + actual.extend(cases) + self.assertTrue(expected) + self.assertEqual(len(actual), len(set(actual))) + self.assertCountEqual(actual, expected) + print(f'Android coverage: {len(expected)} runnable cases, ' + f'{self.count} shards of {min(sizes)}-{max(sizes)}; no gaps or duplicates') + finally: + os.chdir(saved_cwd) + sys.argv = saved_argv + + def run_fake_suite(self, exit_code, args): + with tempfile.TemporaryDirectory() as folder: + root = Path(folder) + script = root / 'tools/mobile-test/android/run-suite-shard.sh' + script.parent.mkdir(parents=True) + shutil.copy2(RUNNER, script) + record = root / 'invocation.json' + test = root / 'tools/test.py' + test.write_text(f'#!{sys.executable}\nimport json,sys\n' + f'open({str(record)!r}, "w").write(json.dumps(sys.argv[1:]))\n' + 'print("suite output")\n' + f'sys.exit({exit_code})\n') + test.chmod(0o755) + # Keep diagnostics local; no device or adb daemon is required. + fake_adb = root / 'adb' + fake_adb.write_text('#!/bin/sh\nexit 0\n') + fake_adb.chmod(0o755) + proc = subprocess.run(['bash', str(script), *args], cwd=root, + env=dict(os.environ, PATH=str(root) + os.pathsep + os.environ['PATH']), + capture_output=True, text=True, timeout=20) + invocation = json.loads(record.read_text()) if record.exists() else None + log = (root / 'shard.log').read_text() if (root / 'shard.log').exists() else '' + resources = (root / 'shard-resources.log').exists() + return proc, invocation, log, resources + + def test_success_keeps_test_output_and_selector(self): + proc, invocation, log, resources = self.run_fake_suite(0, ['3', str(self.count)]) + self.assertEqual(proc.returncode, 0, proc.stderr) + self.assertIn(f'--run=3,{self.count}', invocation) + self.assertEqual(invocation[-2:], ['parallel', 'sequential']) + self.assertIn('suite output', log) + self.assertTrue(resources) + + def test_failure_is_not_hidden_by_tee_or_cleanup(self): + proc, _, log, resources = self.run_fake_suite(7, ['0', str(self.count)]) + self.assertEqual(proc.returncode, 7, proc.stderr) + self.assertIn('suite output', log) + self.assertTrue(resources) + + def test_invalid_shard_cannot_run_tests(self): + for args in (['-1', '16'], ['16', '16'], ['0', '0'], ['x', '16']): + with self.subTest(args=args): + proc, invocation, _, _ = self.run_fake_suite(0, args) + self.assertEqual(proc.returncode, 2, proc.stderr) + self.assertIsNone(invocation) + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/test-device-compat.py b/scripts/test-device-compat.py new file mode 100644 index 0000000000..f75c2b1b24 --- /dev/null +++ b/scripts/test-device-compat.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Verify Intl keeps its in-process assertions without spawning on mobile.""" +from pathlib import Path +import json +import subprocess +import sys +import unittest + +SOURCE = Path(sys.argv.pop(1)).resolve() if len(sys.argv) > 1 else Path(__file__).resolve().parent.parent / 'out' + + +class DeviceCompatibility(unittest.TestCase): + def test_intl_retains_assertions_and_desktop_locale_startup_checks(self): + script = r''' +const fs = require('node:fs'); +const vm = require('node:vm'); +const assert = require('node:assert'); +const source = fs.readFileSync(process.argv[2], 'utf8'); +const platform = process.argv[3]; +let assertions = 0; +const assertionAPI = new Proxy(assert, { + get(target, key) { + if (key !== 'strictEqual') return Reflect.get(target, key); + return (...args) => { assertions++; return assert.strictEqual(...args); }; + }, +}); +const children = []; +const common = { + hasIntl: true, + isAndroid: platform === 'android', + isIOS: platform === 'ios', + mustSucceed: () => (error) => assert.ifError(error), +}; +vm.runInNewContext(`(function() {\n${source}\n})();`, { + process: { execPath: process.execPath, env: {}, config: { variables: { + v8_enable_i18n_support: 1, icu_locales: '', + } } }, + console, + require(name) { + if (name === '../common') return common; + if (name === 'assert') return assertionAPI; + if (name === 'child_process') return { execFile(...args) { + assert.strictEqual(platform, 'linux', 'mobile attempted to spawn a child Node'); + children.push(args[2].env.LC_ALL); + args[3](null); + } }; + throw new Error(`Unexpected import ${name}`); + }, +}, { filename: process.argv[2] }); +console.log(JSON.stringify({ assertions, children })); +''' + counts = [] + for platform in ('android', 'ios', 'linux'): + with self.subTest(platform=platform): + result = subprocess.run( + ['node', '-', str(SOURCE / 'test/parallel/test-intl.js'), platform], + input=script, text=True, capture_output=True) + self.assertEqual(result.returncode, 0, result.stderr) + report = json.loads(result.stdout) + self.assertGreaterEqual(report['assertions'], 15) + counts.append(report['assertions']) + self.assertEqual(report['children'], ['ja', 'fr@EURO'] if platform == 'linux' else []) + self.assertEqual(len(set(counts)), 1, 'mobile must retain every in-process assertion') + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/test-emulator-preparation.py b/scripts/test-emulator-preparation.py new file mode 100644 index 0000000000..4ba474c87e --- /dev/null +++ b/scripts/test-emulator-preparation.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +"""Exercise verified emulator installs and SDK forwarding without the network.""" +from pathlib import Path +import hashlib +import json +import os +import shutil +import subprocess +import tempfile +import unittest +import zipfile + +ROOT = Path(__file__).resolve().parent.parent +ACTION = ROOT / '.github/actions/prepare-android-emulator' + + +class EmulatorPreparation(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix='mobile-emulator-test-') + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.action = self.root / 'action' + shutil.copytree(ACTION, self.action) + self.sdk = self.root / 'android sdk' + self.manager = self.sdk / 'cmdline-tools/latest/bin/sdkmanager' + self.manager.parent.mkdir(parents=True) + self.manager.write_text( + '#!/usr/bin/env python3\nimport json,os,sys\n' + 'open(os.environ["FORWARDED"],"w").write(json.dumps(sys.argv[1:]))\n' + 'sys.exit(int(os.environ.get("SDK_EXIT", "0")))\n') + self.manager.chmod(0o755) + self.original = self.manager.read_bytes() + self.runner = self.root / 'runner temp' + self.runner.mkdir() + self.archive = self.runner / 'emulator-linux_x64-16428233.zip' + self.payload = self.root / 'good.zip' + info = zipfile.ZipInfo('emulator/emulator') + info.external_attr = 0o100755 << 16 + with zipfile.ZipFile(self.payload, 'w') as archive: + archive.writestr(info, '#!/bin/sh\nprintf "%s\\n" "Android emulator version ' + '37.2.12.0 (build_id 16428233)"\n') + archive.writestr('emulator/source.properties', + 'Pkg.Revision=37.2.12\nPkg.BuildId=16428233\n') + pin = (self.action / 'pin.sh').read_text() + import re + pin = re.sub(r'^EMULATOR_SIZE=.*$', f'EMULATOR_SIZE={self.payload.stat().st_size}', pin, flags=re.M) + digest = hashlib.sha256(self.payload.read_bytes()).hexdigest() + pin = re.sub(r'^EMULATOR_SHA256=.*$', 'EMULATOR_SHA256=' + digest, pin, flags=re.M) + (self.action / 'pin.sh').write_text(pin) + self.bin = self.root / 'bin' + self.bin.mkdir() + curl = self.bin / 'curl' + curl.write_text('''#!/usr/bin/env python3 +import os,pathlib,shutil,sys +count=pathlib.Path(os.environ['CURL_COUNT']) +n=int(count.read_text())+1 if count.exists() else 1 +count.write_text(str(n)) +output=pathlib.Path(sys.argv[sys.argv.index('-o')+1]) +if n <= int(os.environ.get('BAD_TRANSFERS','0')): + output.write_bytes(b'truncated zip') +else: + shutil.copyfile(os.environ['GOOD_ARCHIVE'],output) +''') + curl.chmod(0o755) + sleep = self.bin / 'sleep' + sleep.write_text('#!/bin/sh\nexit 0\n') + sleep.chmod(0o755) + self.env = dict(os.environ, ANDROID_HOME=str(self.sdk), RUNNER_TEMP=str(self.runner), + PATH=str(self.bin) + os.pathsep + os.environ['PATH'], + FORWARDED=str(self.root / 'forwarded.json'), + CURL_COUNT=str(self.root / 'curl-count'), GOOD_ARCHIVE=str(self.payload)) + + def install(self, **overrides): + return subprocess.run(['bash', str(self.action / 'install.sh')], + env=dict(self.env, **overrides), text=True, capture_output=True) + + def sdkmanager(self, *args, **overrides): + return subprocess.run([str(self.manager), *args], env=dict(self.env, **overrides), + text=True, capture_output=True) + + def test_verified_cache_avoids_network_and_stable_update(self): + shutil.copyfile(self.payload, self.archive) + result = self.install() + self.assertEqual(result.returncode, 0, result.stderr) + self.assertFalse(Path(self.env['CURL_COUNT']).exists()) + result = self.sdkmanager('--install', 'emulator', '--channel=0') + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn('checksum-verified', result.stdout) + self.assertFalse(Path(self.env['FORWARDED']).exists()) + + def test_corrupt_cache_and_transfer_are_replaced_only_after_verification(self): + self.archive.write_bytes(b'corrupt cached response') + result = self.install(BAD_TRANSFERS='1') + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(Path(self.env['CURL_COUNT']).read_text(), '2') + self.assertEqual(self.archive.read_bytes(), self.payload.read_bytes()) + + def test_exhausted_download_does_not_replace_tools_or_start_tests(self): + result = self.install(BAD_TRANSFERS='9') + self.assertNotEqual(result.returncode, 0) + self.assertEqual(Path(self.env['CURL_COUNT']).read_text(), '3') + self.assertEqual(self.manager.read_bytes(), self.original) + self.assertFalse((self.sdk / 'emulator').exists()) + + def test_sdk_installs_arguments_and_failures_are_forwarded_unchanged(self): + self.assertEqual(self.install().returncode, 0) + for args in [('--licenses',), ('--install', 'system-images;android-30;default;x86_64', '--channel=0'), + ('--install', 'emulator', 'platform-tools', '--channel=0'), + ('--install', 'emulator', '--channel=1'), ('ndk;27.3.13750724',)]: + with self.subTest(args=args): + result = self.sdkmanager(*args, SDK_EXIT='7') + self.assertEqual(result.returncode, 7, result.stderr) + self.assertEqual(json.loads(Path(self.env['FORWARDED']).read_text()), list(args)) + + def test_preparation_is_idempotent_and_damaged_binary_fails(self): + self.assertEqual(self.install().returncode, 0) + self.assertEqual(self.install().returncode, 0) + emulator = self.sdk / 'emulator/emulator' + emulator.write_text('#!/bin/sh\nexit 9\n') + result = self.sdkmanager('--install', 'emulator', '--channel=0') + self.assertNotEqual(result.returncode, 0) + self.assertFalse(Path(self.env['FORWARDED']).exists()) + + def test_all_emulator_jobs_prepare_before_launch(self): + import yaml + found = 0 + for path in (ROOT / '.github/workflows').glob('*.yml'): + workflow = yaml.load(path.read_text(), Loader=yaml.BaseLoader) + for job in workflow.get('jobs', {}).values(): + prepared = False + for step in job.get('steps', []): + if step.get('uses') == './.github/actions/prepare-android-emulator': + prepared = True + if step.get('uses', '').startswith('reactivecircus/android-emulator-runner@'): + self.assertTrue(prepared, str(path)) + found += 1 + self.assertEqual(found, 3) + + def test_emulator_runtime_dependency_is_installed_before_binary_validation(self): + import yaml + action = yaml.load((ACTION / 'action.yml').read_text(), Loader=yaml.BaseLoader) + steps = action['runs']['steps'] + installer = next(i for i, step in enumerate(steps) if 'install.sh' in step.get('run', '')) + self.assertTrue(any('apt-get install -y libpulse0' in step.get('run', '') + for step in steps[:installer])) + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/test-ios-smoke.py b/scripts/test-ios-smoke.py new file mode 100644 index 0000000000..a40ad6d6a6 --- /dev/null +++ b/scripts/test-ios-smoke.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Check iOS smoke launch/output handling and authoritative verdicts.""" +from pathlib import Path +import json +import os +import shutil +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parent.parent +SMOKE = ROOT / 'mobile-src/tools/mobile-test/smoke/run-ios-smoke.sh' + + +class IOSSmoke(unittest.TestCase): + def run_smoke(self, verdicts): + with tempfile.TemporaryDirectory(prefix='ios-smoke-') as folder: + root = Path(folder) + docs = root / 'container/Documents' + docs.mkdir(parents=True) + scripts = root / 'scripts' + scripts.mkdir() + shutil.copy2(SMOKE, scripts / SMOKE.name) + (scripts / 'mobile-features.js').write_text('// fixture\n') + tools = root / 'bin' + tools.mkdir() + uuid = tools / 'uuidgen' + uuid.write_text('#!/bin/sh\nprintf "%s\\n" "12345678-1234-1234-1234-123456789ABC"\n') + uuid.chmod(0o755) + xcrun = tools / 'xcrun' + xcrun.write_text('''#!/usr/bin/env python3 +import json,os,pathlib,sys +args=sys.argv[1:] +record=pathlib.Path(os.environ['COMMANDS']) +with record.open('a') as f: f.write(json.dumps(args)+'\\n') +if '--console' in args: sys.exit(99) +if args[1]=='get_app_container': print(os.environ['CONTAINER']) +if args[1]=='launch': + count=pathlib.Path(os.environ['COUNT']) + n=int(count.read_text()) if count.exists() else 0 + count.write_text(str(n+1)) + verdicts=json.loads(os.environ['VERDICTS']) + verdict=verdicts[min(n,len(verdicts)-1)] + token=args[args.index('--run-token')+1] + docs=pathlib.Path(os.environ['CONTAINER'])/'Documents' + if verdict: + (docs/f'result-{token}.txt').write_text(verdict+'\\n') + (docs/f'stdout-{token}.txt').write_text('sandbox smoke output\\n') + print('nodejsmobile.test: 2147483647') +''') + xcrun.chmod(0o755) + env = dict(os.environ, PATH=str(tools) + os.pathsep + os.environ['PATH'], + COMMANDS=str(root / 'commands'), COUNT=str(root / 'count'), + CONTAINER=str(docs.parent), VERDICTS=json.dumps(verdicts), + SMOKE_POLL_SECONDS='1', SMOKE_MAX_ATTEMPTS='3') + result = subprocess.run(['bash', str(scripts / SMOKE.name), 'fixture.app', 'fixture-udid'], + text=True, capture_output=True, env=env, timeout=15) + commands = [json.loads(line) for line in (root / 'commands').read_text().splitlines()] + return result, commands + + def test_pass_reads_sandbox_output_and_avoids_console_fifo(self): + result, commands = self.run_smoke(['PASS']) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn('sandbox smoke output', result.stderr) + self.assertTrue(all('--console' not in args for args in commands)) + self.assertEqual(sum(args[1] == 'launch' for args in commands), 1) + + def test_real_fail_is_not_retried_or_turned_into_pass(self): + result, commands = self.run_smoke(['FAIL', 'PASS']) + self.assertEqual(result.returncode, 1) + self.assertEqual(sum(args[1] == 'launch' for args in commands), 1) + self.assertIn('real failure', result.stdout) + + def test_no_verdict_retries_only_within_bound(self): + result, commands = self.run_smoke(['', 'PASS']) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(sum(args[1] == 'launch' for args in commands), 2) + self.assertTrue(any(args[1] == 'shutdown' for args in commands)) + + def test_no_verdict_never_succeeds_without_pass(self): + result, commands = self.run_smoke(['']) + self.assertEqual(result.returncode, 1) + self.assertEqual(sum(args[1] == 'launch' for args in commands), 3) + self.assertIn('after 3 attempts', result.stdout) + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/test-mobile-flavors.py b/scripts/test-mobile-flavors.py index c3adad71f4..54d5869e8f 100755 --- a/scripts/test-mobile-flavors.py +++ b/scripts/test-mobile-flavors.py @@ -1,21 +1,23 @@ #!/usr/bin/env python3 """Check flavor flags, iOS archive selection and danmu_api ICU regressions.""" +import hashlib import json import os from pathlib import Path +import re import shlex import subprocess import sys import tempfile import unittest +import zipfile ROOT = Path(__file__).resolve().parent.parent SOURCE = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else ROOT / 'out' if len(sys.argv) > 1: del sys.argv[1] -MAJOR = int(next(line.strip().lstrip('v').split('.')[0] - for line in (ROOT / 'upstream-base.txt').read_text().splitlines() - if line.strip() and not line.startswith('#'))) +MAJOR = int(re.search(r'^#define NODE_MAJOR_VERSION (\d+)$', + (SOURCE / 'src/node_version.h').read_text(), re.M)[1]) # android_configure supports Linux/macOS build hosts; simulate Linux so this # configure-only test also runs on Termux's Python (which reports Android). ANDROID_WRAPPER = [sys.executable, '-c', @@ -105,6 +107,46 @@ def test_ios_flags_and_framework_archives(self): for archive in ('libffi.a', 'libnode_crates.a'): self.assertEqual(archive in archives, flavor == 'full') + def test_preview_metadata_matches_flavors(self): + with tempfile.TemporaryDirectory() as temporary: + folder = Path(temporary) + source, output = folder / 'input', folder / 'output' + for platform in ('android', 'ios'): + for flavor in ('full', 'lite'): + suffix = '-lite' if flavor == 'lite' else '' + artifact = source / f'nodejs-mobile-{platform}{suffix}' + required = ['include/node/node_version.h'] + if platform == 'android': + required += [f'bin/{abi}/libnode.so' + for abi in ('arm64-v8a', 'armeabi-v7a', 'x86_64')] + else: + required += ['NodeMobile.xcframework/Info.plist'] + for name in required: + path = artifact / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(b'fixture') + subprocess.run([sys.executable, str(ROOT / 'scripts/package-preview.py'), + str(source), str(output)], cwd=ROOT, + env=dict(os.environ, GITHUB_REPOSITORY='fogtape/nodejs-mobile', + GITHUB_RUN_ID='1'), check=True, + capture_output=True, text=True) + for package in output.glob('*.zip'): + with zipfile.ZipFile(package) as archive: + info = json.loads(archive.read('BUILD-INFO.json')) + lite = info['flavor'] == 'lite' + self.assertEqual(info['icu_api_mode'], 'full-icu') + self.assertEqual(info['icu_data_profile'], 'danmu-lite' if lite else 'complete') + self.assertEqual('full-icu' in info['enabled_features'], not lite) + self.assertEqual('danmu-icu-profile' in info['enabled_features'], lite) + for feature in ('node:ffi', 'Temporal'): + self.assertEqual(feature in info['enabled_features'], not lite and MAJOR >= 26) + self.assertEqual(feature in info['disabled_features'], lite and MAJOR >= 26) + lines = (output / 'SHA256SUMS').read_text().splitlines() + self.assertEqual(len(lines), 4) + for line in lines: + digest, name = line.split() + self.assertEqual(hashlib.sha256((output / name).read_bytes()).hexdigest(), digest) + def test_icu_profile_required_resources_exist_in_pinned_data(self): # A version upgrade must fail loudly if renamed ICU data invalidates the # reviewed locale/converter closure. Parse the actual canned package. diff --git a/scripts/test-release-control.py b/scripts/test-release-control.py index dd6846a1d2..9b1b9b74ec 100644 --- a/scripts/test-release-control.py +++ b/scripts/test-release-control.py @@ -159,6 +159,18 @@ def test_node24_and_node26_use_independent_lines(self): self.assertEqual(self.plan('24.21.0-1', 'recipe')['version'], '24.21.0-1') self.assertEqual(self.check(ref='refs/heads/recipe', version='24.21.0-1')['release'], 'true') + def test_snapshot_builds_work_but_release_preparation_requires_an_official_tag(self): + self.fixture('24.22.0-0') + (self.root / 'upstream-base.txt').write_text('a' * 40 + '\n') + self.assertEqual(self.check(operation='build', version='', ref='refs/heads/recipe')['release'], + 'false') + self.assertEqual(self.check(event='pull_request')['release'], 'false') + with self.assertRaisesRegex(CONTROL.ReleaseError, 'unreleased upstream PR commit'): + self.plan('24.22.0-0', 'recipe') + self.arm('24.22.0-0') + with self.assertRaisesRegex(CONTROL.ReleaseError, 'unreleased upstream PR commit'): + self.check(ref='refs/heads/recipe', version='24.22.0-0') + def test_only_404_means_missing_ref(self): responses = [ (subprocess.CompletedProcess([], 0, '{}', ''), True), diff --git a/scripts/test-upstream-clone.py b/scripts/test-upstream-clone.py new file mode 100755 index 0000000000..2039860b04 --- /dev/null +++ b/scripts/test-upstream-clone.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +"""Verify immutable upstream clones and cache transport without public network.""" +from pathlib import Path +import os +import re +import subprocess +import tempfile +import unittest + +CLONE = Path(__file__).with_name('clone-upstream.sh').resolve() + + +class UpstreamClone(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix='node-mobile-upstream-') + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.repo = self.root / 'upstream' + self.repo.mkdir() + self.git(self.repo, 'init', '-q') + self.git(self.repo, 'config', 'user.name', 'fixture') + self.git(self.repo, 'config', 'user.email', 'fixture@invalid') + (self.repo / 'version').write_text('released\n') + self.git(self.repo, 'add', 'version') + self.git(self.repo, 'commit', '-qm', 'release') + self.git(self.repo, 'tag', '-a', 'v26.10.0', '-m', 'release') + self.released = self.git(self.repo, 'rev-parse', 'HEAD') + (self.repo / 'version').write_text('proposal\n') + self.git(self.repo, 'commit', '-qam', 'proposal') + self.proposal = self.git(self.repo, 'rev-parse', 'HEAD') + + def git(self, repo, *args): + return subprocess.check_output(['git', '-C', str(repo), *args], text=True).strip() + + def clone(self, repo, ref, dest, *, check=True): + return subprocess.run([str(CLONE), repo.as_uri(), ref, str(dest)], + text=True, capture_output=True, check=check) + + def test_sha_clone_and_cached_reclone_preserve_exact_commit_without_tags(self): + cache, output = self.root / 'cache', self.root / 'output' + self.clone(self.repo, self.proposal, cache) + self.clone(cache, self.proposal, output) + self.assertEqual(self.git(output, 'rev-parse', 'HEAD'), self.proposal) + self.assertEqual(self.git(output, 'rev-parse', 'refs/nodejs-mobile/upstream-base'), + self.proposal) + self.assertEqual((output / 'version').read_text(), 'proposal\n') + self.assertEqual(self.git(output, 'tag', '-l'), '') + self.assertEqual(self.git(output, 'rev-parse', '--is-shallow-repository'), 'true') + + def test_annotated_release_tag_still_clones(self): + output = self.root / 'output' + self.clone(self.repo, 'v26.10.0', output) + self.assertEqual(self.git(output, 'rev-parse', 'HEAD'), self.released) + self.assertEqual(self.git(output, 'rev-parse', 'v26.10.0^{commit}'), self.released) + + def test_existing_destination_is_preserved(self): + output = self.root / 'output' + output.mkdir() + (output / 'keep').write_text('existing work\n') + self.assertNotEqual(self.clone(self.repo, self.proposal, output, check=False).returncode, 0) + self.assertEqual((output / 'keep').read_text(), 'existing work\n') + + def test_missing_sha_fails_without_checked_out_source(self): + output = self.root / 'output' + self.assertNotEqual(self.clone(self.repo, '0' * 40, output, check=False).returncode, 0) + self.assertFalse((output / 'version').exists()) + + def test_ci_upstream_check_verifies_snapshot_sha_and_rejects_mismatch(self): + workflow = CLONE.parent.parent / '.github/workflows/build.yml' + match = re.search(r' - name: Check the pinned upstream base is on this fork\n' + r'.*? run: \|\n((?: {10}[^\n]*\n|\n)+)', + workflow.read_text(), re.S) + self.assertIsNotNone(match) + script = '\n'.join(line[10:] if line else '' for line in match[1].splitlines()) + (self.root / 'upstream-base.txt').write_text(self.proposal + '\n') + tools = self.root / 'tools' + tools.mkdir() + gh = tools / 'gh' + gh.write_text('#!/bin/sh\nprintf "%s\\n" "$*" > "$API_RECORD"\n' + 'printf "%s\\n" "$API_SHA"\n') + gh.chmod(0o755) + record = self.root / 'api-record' + env = dict(os.environ, PATH=str(tools) + os.pathsep + os.environ['PATH'], + API_RECORD=str(record), API_SHA=self.proposal) + result = subprocess.run(['bash', '-c', script], cwd=self.root, + env=env, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn('Verified upstream PR snapshot', result.stdout) + self.assertEqual(record.read_text().strip(), + f'api repos/nodejs/node/commits/{self.proposal} --jq .sha') + result = subprocess.run(['bash', '-c', script], cwd=self.root, + env=dict(env, API_SHA='0' * 40), capture_output=True, text=True) + self.assertNotEqual(result.returncode, 0) + self.assertIn('snapshot SHA mismatch', result.stdout) + + +if __name__ == '__main__': + unittest.main() diff --git a/upstream-base.txt b/upstream-base.txt index 9d44756013..ef590b49ca 100644 --- a/upstream-base.txt +++ b/upstream-base.txt @@ -1,4 +1,4 @@ -# The upstream nodejs/node release the patch series applies to. -# First non-comment line is the tag (or SHA); update it as part of every -# upstream bump (see README.md). -v24.21.0 +# Node.js 24.22.0 LTS release proposal; the official tag is not published yet. +# https://github.com/nodejs/node/pull/66667 (v24.22.0-proposal) +# Pin the reviewed PR commit rather than following the moving proposal branch. +a5d0eb95dd03b90986ad1a7c0cd3f2de1a0f383c