Skip to content

Commit

Permalink
Type circle-ci-artifacts-util (facebook#42608)
Browse files Browse the repository at this point in the history
Summary:

Changelog: [Internal] - Type circle-ci-artifacts-util and migrate it to node-fetch

Reviewed By: NickGerleman

Differential Revision: D52932555
  • Loading branch information
lunaleaps authored and facebook-github-bot committed Jan 24, 2024
1 parent cbd818d commit 1dffacf
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 57 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"micromatch": "^4.0.4",
"mkdirp": "^0.5.1",
"mock-fs": "^5.1.4",
"node-fetch": "^2.2.0",
"nullthrows": "^1.1.1",
"prettier": "2.8.8",
"prettier-plugin-hermes-parser": "0.18.2",
Expand Down
200 changes: 143 additions & 57 deletions scripts/circle-ci-artifacts-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,81 @@
* 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 asyncRequest = require('request');
const fetch = require('node-fetch');
const {exec} = require('shelljs');
const util = require('util');
const request = util.promisify(asyncRequest);

let circleCIHeaders;
let jobs;
let baseTemporaryPath;

async function initialize(circleCIToken, baseTempPath, branchName) {
/*::
type Job = {
job_number: number,
id: string,
name: string,
type: 'build' | 'approval',
status:
| 'success'
| 'running'
| 'not_run'
| 'failed'
| 'retried'
| 'queued'
| 'not_running'
| 'infrastructure_fail'
| 'timedout'
| 'on_hold'
| 'terminated-unknown'
| 'blocked'
| 'canceled'
| 'unauthorized',
...
};
type Workflow = {
pipeline_id: string,
id: string,
name: string,
project_slug: string,
status:
| 'success'
| 'running'
| 'not_run'
| 'failed'
| 'error'
| 'failing'
| 'on_hold'
| 'canceled'
| 'unauthorized',
pipeline_number: number,
...
};
type Artifact = {
path: string,
node_index: number,
url: string,
...
};
type Pipeline = {
id: string,
number: number,
...
}
*/

async function initialize(
circleCIToken /*: string */,
baseTempPath /*: string */,
branchName /*: string */,
) {
console.info('Getting CircleCI information');
circleCIHeaders = {'Circle-Token': circleCIToken};
baseTemporaryPath = baseTempPath;
Expand All @@ -31,26 +91,31 @@ async function initialize(circleCIToken, baseTempPath, branchName) {
jobs = jobsResults.flatMap(j => j);
}

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

async function _getLastCircleCIPipelineID(branchName) {
async function _getLastCircleCIPipelineID(branchName /*: string */) {
const qs = new URLSearchParams({branch: branchName}).toString();
const url =
'https://circleci.com/api/v2/project/gh/facebook/react-native/pipeline?' +
qs;
const options = {
method: 'GET',
url: 'https://circleci.com/api/v2/project/gh/facebook/react-native/pipeline',
qs: {
branch: branchName,
},
headers: circleCIHeaders,
};

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

const items = JSON.parse(response.body).items;
const responseJSON = await response
// eslint-disable-next-line func-call-spacing
.json /*::<{items: Array<Pipeline>}>*/
();
const items = responseJSON.items;

if (!items || items.length === 0) {
throw new Error(
Expand All @@ -62,103 +127,117 @@ async function _getLastCircleCIPipelineID(branchName) {
return {id: lastPipeline.id, number: lastPipeline.number};
}

async function _getSpecificWorkflow(pipelineId, workflowName) {
async function _getSpecificWorkflow(
pipelineId /*: string */,
workflowName /*: string */,
) {
const url = `https://circleci.com/api/v2/pipeline/${pipelineId}/workflow`;
const options = {
method: 'GET',
url: `https://circleci.com/api/v2/pipeline/${pipelineId}/workflow`,
headers: circleCIHeaders,
};
const response = await request(options);
if (response.error) {
throw new Error(error);

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

const body = JSON.parse(response.body);
const body = await response.json();
let workflow = body.items.find(w => w.name === workflowName);
_throwIfWorkflowNotFound(workflow, workflowName);
return workflow;
}

function _throwIfWorkflowNotFound(workflow, name) {
function _throwIfWorkflowNotFound(workflow /*: string */, name /*: string */) {
if (!workflow) {
throw new Error(
`Can't find a workflow named ${name}. Please check whether that workflow has started.`,
);
}
}

async function _getTestsWorkflow(pipelineId) {
async function _getTestsWorkflow(pipelineId /*: string */) {
return _getSpecificWorkflow(pipelineId, 'tests');
}

async function _getCircleCIJobs(workflowId) {
async function _getCircleCIJobs(
workflowId /*: string */,
) /*: Promise<Array<Job>> */ {
const url = `https://circleci.com/api/v2/workflow/${workflowId}/job`;
const options = {
method: 'GET',
url: `https://circleci.com/api/v2/workflow/${workflowId}/job`,
headers: circleCIHeaders,
};
const response = await request(options);
if (response.error) {
throw new Error(error);
// $FlowIgnore[prop-missing] Conflicting .flowconfig in Meta's monorepo
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(response);
}

const body = JSON.parse(response.body);
const body = await response
// eslint-disable-next-line func-call-spacing
.json /*::<{items: Array<Job>}>*/
();
return body.items;
}

async function _getJobsArtifacts(jobNumber) {
async function _getJobsArtifacts(
jobNumber /*: number */,
) /*: Promise<Array<Artifact>> */ {
const url = `https://circleci.com/api/v2/project/gh/facebook/react-native/${jobNumber}/artifacts`;
const options = {
method: 'GET',
url: `https://circleci.com/api/v2/project/gh/facebook/react-native/${jobNumber}/artifacts`,
headers: circleCIHeaders,
};
const response = await request(options);
if (response.error) {
throw new Error(error);

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

const body = JSON.parse(response.body);
const body = await response
// eslint-disable-next-line func-call-spacing
.json /*::<{items: Array<Artifact>}>*/
();
return body.items;
}

async function _findUrlForJob(jobName, artifactPath) {
async function _findUrlForJob(
jobName /*: string */,
artifactPath /*: string */,
) /*: Promise<string> */ {
const job = jobs.find(j => j.name === jobName);
_throwIfJobIsNull(job);
_throwIfJobIsUnsuccessful(job);

const artifacts = await _getJobsArtifacts(job.job_number);
let artifact = artifacts.find(a => a.path.indexOf(artifactPath) > -1);
if (!artifact) {
throw new Error(`I could not find the artifact with path ${artifactPath}`);
}
return artifact.url;
}

function _throwIfJobIsNull(job) {
if (!job) {
if (job == null) {
throw new Error(
`Can't find a job with name ${job.name}. Please verify that it has been executed and that all its dependencies completed successfully.`,
`Can't find a job with name ${jobName}. Please verify that it has been executed and that all its dependencies completed successfully.`,
);
}
}

function _throwIfJobIsUnsuccessful(job) {
if (job.status !== 'success') {
throw new Error(
`The job ${job.name} status is ${job.status}. We need a 'success' status to proceed with the testing.`,
);
}

const artifacts = await _getJobsArtifacts(job.job_number);
let artifact = artifacts.find(a => a.path.indexOf(artifactPath) > -1);
if (artifact == null) {
throw new Error(`I could not find the artifact with path ${artifactPath}`);
}
return artifact.url;
}

async function artifactURLHermesDebug() {
async function artifactURLHermesDebug() /*: Promise<string> */ {
return _findUrlForJob('build_hermes_macos-Debug', 'hermes-ios-debug.tar.gz');
}

async function artifactURLForMavenLocal() {
async function artifactURLForMavenLocal() /*: Promise<string> */ {
return _findUrlForJob('build_npm_package', 'maven-local.zip');
}

async function artifactURLForReactNative() {
async function artifactURLForReactNative() /*: Promise<string> */ {
let shortCommit = exec('git rev-parse HEAD', {silent: true})
.toString()
.trim()
Expand All @@ -169,21 +248,28 @@ async function artifactURLForReactNative() {
);
}

async function artifactURLForHermesRNTesterAPK(emulatorArch) {
async function artifactURLForHermesRNTesterAPK(
emulatorArch /*: string */,
) /*: Promise<string> */ {
return _findUrlForJob(
'test_android',
`rntester-apk/hermes/debug/app-hermes-${emulatorArch}-debug.apk`,
);
}

async function artifactURLForJSCRNTesterAPK(emulatorArch) {
async function artifactURLForJSCRNTesterAPK(
emulatorArch /*: string */,
) /*: Promise<string> */ {
return _findUrlForJob(
'test_android',
`rntester-apk/jsc/debug/app-jsc-${emulatorArch}-debug.apk`,
);
}

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

0 comments on commit 1dffacf

Please sign in to comment.