diff --git a/package.json b/package.json index 06385227c32..84f74ca6782 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,8 @@ "devtools:devserver:demo-standalone": "ibazel run //devtools/projects/demo-standalone/src:devserver", "devtools:build:chrome": "bazelisk build --config snapshot-build --//devtools/projects/shell-browser/src:flag_browser=chrome -- devtools/projects/shell-browser/src:prodapp", "devtools:build:firefox": "bazelisk build --config snapshot-build --//devtools/projects/shell-browser/src:flag_browser=firefox -- devtools/projects/shell-browser/src:prodapp", - "devtools:test": "bazelisk test --config snapshot-build --//devtools/projects/shell-browser/src:flag_browser=chrome -- //devtools/..." + "devtools:test": "bazelisk test --config snapshot-build --//devtools/projects/shell-browser/src:flag_browser=chrome -- //devtools/...", + "benchmarks": "ts-node --esm scripts/benchmarks/index.mts" }, "// 1": "dependencies are used locally and by bazel", "dependencies": { @@ -156,6 +157,7 @@ }, "// 2": "devDependencies are not used under Bazel. Many can be removed after test.sh is deleted.", "devDependencies": { + "@actions/core": "^1.10.0", "@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#7dad055464ea9847e4870b9e3baad1f0c417bdf7", "@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#dec40448ead7b52cddae4d32bdb21d8c48589413", "@babel/helper-remap-async-to-generator": "^7.18.9", @@ -163,9 +165,12 @@ "@bazel/buildifier": "^6.0.0", "@bazel/ibazel": "^0.16.0", "@octokit/graphql": "^5.0.0", + "@types/adm-zip": "^0.5.0", "@types/cldrjs": "^0.4.22", "@types/cli-progress": "^3.4.2", + "@types/inquirer": "^9.0.3", "@yarnpkg/lockfile": "^1.1.0", + "adm-zip": "^0.5.10", "check-side-effects": "0.0.23", "cldr": "7.4.1", "cldrjs": "0.5.5", @@ -176,6 +181,7 @@ "gulp": "^4.0.2", "gulp-conventional-changelog": "^3.0.0", "husky": "8.0.3", + "inquirer": "^9.2.7", "karma-sauce-launcher": "^4.3.6", "madge": "^6.0.0", "multimatch": "^6.0.0", diff --git a/scripts/benchmarks/index.mts b/scripts/benchmarks/index.mts new file mode 100644 index 00000000000..b97023d42e1 --- /dev/null +++ b/scripts/benchmarks/index.mts @@ -0,0 +1,172 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import yargs from 'yargs'; +import {bold, yellow, GitClient, green, Log} from '@angular/ng-dev'; +import inquirer from 'inquirer'; +import {exec} from './utils.mjs'; +import {ResolvedTarget, findBenchmarkTargets, getTestlogPath, resolveTarget} from './targets.mjs'; +import {collectBenchmarkResults} from './results.mjs'; +import {setOutput} from '@actions/core'; + +const benchmarkTestFlags = [ + '--cache_test_results=no', + '--color=yes', + '--curses=no', + // We may have RBE set up, but test should run locally on the same machine to + // reduce fluctuation. Output streamed ensures that deps can build with RBE, but + // tests run locally while also providing useful output for debugging. + '--test_output=streamed', +]; + +await yargs(process.argv.slice(2)) + .command( + 'run-compare [bazel-target]', + 'Runs a benchmark between two SHAs', + (argv) => + argv + .positional('compare-ref', {description: 'Comparison SHA', type: 'string', demandOption: true}) + .positional('bazel-target', {description: 'Bazel target', type: 'string'}), + (args) => runCompare(args.bazelTarget, args.compareRef) + ) + .command( + 'run [bazel-target]', + 'Runs a benchmark', + (argv) => argv.positional('bazel-target', {description: 'Bazel target', type: 'string'}), + (args) => runBenchmarkCmd(args.bazelTarget) + ) + .command( + 'extract-compare-comment ', + false, // Do not show in help. + (argv) => argv.positional('comment-body', {demandOption: true, type: 'string'}), + (args) => extractCompareComment(args.commentBody) + ) + .demandCommand() + .scriptName('$0') + .help() + .strict() + .parseAsync(); + +/** Prompts for a benchmark target. */ +async function promptForBenchmarkTarget(): Promise { + const targets = await findBenchmarkTargets(); + + return ( + await inquirer.prompt<{bazelTarget: string}>({ + name: 'bazelTarget', + message: 'Select benchmark target to run:', + type: 'list', + choices: targets.map((t) => ({value: t, name: t})), + }) + ).bazelTarget; +} + +/** + * Extracts arguments from a benchmark compare comment. + * + * This is a helper used by the GitHub action to perform benchmark + * comparisons. Commands follow the format of: `/benchmark-compare `. + */ +async function extractCompareComment(commentBody: string): Promise { + const matches = /\/[^ ]+ ([^ ]+) ([^ ]+)/.exec(commentBody); + if (matches === null) { + Log.error('Could not extract information from comment', commentBody); + process.exit(1); + } + + setOutput('compareRef', matches[1]); + setOutput('benchmarkTarget', matches[2]); +} + +/** Runs a specified benchmark, or a benchmark selected via prompt. */ +async function runBenchmarkCmd(bazelTargetRaw: string | undefined): Promise { + if (bazelTargetRaw === undefined) { + bazelTargetRaw = await promptForBenchmarkTarget(); + } + await runBenchmarkTarget(await resolveTarget(bazelTargetRaw)); +} + +/** Runs a benchmark Bazel target. */ +async function runBenchmarkTarget(bazelTarget: ResolvedTarget): Promise { + await exec('bazel', ['test', bazelTarget, ...benchmarkTestFlags]); +} + +/** + * Performs a comparison of benchmark results between the current + * working stage and the comparison Git reference. + */ +async function runCompare(bazelTargetRaw: string | undefined, compareRef: string): Promise { + const git = await GitClient.get(); + const initialRef = git.getCurrentBranchOrRevision(); + + if (git.hasUncommittedChanges()) { + Log.warn(bold('You have uncommitted changes.')); + Log.warn('The script will stash your changes and re-apply them so that'); + Log.warn('the comparison ref can be checked out.'); + Log.warn(''); + } + + if (bazelTargetRaw === undefined) { + bazelTargetRaw = await promptForBenchmarkTarget(); + } + + const bazelTarget = await resolveTarget(bazelTargetRaw); + const testlogPath = await getTestlogPath(bazelTarget); + + Log.log(green('Test log path:', testlogPath)); + + // Run benchmark with the current working stage. + await runBenchmarkTarget(bazelTarget); + + const workingDirResults = await collectBenchmarkResults(testlogPath); + + // Stash working directory as we might be in the middle of developing + // and we wouldn't want to discard changes when checking out the compare SHA. + git.run(['stash']); + + try { + Log.log(green('Fetching comparison revision.')); + git.run(['fetch', '--depth=1', git.getRepoGitUrl(), compareRef]); + Log.log(green('Checking out comparison revision.')); + git.run(['checkout', 'FETCH_HEAD']); + + await exec('yarn'); + await runBenchmarkTarget(bazelTarget); + } finally { + restoreWorkingStage(git, initialRef); + } + + // Re-install dependencies for `HEAD`. + await exec('yarn'); + + const comparisonResults = await collectBenchmarkResults(testlogPath); + + // If we are running in a GitHub action, expose the benchmark text + // results as outputs. Useful if those are exposed as a GitHub comment then. + if (process.env.GITHUB_ACTION !== undefined) { + setOutput('comparisonResultsText', comparisonResults.textSummary); + setOutput('workingStageResultsText', workingDirResults.textSummary); + } + + Log.info('\n\n\n'); + Log.info(bold(green('Results!'))); + + Log.info(bold(yellow('Comparison results')), '\n'); + Log.info(comparisonResults.textSummary); + + Log.info(bold(yellow('Working stage results')), '\n'); + Log.info(workingDirResults.textSummary); +} + +function restoreWorkingStage(git: GitClient, initialRef: string) { + Log.log(green('Restoring working stage')); + git.run(['checkout', '-f', initialRef]); + + // Stash apply could fail if there were not changes in the working stage. + git.runGraceful(['stash', 'apply']); +} diff --git a/scripts/benchmarks/results.mts b/scripts/benchmarks/results.mts new file mode 100644 index 00000000000..7824d07fea2 --- /dev/null +++ b/scripts/benchmarks/results.mts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import path from 'path'; +import Zip from 'adm-zip'; + +import type {JsonReport} from '../../packages/benchpress/src/reporter/json_file_reporter_types.js'; + +/** Results of an individual benchmark scenario. */ +export interface ScenarioResult { + id: string; + data: JsonReport; + textSummary: string; +} + +/** + * Overall result of a benchmark target. + * A benchmark target may contain multiple scenarios. + */ +export interface OverallResult { + scenarios: ScenarioResult[]; + textSummary: string; +} + +/** Collects and parses the benchmark results of the given Bazel target testlog directory. */ +export function collectBenchmarkResults(testlogDir: string): OverallResult { + const z = new Zip(path.join(testlogDir, 'test.outputs/outputs.zip')); + const scenarioResults: ScenarioResult[] = []; + + for (const e of z.getEntries()) { + if (path.extname(e.entryName) !== '.json') { + continue; + } + + const data = JSON.parse(z.readAsText(e.entryName)); + + // Skip files that do not look like benchpress reports. + if (!isJsonReport(data)) { + continue; + } + + scenarioResults.push({ + id: data.description.id, + data, + textSummary: `${data.metricsText}\n${data.validSampleTexts.join('\n')}\n${data.statsText}`, + }); + } + + return { + scenarios: scenarioResults, + textSummary: scenarioResults.map((s) => `### ${s.id}\n\n${s.textSummary}`).join('`\n'), + }; +} + +/** Whether the object corresponds to a benchpress JSON report. */ +function isJsonReport(data: any): data is JsonReport { + return data['completeSample'] !== undefined; +} diff --git a/scripts/benchmarks/targets.mts b/scripts/benchmarks/targets.mts new file mode 100644 index 00000000000..07b85931d77 --- /dev/null +++ b/scripts/benchmarks/targets.mts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import path from 'path'; +import {exec} from './utils.mjs'; + +/** Branded string representing a resolved Bazel benchmark target. */ +export type ResolvedTarget = string & { + __resolvedTarget: true; +}; + +/** Finds all benchmark Bazel targets in the project. */ +export async function findBenchmarkTargets(): Promise { + return ( + await exec('bazel', [ + 'query', + '--output=label', + `'kind("^web_test", //modules/...) intersect attr("name", "perf", //modules/...)'`, + ]) + ) + .split(/\r?\n/) + .filter((t) => t !== ''); +} + +/** Gets the testlog path of a given Bazel target. */ +export async function getTestlogPath(target: ResolvedTarget): Promise { + return path.join(await bazelTestlogDir(), target.substring(2).replace(':', '/')); +} + +/** Resolves a given benchmark Bazel target to the fully expanded label. */ +export async function resolveTarget(target: string): Promise { + // If the target does not specify an explicit browser test target, we attempt + // to automatically add the Chromium suffix. This is necessary for e.g. + // resolving testlogs which would reside under the actual test target. + if (!target.endsWith('_chromium')) { + target = `${target}_chromium`; + } + + return (await exec('bazel', ['query', '--output=label', target])).trim() as ResolvedTarget; +} + +let testlogDir: string | null = null; +async function bazelTestlogDir(): Promise { + return testlogDir ?? (testlogDir = (await exec('bazel', ['info', 'bazel-testlogs'])).trim()); +} diff --git a/scripts/benchmarks/utils.mts b/scripts/benchmarks/utils.mts new file mode 100644 index 00000000000..936546f6351 --- /dev/null +++ b/scripts/benchmarks/utils.mts @@ -0,0 +1,51 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Log} from '@angular/ng-dev'; +import childProcess from 'child_process'; +import path from 'path'; +import url from 'url'; + +const scriptDir = path.dirname(url.fileURLToPath(import.meta.url)); + +/** Absolute disk path to the project directory. */ +export const projectDir = path.join(scriptDir, '../..'); + +/** + * Executes the given command, forwarding stdin, stdout and stderr while + * still capturing stdout in order to return it. + */ +export function exec(cmd: string, args: string[] = []): Promise { + return new Promise((resolve, reject) => { + Log.info('Running command:', cmd, args.join(' ')); + + const proc = childProcess.spawn(cmd, args, { + shell: true, + cwd: projectDir, + // Only capture `stdout`. Forward the rest to the parent TTY. + stdio: ['inherit', 'pipe', 'inherit'], + }); + let stdout = ''; + + proc.stdout.on('data', (chunk) => { + stdout += chunk.toString('utf8'); + process.stdout.write(chunk); + }); + + proc.on('close', (status, signal) => { + if (status !== 0 || signal !== null) { + reject(`Command failed. Status code: ${status}. Signal: ${signal}`); + } + resolve(stdout); + }); + + proc.on('error', (err) => { + reject(`Command failed: ${err}`); + }); + }); +} diff --git a/yarn.lock b/yarn.lock index a877a070c63..078631e1da2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,21 @@ # yarn lockfile v1 +"@actions/core@^1.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.10.0.tgz#44551c3c71163949a2f06e94d9ca2157a0cfac4f" + integrity sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug== + dependencies: + "@actions/http-client" "^2.0.1" + uuid "^8.3.2" + +"@actions/http-client@^2.0.1": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-2.1.0.tgz#b6d8c3934727d6a50d10d19f00a711a964599a9f" + integrity sha512-BonhODnXr3amchh4qkmjPMUO8mFi/zLaaCeCAJZqch8iQqyDnVIkySjB38VHAC8IJ+bnlgfOqlhpyCUZHlQsqw== + dependencies: + tunnel "^0.0.6" + "@ampproject/remapping@2.2.1", "@ampproject/remapping@^2.2.0": version "2.2.1" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" @@ -3367,6 +3382,13 @@ "@tufjs/canonical-json" "1.0.0" minimatch "^9.0.0" +"@types/adm-zip@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@types/adm-zip/-/adm-zip-0.5.0.tgz#94c90a837ce02e256c7c665a6a1eb295906333c1" + integrity sha512-FCJBJq9ODsQZUNURo5ILAQueuA8WJhRvuihS3ke2iI25mJlfV2LK8jG2Qj2z2AWg8U0FtWWqBHVRetceLskSaw== + dependencies: + "@types/node" "*" + "@types/angular@^1.6.47": version "1.8.5" resolved "https://registry.yarnpkg.com/@types/angular/-/angular-1.8.5.tgz#71c7d3581898f1863100eeed9d25bb737a8e1062" @@ -3617,6 +3639,14 @@ dependencies: "@types/node" "*" +"@types/inquirer@^9.0.3": + version "9.0.3" + resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-9.0.3.tgz#dc99da4f2f6de9d26c284b4f6aaab4d98c456db1" + integrity sha512-CzNkWqQftcmk2jaCWdBTf9Sm7xSw4rkI1zpU/Udw3HX5//adEZUIm9STtoRP1qgWj0CWQtJ9UTvqmO2NNjhMJw== + dependencies: + "@types/through" "*" + rxjs "^7.2.0" + "@types/is-windows@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/is-windows/-/is-windows-1.0.0.tgz#1011fa129d87091e2f6faf9042d6704cdf2e7be0" @@ -3880,6 +3910,13 @@ resolved "https://registry.yarnpkg.com/@types/systemjs/-/systemjs-0.19.32.tgz#e9204c4cdbc8e275d645c00e6150e68fc5615a24" integrity sha512-8jtvyxk9DI+KTQ/uh8iMW3FqCG2JnBPO6tEb2Z9r+4NDNpdBwOLjwIAGnprgyX/4yA6sgnc3sGqNpApZEJsXFg== +"@types/through@*": + version "0.0.30" + resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" + integrity sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg== + dependencies: + "@types/node" "*" + "@types/tmp@^0.2.1": version "0.2.3" resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.2.3.tgz#908bfb113419fd6a42273674c00994d40902c165" @@ -4354,7 +4391,7 @@ adjust-sourcemap-loader@^4.0.0: loader-utils "^2.0.0" regex-parser "^2.2.11" -adm-zip@^0.5.2: +adm-zip@^0.5.10, adm-zip@^0.5.2: version "0.5.10" resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.10.tgz#4a51d5ab544b1f5ce51e1b9043139b639afff45b" integrity sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ== @@ -4488,7 +4525,7 @@ ansi-colors@^1.0.1: dependencies: ansi-wrap "^0.1.0" -ansi-escapes@^4.2.1: +ansi-escapes@^4.2.1, ansi-escapes@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== @@ -5841,6 +5878,11 @@ cli-width@^3.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== +cli-width@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.0.0.tgz#a5622f6a3b0a9e3e711a25f099bf2399f608caf6" + integrity sha512-ZksGS2xpa/bYkNzN3BAw1wEjsLV/ZKOf/CCrJ/QOBsxx6fOARIkwTutxp1XIOIohi6HKmOFjMoK/XaqDVUpEEw== + cliui@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" @@ -7034,6 +7076,12 @@ decompress-tarbz2@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz#3082a5b880ea4043816349f378b56c516be1a39b" integrity sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A== + dependencies: + decompress-tar "^4.1.0" + file-type "^6.1.0" + is-stream "^1.1.0" + seek-bzip "^1.0.5" + unbzip2-stream "^1.0.9" decompress-targz@^4.0.0: version "4.1.1" @@ -7599,6 +7647,9 @@ ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: version "1.0.11" @@ -7930,6 +7981,11 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escape-string-regexp@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" + integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== + escodegen@^1.13.0, escodegen@^1.8.1: version "1.14.3" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" @@ -8337,6 +8393,14 @@ figures@^3.0.0: dependencies: escape-string-regexp "^1.0.5" +figures@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-5.0.0.tgz#126cd055052dea699f8a54e8c9450e6ecfc44d5f" + integrity sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg== + dependencies: + escape-string-regexp "^5.0.0" + is-unicode-supported "^1.2.0" + file-type@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/file-type/-/file-type-11.1.0.tgz#93780f3fed98b599755d846b99a1617a2ad063b8" @@ -10027,6 +10091,27 @@ inquirer@^8.2.0: through "^2.3.6" wrap-ansi "^7.0.0" +inquirer@^9.2.7: + version "9.2.7" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-9.2.7.tgz#61e00658efa9b4c76a83c2c3cb3ceb88fec70ac7" + integrity sha512-Bf52lnfvNxGPJPltiNO2tLBp3zC339KNlGMqOkW+dsvNikBhcVDK5kqU2lVX2FTPzuXUFX5WJDlsw//w3ZwoTw== + dependencies: + ansi-escapes "^4.3.2" + chalk "^5.2.0" + cli-cursor "^3.1.0" + cli-width "^4.0.0" + external-editor "^3.0.3" + figures "^5.0.0" + lodash "^4.17.21" + mute-stream "1.0.0" + ora "^5.4.1" + run-async "^3.0.0" + rxjs "^7.8.1" + string-width "^4.2.3" + strip-ansi "^6.0.1" + through "^2.3.6" + wrap-ansi "^6.0.1" + install-artifact-from-github@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.3.3.tgz#57d89bacfa0f47d7307fe41b6247cda9f9a8079c" @@ -10423,6 +10508,11 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +is-unicode-supported@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz#d824984b616c292a2e198207d4a609983842f714" + integrity sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ== + is-url-superb@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/is-url-superb/-/is-url-superb-4.0.0.tgz#b54d1d2499bb16792748ac967aa3ecb41a33a8c2" @@ -12078,6 +12168,11 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +mute-stream@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e" + integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA== + nan@^2.12.1, nan@^2.17.0: version "2.17.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" @@ -14280,6 +14375,11 @@ run-async@^2.4.0: resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== +run-async@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-3.0.0.tgz#42a432f6d76c689522058984384df28be379daad" + integrity sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q== + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -14304,7 +14404,7 @@ rxjs@7.8.0: dependencies: tslib "^2.1.0" -rxjs@7.8.1, rxjs@^7.5.5: +rxjs@7.8.1, rxjs@^7.2.0, rxjs@^7.5.5, rxjs@^7.8.1: version "7.8.1" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== @@ -16870,7 +16970,7 @@ wrap-ansi@^2.0.0: string-width "^1.0.1" strip-ansi "^3.0.1" -wrap-ansi@^6.2.0: +wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==