mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
641be98b1e
Summary: Right now, every PR runs the whole test suite. For example, a changelog PR, will run all the tests. As of last month, that meant quite a few $s per single run. With this PR, we are going to leverage dynamic configuration and file filtering to create a config.yml on the flight, depending on the files changed by the commit/pr. They way it works is the following: - It starts a setup workflow in CircleCI. - This workflow fetch the list of files that have been changed in the current commit. - It executes a bunch of filtering and computation to understand which tests makes sense to run. - It creates a config on the flight to run those. - It continue the pipeline on that config. Currently, the way it works is the following: - If a `.md` file has been modified => run nothing - If only files in the `ReactAndroid` folder are modified => run tests for android only - If only files in the `React` folder are modified or `ruby` files are modified => run only iOS tests - If only js files, not in the scripts folder are modified => run only JS tests - if only files in the e2e folder are modified => run only e2e tests - else => run everything. Of course, we can play and modify those filters t make sure that they reflect the work and the tests to the best we can. bypass-github-exports-checks ## Changelog: [Internal] - Split circleci config and run test selectively. Pull Request resolved: https://github.com/facebook/react-native/pull/39042 Test Plan: - [X] Tested on the local branch for general sanity check. - [X] Import it in fbsource - [x] Create a stacked diff which changes only a md file => verify that no tests are run. - [x] Create a stacked diff which changes only files in ReactAndroid => verify that only android tests run. - [x] Create a stacked diff which changes only files in React => verify that only iOS tests run. - [x] Create a stacked diff which changes only ruby files => verify that only iOS tests run. - [x] Create a stacked diff which changes ruby files and file in React => verify that only iOS tests run. - [x] Create a stacked diff which changes only files JS not in the script folder => verify that JS tests run. - [x] Create a stacked diff which changes only JS files in the script folder => verify that the whole suite starts. - [x] Create a stacked diff which changes only files in the E2E folder => verify that only E2E files runs. - [x] Trigger a nightly pipeline => verify that parameters are passed to the generated config. Reviewed By: NickGerleman Differential Revision: D48394437 Pulled By: cipolleschi fbshipit-source-id: 771f3e68daa8318d2b73dd91ce85a41488110c04
231 lines
5.9 KiB
JavaScript
231 lines
5.9 KiB
JavaScript
/**
|
|
* 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.
|
|
*
|
|
* @format
|
|
*/
|
|
|
|
/**
|
|
* This script is meant to be used only in CircleCI to compute which tests we should execute
|
|
* based on the files modified by the commit.
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const yargs = require('yargs');
|
|
const {execSync} = require('child_process');
|
|
|
|
/**
|
|
* Check whether the filename is a JS/TS file and not in the script folder
|
|
*/
|
|
function isJSChange(name) {
|
|
const isJS =
|
|
name.endsWith('.js') ||
|
|
name.endsWith('.jsx') ||
|
|
name.endsWith('.ts') ||
|
|
name.endsWith('.tsx');
|
|
const notAScript = name.indexOf('scripts/') < 0;
|
|
return isJS && notAScript;
|
|
}
|
|
|
|
function isIOS(name) {
|
|
return (
|
|
name.indexOf('React/') > -1 ||
|
|
name.endsWith('.rb') ||
|
|
name.endsWith('.podspec')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* This maps some high level pipelines to some conditions.
|
|
* If those conditions are true, we are going to run the associated pipeline in CI.
|
|
* So, for example, is the commit the CI is analyzing only contains changes in the React/ folder or changes to ruby files
|
|
* the RUN_IOS flag will be turned on and we will run only the CI suite for iOS tests.
|
|
*
|
|
* This mapping is not final and we can update it with more pipelines or we can update the filter condition in the future.
|
|
*/
|
|
const mapping = [
|
|
{
|
|
name: 'RUN_IOS',
|
|
filterFN: name => isIOS(name),
|
|
},
|
|
{
|
|
name: 'RUN_ANDROID',
|
|
filterFN: name => name.indexOf('ReactAndroid/') > -1,
|
|
},
|
|
{
|
|
name: 'RUN_E2E',
|
|
filterFN: name => name.indexOf('rn-tester-e2e/') > -1,
|
|
},
|
|
{
|
|
name: 'RUN_JS',
|
|
filterFN: name => isJSChange(name),
|
|
},
|
|
{
|
|
name: 'SKIP',
|
|
filterFN: name => name.endsWith('.md'),
|
|
},
|
|
];
|
|
|
|
// ===== //
|
|
// Yargs //
|
|
// ===== //
|
|
|
|
const createConfigsOption = {
|
|
input_path: {
|
|
alias: 'i',
|
|
describe: 'The path to the folder where the Config parts are located',
|
|
default: '.circleci/configurations',
|
|
},
|
|
output_path: {
|
|
alias: 'o',
|
|
describe: 'The path where the `generated_config.yml` will be created',
|
|
default: '.circleci',
|
|
},
|
|
config_file: {
|
|
alias: 'c',
|
|
describe: 'The configuration file with the parameter to set',
|
|
default: '/tmp/circleci/pipeline_config.json',
|
|
},
|
|
};
|
|
|
|
const filterJobsOptions = {
|
|
output_path: {
|
|
alias: 'o',
|
|
ddescribe: 'The output path for the configurations',
|
|
default: '/tmp/circleci',
|
|
},
|
|
};
|
|
|
|
yargs
|
|
.command(
|
|
'create-configs',
|
|
'Creates the circleci config from its files',
|
|
createConfigsOption,
|
|
argv => createConfigs(argv.input_path, argv.output_path, argv.config_file),
|
|
)
|
|
.command(
|
|
'filter-jobs',
|
|
'Filters the jobs based on the list of chaged files in the PR',
|
|
filterJobsOptions,
|
|
argv =>
|
|
filterJobs(argv.output_path).then(() => console.info('Filtering done!')),
|
|
)
|
|
.demandCommand()
|
|
.strict()
|
|
.help().argv;
|
|
|
|
// ============== //
|
|
// GITHUB UTILS //
|
|
// ============== //
|
|
|
|
function _getFilesFromGit() {
|
|
try {
|
|
execSync('git fetch');
|
|
const commonCommit = String(
|
|
execSync('git merge-base HEAD origin/HEAD '),
|
|
).trim();
|
|
return String(execSync(`git diff --name-only HEAD ${commonCommit}`))
|
|
.trim()
|
|
.split('\n');
|
|
} catch (error) {
|
|
console.error(error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async function _computeAndSavePipelineParameters(pipelineType, outputPath) {
|
|
fs.mkdirSync(outputPath, {recursive: true});
|
|
const filePath = `${outputPath}/pipeline_config.json`;
|
|
|
|
if (pipelineType === 'SKIP') {
|
|
fs.writeFileSync(filePath, JSON.stringify({}, null, 2));
|
|
return;
|
|
}
|
|
|
|
if (pipelineType === 'ALL') {
|
|
fs.writeFileSync(filePath, JSON.stringify({run_all: true}, null, 2));
|
|
return;
|
|
}
|
|
|
|
const params = {
|
|
run_all: false,
|
|
run_ios: pipelineType === 'RUN_IOS',
|
|
run_android: pipelineType === 'RUN_ANDROID',
|
|
run_js: pipelineType === 'RUN_JS',
|
|
run_e2e: pipelineType === 'RUN_E2E' || pipelineType === 'RUN_JS',
|
|
};
|
|
|
|
const stringifiedParams = JSON.stringify(params, null, 2);
|
|
fs.writeFileSync(filePath, stringifiedParams);
|
|
console.info(`Generated params:\n${stringifiedParams}`);
|
|
}
|
|
|
|
// ============== //
|
|
// COMMANDS //
|
|
// ============== //
|
|
|
|
function createConfigs(inputPath, outputPath, configFile) {
|
|
// Order is important!
|
|
const baseConfigFiles = [
|
|
'top_level.yml',
|
|
'executors.yml',
|
|
'commands.yml',
|
|
'jobs.yml',
|
|
'workflows.yml',
|
|
];
|
|
|
|
const baseFolder = 'test_workflows';
|
|
const testConfigs = {
|
|
run_ios: ['testIOS.yml'],
|
|
run_android: ['testAndroid.yml'],
|
|
run_e2e: ['testE2E.yml'],
|
|
run_all: ['testE2E.yml', 'testJS.yml', 'testAll.yml'],
|
|
run_js: ['testJS.yml'],
|
|
};
|
|
|
|
if (!fs.existsSync(configFile)) {
|
|
throw new Error(`Config file: ${configFile} does not exists`);
|
|
}
|
|
|
|
const jsonParams = JSON.parse(fs.readFileSync(configFile));
|
|
let configurationPartsFiles = baseConfigFiles;
|
|
|
|
for (const [key, shouldTest] of Object.entries(jsonParams)) {
|
|
if (shouldTest) {
|
|
const mappedFiles = testConfigs[key].map(f => `${baseFolder}/${f}`);
|
|
configurationPartsFiles = configurationPartsFiles.concat(mappedFiles);
|
|
}
|
|
}
|
|
console.log(configurationPartsFiles);
|
|
let configParts = [];
|
|
configurationPartsFiles.forEach(yamlPart => {
|
|
const fileContent = fs.readFileSync(`${inputPath}/${yamlPart}`);
|
|
configParts.push(fileContent);
|
|
});
|
|
console.info(`writing to: ${outputPath}/generated_config.yml`);
|
|
fs.writeFileSync(
|
|
`${outputPath}/generated_config.yml`,
|
|
configParts.join('\n'),
|
|
);
|
|
}
|
|
|
|
async function filterJobs(outputPath) {
|
|
const fileList = _getFilesFromGit();
|
|
|
|
if (fileList.length === 0) {
|
|
await _computeAndSavePipelineParameters('SKIP', outputPath);
|
|
return;
|
|
}
|
|
|
|
for (const filter of mapping) {
|
|
const found = fileList.every(filter.filterFN);
|
|
if (found) {
|
|
await _computeAndSavePipelineParameters(filter.name, outputPath);
|
|
return;
|
|
}
|
|
}
|
|
await _computeAndSavePipelineParameters('ALL', outputPath);
|
|
}
|