Repository: facebook/react
Pull request: [scripts] Verify artifact integrity when downloading


Uses https://cli.github.com/manual/gh_attestation_verify to verify that the downloaded artifact matches the attestation generated during the build process in runtime_commit_artifacts.

Example:

On a workflow run of runtime_build_and_test.yml with no attestations:
```
$ scripts/release/download-experimental-build.js --commit=ea5f065745b777cb41cc9e54a3b29ed8c727a574

Command failed: gh attestation verify artifacts_combined.zip --repo=facebook/react

Error: failed to fetch attestations from facebook/react: HTTP 404: Not Found (https://api.github.com/repos/facebook/react/attestations/sha256:7adba0992ba477a927aad5a07f95ee2deb7d18427c84279d33fc40a3bc28ebaa?per_page=30)
`gh attestation verify artifacts_combined.zip --repo=facebook/react` (exited with error code 1)
```

On one which does:

```
$ scripts/release/download-experimental-build.js --commit=12e85d74c1c233cdc2f3228a97473a4435d50c3b

✓ Downloading artifacts from GitHub for commit 12e85d74c1c233cdc2f3228a97473a4435d50c3b) 10.5 secs
An experimental build has been downloaded!

You can download this build again by running:
  scripts/download-experimental-build.js --commit=12e85d74c1c233cdc2f3228a97473a4435d50c3b
```
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32728).
* #32729
* __->__ #32728

Changed files:
filename: scripts/release/shared-commands/download-build-artifacts.js
patch: @@ -3,8 +3,9 @@
 const {join} = require('path');
 const theme = require('../theme');
 const {exec} = require('child-process-promise');
-const {existsSync, readFileSync} = require('fs');
+const {existsSync, mkdtempSync, readFileSync} = require('fs');
 const {logPromise} = require('../utils');
+const os = require('os');
 
 if (process.env.GH_TOKEN == null) {
   console.log(
@@ -21,6 +22,15 @@ const GITHUB_HEADERS = `
   -H "Authorization: Bearer ${process.env.GH_TOKEN}" \
   -H "X-GitHub-Api-Version: 2022-11-28"`.trim();
 
+async function executableIsAvailable(name) {
+  try {
+    await exec(`which ${name}`);
+    return true;
+  } catch (_error) {
+    return false;
+  }
+}
+
 function sleep(ms) {
   return new Promise(resolve => setTimeout(resolve, ms));
 }
@@ -78,10 +88,27 @@ async function getArtifact(workflowRunId, artifactName) {
 async function processArtifact(artifact, commit, releaseChannel) {
   // Download and extract artifact
   const cwd = join(__dirname, '..', '..', '..');
+  const tmpDir = mkdtempSync(join(os.tmpdir(), 'react_'));
   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`,
+    `curl -L ${GITHUB_HEADERS} ${artifact.archive_download_url} > artifacts_combined.zip`,
+    {
+      cwd: tmpDir,
+    }
+  );
+
+  // Use https://cli.github.com/manual/gh_attestation_verify to verify artifact
+  if (executableIsAvailable('gh')) {
+    await exec(
+      `gh attestation verify artifacts_combined.zip --repo=${OWNER}/${REPO}`,
+      {
+        cwd: tmpDir,
+      }
+    );
+  }
+
+  await exec(
+    `unzip ${tmpDir}/artifacts_combined.zip -d . && rm build2.tgz && tar -xvzf build.tgz && rm build.tgz`,
     {
       cwd,
     }