@samitouri / QOS-React / commits / 3043e1ae21

[ci] Add polling to download-build-artifacts

Several CI workflows depend on the runtime_build_and_test.yml workflow to complete before it can successfully download the build artifact. However it is possible to encounter a race condition where the build hasn't completed when the new workflow is started. This PR adds a simple polling mechanism that waits up to 10 minutes for the build for that revision to complete. ghstack-source-id: 6a954638a800fbea8081e6fba35ee4b4437731c5 Pull Request resolved: https://github.com/facebook/react/pull/30515

Lauren Tan committed Jul 29, 2024 at 18:51 UTC 3043e1ae21c3847fb4a762dd2128596d55a7c4cb
1 file changed +86 -37
scripts/release/shared-commands/download-build-artifacts.js
+86 -37
@@ -21,6 +21,10 @@ const GITHUB_HEADERS = `
21 -H "Authorization: Bearer ${process.env.GH_TOKEN}" \
22 -H "X-GitHub-Api-Version: 2022-11-28"`.trim();
23
24 +function sleep(ms) {
25 + return new Promise(resolve => setTimeout(resolve, ms));
26 +}
27 +
28 function getWorkflowId() {
29 if (
30 existsSync(join(__dirname, `../../../.github/workflows/${WORKFLOW_ID}`))
@@ -33,7 +37,7 @@ function getWorkflowId() {
37 }
38 }
39
36 -async function getWorkflowRunId(commit) {
40 +async function getWorkflowRun(commit) {
41 const res = await exec(
42 `curl -L ${GITHUB_HEADERS} https://api.github.com/repos/${OWNER}/${REPO}/actions/workflows/${getWorkflowId()}/runs?head_sha=${commit}&branch=main&exclude_pull_requests=true`
43 );
@@ -55,7 +59,7 @@ async function getWorkflowRunId(commit) {
59 process.exit(1);
60 }
61
58 - return workflowRun.id;
62 + return workflowRun;
63 }
64
65 async function getArtifact(workflowRunId, artifactName) {
@@ -84,43 +88,88 @@ async function getArtifact(workflowRunId, artifactName) {
88 }
89
90 async function downloadArtifactsFromGitHub(commit, releaseChannel) {
87 - const workflowRunId = await getWorkflowRunId(commit);
88 - const artifact = await getArtifact(workflowRunId, 'artifacts_combined');
89 -
90 - // Download and extract artifact
91 - const cwd = join(__dirname, '..', '..', '..');
92 - await exec(`rm -rf ./build`, {cwd});
93 - await exec(
94 - `curl -L ${GITHUB_HEADERS} ${artifact.archive_download_url} \
95 - > a.zip && unzip a.zip -d . && rm a.zip build2.tgz && tar -xvzf build.tgz && rm build.tgz`,
96 - {
97 - cwd,
91 + const workflowRun = await getWorkflowRun(commit);
92 + let retries = 0;
93 + // wait up to 10 mins for build to finish: 10 * 60 * 1_000) / 30_000 = 20
94 + while (retries < 20) {
95 + if (typeof workflowRun.status === 'string') {
96 + switch (workflowRun.status) {
97 + case 'queued':
98 + case 'in_progress':
99 + case 'waiting': {
100 + retries++;
101 + console.log(theme`Build still in progress, waiting 30s...`);
102 + await sleep(30_000);
103 + break;
104 + }
105 + case 'completed': {
106 + if (workflowRun.conclusion === 'success') {
107 + const artifact = await getArtifact(
108 + workflowRun.id,
109 + 'artifacts_combined'
110 + );
111 +
112 + // Download and extract artifact
113 + const cwd = join(__dirname, '..', '..', '..');
114 + await exec(`rm -rf ./build`, {cwd});
115 + await exec(
116 + `curl -L ${GITHUB_HEADERS} ${artifact.archive_download_url} \
117 + > a.zip && unzip a.zip -d . && rm a.zip build2.tgz && tar -xvzf build.tgz && rm build.tgz`,
118 + {
119 + cwd,
120 + }
121 + );
122 +
123 + // Copy to staging directory
124 + // TODO: Consider staging the release in a different directory from the CI
125 + // build artifacts: `./build/node_modules` -> `./staged-releases`
126 + if (!existsSync(join(cwd, 'build'))) {
127 + await exec(`mkdir ./build`, {cwd});
128 + } else {
129 + await exec(`rm -rf ./build/node_modules`, {cwd});
130 + }
131 + let sourceDir;
132 + // TODO: Rename release channel to `next`
133 + if (releaseChannel === 'stable') {
134 + sourceDir = 'oss-stable';
135 + } else if (releaseChannel === 'experimental') {
136 + sourceDir = 'oss-experimental';
137 + } else if (releaseChannel === 'rc') {
138 + sourceDir = 'oss-stable-rc';
139 + } else if (releaseChannel === 'latest') {
140 + sourceDir = 'oss-stable-semver';
141 + } else {
142 + console.error(
143 + 'Internal error: Invalid release channel: ' + releaseChannel
144 + );
145 + process.exit(releaseChannel);
146 + }
147 + await exec(`cp -r ./build/${sourceDir} ./build/node_modules`, {
148 + cwd,
149 + });
150 + return;
151 + } else {
152 + console.log(
153 + theme`{error Could not download build for ${commit} from GitHub as its conclusion was: ${workflowRun.conclusion}}`
154 + );
155 + process.exit(1);
156 + }
157 + break;
158 + }
159 + default: {
160 + console.log(
161 + theme`{error Unhandled workflow run status: ${workflowRun.status}}`
162 + );
163 + process.exit(1);
164 + }
165 + }
166 }
99 - );
100 -
101 - // Copy to staging directory
102 - // TODO: Consider staging the release in a different directory from the CI
103 - // build artifacts: `./build/node_modules` -> `./staged-releases`
104 - if (!existsSync(join(cwd, 'build'))) {
105 - await exec(`mkdir ./build`, {cwd});
106 - } else {
107 - await exec(`rm -rf ./build/node_modules`, {cwd});
167 }
109 - let sourceDir;
110 - // TODO: Rename release channel to `next`
111 - if (releaseChannel === 'stable') {
112 - sourceDir = 'oss-stable';
113 - } else if (releaseChannel === 'experimental') {
114 - sourceDir = 'oss-experimental';
115 - } else if (releaseChannel === 'rc') {
116 - sourceDir = 'oss-stable-rc';
117 - } else if (releaseChannel === 'latest') {
118 - sourceDir = 'oss-stable-semver';
119 - } else {
120 - console.error('Internal error: Invalid release channel: ' + releaseChannel);
121 - process.exit(releaseChannel);
122 - }
123 - await exec(`cp -r ./build/${sourceDir} ./build/node_modules`, {cwd});
168 +
169 + console.log(
170 + theme`{error Could not download build for ${commit} from GitHub as it timed out}`
171 + );
172 + process.exit(1);
173 }
174
175 async function downloadBuildArtifacts(commit, releaseChannel) {