| 
 | 1 | +#!/usr/bin/env node  | 
 | 2 | +/**  | 
 | 3 | + * Copyright (c) Meta Platforms, Inc. and affiliates.  | 
 | 4 | + *  | 
 | 5 | + * This source code is licensed under the MIT license found in the  | 
 | 6 | + * LICENSE file in the root directory of this source tree.  | 
 | 7 | + *  | 
 | 8 | + * @format  | 
 | 9 | + */  | 
 | 10 | + | 
 | 11 | +'use strict';  | 
 | 12 | + | 
 | 13 | +const {exec} = require('shelljs');  | 
 | 14 | + | 
 | 15 | +const util = require('util');  | 
 | 16 | +const asyncRequest = require('request');  | 
 | 17 | +const request = util.promisify(asyncRequest);  | 
 | 18 | + | 
 | 19 | +let circleCIHeaders;  | 
 | 20 | +let jobs;  | 
 | 21 | +let baseTemporaryPath;  | 
 | 22 | + | 
 | 23 | +async function initialize(circleCIToken, baseTempPath, branchName) {  | 
 | 24 | +  console.info('Getting CircleCI information');  | 
 | 25 | +  circleCIHeaders = {'Circle-Token': circleCIToken};  | 
 | 26 | +  baseTemporaryPath = baseTempPath;  | 
 | 27 | +  exec(`mkdir -p ${baseTemporaryPath}`);  | 
 | 28 | +  const pipeline = await _getLastCircleCIPipelineID(branchName);  | 
 | 29 | +  const packageAndReleaseWorkflow = await _getPackageAndReleaseWorkflow(  | 
 | 30 | +    pipeline.id,  | 
 | 31 | +  );  | 
 | 32 | +  const testsWorkflow = await _getTestsWorkflow(pipeline.id);  | 
 | 33 | +  const jobsPromises = [  | 
 | 34 | +    _getCircleCIJobs(packageAndReleaseWorkflow.id),  | 
 | 35 | +    _getCircleCIJobs(testsWorkflow.id),  | 
 | 36 | +  ];  | 
 | 37 | + | 
 | 38 | +  const jobsResults = await Promise.all(jobsPromises);  | 
 | 39 | + | 
 | 40 | +  jobs = jobsResults.flatMap(j => j);  | 
 | 41 | +}  | 
 | 42 | + | 
 | 43 | +function baseTmpPath() {  | 
 | 44 | +  return baseTemporaryPath;  | 
 | 45 | +}  | 
 | 46 | + | 
 | 47 | +async function _getLastCircleCIPipelineID(branchName) {  | 
 | 48 | +  const options = {  | 
 | 49 | +    method: 'GET',  | 
 | 50 | +    url: 'https://circleci.com/api/v2/project/gh/facebook/react-native/pipeline',  | 
 | 51 | +    qs: {  | 
 | 52 | +      branch: branchName,  | 
 | 53 | +    },  | 
 | 54 | +    headers: circleCIHeaders,  | 
 | 55 | +  };  | 
 | 56 | + | 
 | 57 | +  const response = await request(options);  | 
 | 58 | +  if (response.error) {  | 
 | 59 | +    throw new Error(error);  | 
 | 60 | +  }  | 
 | 61 | + | 
 | 62 | +  const items = JSON.parse(response.body).items;  | 
 | 63 | + | 
 | 64 | +  if (!items || items.length === 0) {  | 
 | 65 | +    throw new Error(  | 
 | 66 | +      'No pipelines found on this branch. Make sure that the CI has run at least once, successfully',  | 
 | 67 | +    );  | 
 | 68 | +  }  | 
 | 69 | + | 
 | 70 | +  const lastPipeline = items[0];  | 
 | 71 | +  return {id: lastPipeline.id, number: lastPipeline.number};  | 
 | 72 | +}  | 
 | 73 | + | 
 | 74 | +async function _getSpecificWorkflow(pipelineId, workflowName) {  | 
 | 75 | +  const options = {  | 
 | 76 | +    method: 'GET',  | 
 | 77 | +    url: `https://circleci.com/api/v2/pipeline/${pipelineId}/workflow`,  | 
 | 78 | +    headers: circleCIHeaders,  | 
 | 79 | +  };  | 
 | 80 | +  const response = await request(options);  | 
 | 81 | +  if (response.error) {  | 
 | 82 | +    throw new Error(error);  | 
 | 83 | +  }  | 
 | 84 | + | 
 | 85 | +  const body = JSON.parse(response.body);  | 
 | 86 | +  let workflow = body.items.find(workflow => workflow.name === workflowName);  | 
 | 87 | +  _throwIfWorkflowNotFound(workflow, workflowName);  | 
 | 88 | +  return workflow;  | 
 | 89 | +}  | 
 | 90 | + | 
 | 91 | +function _throwIfWorkflowNotFound(workflow, name) {  | 
 | 92 | +  if (!workflow) {  | 
 | 93 | +    throw new Error(  | 
 | 94 | +      `Can't find a workflow named ${name}. Please check whether that workflow has started.`,  | 
 | 95 | +    );  | 
 | 96 | +  }  | 
 | 97 | +}  | 
 | 98 | + | 
 | 99 | +async function _getPackageAndReleaseWorkflow(pipelineId) {  | 
 | 100 | +  return _getSpecificWorkflow(pipelineId, 'package_and_publish_release_dryrun');  | 
 | 101 | +}  | 
 | 102 | + | 
 | 103 | +async function _getTestsWorkflow(pipelineId) {  | 
 | 104 | +  return _getSpecificWorkflow(pipelineId, 'tests');  | 
 | 105 | +}  | 
 | 106 | + | 
 | 107 | +async function _getCircleCIJobs(workflowId) {  | 
 | 108 | +  const options = {  | 
 | 109 | +    method: 'GET',  | 
 | 110 | +    url: `https://circleci.com/api/v2/workflow/${workflowId}/job`,  | 
 | 111 | +    headers: circleCIHeaders,  | 
 | 112 | +  };  | 
 | 113 | +  const response = await request(options);  | 
 | 114 | +  if (response.error) {  | 
 | 115 | +    throw new Error(error);  | 
 | 116 | +  }  | 
 | 117 | + | 
 | 118 | +  const body = JSON.parse(response.body);  | 
 | 119 | +  return body.items;  | 
 | 120 | +}  | 
 | 121 | + | 
 | 122 | +async function _getJobsArtifacts(jobNumber) {  | 
 | 123 | +  const options = {  | 
 | 124 | +    method: 'GET',  | 
 | 125 | +    url: `https://circleci.com/api/v2/project/gh/facebook/react-native/${jobNumber}/artifacts`,  | 
 | 126 | +    headers: circleCIHeaders,  | 
 | 127 | +  };  | 
 | 128 | +  const response = await request(options);  | 
 | 129 | +  if (response.error) {  | 
 | 130 | +    throw new Error(error);  | 
 | 131 | +  }  | 
 | 132 | + | 
 | 133 | +  const body = JSON.parse(response.body);  | 
 | 134 | +  return body.items;  | 
 | 135 | +}  | 
 | 136 | + | 
 | 137 | +async function _findUrlForJob(jobName, artifactPath) {  | 
 | 138 | +  const job = jobs.find(j => j.name === jobName);  | 
 | 139 | +  _throwIfJobIsNull(job);  | 
 | 140 | +  _throwIfJobIsUnsuccessful(job);  | 
 | 141 | + | 
 | 142 | +  const artifacts = await _getJobsArtifacts(job.job_number);  | 
 | 143 | +  return artifacts.find(artifact => artifact.path.indexOf(artifactPath) > -1)  | 
 | 144 | +    .url;  | 
 | 145 | +}  | 
 | 146 | + | 
 | 147 | +function _throwIfJobIsNull(job) {  | 
 | 148 | +  if (!job) {  | 
 | 149 | +    throw new Error(  | 
 | 150 | +      `Can't find a job with name ${job.name}. Please verify that it has been executed and that all its dependencies completed successfully.`,  | 
 | 151 | +    );  | 
 | 152 | +  }  | 
 | 153 | +}  | 
 | 154 | + | 
 | 155 | +function _throwIfJobIsUnsuccessful(job) {  | 
 | 156 | +  if (job.status !== 'success') {  | 
 | 157 | +    throw new Error(  | 
 | 158 | +      `The job ${job.name} status is ${job.status}. We need a 'success' status to proceed with the testing.`,  | 
 | 159 | +    );  | 
 | 160 | +  }  | 
 | 161 | +}  | 
 | 162 | + | 
 | 163 | +async function artifactURLHermesDebug() {  | 
 | 164 | +  return _findUrlForJob('build_hermes_macos-Debug', 'hermes-ios-debug.tar.gz');  | 
 | 165 | +}  | 
 | 166 | + | 
 | 167 | +async function artifactURLForMavenLocal() {  | 
 | 168 | +  return _findUrlForJob('build_and_publish_npm_package-2', 'maven-local.zip');  | 
 | 169 | +}  | 
 | 170 | + | 
 | 171 | +async function artifactURLForHermesRNTesterAPK(emulatorArch) {  | 
 | 172 | +  return _findUrlForJob(  | 
 | 173 | +    'test_android',  | 
 | 174 | +    `rntester-apk/hermes/debug/app-hermes-${emulatorArch}-debug.apk`,  | 
 | 175 | +  );  | 
 | 176 | +}  | 
 | 177 | + | 
 | 178 | +async function artifactURLForJSCRNTesterAPK(emulatorArch) {  | 
 | 179 | +  return _findUrlForJob(  | 
 | 180 | +    'test_android',  | 
 | 181 | +    `rntester-apk/jsc/debug/app-jsc-${emulatorArch}-debug.apk`,  | 
 | 182 | +  );  | 
 | 183 | +}  | 
 | 184 | + | 
 | 185 | +function downloadArtifact(artifactURL, destination) {  | 
 | 186 | +  exec(`rm -rf ${destination}`);  | 
 | 187 | +  exec(`curl ${artifactURL} -Lo ${destination}`);  | 
 | 188 | +}  | 
 | 189 | + | 
 | 190 | +module.exports = {  | 
 | 191 | +  initialize,  | 
 | 192 | +  downloadArtifact,  | 
 | 193 | +  artifactURLForJSCRNTesterAPK,  | 
 | 194 | +  artifactURLForHermesRNTesterAPK,  | 
 | 195 | +  artifactURLForMavenLocal,  | 
 | 196 | +  artifactURLHermesDebug,  | 
 | 197 | +  baseTmpPath,  | 
 | 198 | +};  | 
0 commit comments