diff --git a/.github/actions/maestro-android/action.yml b/.github/actions/maestro-android/action.yml index 4a24e2c0231eae..d40eb00d82b889 100644 --- a/.github/actions/maestro-android/action.yml +++ b/.github/actions/maestro-android/action.yml @@ -22,6 +22,10 @@ inputs: required: false default: "." description: The directory from which metro should be started + emulator-arch: + required: false + default: x86 + description: The architecture of the emulator to run runs: using: composite @@ -53,7 +57,7 @@ runs: uses: reactivecircus/android-emulator-runner@v2 with: api-level: 24 - arch: x86 + arch: ${{ inputs.emulator-arch }} ram-size: '8192M' heap-size: '4096M' disk-size: '10G' @@ -72,7 +76,7 @@ runs: uses: actions/upload-artifact@v4.3.4 if: always() with: - name: e2e_android_${{ steps.normalize-app-id.outputs.app-id }}_report_${{ inputs.flavor }}_NewArch + name: e2e_android_${{ steps.normalize-app-id.outputs.app-id }}_report_${{ inputs.flavor }}_${{ inputs.emulator-arch }}_NewArch path: | report.xml screen.mp4 @@ -80,5 +84,5 @@ runs: if: steps.run-tests.outcome == 'failure' uses: actions/upload-artifact@v4.3.4 with: - name: maestro-logs-android-${{ steps.normalize-app-id.outputs.app-id }}-${{ inputs.flavor }}-NewArch + name: maestro-logs-android-${{ steps.normalize-app-id.outputs.app-id }}-${{ inputs.flavor }}-${{ inputs.emulator-arch }}-NewArch path: /tmp/MaestroLogs diff --git a/.github/actions/prepare-hermes-v1-app/action.yml b/.github/actions/prepare-hermes-v1-app/action.yml new file mode 100644 index 00000000000000..24f40d0fbacd75 --- /dev/null +++ b/.github/actions/prepare-hermes-v1-app/action.yml @@ -0,0 +1,41 @@ +name: prepare-hermes-v1-app +description: Prepares a React Native app with Hermes V1 enabled +inputs: + retry-count: + description: 'Number of times to retry the yarn install on failure' +runs: + using: composite + steps: + - name: Create new app + shell: bash + run: | + cd /tmp + npx @react-native-community/cli init RNApp --skip-install --version nightly + + - name: Select latest Hermes V1 version + shell: bash + run: | + node "$GITHUB_WORKSPACE/.github/workflow-scripts/selectLatestHermesV1Version.js" + + - name: Apply patch to enable Hermes V1 + shell: bash + run: | + cd /tmp/RNApp + git apply --binary --3way --whitespace=nowarn "$GITHUB_WORKSPACE/.github/workflow-scripts/hermes-v1.patch" + echo "✅ Patch applied successfully" + + - name: Install app dependencies with retry + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: ${{ inputs.retry-count }} + retry_wait_seconds: 15 + shell: bash + command: | + cd /tmp/RNApp + yarn install + on_retry_command: | + echo "Cleaning up for yarn retry..." + cd /tmp/RNApp + rm -rf node_modules yarn.lock || true + yarn cache clean || true diff --git a/.github/workflow-scripts/hermes-v1.patch b/.github/workflow-scripts/hermes-v1.patch new file mode 100644 index 00000000000000..213020381fc478 --- /dev/null +++ b/.github/workflow-scripts/hermes-v1.patch @@ -0,0 +1,30 @@ +diff --git a/android/settings.gradle b/android/settings.gradle +index 63b5d4e..6359ec3 100644 +--- a/android/settings.gradle ++++ b/android/settings.gradle +@@ -4,3 +4,11 @@ extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autoli + rootProject.name = 'RNApp' + include ':app' + includeBuild('../node_modules/@react-native/gradle-plugin') ++ ++includeBuild('../node_modules/react-native') { ++ dependencySubstitution { ++ substitute(module("com.facebook.react:react-android")).using(project(":packages:react-native:ReactAndroid")) ++ substitute(module("com.facebook.react:react-native")).using(project(":packages:react-native:ReactAndroid")) ++ substitute(project(":packages:react-native:ReactAndroid:hermes-engine")).using(module("com.facebook.hermes:hermes-android:$HERMES_V1_VERSION")) ++ } ++} +diff --git a/package.json b/package.json +index f05d51b..69938af 100644 +--- a/package.json ++++ b/package.json +@@ -35,6 +35,9 @@ + "react-test-renderer": "19.2.0", + "typescript": "^5.8.3" + }, ++ "resolutions": { ++ "hermes-compiler": "$HERMES_V1_VERSION" ++ }, + "engines": { + "node": ">=20" + } diff --git a/.github/workflow-scripts/selectLatestHermesV1Version.js b/.github/workflow-scripts/selectLatestHermesV1Version.js new file mode 100644 index 00000000000000..db11b9278c1b69 --- /dev/null +++ b/.github/workflow-scripts/selectLatestHermesV1Version.js @@ -0,0 +1,38 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const PATCH_FILE_PATH = path.join(__dirname, 'hermes-v1.patch'); + +function getLatestHermesV1Version() { + const npmString = "npm view hermes-compiler@latest-v1 version"; + + try { + const result = execSync(npmString, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim(); + return result; + } catch (error) { + throw new Error(`Failed to get package version for hermes-compiler@latest-v1`); + } +} + +function setHermesV1VersionInPatch(version) { + if (!fs.existsSync(PATCH_FILE_PATH)) { + throw new Error(`Patch file not found at path: ${PATCH_FILE_PATH}`); + } + + let patchContent = fs.readFileSync(PATCH_FILE_PATH, 'utf8'); + const updatedContent = patchContent.replaceAll( + "$HERMES_V1_VERSION", + version + ); + fs.writeFileSync(PATCH_FILE_PATH, updatedContent, 'utf8'); +} + +setHermesV1VersionInPatch(getLatestHermesV1Version()); diff --git a/.github/workflows/test-hermes-v1-android.yml b/.github/workflows/test-hermes-v1-android.yml new file mode 100644 index 00000000000000..644fd3caa503a3 --- /dev/null +++ b/.github/workflows/test-hermes-v1-android.yml @@ -0,0 +1,70 @@ +name: Test Hermes V1 with nightly on Android + +on: + workflow_call: + inputs: + retry-count: + description: 'Number of times to retry the build on failure' + required: false + type: number + default: 3 + +jobs: + test-hermes-v1-android: + name: Test Hermes V1 on Android + runs-on: 4-core-ubuntu + strategy: + matrix: + flavor: [debug, release] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup node.js + uses: actions/setup-node@v4 + with: + node-version: '22.14.0' + cache: yarn + + - name: Set up JDK 17 + uses: actions/setup-java@v2 + with: + java-version: '17' + distribution: 'zulu' + + - name: Prepare the app with Hermes V1 + uses: ./.github/actions/prepare-hermes-v1-app + with: + retry-count: ${{ inputs.retry-count }} + + - name: Build Android with retry + uses: nick-fields/retry@v3 + env: + CMAKE_VERSION: 3.31.5 + ORG_GRADLE_PROJECT_reactNativeArchitectures: x86_64 + with: + timeout_minutes: 45 + max_attempts: ${{ inputs.retry-count }} + retry_wait_seconds: 30 + shell: bash + command: | + cd /tmp/RNApp/android + CAPITALIZED_FLAVOR=$(echo "${{ matrix.flavor }}" | awk '{print toupper(substr($0, 1, 1)) substr($0, 2)}') + ./gradlew assemble${CAPITALIZED_FLAVOR} -PhermesV1Enabled=true + on_retry_command: | + echo "Cleaning up for Android retry..." + cd /tmp/RNApp/android + ./gradlew clean || true + rm -rf build app/build .gradle || true + + - name: Run E2E Tests + uses: ./.github/actions/maestro-android + timeout-minutes: 60 + with: + app-path: /tmp/RNApp/android/app/build/outputs/apk/${{ matrix.flavor }}/app-${{ matrix.flavor }}.apk + app-id: com.rnapp + maestro-flow: ./scripts/e2e/.maestro/ + install-java: 'false' + flavor: ${{ matrix.flavor }} + working-directory: /tmp/RNApp + emulator-arch: x86_64 diff --git a/.github/workflows/test-hermes-v1-ios.yml b/.github/workflows/test-hermes-v1-ios.yml new file mode 100644 index 00000000000000..65495dfb5a7e09 --- /dev/null +++ b/.github/workflows/test-hermes-v1-ios.yml @@ -0,0 +1,78 @@ +name: Test Hermes V1 with nightly on iOS + +on: + workflow_call: + inputs: + retry-count: + description: 'Number of times to retry the build on failure' + required: false + type: number + default: 3 + +jobs: + test-hermes-v1-ios: + name: Test Hermes V1 on iOS + runs-on: macos-15-large + strategy: + matrix: + flavor: [debug, release] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup node.js + uses: actions/setup-node@v4 + with: + node-version: '22.14.0' + cache: yarn + + - name: Prepare capitalized flavor + id: prepare-flavor + shell: bash + run: | + CAPITALIZED_FLAVOR=$(echo "${{ matrix.flavor }}" | awk '{print toupper(substr($0, 1, 1)) substr($0, 2)}') + echo "capitalized_flavor=$CAPITALIZED_FLAVOR" >> $GITHUB_OUTPUT + + - name: Prepare the app with Hermes V1 + uses: ./.github/actions/prepare-hermes-v1-app + with: + retry-count: ${{ inputs.retry-count }} + + - name: Setup xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: 16.4.0 + + - name: Build iOS with retry + uses: nick-fields/retry@v3 + with: + timeout_minutes: 45 + max_attempts: ${{ inputs.retry-count }} + retry_wait_seconds: 30 + shell: bash + command: | + cd /tmp/RNApp/ios + bundle install + RCT_HERMES_V1_ENABLED=1 bundle exec pod install + xcodebuild build \ + -workspace "RNApp.xcworkspace" \ + -scheme "RNApp" \ + -configuration "${{ steps.prepare-flavor.outputs.capitalized_flavor }}" \ + -sdk "iphonesimulator" \ + -destination "generic/platform=iOS Simulator" \ + -derivedDataPath "/tmp/RNApp" \ + -quiet + on_retry_command: | + echo "Cleaning up for iOS retry..." + cd /tmp/RNApp/ios + rm -rf Pods Podfile.lock build + rm -rf ~/Library/Developer/Xcode/DerivedData/* || true + + - name: Run E2E Tests + uses: ./.github/actions/maestro-ios + with: + app-path: "/tmp/RNApp/Build/Products/${{ steps.prepare-flavor.outputs.capitalized_flavor }}-iphonesimulator/RNApp.app" + app-id: org.reactjs.native.example.RNApp + maestro-flow: ./scripts/e2e/.maestro/ + flavor: ${{ steps.prepare-flavor.outputs.capitalized_flavor }} + working-directory: /tmp/RNApp diff --git a/.github/workflows/test-hermes-v1.yml b/.github/workflows/test-hermes-v1.yml new file mode 100644 index 00000000000000..06e60a22956dd9 --- /dev/null +++ b/.github/workflows/test-hermes-v1.yml @@ -0,0 +1,39 @@ +# This jobs runs every day 2 hours after the nightly job for React Native so we can verify how the nightly is behaving. +name: Check Hermes V1 with the nightly build + +on: + workflow_dispatch: + # nightly build @ 4:15 AM UTC + schedule: + - cron: '15 4 * * *' + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check-nightly: + runs-on: ubuntu-latest + if: github.repository == 'facebook/react-native' + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Check nightly + run: | + TODAY=$(date "+%Y%m%d") + echo "Checking nightly for $TODAY" + NIGHTLY="$(npm view react-native | grep $TODAY)" + if [[ -z $NIGHTLY ]]; then + echo 'Nightly job failed.' + exit 1 + else + echo 'Nightly Worked, All Good!' + fi + + test-hermes-v1-ios: + uses: ./.github/workflows/test-hermes-v1-ios.yml + needs: check-nightly + + test-hermes-v1-android: + uses: ./.github/workflows/test-hermes-v1-android.yml + needs: check-nightly