main
js 225 lines 6.31 KB
Raw
1 'use strict';
2
3 const {join} = require('path');
4 const theme = require('../theme');
5 const {exec} = require('child-process-promise');
6 const {existsSync, mkdtempSync, readFileSync} = require('fs');
7 const {logPromise} = require('../utils');
8 const os = require('os');
9
10 if (process.env.GH_TOKEN == null) {
11 console.log(
12 theme`{error Expected GH_TOKEN to be provided as an env variable}`
13 );
14 process.exit(1);
15 }
16
17 const REPO = process.env.GITHUB_REPOSITORY || 'facebook/react';
18 const WORKFLOW_ID = 'runtime_build_and_test.yml';
19 const GITHUB_HEADERS = `
20 -H "Accept: application/vnd.github+json" \
21 -H "Authorization: Bearer ${process.env.GH_TOKEN}" \
22 -H "X-GitHub-Api-Version: 2022-11-28"`.trim();
23
24 async function executableIsAvailable(name) {
25 try {
26 await exec(`which ${name}`);
27 return true;
28 } catch (_error) {
29 return false;
30 }
31 }
32
33 function sleep(ms) {
34 return new Promise(resolve => setTimeout(resolve, ms));
35 }
36
37 function getWorkflowId() {
38 if (
39 existsSync(join(__dirname, `../../../.github/workflows/${WORKFLOW_ID}`))
40 ) {
41 return WORKFLOW_ID;
42 } else {
43 throw new Error(
44 `Incorrect workflow ID: .github/workflows/${WORKFLOW_ID} does not exist. Please check the name of the workflow being downloaded from.`
45 );
46 }
47 }
48
49 async function getWorkflowRun(commit) {
50 const res = await exec(
51 `curl -L ${GITHUB_HEADERS} https://api.github.com/repos/${REPO}/actions/workflows/${getWorkflowId()}/runs?head_sha=${commit}`
52 );
53
54 const json = JSON.parse(res.stdout);
55 const workflowRun = json.workflow_runs.find(run => run.head_sha === commit);
56
57 if (workflowRun == null || workflowRun.id == null) {
58 console.log(
59 theme`{error The workflow run for the specified commit (${commit}) could not be found.}`
60 );
61 process.exit(1);
62 }
63
64 return workflowRun;
65 }
66
67 async function getArtifact(workflowRunId, artifactName) {
68 const res = await exec(
69 `curl -L ${GITHUB_HEADERS} https://api.github.com/repos/${REPO}/actions/runs/${workflowRunId}/artifacts?per_page=100&name=${artifactName}`
70 );
71
72 const json = JSON.parse(res.stdout);
73 const artifact = json.artifacts.find(
74 _artifact => _artifact.name === artifactName
75 );
76
77 if (artifact == null) {
78 console.log(
79 theme`{error The specified workflow run (${workflowRunId}) does not contain any build artifacts.}`
80 );
81 process.exit(1);
82 }
83
84 return artifact;
85 }
86
87 async function processArtifact(artifact, opts) {
88 // Download and extract artifact
89 const cwd = join(__dirname, '..', '..', '..');
90 const tmpDir = mkdtempSync(join(os.tmpdir(), 'react_'));
91 await exec(`rm -rf ./build`, {cwd});
92 await exec(
93 `curl -L ${GITHUB_HEADERS} ${artifact.archive_download_url} > artifacts_combined.zip`,
94 {
95 cwd: tmpDir,
96 }
97 );
98
99 if (opts.noVerify === true) {
100 console.log(theme`{caution Skipping verification of build artifact.}`);
101 } else {
102 // Use https://cli.github.com/manual/gh_attestation_verify to verify artifact
103 if (executableIsAvailable('gh')) {
104 await exec(
105 `gh attestation verify artifacts_combined.zip --repo=${REPO}`,
106 {
107 cwd: tmpDir,
108 }
109 );
110 }
111 }
112
113 await exec(
114 `unzip ${tmpDir}/artifacts_combined.zip -d . && rm build2.tgz && tar -xvzf build.tgz && rm build.tgz`,
115 {
116 cwd,
117 }
118 );
119
120 // Copy to staging directory
121 // TODO: Consider staging the release in a different directory from the CI
122 // build artifacts: `./build/node_modules` -> `./staged-releases`
123 if (!existsSync(join(cwd, 'build'))) {
124 await exec(`mkdir ./build`, {cwd});
125 } else {
126 await exec(`rm -rf ./build/node_modules`, {cwd});
127 }
128 let sourceDir;
129 // TODO: Rename release channel to `next`
130 if (opts.releaseChannel === 'stable') {
131 sourceDir = 'oss-stable';
132 } else if (opts.releaseChannel === 'experimental') {
133 sourceDir = 'oss-experimental';
134 } else if (opts.releaseChannel === 'rc') {
135 sourceDir = 'oss-stable-rc';
136 } else if (opts.releaseChannel === 'latest') {
137 sourceDir = 'oss-stable-semver';
138 } else {
139 console.error(
140 'Internal error: Invalid release channel: ' + opts.releaseChannel
141 );
142 process.exit(opts.releaseChannel);
143 }
144 await exec(`cp -r ./build/${sourceDir} ./build/node_modules`, {
145 cwd,
146 });
147
148 // Validate artifact
149 const buildSha = readFileSync('./build/COMMIT_SHA', 'utf8').replace(
150 /[\u0000-\u001F\u007F-\u009F]/g,
151 ''
152 );
153 if (buildSha !== opts.commit) {
154 throw new Error(
155 `Requested commit sha does not match downloaded artifact. Expected: ${opts.commit}, got: ${buildSha}`
156 );
157 }
158 }
159
160 async function downloadArtifactsFromGitHub(opts) {
161 let workflowRun;
162 let retries = 0;
163 // wait up to 10 mins for build to finish: 10 * 60 * 1_000) / 30_000 = 20
164 while (retries < 20) {
165 workflowRun = await getWorkflowRun(opts.commit);
166 if (typeof workflowRun.status === 'string') {
167 switch (workflowRun.status) {
168 case 'queued':
169 case 'in_progress':
170 case 'waiting': {
171 retries++;
172 console.log(theme`Build still in progress, waiting 30s...`);
173 await sleep(30_000);
174 break;
175 }
176 case 'completed': {
177 if (workflowRun.conclusion === 'success') {
178 const artifact = await getArtifact(
179 workflowRun.id,
180 'artifacts_combined'
181 );
182 await processArtifact(artifact, opts);
183 return;
184 } else {
185 console.log(
186 theme`{error Could not download build as its conclusion was: ${workflowRun.conclusion}}`
187 );
188 process.exit(1);
189 }
190 break;
191 }
192 default: {
193 console.log(
194 theme`{error Unhandled workflow run status: ${workflowRun.status}}`
195 );
196 process.exit(1);
197 }
198 }
199 } else {
200 retries++;
201 console.log(
202 theme`{error Expected workflow run status to be a string, got: ${workflowRun.status}. Retrying...}`
203 );
204 }
205 }
206
207 console.log(
208 theme`{error Could not download build from GitHub. Last workflow run: }
209
210 ${workflowRun != null ? JSON.stringify(workflowRun, null, '\t') : workflowRun}`
211 );
212 process.exit(1);
213 }
214
215 async function downloadBuildArtifacts(opts) {
216 const label = theme`commit {commit ${opts.commit}})`;
217 return logPromise(
218 downloadArtifactsFromGitHub(opts),
219 theme`Downloading artifacts from GitHub for ${label}`
220 );
221 }
222
223 module.exports = {
224 downloadBuildArtifacts,
225 };