diff --git a/package.json b/package.json index 6e47cb19180574..68bfdde88089f5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/circle-ci-artifacts-utils.js b/scripts/circle-ci-artifacts-utils.js index c2a9e755a4c03a..81963f7e55d87d 100644 --- a/scripts/circle-ci-artifacts-utils.js +++ b/scripts/circle-ci-artifacts-utils.js @@ -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; @@ -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}>*/ + (); + const items = responseJSON.items; if (!items || items.length === 0) { throw new Error( @@ -62,24 +127,29 @@ 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.`, @@ -87,78 +157,87 @@ function _throwIfWorkflowNotFound(workflow, name) { } } -async function _getTestsWorkflow(pipelineId) { +async function _getTestsWorkflow(pipelineId /*: string */) { return _getSpecificWorkflow(pipelineId, 'tests'); } -async function _getCircleCIJobs(workflowId) { +async function _getCircleCIJobs( + workflowId /*: string */, +) /*: Promise> */ { + 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}>*/ + (); return body.items; } -async function _getJobsArtifacts(jobNumber) { +async function _getJobsArtifacts( + jobNumber /*: number */, +) /*: Promise> */ { + 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}>*/ + (); return body.items; } -async function _findUrlForJob(jobName, artifactPath) { +async function _findUrlForJob( + jobName /*: string */, + artifactPath /*: string */, +) /*: Promise */ { 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 */ { return _findUrlForJob('build_hermes_macos-Debug', 'hermes-ios-debug.tar.gz'); } -async function artifactURLForMavenLocal() { +async function artifactURLForMavenLocal() /*: Promise */ { return _findUrlForJob('build_npm_package', 'maven-local.zip'); } -async function artifactURLForReactNative() { +async function artifactURLForReactNative() /*: Promise */ { let shortCommit = exec('git rev-parse HEAD', {silent: true}) .toString() .trim() @@ -169,21 +248,28 @@ async function artifactURLForReactNative() { ); } -async function artifactURLForHermesRNTesterAPK(emulatorArch) { +async function artifactURLForHermesRNTesterAPK( + emulatorArch /*: string */, +) /*: Promise */ { return _findUrlForJob( 'test_android', `rntester-apk/hermes/debug/app-hermes-${emulatorArch}-debug.apk`, ); } -async function artifactURLForJSCRNTesterAPK(emulatorArch) { +async function artifactURLForJSCRNTesterAPK( + emulatorArch /*: string */, +) /*: Promise */ { 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}`); }