From f6bc05a22a7b3ff609d441053b73a4f7e65a38e0 Mon Sep 17 00:00:00 2001 From: Lauren Tan Date: Thu, 18 Jul 2024 16:19:11 -0400 Subject: [PATCH] [ci] Add script to download artifact from github Updates the forked script to download build artifacts from GitHub. Note that this PR just adds the script, it does not add it to GH actions yet. ghstack-source-id: 08b0d2f93a88d55386e43a7d1bf88a67a117e899 Pull Request resolved: https://github.com/facebook/react/pull/30376 --- .../download-experimental-build-ghaction.js | 149 +++++++++++++++--- 1 file changed, 130 insertions(+), 19 deletions(-) diff --git a/scripts/release/download-experimental-build-ghaction.js b/scripts/release/download-experimental-build-ghaction.js index a8f79d8994..8f670f4e96 100755 --- a/scripts/release/download-experimental-build-ghaction.js +++ b/scripts/release/download-experimental-build-ghaction.js @@ -3,10 +3,12 @@ 'use strict'; const {join, relative} = require('path'); -const {getPublicPackages, handleError} = require('./utils'); +const {logPromise, handleError} = require('./utils'); const yargs = require('yargs'); const clear = require('clear'); const theme = require('./theme'); +const {exec} = require('child-process-promise'); +const {existsSync} = require('fs'); const argv = yargs.wrap(yargs.terminalWidth()).options({ releaseChannel: { @@ -21,21 +23,11 @@ const argv = yargs.wrap(yargs.terminalWidth()).options({ alias: 'c', describe: 'Commit hash to download.', requiresArg: true, + demandOption: true, type: 'string', }, - skipTests: { - requiresArg: false, - type: 'boolean', - default: false, - }, - allowBrokenCI: { - requiresArg: false, - type: 'boolean', - default: false, - }, }).argv; -// Inlined from scripts/release/download-experimental-build-commands/print-summary.js function printSummary(commit) { const commandPath = relative( process.env.PWD, @@ -54,17 +46,136 @@ function printSummary(commit) { console.log(message.replace(/\n +/g, '\n').trim()); } -const run = async () => { +const OWNER = 'facebook'; +const REPO = 'react'; +const WORKFLOW_ID = 'runtime_build_and_test.yml'; +const GITHUB_HEADERS = ` + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${process.env.GH_TOKEN}" \ + -H "X-GitHub-Api-Version: 2022-11-28"`.trim(); + +function getWorkflowId() { + if (existsSync(join(__dirname, `../../.github/workflows/${WORKFLOW_ID}`))) { + return WORKFLOW_ID; + } else { + throw new Error( + `Incorrect workflow ID: .github/workflows/${WORKFLOW_ID} does not exist. Please check the name of the workflow being downloaded from.` + ); + } +} + +async function getWorkflowRunId(commit) { + const res = await exec( + `curl -L ${GITHUB_HEADERS} https://api.github.com/repos/${OWNER}/${REPO}/actions/workflows/${getWorkflowId()}/runs?head_sha=${commit}&branch=main&exclude_pull_requests=true` + ); + + const json = JSON.parse(res.stdout); + let workflowRun; + if (json.total_count === 1) { + workflowRun = json.workflow_runs[0]; + } else { + workflowRun = json.workflow_runs.find( + run => run.head_sha === commit && run.head_branch === 'main' + ); + } + + if (workflowRun == null || workflowRun.id == null) { + console.log( + theme`{error The workflow run for the specified commit (${commit}) could not be found.}` + ); + process.exit(1); + } + + return workflowRun.id; +} + +async function getArtifact(workflowRunId, artifactName) { + const res = await exec( + `curl -L ${GITHUB_HEADERS} https://api.github.com/repos/${OWNER}/${REPO}/actions/runs/${workflowRunId}/artifacts?per_page=100&name=${artifactName}` + ); + + const json = JSON.parse(res.stdout); + let artifact; + if (json.total_count === 1) { + artifact = json.artifacts[0]; + } else { + artifact = json.artifacts.find( + _artifact => _artifact.name === artifactName + ); + } + + if (artifact == null) { + console.log( + theme`{error The specified workflow run (${workflowRunId}) does not contain any build artifacts.}` + ); + process.exit(1); + } + + return artifact; +} + +async function downloadArtifactsFromGitHub(commit, releaseChannel) { + const workflowRunId = await getWorkflowRunId(commit); + const artifact = await getArtifact(workflowRunId, 'artifacts_combined'); + + // Download and extract artifact + const cwd = join(__dirname, '..', '..'); + await exec(`rm -rf ./build`, {cwd}); + await exec( + `curl -L ${GITHUB_HEADERS} ${artifact.archive_download_url} \ + > a.zip && unzip a.zip -d . && rm a.zip build2.tgz && tar -xvzf build.tgz && rm build.tgz`, + { + cwd, + } + ); + + // Copy to staging directory + // TODO: Consider staging the release in a different directory from the CI + // build artifacts: `./build/node_modules` -> `./staged-releases` + if (!existsSync(join(cwd, 'build'))) { + await exec(`mkdir ./build`, {cwd}); + } else { + await exec(`rm -rf ./build/node_modules`, {cwd}); + } + let sourceDir; + // TODO: Rename release channel to `next` + if (releaseChannel === 'stable') { + sourceDir = 'oss-stable'; + } else if (releaseChannel === 'experimental') { + sourceDir = 'oss-experimental'; + } else if (releaseChannel === 'rc') { + sourceDir = 'oss-stable-rc'; + } else if (releaseChannel === 'latest') { + sourceDir = 'oss-stable-semver'; + } else { + console.error('Internal error: Invalid release channel: ' + releaseChannel); + process.exit(releaseChannel); + } + await exec(`cp -r ./build/${sourceDir} ./build/node_modules`, {cwd}); +} + +async function downloadBuildArtifacts(commit, releaseChannel) { + const label = theme`commit {commit ${commit}})`; + return logPromise( + downloadArtifactsFromGitHub(commit, releaseChannel), + theme`Downloading artifacts from GitHub for ${label}` + ); +} + +const main = async () => { try { - argv.cwd = join(__dirname, '..', '..'); - argv.packages = await getPublicPackages(true); - - console.log(argv); - + await downloadBuildArtifacts(argv.commit, argv.releaseChannel); printSummary(argv.commit); } catch (error) { handleError(error); } }; -run(); +if (process.env.GH_TOKEN == null) { + console.log( + theme`{error Expected GH_TOKEN to be provided as an env variable}` + ); + process.exit(1); +} + +main();