From 2866f6d9479064eab79ec6e9eb5abcfbfdb121f9 Mon Sep 17 00:00:00 2001 From: Samuel Maddock Date: Thu, 14 Nov 2024 19:19:24 -0500 Subject: [PATCH 01/13] feat: e pr download-dist --- .gitignore | 1 + package.json | 1 + src/e-pr.js | 170 ++++++++++++++++++++++++++++++++++++++++++++++----- yarn.lock | 93 +++++++++++++++++++--------- 4 files changed, 221 insertions(+), 44 deletions(-) diff --git a/.gitignore b/.gitignore index d0901c12..0e64e1e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .nyc_output/ +artifacts configs coverage node_modules diff --git a/package.json b/package.json index 94291a99..bb6ee415 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "command-exists": "^1.2.8", "commander": "^9.0.0", "debug": "^4.3.1", + "extract-zip": "^2.0.1", "inquirer": "^8.2.4", "node-gyp": "^10.0.1", "open": "^6.4.0", diff --git a/src/e-pr.js b/src/e-pr.js index 0ceb756b..3bf51def 100644 --- a/src/e-pr.js +++ b/src/e-pr.js @@ -1,13 +1,17 @@ #!/usr/bin/env node const childProcess = require('child_process'); +const fs = require('fs'); const path = require('path'); + +const extractZip = require('extract-zip'); const querystring = require('querystring'); const semver = require('semver'); - const open = require('open'); const program = require('commander'); +const { Octokit } = require('@octokit/rest'); +const { getGitHubAuthToken } = require('./utils/github-auth'); const { current } = require('./evm-config'); const { color, fatal } = require('./utils/logging'); @@ -134,27 +138,23 @@ function pullRequestSource(source) { } program + .command('open') .description('Open a GitHub URL where you can PR your changes') - .option( - '-s, --source ', - 'Where the changes are coming from', - guessPRSource(current()), - ) - .option( - '-t, --target ', - 'Where the changes are going to', - guessPRTarget(current()), - ) + .option('-s, --source [source_branch]', 'Where the changes are coming from') + .option('-t, --target [target_branch]', 'Where the changes are going to') .option('-b, --backport ', 'Pull request being backported') .action(async (options) => { - if (!options.source) { + const source = options.source || guessPRSource(current()); + const target = options.target || guessPRSource(current()); + + if (!source) { fatal(`'source' is required to create a PR`); - } else if (!options.target) { + } else if (!target) { fatal(`'target' is required to create a PR`); } const repoBaseUrl = 'https://github.com/electron/electron'; - const comparePath = `${options.target}...${pullRequestSource(options.source)}`; + const comparePath = `${target}...${pullRequestSource(source)}`; const queryParams = { expand: 1 }; if (!options.backport) { @@ -188,5 +188,143 @@ program } return open(`${repoBaseUrl}/compare/${comparePath}?${querystring.stringify(queryParams)}`); - }) - .parse(process.argv); + }); + +program + .command('download-dist ') + .description('Download a pull request dist') + .option('--platform [platform]', 'Platform to download dist for', process.platform) + .option('--arch [arch]', 'Architecture to download dist for', process.arch) + .action(async (pullRequestNumber, options) => { + if (!pullRequestNumber) { + fatal(`Pull request number is required to download a PR`); + } + + const octokit = new Octokit({ + auth: await getGitHubAuthToken(['repo']), + }); + + let pullRequest; + try { + const { data } = await octokit.pulls.get({ + owner: 'electron', + repo: 'electron', + pull_number: pullRequestNumber, + }); + pullRequest = data; + } catch (error) { + console.error(`Failed to get pull request: ${error}`); + return; + } + + let workflowRuns; + try { + const { data } = await octokit.rest.actions.listWorkflowRunsForRepo({ + owner: 'electron', + repo: 'electron', + branch: pullRequest.head.ref, + name: 'Build', + event: 'pull_request', + status: 'completed', + per_page: 10, + sort: 'created', + direction: 'desc', + }); + workflowRuns = data.workflow_runs; + } catch (error) { + console.error(`Failed to list workflow runs: ${error}`); + return; + } + + const latestBuildWorkflowRun = workflowRuns.find((run) => run.name === 'Build'); + if (!latestBuildWorkflowRun) { + fatal(`No 'Build' workflow runs found for pull request #${pullRequestNumber}`); + return; + } + + let artifacts; + try { + const { data } = await octokit.actions.listWorkflowRunArtifacts({ + owner: 'electron', + repo: 'electron', + run_id: latestBuildWorkflowRun.id, + }); + artifacts = data.artifacts; + } catch (error) { + console.error(`Failed to list artifacts: ${error}`); + return; + } + + const artifactName = `generated_artifacts_${options.platform}_${options.arch}`; + const artifact = artifacts.find((artifact) => artifact.name === artifactName); + if (!artifact) { + console.error(`Failed to find artifact: ${artifactName}`); + return; + } + + const prDir = path.resolve( + __dirname, + '..', + 'artifacts', + `pr_${pullRequest.number}_${options.platform}_${options.arch}`, + ); + + // Clean up the directory if it exists + try { + await fs.promises.rm(prDir, { recursive: true, force: true }); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } + + // Create the directory + await fs.promises.mkdir(prDir, { recursive: true }); + console.log( + `Downloading artifact '${artifactName}' from pull request #${pullRequestNumber}...`, + ); + + // Download the artifact + // TODO: use write stream + const response = await octokit.actions.downloadArtifact({ + owner: 'electron', + repo: 'electron', + artifact_id: artifact.id, + archive_format: 'zip', + }); + + const artifactPath = path.join(prDir, `${artifactName}.zip`); + await fs.promises.writeFile(artifactPath, Buffer.from(response.data)); + + console.log('Extracting dist...'); + + // Extract the artifact zip + const extractPath = path.join(prDir, artifactName); + await fs.promises.mkdir(extractPath, { recursive: true }); + await extractZip(artifactPath, { dir: extractPath }); + + // Check if dist.zip exists within the extracted artifact + const distZipPath = path.join(extractPath, 'dist.zip'); + if (!(await fs.promises.stat(distZipPath).catch(() => false))) { + fatal(`dist.zip not found within the extracted artifact.`); + return; + } + + // Extract dist.zip + await extractZip(distZipPath, { dir: prDir }); + + // Check if Electron.app exists within the extracted dist.zip + const electronAppPath = path.join(prDir, 'Electron.app'); + if (!(await fs.promises.stat(electronAppPath).catch(() => false))) { + fatal(`Electron.app not found within the extracted dist.zip.`); + return; + } + + // Remove the artifact and extracted artifact zip + await fs.promises.rm(artifactPath); + await fs.promises.rm(extractPath, { recursive: true }); + + console.info(`Downloaded to ${electronAppPath}`); + }); + +program.parse(process.argv); diff --git a/yarn.lock b/yarn.lock index 5f0edcf5..d4e6b9a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -775,11 +775,25 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== +"@types/node@*": + version "22.9.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.9.0.tgz#b7f16e5c3384788542c72dc3d561a7ceae2c0365" + integrity sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ== + dependencies: + undici-types "~6.19.8" + "@types/unist@*", "@types/unist@^2.0.0": version "2.0.6" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/yauzl@^2.9.1": + version "2.10.3" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999" + integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q== + dependencies: + "@types/node" "*" + "@vitest/expect@2.1.3": version "2.1.3" resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.3.tgz#4b9a6fff22be4c4cd5d57e687cfda611b514b0ad" @@ -1151,6 +1165,11 @@ btoa-lite@^1.0.0: resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== + buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -2014,6 +2033,17 @@ external-editor@^3.0.3: iconv-lite "^0.4.24" tmp "^0.0.33" +extract-zip@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== + dependencies: + debug "^4.1.1" + get-stream "^5.1.0" + yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -2036,6 +2066,13 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== + dependencies: + pend "~1.2.0" + figures@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -2266,6 +2303,13 @@ get-stdin@~9.0.0: resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-9.0.0.tgz#3983ff82e03d56f1b2ea0d3e60325f39d703a575" integrity sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA== +get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + get-stream@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" @@ -4091,6 +4135,11 @@ pathval@^2.0.0: resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.0.tgz#7e2550b422601d4f6b8e26f1301bc8f15a741a25" integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA== +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== + picocolors@^1.0.0, picocolors@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" @@ -4698,16 +4747,7 @@ string-argv@~0.3.2: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -4782,14 +4822,7 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -5045,6 +5078,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undici-types@~6.19.8: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + unique-filename@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" @@ -5333,7 +5371,7 @@ word-wrap@^1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -5351,15 +5389,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -5448,6 +5477,14 @@ yargs@^15.0.2: y18n "^4.0.0" yargs-parser "^18.1.2" +yauzl@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" From 6ef4122548803e6d5c660d7293c61b6da3d75de6 Mon Sep 17 00:00:00 2001 From: Samuel Maddock Date: Thu, 14 Nov 2024 19:30:23 -0500 Subject: [PATCH 02/13] update docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6cd747f5..b81a926b 100644 --- a/README.md +++ b/README.md @@ -482,7 +482,7 @@ git cherry-pick --continue git push # create pull request -e pr --backport 1234 +e pr open --backport 1234 ``` ## Common Usage From 9b2ac1753d104de1cbd39a8a687dda32b49ac665 Mon Sep 17 00:00:00 2001 From: Sam Maddock Date: Thu, 14 Nov 2024 19:53:09 -0500 Subject: [PATCH 03/13] Update src/e-pr.js Co-authored-by: Will Anderson --- src/e-pr.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/e-pr.js b/src/e-pr.js index 3bf51def..21545ce2 100644 --- a/src/e-pr.js +++ b/src/e-pr.js @@ -193,8 +193,8 @@ program program .command('download-dist ') .description('Download a pull request dist') - .option('--platform [platform]', 'Platform to download dist for', process.platform) - .option('--arch [arch]', 'Architecture to download dist for', process.arch) + .option('--platform [platform]', 'Platform to download dist for. Defaults to current platform.', process.platform) + .option('--arch [arch]', 'Architecture to download dist for. Defaults to current arch.', process.arch) .action(async (pullRequestNumber, options) => { if (!pullRequestNumber) { fatal(`Pull request number is required to download a PR`); From 705163f17ce1882be00150f6232b6fa7cb6a99f3 Mon Sep 17 00:00:00 2001 From: Samuel Maddock Date: Thu, 14 Nov 2024 19:57:43 -0500 Subject: [PATCH 04/13] use correct executable names per platform --- src/e-pr.js | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/e-pr.js b/src/e-pr.js index 21545ce2..12576595 100644 --- a/src/e-pr.js +++ b/src/e-pr.js @@ -193,8 +193,16 @@ program program .command('download-dist ') .description('Download a pull request dist') - .option('--platform [platform]', 'Platform to download dist for. Defaults to current platform.', process.platform) - .option('--arch [arch]', 'Architecture to download dist for. Defaults to current arch.', process.arch) + .option( + '--platform [platform]', + 'Platform to download dist for. Defaults to current platform.', + process.platform, + ) + .option( + '--arch [arch]', + 'Architecture to download dist for. Defaults to current arch.', + process.arch, + ) .action(async (pullRequestNumber, options) => { if (!pullRequestNumber) { fatal(`Pull request number is required to download a PR`); @@ -313,10 +321,17 @@ program // Extract dist.zip await extractZip(distZipPath, { dir: prDir }); - // Check if Electron.app exists within the extracted dist.zip - const electronAppPath = path.join(prDir, 'Electron.app'); + // Check if Electron exists within the extracted dist.zip + const platformExecutables = { + win32: 'electron.exe', + darwin: 'Electron.app', + linux: 'electron', + }; + const executableName = platformExecutables[options.platform]; + + const electronAppPath = path.join(prDir, executableName); if (!(await fs.promises.stat(electronAppPath).catch(() => false))) { - fatal(`Electron.app not found within the extracted dist.zip.`); + fatal(`${executableName} not found within the extracted dist.zip.`); return; } From 533cef9e5f129274b06e3d2541d9740d6a0a9dee Mon Sep 17 00:00:00 2001 From: Samuel Maddock Date: Thu, 14 Nov 2024 23:00:43 -0500 Subject: [PATCH 05/13] fix guess target --- src/e-pr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/e-pr.js b/src/e-pr.js index 12576595..0a498123 100644 --- a/src/e-pr.js +++ b/src/e-pr.js @@ -145,7 +145,7 @@ program .option('-b, --backport ', 'Pull request being backported') .action(async (options) => { const source = options.source || guessPRSource(current()); - const target = options.target || guessPRSource(current()); + const target = options.target || guessPRTarget(current()); if (!source) { fatal(`'source' is required to create a PR`); From 9e2ad2696b201633cd5ce4fb2dc429a1eecad91a Mon Sep 17 00:00:00 2001 From: Samuel Maddock Date: Thu, 14 Nov 2024 23:02:35 -0500 Subject: [PATCH 06/13] set e pr open as default command --- src/e | 2 +- src/e-pr.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/e b/src/e index 92ec439b..ec0ef6ba 100755 --- a/src/e +++ b/src/e @@ -169,7 +169,7 @@ program .command('backport [pr]', 'Assists with manual backport processes') .command('show ', 'Show info about the current build config') .command('test [specRunnerArgs...]', `Run Electron's spec runner`) - .command('pr [options]', 'Open a GitHub URL where you can PR your changes') + .command('pr [subcommand]', 'Open a GitHub URL where you can PR your changes') .command('patches ', 'Refresh the patches in $root/src/electron/patches/$target') .command('open ', 'Open a GitHub URL for the given commit hash / pull # / issue #') .command('auto-update', 'Check for build-tools updates or enable/disable automatic updates') diff --git a/src/e-pr.js b/src/e-pr.js index 0a498123..4aba2936 100644 --- a/src/e-pr.js +++ b/src/e-pr.js @@ -138,7 +138,7 @@ function pullRequestSource(source) { } program - .command('open') + .command('open', null, { isDefault: true }) .description('Open a GitHub URL where you can PR your changes') .option('-s, --source [source_branch]', 'Where the changes are coming from') .option('-t, --target [target_branch]', 'Where the changes are going to') From 850933dc813091a5dfffe6e2ef49ac8061597bf4 Mon Sep 17 00:00:00 2001 From: Samuel Maddock Date: Fri, 15 Nov 2024 11:03:15 -0500 Subject: [PATCH 07/13] allow specifying output directory --- src/e-pr.js | 62 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/src/e-pr.js b/src/e-pr.js index 4aba2936..9edd4acd 100644 --- a/src/e-pr.js +++ b/src/e-pr.js @@ -203,6 +203,11 @@ program 'Architecture to download dist for. Defaults to current arch.', process.arch, ) + .option( + '-o, --output ', + 'Specify the output directory for downloaded artifacts. ' + + 'Defaults to ~/.electron_build_tools/artifacts/pr_{number}_{platform}_{arch}', + ) .action(async (pullRequestNumber, options) => { if (!pullRequestNumber) { fatal(`Pull request number is required to download a PR`); @@ -227,7 +232,7 @@ program let workflowRuns; try { - const { data } = await octokit.rest.actions.listWorkflowRunsForRepo({ + const { data } = await octokit.actions.listWorkflowRunsForRepo({ owner: 'electron', repo: 'electron', branch: pullRequest.head.ref, @@ -270,24 +275,38 @@ program return; } - const prDir = path.resolve( - __dirname, - '..', - 'artifacts', - `pr_${pullRequest.number}_${options.platform}_${options.arch}`, - ); + let outputDir; - // Clean up the directory if it exists - try { - await fs.promises.rm(prDir, { recursive: true, force: true }); - } catch (error) { - if (error.code !== 'ENOENT') { - throw error; + if (options.output) { + outputDir = path.resolve(options.output); + + if (!(await fs.promises.stat(outputDir).catch(() => false))) { + fatal(`The output directory '${options.output}' does not exist`); + return; + } + } else { + const defaultDir = path.resolve( + __dirname, + '..', + 'artifacts', + `pr_${pullRequest.number}_${options.platform}_${options.arch}`, + ); + + // Clean up the directory if it exists + try { + await fs.promises.rm(defaultDir, { recursive: true, force: true }); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } } + + // Create the directory + await fs.promises.mkdir(defaultDir, { recursive: true }); + + outputDir = defaultDir; } - // Create the directory - await fs.promises.mkdir(prDir, { recursive: true }); console.log( `Downloading artifact '${artifactName}' from pull request #${pullRequestNumber}...`, ); @@ -301,13 +320,13 @@ program archive_format: 'zip', }); - const artifactPath = path.join(prDir, `${artifactName}.zip`); + const artifactPath = path.join(outputDir, `${artifactName}.zip`); await fs.promises.writeFile(artifactPath, Buffer.from(response.data)); console.log('Extracting dist...'); // Extract the artifact zip - const extractPath = path.join(prDir, artifactName); + const extractPath = path.join(outputDir, artifactName); await fs.promises.mkdir(extractPath, { recursive: true }); await extractZip(artifactPath, { dir: extractPath }); @@ -319,7 +338,7 @@ program } // Extract dist.zip - await extractZip(distZipPath, { dir: prDir }); + await extractZip(distZipPath, { dir: outputDir }); // Check if Electron exists within the extracted dist.zip const platformExecutables = { @@ -329,7 +348,12 @@ program }; const executableName = platformExecutables[options.platform]; - const electronAppPath = path.join(prDir, executableName); + if (!executableName) { + fatal(`Unable to extract executable for platform '${options.platform}'`); + return; + } + + const electronAppPath = path.join(outputDir, executableName); if (!(await fs.promises.stat(electronAppPath).catch(() => false))) { fatal(`${executableName} not found within the extracted dist.zip.`); return; From a31b0a23acc4ab871001c80689a101b7412ef742 Mon Sep 17 00:00:00 2001 From: Samuel Maddock Date: Fri, 15 Nov 2024 11:07:11 -0500 Subject: [PATCH 08/13] fix wording --- src/e-pr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/e-pr.js b/src/e-pr.js index 9edd4acd..6fbe35cd 100644 --- a/src/e-pr.js +++ b/src/e-pr.js @@ -349,7 +349,7 @@ program const executableName = platformExecutables[options.platform]; if (!executableName) { - fatal(`Unable to extract executable for platform '${options.platform}'`); + fatal(`Unable to find executable for platform '${options.platform}'`); return; } From dc8c56e174c99304d1efbca961e984b03ba3eb2f Mon Sep 17 00:00:00 2001 From: Samuel Maddock Date: Fri, 15 Nov 2024 11:29:46 -0500 Subject: [PATCH 09/13] download to temporary directory --- src/e-pr.js | 100 ++++++++++++++++++++++++++++------------------------ 1 file changed, 53 insertions(+), 47 deletions(-) diff --git a/src/e-pr.js b/src/e-pr.js index 6fbe35cd..7a4fbcb3 100644 --- a/src/e-pr.js +++ b/src/e-pr.js @@ -2,6 +2,7 @@ const childProcess = require('child_process'); const fs = require('fs'); +const os = require('os'); const path = require('path'); const extractZip = require('extract-zip'); @@ -307,63 +308,68 @@ program outputDir = defaultDir; } - console.log( - `Downloading artifact '${artifactName}' from pull request #${pullRequestNumber}...`, - ); - - // Download the artifact - // TODO: use write stream - const response = await octokit.actions.downloadArtifact({ - owner: 'electron', - repo: 'electron', - artifact_id: artifact.id, - archive_format: 'zip', - }); + // Download the artifact to a temporary directory + const tempDir = path.join(os.tmpdir(), 'electron-tmp'); + await fs.promises.mkdir(tempDir); - const artifactPath = path.join(outputDir, `${artifactName}.zip`); - await fs.promises.writeFile(artifactPath, Buffer.from(response.data)); + let distExecutablePath; - console.log('Extracting dist...'); + try { + console.log( + `Downloading artifact '${artifactName}' from pull request #${pullRequestNumber}...`, + ); - // Extract the artifact zip - const extractPath = path.join(outputDir, artifactName); - await fs.promises.mkdir(extractPath, { recursive: true }); - await extractZip(artifactPath, { dir: extractPath }); + const artifactPath = path.join(tempDir, `${artifactName}.zip`); - // Check if dist.zip exists within the extracted artifact - const distZipPath = path.join(extractPath, 'dist.zip'); - if (!(await fs.promises.stat(distZipPath).catch(() => false))) { - fatal(`dist.zip not found within the extracted artifact.`); - return; - } + const response = await octokit.actions.downloadArtifact({ + owner: 'electron', + repo: 'electron', + artifact_id: artifact.id, + archive_format: 'zip', + }); + await fs.promises.writeFile(artifactPath, Buffer.from(response.data)); - // Extract dist.zip - await extractZip(distZipPath, { dir: outputDir }); + console.log('Extracting dist...'); - // Check if Electron exists within the extracted dist.zip - const platformExecutables = { - win32: 'electron.exe', - darwin: 'Electron.app', - linux: 'electron', - }; - const executableName = platformExecutables[options.platform]; + // Extract the artifact zip + const extractPath = path.join(tempDir, artifactName); + await fs.promises.mkdir(extractPath, { recursive: true }); + await extractZip(artifactPath, { dir: extractPath }); - if (!executableName) { - fatal(`Unable to find executable for platform '${options.platform}'`); - return; - } + // Check if dist.zip exists within the extracted artifact + const distZipPath = path.join(extractPath, 'dist.zip'); + if (!(await fs.promises.stat(distZipPath).catch(() => false))) { + fatal(`dist.zip not found within the extracted artifact.`); + return; + } - const electronAppPath = path.join(outputDir, executableName); - if (!(await fs.promises.stat(electronAppPath).catch(() => false))) { - fatal(`${executableName} not found within the extracted dist.zip.`); - return; - } + // Extract dist.zip to the final outputDir + await extractZip(distZipPath, { dir: outputDir }); + + // Check if Electron exists within the extracted dist.zip + const platformExecutables = { + win32: 'electron.exe', + darwin: 'Electron.app', + linux: 'electron', + }; + const executableName = platformExecutables[options.platform]; + + if (!executableName) { + fatal(`Unable to find executable for platform '${options.platform}'`); + return; + } - // Remove the artifact and extracted artifact zip - await fs.promises.rm(artifactPath); - await fs.promises.rm(extractPath, { recursive: true }); + distExecutablePath = path.join(outputDir, executableName); + if (!(await fs.promises.stat(distExecutablePath).catch(() => false))) { + fatal(`${executableName} not found within the extracted dist.zip.`); + return; + } + } finally { + // Cleanup temporary directory + await fs.promises.rm(tempDir, { recursive: true }); + } - console.info(`Downloaded to ${electronAppPath}`); + console.info(`Downloaded to ${distExecutablePath}`); }); program.parse(process.argv); From 9ac28164da6cedab1200ecff0d6663cec38acd24 Mon Sep 17 00:00:00 2001 From: Sam Maddock Date: Fri, 15 Nov 2024 17:44:18 -0500 Subject: [PATCH 10/13] Update src/e Co-authored-by: David Sanders --- src/e | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/e b/src/e index ec0ef6ba..65f02ec5 100755 --- a/src/e +++ b/src/e @@ -169,7 +169,7 @@ program .command('backport [pr]', 'Assists with manual backport processes') .command('show ', 'Show info about the current build config') .command('test [specRunnerArgs...]', `Run Electron's spec runner`) - .command('pr [subcommand]', 'Open a GitHub URL where you can PR your changes') + .command('pr [subcommand]', 'Work with PRs to electron/electron') .command('patches ', 'Refresh the patches in $root/src/electron/patches/$target') .command('open ', 'Open a GitHub URL for the given commit hash / pull # / issue #') .command('auto-update', 'Check for build-tools updates or enable/disable automatic updates') From 4bfafc9c4521d3e24476a5e396e27bf0d803ca69 Mon Sep 17 00:00:00 2001 From: Samuel Maddock Date: Fri, 15 Nov 2024 18:30:33 -0500 Subject: [PATCH 11/13] download and extract in-memory --- package.json | 2 +- src/e-pr.js | 98 +++++++++++++++++++++++----------------------------- yarn.lock | 67 +++-------------------------------- 3 files changed, 49 insertions(+), 118 deletions(-) diff --git a/package.json b/package.json index bb6ee415..8518f2c8 100644 --- a/package.json +++ b/package.json @@ -22,13 +22,13 @@ "@marshallofsound/chrome-cookies-secure": "^2.1.1", "@octokit/auth-oauth-device": "^3.1.1", "@octokit/rest": "^18.5.2", + "adm-zip": "^0.5.16", "ajv": "^8.11.0", "ajv-formats": "^2.1.1", "chalk": "^2.4.1", "command-exists": "^1.2.8", "commander": "^9.0.0", "debug": "^4.3.1", - "extract-zip": "^2.0.1", "inquirer": "^8.2.4", "node-gyp": "^10.0.1", "open": "^6.4.0", diff --git a/src/e-pr.js b/src/e-pr.js index 7a4fbcb3..1586d161 100644 --- a/src/e-pr.js +++ b/src/e-pr.js @@ -2,10 +2,9 @@ const childProcess = require('child_process'); const fs = require('fs'); -const os = require('os'); const path = require('path'); -const extractZip = require('extract-zip'); +const AdmZip = require('adm-zip'); const querystring = require('querystring'); const semver = require('semver'); const open = require('open'); @@ -16,6 +15,8 @@ const { getGitHubAuthToken } = require('./utils/github-auth'); const { current } = require('./evm-config'); const { color, fatal } = require('./utils/logging'); +const d = require('debug')('build-tools:pr'); + // Adapted from https://github.com/electron/clerk function findNoteInPRBody(body) { const onelineMatch = /(?:(?:\r?\n)|^)notes: (.+?)(?:(?:\r?\n)|$)/gi.exec(body); @@ -214,10 +215,12 @@ program fatal(`Pull request number is required to download a PR`); } + d('checking auth...'); const octokit = new Octokit({ auth: await getGitHubAuthToken(['repo']), }); + d('fetching pr info...'); let pullRequest; try { const { data } = await octokit.pulls.get({ @@ -231,6 +234,7 @@ program return; } + d('fetching workflow runs...'); let workflowRuns; try { const { data } = await octokit.actions.listWorkflowRunsForRepo({ @@ -256,6 +260,7 @@ program return; } + d('fetching artifacts...'); let artifacts; try { const { data } = await octokit.actions.listWorkflowRunArtifacts({ @@ -308,68 +313,51 @@ program outputDir = defaultDir; } - // Download the artifact to a temporary directory - const tempDir = path.join(os.tmpdir(), 'electron-tmp'); - await fs.promises.mkdir(tempDir); - - let distExecutablePath; - - try { - console.log( - `Downloading artifact '${artifactName}' from pull request #${pullRequestNumber}...`, - ); - - const artifactPath = path.join(tempDir, `${artifactName}.zip`); + console.log( + `Downloading artifact '${artifactName}' from pull request #${pullRequestNumber}...`, + ); - const response = await octokit.actions.downloadArtifact({ - owner: 'electron', - repo: 'electron', - artifact_id: artifact.id, - archive_format: 'zip', - }); - await fs.promises.writeFile(artifactPath, Buffer.from(response.data)); - - console.log('Extracting dist...'); + const response = await octokit.actions.downloadArtifact({ + owner: 'electron', + repo: 'electron', + artifact_id: artifact.id, + archive_format: 'zip', + }); - // Extract the artifact zip - const extractPath = path.join(tempDir, artifactName); - await fs.promises.mkdir(extractPath, { recursive: true }); - await extractZip(artifactPath, { dir: extractPath }); + // Extract artifact zip in-memory + const artifactZip = new AdmZip(Buffer.from(response.data)); - // Check if dist.zip exists within the extracted artifact - const distZipPath = path.join(extractPath, 'dist.zip'); - if (!(await fs.promises.stat(distZipPath).catch(() => false))) { - fatal(`dist.zip not found within the extracted artifact.`); - return; - } + const distZipEntry = artifactZip.getEntry('dist.zip'); + if (!distZipEntry) { + fatal(`dist.zip not found in build artifact.`); + return; + } - // Extract dist.zip to the final outputDir - await extractZip(distZipPath, { dir: outputDir }); + // Extract dist.zip in-memory + const distZipContents = artifactZip.readFile(distZipEntry); + const distZip = new AdmZip(distZipContents); - // Check if Electron exists within the extracted dist.zip - const platformExecutables = { - win32: 'electron.exe', - darwin: 'Electron.app', - linux: 'electron', - }; - const executableName = platformExecutables[options.platform]; + const platformExecutables = { + win32: 'electron.exe', + darwin: 'Electron.app/', + linux: 'electron', + }; - if (!executableName) { - fatal(`Unable to find executable for platform '${options.platform}'`); - return; - } + const executableName = platformExecutables[options.platform]; + if (!executableName) { + fatal(`Unable to find executable for platform '${options.platform}'`); + return; + } - distExecutablePath = path.join(outputDir, executableName); - if (!(await fs.promises.stat(distExecutablePath).catch(() => false))) { - fatal(`${executableName} not found within the extracted dist.zip.`); - return; - } - } finally { - // Cleanup temporary directory - await fs.promises.rm(tempDir, { recursive: true }); + if (!distZip.getEntry(executableName)) { + fatal(`${executableName} not found within dist.zip.`); + return; } - console.info(`Downloaded to ${distExecutablePath}`); + // Extract dist.zip to the output directory + await distZip.extractAllToAsync(outputDir); + + console.info(`Downloaded to ${outputDir}`); }); program.parse(process.argv); diff --git a/yarn.lock b/yarn.lock index d4e6b9a4..1e064907 100644 --- a/yarn.lock +++ b/yarn.lock @@ -775,25 +775,11 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== -"@types/node@*": - version "22.9.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.9.0.tgz#b7f16e5c3384788542c72dc3d561a7ceae2c0365" - integrity sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ== - dependencies: - undici-types "~6.19.8" - "@types/unist@*", "@types/unist@^2.0.0": version "2.0.6" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== -"@types/yauzl@^2.9.1": - version "2.10.3" - resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999" - integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q== - dependencies: - "@types/node" "*" - "@vitest/expect@2.1.3": version "2.1.3" resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.3.tgz#4b9a6fff22be4c4cd5d57e687cfda611b514b0ad" @@ -878,6 +864,11 @@ acorn@^8.8.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== +adm-zip@^0.5.16: + version "0.5.16" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.16.tgz#0b5e4c779f07dedea5805cdccb1147071d94a909" + integrity sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ== + agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -1165,11 +1156,6 @@ btoa-lite@^1.0.0: resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= -buffer-crc32@~0.2.3: - version "0.2.13" - resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" - integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== - buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -2033,17 +2019,6 @@ external-editor@^3.0.3: iconv-lite "^0.4.24" tmp "^0.0.33" -extract-zip@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" - integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== - dependencies: - debug "^4.1.1" - get-stream "^5.1.0" - yauzl "^2.10.0" - optionalDependencies: - "@types/yauzl" "^2.9.1" - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -2066,13 +2041,6 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -fd-slicer@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" - integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== - dependencies: - pend "~1.2.0" - figures@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -2303,13 +2271,6 @@ get-stdin@~9.0.0: resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-9.0.0.tgz#3983ff82e03d56f1b2ea0d3e60325f39d703a575" integrity sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA== -get-stream@^5.1.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" - integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== - dependencies: - pump "^3.0.0" - get-stream@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" @@ -4135,11 +4096,6 @@ pathval@^2.0.0: resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.0.tgz#7e2550b422601d4f6b8e26f1301bc8f15a741a25" integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA== -pend@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" - integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== - picocolors@^1.0.0, picocolors@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" @@ -5078,11 +5034,6 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" -undici-types@~6.19.8: - version "6.19.8" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" - integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== - unique-filename@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" @@ -5477,14 +5428,6 @@ yargs@^15.0.2: y18n "^4.0.0" yargs-parser "^18.1.2" -yauzl@^2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" - integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== - dependencies: - buffer-crc32 "~0.2.3" - fd-slicer "~1.1.0" - yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" From 1e45220ee6355692769fc42d15a6b61dedf5f758 Mon Sep 17 00:00:00 2001 From: Samuel Maddock Date: Fri, 15 Nov 2024 19:00:38 -0500 Subject: [PATCH 12/13] add confirmation prompt --- src/e-pr.js | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/e-pr.js b/src/e-pr.js index 1586d161..60542917 100644 --- a/src/e-pr.js +++ b/src/e-pr.js @@ -10,6 +10,7 @@ const semver = require('semver'); const open = require('open'); const program = require('commander'); const { Octokit } = require('@octokit/rest'); +const inquirer = require('inquirer'); const { getGitHubAuthToken } = require('./utils/github-auth'); const { current } = require('./evm-config'); @@ -210,6 +211,7 @@ program 'Specify the output directory for downloaded artifacts. ' + 'Defaults to ~/.electron_build_tools/artifacts/pr_{number}_{platform}_{arch}', ) + .option('-s, --skip-confirmation', 'Skip the confirmation prompt before downloading the dist.') .action(async (pullRequestNumber, options) => { if (!pullRequestNumber) { fatal(`Pull request number is required to download a PR`); @@ -234,6 +236,24 @@ program return; } + if (!options.skipConfirmation) { + const isElectronRepo = pullRequest.head.repo.full_name !== 'electron/electron'; + const { proceed } = await inquirer.prompt([ + { + type: 'confirm', + name: 'proceed', + message: `You are about to download artifacts from: + +“${pullRequest.title} (#${pullRequest.number})” by ${pullRequest.user.login} +${pullRequest.head.repo.html_url}${isElectronRepo ? ' (fork)' : ''} + +Proceed?`, + }, + ]); + + if (!proceed) return; + } + d('fetching workflow runs...'); let workflowRuns; try { @@ -291,10 +311,9 @@ program return; } } else { + const artifactsDir = path.resolve(__dirname, '..', 'artifacts'); const defaultDir = path.resolve( - __dirname, - '..', - 'artifacts', + artifactsDir, `pr_${pullRequest.number}_${options.platform}_${options.arch}`, ); @@ -329,7 +348,7 @@ program const distZipEntry = artifactZip.getEntry('dist.zip'); if (!distZipEntry) { - fatal(`dist.zip not found in build artifact.`); + fatal('dist.zip not found in build artifact.'); return; } From 90e1769264b6dd92699eeb6dd23a604b5204d333 Mon Sep 17 00:00:00 2001 From: Samuel Maddock Date: Sat, 16 Nov 2024 15:35:25 -0500 Subject: [PATCH 13/13] feat: 'e clean' for removing artifacts --- .gitignore | 1 + src/e | 3 +- src/e-clean.js | 29 +++++++++++++++++ src/e-pr.js | 6 ++-- src/utils/artifacts.js | 51 ++++++++++++++++++++++++++++++ src/utils/local-storage.js | 65 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 src/e-clean.js create mode 100644 src/utils/artifacts.js create mode 100644 src/utils/local-storage.js diff --git a/.gitignore b/.gitignore index 0e64e1e1..fb51602f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ artifacts configs coverage +local-storage.json node_modules package-lock.json third_party diff --git a/src/e b/src/e index 65f02ec5..8aae46d3 100755 --- a/src/e +++ b/src/e @@ -179,7 +179,8 @@ program 'Opens a PR to electron/electron that backport the given CL into our patches folder', ) .alias('auto-cherry-pick') - .command('gh-auth', 'Generates a device oauth token'); + .command('gh-auth', 'Generates a device oauth token') + .command('clean', 'Cleanup artifacts produced by build tools'); const ci = program.command('ci').executableDir(path.join(__dirname, 'ci')); ci.command('status', 'Show information about CI job statuses'); diff --git a/src/e-clean.js b/src/e-clean.js new file mode 100644 index 00000000..db76392c --- /dev/null +++ b/src/e-clean.js @@ -0,0 +1,29 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const { ARTIFACTS_DIR, getStaleArtifacts } = require('./utils/artifacts'); + +const program = require('commander'); +const { color, fatal } = require('./utils/logging'); + +function clean(options) { + try { + if (options.stale) { + const staleFiles = getStaleArtifacts(); + staleFiles.forEach((file) => { + const filePath = path.join(ARTIFACTS_DIR, file); + fs.rmSync(filePath, { recursive: true, force: true }); + }); + console.log(color.success, `${staleFiles.length} stale artifact(s) removed.`); + } else { + fs.rmSync(ARTIFACTS_DIR, { recursive: true, force: true }); + console.log(color.success, 'Artifacts directory cleaned successfully.'); + } + } catch (error) { + fatal(error); + } +} + +program.action(clean).option('--stale', 'Only clean stale artifacts'); + +program.parse(process.argv); diff --git a/src/e-pr.js b/src/e-pr.js index 60542917..7a2a1f99 100644 --- a/src/e-pr.js +++ b/src/e-pr.js @@ -15,6 +15,7 @@ const inquirer = require('inquirer'); const { getGitHubAuthToken } = require('./utils/github-auth'); const { current } = require('./evm-config'); const { color, fatal } = require('./utils/logging'); +const { ARTIFACTS_DIR, maybeCheckStaleArtifacts } = require('./utils/artifacts'); const d = require('debug')('build-tools:pr'); @@ -217,6 +218,8 @@ program fatal(`Pull request number is required to download a PR`); } + maybeCheckStaleArtifacts(); + d('checking auth...'); const octokit = new Octokit({ auth: await getGitHubAuthToken(['repo']), @@ -311,9 +314,8 @@ Proceed?`, return; } } else { - const artifactsDir = path.resolve(__dirname, '..', 'artifacts'); const defaultDir = path.resolve( - artifactsDir, + ARTIFACTS_DIR, `pr_${pullRequest.number}_${options.platform}_${options.arch}`, ); diff --git a/src/utils/artifacts.js b/src/utils/artifacts.js new file mode 100644 index 00000000..df165c72 --- /dev/null +++ b/src/utils/artifacts.js @@ -0,0 +1,51 @@ +const path = require('path'); +const { localStorage } = require('./local-storage'); +const fs = require('fs'); + +const ARTIFACTS_DIR = path.join(__dirname, '..', '..', 'artifacts'); + +/** How often to check for stale files. */ +const STALE_CHECK_INTERVAL = 7 * 24 * 60 * 60 * 1000; // 1 week + +/** How old a file is to be considered stale. */ +const STALE_FILE_AGE = 30 * 24 * 60 * 60 * 1000; // 1 month + +function getStaleArtifacts() { + const now = Date.now(); + let files; + try { + files = fs.readdirSync(ARTIFACTS_DIR); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + return []; + } + const staleFiles = files.filter((file) => { + const filePath = path.join(ARTIFACTS_DIR, file); + const stats = fs.statSync(filePath); + return stats.mtimeMs < now - STALE_FILE_AGE; + }); + return staleFiles; +} + +function maybeCheckStaleArtifacts() { + const lastChecked = parseInt(localStorage.getItem('lastArtifactsCheck'), 10); + const now = Date.now(); + + if (!lastChecked || lastChecked < now - STALE_CHECK_INTERVAL) { + const staleArtifacts = getStaleArtifacts(); + if (staleArtifacts.length > 0) { + console.warn( + `Stale artifact(s) found:\n\t${staleArtifacts.join('\n\t')}\n\nRun 'e clean --stale' to cleanup artifacts.`, + ); + } + localStorage.setItem('lastArtifactsCheck', now); + } +} + +module.exports = { + ARTIFACTS_DIR, + getStaleArtifacts, + maybeCheckStaleArtifacts, +}; diff --git a/src/utils/local-storage.js b/src/utils/local-storage.js new file mode 100644 index 00000000..1e5635c4 --- /dev/null +++ b/src/utils/local-storage.js @@ -0,0 +1,65 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * Simple localStorage implementation before it becomes widely available in NodeJS. + * https://github.com/nodejs/node/blob/main/doc/api/globals.md#localstorage + */ +class LocalStorage { + constructor() { + this.filePath = path.resolve(__dirname, '..', '..', 'local-storage.json'); + } + + #load() { + try { + return JSON.parse(fs.readFileSync(this.filePath, 'utf8')); + } catch { + return {}; + } + } + + #save() { + fs.writeFileSync(this.filePath, JSON.stringify(this.store, null, 2)); + } + + /** Lazy-load reading from disk. */ + get store() { + return (this.store = this.#load()); + } + set store(value) { + Object.defineProperty(this, 'store', { value }); + } + + getItem(key) { + return this.store[key]; + } + + setItem(key, value) { + this.store[key] = JSON.stringify(value); + this.#save(); + } + + removeItem(key) { + delete this.store[key]; + this.#save(); + } + + clear() { + this.store = {}; + this.#save(); + } + + key(index) { + return Object.keys(this.store)[index]; + } + + get length() { + return Object.keys(this.store).length; + } +} + +const localStorage = new LocalStorage(); + +module.exports = { + localStorage, +};