Skip to content

Commit

Permalink
[RN][CI]Update the testing-script to use github actions
Browse files Browse the repository at this point in the history
  • Loading branch information
cipolleschi committed Jun 13, 2024
1 parent b19bf2b commit dfa5edc
Show file tree
Hide file tree
Showing 4 changed files with 285 additions and 44 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/test-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ name: Test All

on:
workflow_dispatch:
pull_request:
push:
branches:
- main
Expand Down Expand Up @@ -747,6 +748,7 @@ jobs:
- name: Publish NPM
shell: bash
run: |
git config --global --add safe.directory /__w/react-native/react-native
echo "GRADLE_OPTS = $GRADLE_OPTS"
# We can't have a separate step because each command is executed in a separate shell
# so variables exported in a command are not visible in another.
Expand Down
69 changes: 41 additions & 28 deletions scripts/release-testing/test-e2e-local.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const {
maybeLaunchAndroidEmulator,
prepareArtifacts,
setupCircleCIArtifacts,
setupGHAArtifacts,
} = require('./utils/testing-utils');
const chalk = require('chalk');
const debug = require('debug')('test-e2e-local');
Expand Down Expand Up @@ -56,7 +57,7 @@ const argv = yargs
default: true,
})
.option('c', {
alias: 'circleciToken',
alias: 'ciToken',
type: 'string',
})
.option('useLastSuccessfulPipeline', {
Expand All @@ -78,7 +79,7 @@ const argv = yargs
* - @onReleaseBranch whether we are on a release branch or not
*/
async function testRNTesterIOS(
circleCIArtifacts /*: Unwrap<ReturnType<typeof setupCircleCIArtifacts>> */,
ciArtifacts /*: Unwrap<ReturnType<typeof setupCircleCIArtifacts>> */,
onReleaseBranch /*: boolean */,
) {
console.info(
Expand All @@ -90,14 +91,19 @@ async function testRNTesterIOS(
// remember that for this to be successful
// you should have run bundle install once
// in your local setup
if (argv.hermes === true && circleCIArtifacts != null) {
const hermesURL = await circleCIArtifacts.artifactURLHermesDebug();
const hermesPath = path.join(
circleCIArtifacts.baseTmpPath(),
'hermes-ios-debug.tar.gz',
if (argv.hermes === true && ciArtifacts != null) {
const hermesURL = await ciArtifacts.artifactURLHermesDebug();
const hermesZipPath = path.join(
ciArtifacts.baseTmpPath(),
'hermes.zip',
);
// download hermes source code from manifold
circleCIArtifacts.downloadArtifact(hermesURL, hermesPath);
ciArtifacts.downloadArtifact(hermesURL, hermesZipPath);
// GHA zips by default the artifacts.
const outputFolder = path.join(ciArtifacts.baseTmpPath(), 'hermes');
exec(`unzip ${hermesZipPath} -d ${outputFolder}`)
const hermesPath = path.join(outputFolder, 'hermes-ios-Debug.tar.gz');

console.info(`Downloaded Hermes in ${hermesPath}`);
exec(
`HERMES_ENGINE_TARBALL_PATH=${hermesPath} RCT_NEW_ARCH_ENABLED=1 bundle exec pod install --ansi`,
Expand All @@ -115,7 +121,7 @@ async function testRNTesterIOS(
launchPackagerInSeparateWindow(pwd().toString());

// launch the app on iOS simulator
exec('npx react-native run-ios --scheme RNTester --simulator "iPhone 14"');
exec('npx react-native run-ios --scheme RNTester --simulator "iPhone 15 Pro"');
}

/**
Expand All @@ -125,7 +131,7 @@ async function testRNTesterIOS(
* - @circleCIArtifacts manager object to manage all the download of CircleCIArtifacts. If null, it will fallback not to use them.
*/
async function testRNTesterAndroid(
circleCIArtifacts /*: Unwrap<ReturnType<typeof setupCircleCIArtifacts>> */,
ciArtifacts /*: Unwrap<ReturnType<typeof setupCircleCIArtifacts>> */,
) {
maybeLaunchAndroidEmulator();

Expand All @@ -143,22 +149,29 @@ async function testRNTesterAndroid(
"adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done; input keyevent 82'",
);

if (circleCIArtifacts != null) {
if (ciArtifacts != null) {
const downloadPath = path.join(
circleCIArtifacts.baseTmpPath(),
'rntester.apk',
ciArtifacts.baseTmpPath(),
'rntester.zip',
);

const emulatorArch = exec('adb shell getprop ro.product.cpu.abi').trim();
const rntesterAPKURL =
argv.hermes === true
? await circleCIArtifacts.artifactURLForHermesRNTesterAPK(emulatorArch)
: await circleCIArtifacts.artifactURLForJSCRNTesterAPK(emulatorArch);

// Github Actions zips all the APKs in a single archive
console.info('Start Downloading APK');
circleCIArtifacts.downloadArtifact(rntesterAPKURL, downloadPath);
const rntesterAPKURL = await ciArtifacts.artifactURLForHermesRNTesterAPK(emulatorArch)
ciArtifacts.downloadArtifact(rntesterAPKURL, downloadPath);
const unzipFolder = path.join(ciArtifacts.baseTmpPath(), 'rntester-apks');
exec(`rm -rf ${unzipFolder}`);
exec(`unzip ${downloadPath} -d ${unzipFolder}`)
let apkPath;
if (argv.hermes === true) {
apkPath = path.join(unzipFolder, 'hermes', 'release', `app-hermes-${emulatorArch}-release.apk`);
} else {
apkPath = path.join(unzipFolder, 'jsc', 'release', `app-jsc-${emulatorArch}-release.apk`);
}

exec(`adb install ${downloadPath}`);
exec(`adb install ${apkPath}`);
} else {
exec(
`../../gradlew :packages:rn-tester:android:app:${
Expand Down Expand Up @@ -205,7 +218,7 @@ async function testRNTester(
// === RNTestProject === //

async function testRNTestProject(
circleCIArtifacts /*: Unwrap<ReturnType<typeof setupCircleCIArtifacts>> */,
ciArtifacts /*: Unwrap<ReturnType<typeof setupCircleCIArtifacts>> */,
) {
console.info("We're going to test a fresh new RN project");

Expand All @@ -227,12 +240,12 @@ async function testRNTestProject(
const localNodeTGZPath = `${reactNativePackagePath}/react-native-${releaseVersion}.tgz`;

const mavenLocalPath =
circleCIArtifacts != null
? path.join(circleCIArtifacts.baseTmpPath(), 'maven-local')
ciArtifacts != null
? path.join(ciArtifacts.baseTmpPath(), 'maven-local')
: '/private/tmp/maven-local';

const hermesPath = await prepareArtifacts(
circleCIArtifacts,
ciArtifacts,
mavenLocalPath,
localNodeTGZPath,
releaseVersion,
Expand All @@ -241,7 +254,7 @@ async function testRNTestProject(
);

// If artifacts were built locally, we need to pack the react-native package
if (circleCIArtifacts == null) {
if (ciArtifacts == null) {
exec('npm pack --pack-destination ', {cwd: reactNativePackagePath});

// node pack does not creates a version of React Native with the right name on main.
Expand Down Expand Up @@ -336,18 +349,18 @@ async function main() {
}).stdout.trim();
const onReleaseBranch = branchName.endsWith('-stable');

let circleCIArtifacts = await setupCircleCIArtifacts(
let ghaArtifacts = await setupGHAArtifacts(
// $FlowIgnoreError[prop-missing]
argv.circleciToken,
argv.ciToken,
branchName,
// $FlowIgnoreError[prop-missing]
argv.useLastSuccessfulPipeline,
);

if (argv.target === 'RNTester') {
await testRNTester(circleCIArtifacts, onReleaseBranch);
await testRNTester(ghaArtifacts, onReleaseBranch);
} else {
await testRNTestProject(circleCIArtifacts);
await testRNTestProject(ghaArtifacts);

console.warn(
chalk.yellow(`
Expand Down
190 changes: 190 additions & 0 deletions scripts/release-testing/utils/github-actions-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
#!/usr/bin/env node
/**
* 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.
*
* @flow strict-local
* @format
*/

'use strict';

const chalk = require('chalk');
const fetch = require('node-fetch');
const {execSync: exec} = require('child_process');

/*::
type CIHeaders = {
Authorization: string,
Accept: string,
'X-GitHub-Api-Version': string
}
type WorkflowRun = {
id: number,
name: string,
run_number: number,
status: string,
workflow_id: number,
url: string,
created_at: string,
};
type Artifact = {
id: number,
name: string,
url: string,
archive_download_url: string,
}
type WorkflowRuns = {
total_count: number,
workflow_runs: Array<WorkflowRun>,
}
type Artifacts = {
total_count: number,
artifacts: Array<Artifact>,
}
*/

let token /*: string */;
let ciHeaders /*: CIHeaders */;
let artifacts /*: Artifacts */;
let branch /*: string */;
let baseTemporaryPath /*: string */;

const reactNativeRepo = 'https://api.github.com/repos/facebook/react-native/'
const reactNativeActionsURL = `${reactNativeRepo}actions/runs`


async function _getActionRunsOnBranch() /*: Promise<WorkflowRuns> */ {
const url = `${reactNativeActionsURL}?branch=${branch}`;
const options = {
method: 'GET',
headers: ciHeaders,
};

// $FlowIgnore[prop-missing] Conflicting .flowconfig in Meta's monorepo
// $FlowIgnore[incompatible-call]
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(JSON.stringify(await response.json()));
}

const body = await response
// eslint-disable-next-line func-call-spacing
.json /*::<WorkflowRuns>*/
();
return body;
}

async function _getArtifacts(run_id /*: number */) /*: Promise<Artifacts> */ {
const url = `${reactNativeActionsURL}/${run_id}/artifacts`;
const options = {
method: 'GET',
headers: ciHeaders,
};

// $FlowIgnore[prop-missing] Conflicting .flowconfig in Meta's monorepo
// $FlowIgnore[incompatible-call]
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(JSON.stringify(await response.json()));
}

const body = await response
// eslint-disable-next-line func-call-spacing
.json /*::<Artifacts>*/
();
return body;
}

// === Public Interface === //
async function initialize(
ciToken /*: string */,
baseTempPath /*: string */,
branchName /*: string */,
useLastSuccessfulPipeline /*: boolean */ = false,
) {
console.info('Getting GHA information');
baseTemporaryPath = baseTempPath;
exec(`mkdir -p ${baseTemporaryPath}`);

branch = branchName;

token = ciToken;
ciHeaders = {
Authorization: `Bearer ${token}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
};

const testAllWorkflow = (await _getActionRunsOnBranch())
.workflow_runs.filter(w => w.name == "Test All")
.sort((a, b) => a.created_at <= b.created_at ? -1 : 1)[0];

artifacts = await _getArtifacts(testAllWorkflow.id);
}

function downloadArtifact(
artifactURL /*: string */,
destination /*: string */,
) {
exec(`rm -rf ${destination}`);

const command = `curl ${artifactURL} \
-Lo ${destination} \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${token}" \
-H "X-GitHub-Api-Version: 2022-11-28"`

exec(command, {stdio: 'inherit'});
}

async function artifactURLForJSCRNTesterAPK(
emulatorArch /*: string */,
) /*: Promise<string> */ {
const url = artifacts.artifacts.filter(a => a.name === 'rntester-apk')[0].archive_download_url
return Promise.resolve(url);
}

async function artifactURLForHermesRNTesterAPK(
emulatorArch /*: string */,
) /*: Promise<string> */ {
const url = artifacts.artifacts.filter(a => a.name === 'rntester-apk')[0].archive_download_url
return Promise.resolve(url);
}

async function artifactURLForMavenLocal() /*: Promise<string> */ {
const url = artifacts.artifacts.filter(a => a.name === 'maven-local')[0].archive_download_url
return Promise.resolve(url);
}

async function artifactURLHermesDebug() /*: Promise<string> */ {
const url = artifacts.artifacts.filter(a => a.name === 'hermes-darwin-bin-Debug')[0].archive_download_url
return Promise.resolve(url);
}

async function artifactURLForReactNative() /*: Promise<string> */ {
const url = artifacts.artifacts.filter(a => a.name === 'react-native-package')[0].archive_download_url
return Promise.resolve(url);
}

function baseTmpPath() /*: string */ {
return baseTemporaryPath;
}

module.exports = {
initialize,
downloadArtifact,
artifactURLForJSCRNTesterAPK,
artifactURLForHermesRNTesterAPK,
artifactURLForMavenLocal,
artifactURLHermesDebug,
artifactURLForReactNative,
baseTmpPath,
}
Loading

0 comments on commit dfa5edc

Please sign in to comment.