Run Android E2E with ARM64 APKs (#58092)

Summary:
Build only the `arm64-v8a` Android ABI for dry-run CI artifacts and run the ARM64 RNTester and template-app APKs on an API 35 `google_apis` x86_64 emulator using the system images built-in NDK translation support.

This also makes the Maestro emulator API level and target configurable, logs the device ABI/native-bridge configuration, and updates local RNTester artifact selection to use the ARM64 split.

Release and nightly publication builds continue to build all supported Android ABIs.

## Changelog:

[INTERNAL] [CHANGED] - Run Android E2E tests with ARM64 APKs through NDK translation.

Pull Request resolved: https://github.com/react/react-native/pull/58092

Test Plan:
- `node --check .github/workflow-scripts/maestro-android.js` — passed.
- `node --check scripts/release-testing/test-release-local.js` — passed.
- Parsed all modified YAML files with the `yaml` Node package — passed.
- `./node_modules/.bin/prettier --check <modified files>` — passed.
- `git diff --check HEAD~3..HEAD` — passed.
- `actionlint` reported only existing repository metadata warnings for the custom `4-core-ubuntu` label and the missing description in the local `yarn-install` action.
- `yarn test .github/workflow-scripts/__tests__/maestro-android-test.js --runInBand` could not start because the local dependency tree is missing `flow-parser`; restoring dependencies was blocked by HTTP 503 responses from the npm registry.
- The draft CI run should validate ARM64 APK installation, Hermes startup, and the complete Maestro suites through `libndk_translation.so`.

Reviewed By: Abbondanzo

Differential Revision: D117195078

Pulled By: cortinico

fbshipit-source-id: a78f2127f04e80c3b5ca4c95d2024dffb92ff122
This commit is contained in:
Nicola Corti
2026-08-24 07:46:44 -07:00
committed by meta-codesync[bot]
parent 3f9cc72309
commit 25ebffd1f7
7 changed files with 47 additions and 17 deletions
+4 -2
View File
@@ -52,8 +52,10 @@ runs:
shell: bash
run: |
if [[ "${{ inputs.release-type }}" == "dry-run" ]]; then
# dry-run: we only build ARM64 to save time/resources. For release/nightlies the default is to build all archs.
export ORG_GRADLE_PROJECT_reactNativeArchitectures="arm64-v8a,x86" # x86 is required for E2E testing
# dry-run: we only build ARM64 to save time/resources. Android E2E
# runs this APK on an x86_64 API 35 image using NDK translation.
# For release/nightlies the default is to build all architectures.
export ORG_GRADLE_PROJECT_reactNativeArchitectures="arm64-v8a"
export HERMES_PREBUILT_FLAG="ORG_GRADLE_PROJECT_react.internal.useHermesNightly=true"
TASKS="publishAllToMavenTempLocal build"
elif [[ "${{ inputs.release-type }}" == "nightly" ]]; then
+10 -1
View File
@@ -26,6 +26,14 @@ inputs:
required: false
default: x86
description: The architecture of the emulator to run
emulator-api-level:
required: false
default: '24'
description: The Android API level of the emulator to run
emulator-target:
required: false
default: default
description: The Android system image target to use
test-state-path:
required: false
default: /tmp/maestro-android-state/results.json
@@ -60,7 +68,8 @@ runs:
id: run-tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 24
api-level: ${{ inputs.emulator-api-level }}
target: ${{ inputs.emulator-target }}
arch: ${{ inputs.emulator-arch }}
ram-size: '8192M'
heap-size: '4096M'
@@ -29,6 +29,22 @@ const MAESTRO_LOG_DIRECTORY = '/tmp/MaestroLogs';
const DIAGNOSTIC_COMMAND_TIMEOUT = 15000;
const STATE_VERSION = 1;
function logAndroidAbiConfiguration() {
const properties = [
'ro.product.cpu.abi',
'ro.product.cpu.abilist',
'ro.dalvik.vm.native.bridge',
];
console.info('Android ABI configuration:');
for (const property of properties) {
const value = childProcess
.execFileSync('adb', ['shell', 'getprop', property], {encoding: 'utf8'})
.trim();
console.info(`- ${property}: ${value || '<empty>'}`);
}
}
function collectFlows(flowPath) {
if (!fs.existsSync(flowPath) || !fs.lstatSync(flowPath).isDirectory()) {
return [flowPath];
@@ -279,6 +295,8 @@ async function main(args = process.argv.slice(2)) {
console.info(`TEST_STATE_PATH: ${statePath}`);
console.info('==============================\n');
logAndroidAbiConfiguration();
console.info('Install app');
childProcess.execSync(`adb install ${appPath}`, {stdio: 'ignore'});
+6 -3
View File
@@ -45,7 +45,7 @@ jobs:
continue-on-error: true
uses: actions/download-artifact@v7
with:
name: e2e_android_rntester_state_${{ matrix.flavor }}_x86_NewArch
name: e2e_android_rntester_state_${{ matrix.flavor }}_arm64-v8a_NewArch
path: /tmp/maestro-android-state
- name: Check for unfinished E2E flows
id: test-state
@@ -64,15 +64,18 @@ jobs:
uses: ./.github/actions/maestro-android
timeout-minutes: 90
with:
app-path: ./packages/rn-tester/android/app/build/outputs/apk/${{ matrix.flavor }}/app-x86-${{ matrix.flavor }}.apk
app-path: ./packages/rn-tester/android/app/build/outputs/apk/${{ matrix.flavor }}/app-arm64-v8a-${{ matrix.flavor }}.apk
app-id: com.facebook.react.uiapp
maestro-flow: ./packages/rn-tester/.maestro
flavor: ${{ matrix.flavor }}
emulator-api-level: '35'
emulator-target: google_apis
emulator-arch: x86_64
- name: Store per-flow test state
if: always()
uses: actions/upload-artifact@v6
with:
name: e2e_android_rntester_state_${{ matrix.flavor }}_x86_NewArch
name: e2e_android_rntester_state_${{ matrix.flavor }}_arm64-v8a_NewArch
overwrite: true
if-no-files-found: warn
path: /tmp/maestro-android-state/results.json
@@ -74,14 +74,14 @@ jobs:
# Build
cd android
CAPITALIZED_FLAVOR=$(echo "${{ matrix.flavor }}" | awk '{print toupper(substr($0, 1, 1)) substr($0, 2)}')
./gradlew assemble$CAPITALIZED_FLAVOR --no-daemon -PreactNativeArchitectures=x86
./gradlew assemble$CAPITALIZED_FLAVOR --no-daemon -PreactNativeArchitectures=arm64-v8a
- name: Download previous per-flow test state
if: ${{ inputs.retry-attempt > 0 }}
continue-on-error: true
uses: actions/download-artifact@v7
with:
name: e2e_android_templateapp_state_${{ matrix.flavor }}_x86_NewArch
name: e2e_android_templateapp_state_${{ matrix.flavor }}_arm64-v8a_NewArch
path: /tmp/maestro-android-state
- name: Check for unfinished E2E flows
id: test-state
@@ -106,11 +106,14 @@ jobs:
install-java: 'false'
flavor: ${{ matrix.flavor }}
working-directory: /tmp/RNTestProject
emulator-api-level: '35'
emulator-target: google_apis
emulator-arch: x86_64
- name: Store per-flow test state
if: always()
uses: actions/upload-artifact@v6
with:
name: e2e_android_templateapp_state_${{ matrix.flavor }}_x86_NewArch
name: e2e_android_templateapp_state_${{ matrix.flavor }}_arm64-v8a_NewArch
overwrite: true
if-no-files-found: warn
path: /tmp/maestro-android-state/results.json
@@ -155,18 +155,15 @@ async function testRNTesterAndroid(
if (ciArtifacts != null) {
const downloadPath = path.join(ciArtifacts.baseTmpPath(), 'rntester.zip');
const emulatorArch = exec('adb shell getprop ro.product.cpu.abi').trim();
// Github Actions zips all the APKs in a single archive
console.info('Start Downloading APK');
const rntesterAPKURL =
await ciArtifacts.artifactURLForRNTesterAPK(emulatorArch);
const rntesterAPKURL = await ciArtifacts.artifactURLForRNTesterAPK();
ciArtifacts.downloadArtifact(rntesterAPKURL, downloadPath);
const unzipFolder = path.join(ciArtifacts.baseTmpPath(), 'rntester-apks');
exec(`rm -rf ${unzipFolder}`);
exec(`unzip ${downloadPath} -d ${unzipFolder}`);
let apkPath = path.join(unzipFolder, `app-${emulatorArch}-debug.apk`);
const apkPath = path.join(unzipFolder, 'app-arm64-v8a-debug.apk');
exec(`adb install ${apkPath}`);
} else {
@@ -203,9 +203,7 @@ function downloadArtifact(
exec(command, {stdio: 'inherit'});
}
async function artifactURLForRNTesterAPK(
emulatorArch /*: string */,
) /*: Promise<string> */ {
async function artifactURLForRNTesterAPK() /*: Promise<string> */ {
return getArtifactURL('rntester-debug');
}