From 992dc93ea37d1079c996a370777e776ff2251b49 Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Thu, 27 May 2021 12:04:39 -0700 Subject: [PATCH] refactor(dev-infra): remove usages and dependency on shelljs (#42911) Remove usages of shelljs and instead use spawn/spawnSync. PR Close #42911 --- .../benchmark/driver-utilities/BUILD.bazel | 2 - .../benchmark/driver-utilities/perf_util.ts | 6 +- dev-infra/build-worker.js | 3 + dev-infra/commit-message/BUILD.bazel | 2 - dev-infra/format/BUILD.bazel | 2 - dev-infra/format/formatters/base-formatter.ts | 3 +- dev-infra/format/formatters/buildifier.ts | 4 +- dev-infra/format/formatters/clang-format.ts | 4 +- dev-infra/format/formatters/prettier.ts | 11 +- dev-infra/format/run-commands-parallel.ts | 14 +- dev-infra/misc/build-and-link/cli.ts | 10 +- dev-infra/ng-dev.js | 272 +++++++++--------- .../pr/discover-new-conflicts/BUILD.bazel | 1 - dev-infra/pr/discover-new-conflicts/index.ts | 18 +- dev-infra/pullapprove/BUILD.bazel | 2 - dev-infra/release/stamping/env-stamp.ts | 38 ++- dev-infra/tmpl-package.json | 1 - dev-infra/utils/BUILD.bazel | 2 - dev-infra/utils/child-process.ts | 20 +- dev-infra/utils/git/git-client.ts | 4 + dev-infra/utils/shelljs.ts | 17 -- 21 files changed, 208 insertions(+), 228 deletions(-) delete mode 100644 dev-infra/utils/shelljs.ts diff --git a/dev-infra/benchmark/driver-utilities/BUILD.bazel b/dev-infra/benchmark/driver-utilities/BUILD.bazel index fd575c92b3..3ef04bd319 100644 --- a/dev-infra/benchmark/driver-utilities/BUILD.bazel +++ b/dev-infra/benchmark/driver-utilities/BUILD.bazel @@ -10,10 +10,8 @@ ts_library( "//packages/benchpress", "@npm//@types/node", "@npm//@types/selenium-webdriver", - "@npm//@types/shelljs", "@npm//node-uuid", "@npm//protractor", "@npm//selenium-webdriver", - "@npm//shelljs", ], ) diff --git a/dev-infra/benchmark/driver-utilities/perf_util.ts b/dev-infra/benchmark/driver-utilities/perf_util.ts index b6d59a4a1d..a9043b9a24 100644 --- a/dev-infra/benchmark/driver-utilities/perf_util.ts +++ b/dev-infra/benchmark/driver-utilities/perf_util.ts @@ -5,7 +5,7 @@ * 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 {mkdir} from 'shelljs'; +import {mkdirSync} from 'fs'; export {verifyNoBrowserErrors} from './e2e_util'; @@ -61,7 +61,9 @@ function createBenchpressRunner(): Runner { runId = process.env.GIT_SHA + ' ' + runId; } const resultsFolder = './dist/benchmark_results'; - mkdir('-p', resultsFolder); + mkdirSync(resultsFolder, { + recursive: true, + }); const providers: StaticProvider[] = [ SeleniumWebDriverAdapter.PROTRACTOR_PROVIDERS, {provide: Options.FORCE_GC, useValue: globalOptions.forceGc}, diff --git a/dev-infra/build-worker.js b/dev-infra/build-worker.js index eb05917793..19a062e471 100644 --- a/dev-infra/build-worker.js +++ b/dev-infra/build-worker.js @@ -145,6 +145,9 @@ var GitCommandError = /** @class */ (function (_super) { // we sanitize the command that will be part of the error message. _super.call(this, "Command failed: git " + client.sanitizeConsoleOutput(args.join(' '))) || this; _this.args = args; + // Set the prototype explicitly because in ES5, the prototype is accidentally lost due to + // a limitation in down-leveling. + // https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work. Object.setPrototypeOf(_this, GitCommandError.prototype); return _this; } diff --git a/dev-infra/commit-message/BUILD.bazel b/dev-infra/commit-message/BUILD.bazel index bbdc0cd5ba..e0299445f8 100644 --- a/dev-infra/commit-message/BUILD.bazel +++ b/dev-infra/commit-message/BUILD.bazel @@ -13,12 +13,10 @@ ts_library( "@npm//@types/git-raw-commits", "@npm//@types/inquirer", "@npm//@types/node", - "@npm//@types/shelljs", "@npm//@types/yargs", "@npm//conventional-commits-parser", "@npm//git-raw-commits", "@npm//inquirer", - "@npm//shelljs", "@npm//yargs", ], ) diff --git a/dev-infra/format/BUILD.bazel b/dev-infra/format/BUILD.bazel index c280f06d51..1c5a82ea85 100644 --- a/dev-infra/format/BUILD.bazel +++ b/dev-infra/format/BUILD.bazel @@ -10,11 +10,9 @@ ts_library( "//dev-infra/utils", "@npm//@types/cli-progress", "@npm//@types/node", - "@npm//@types/shelljs", "@npm//@types/yargs", "@npm//cli-progress", "@npm//multimatch", - "@npm//shelljs", "@npm//yargs", ], ) diff --git a/dev-infra/format/formatters/base-formatter.ts b/dev-infra/format/formatters/base-formatter.ts index 24ba0fa516..65930adadb 100644 --- a/dev-infra/format/formatters/base-formatter.ts +++ b/dev-infra/format/formatters/base-formatter.ts @@ -10,7 +10,8 @@ import {GitClient} from '../../utils/git/git-client'; import {FormatConfig} from '../config'; // A callback to determine if the formatter run found a failure in formatting. -export type CallbackFunc = (file: string, code: number, stdout: string, stderr: string) => boolean; +export type CallbackFunc = + (file: string, code: number|NodeJS.Signals, stdout: string, stderr: string) => boolean; // The actions a formatter can take. export type FormatterAction = 'check'|'format'; diff --git a/dev-infra/format/formatters/buildifier.ts b/dev-infra/format/formatters/buildifier.ts index b18ba62a3b..3f49dc1b3c 100644 --- a/dev-infra/format/formatters/buildifier.ts +++ b/dev-infra/format/formatters/buildifier.ts @@ -26,14 +26,14 @@ export class Buildifier extends Formatter { check: { commandFlags: `${BAZEL_WARNING_FLAG} --lint=warn --mode=check --format=json`, callback: - (_: string, code: number, stdout: string) => { + (_: string, code: number|NodeJS.Signals, stdout: string) => { return code !== 0 || !(JSON.parse(stdout) as {success: string}).success; }, }, format: { commandFlags: `${BAZEL_WARNING_FLAG} --lint=fix --mode=fix`, callback: - (file: string, code: number, _: string, stderr: string) => { + (file: string, code: number|NodeJS.Signals, _: string, stderr: string) => { if (code !== 0) { error(`Error running buildifier on: ${file}`); error(stderr); diff --git a/dev-infra/format/formatters/clang-format.ts b/dev-infra/format/formatters/clang-format.ts index 637f427223..616849a579 100644 --- a/dev-infra/format/formatters/clang-format.ts +++ b/dev-infra/format/formatters/clang-format.ts @@ -26,14 +26,14 @@ export class ClangFormat extends Formatter { check: { commandFlags: `--Werror -n -style=file`, callback: - (_: string, code: number) => { + (_: string, code: number|NodeJS.Signals) => { return code !== 0; }, }, format: { commandFlags: `-i -style=file`, callback: - (file: string, code: number, _: string, stderr: string) => { + (file: string, code: number|NodeJS.Signals, _: string, stderr: string) => { if (code !== 0) { error(`Error running clang-format on: ${file}`); error(stderr); diff --git a/dev-infra/format/formatters/prettier.ts b/dev-infra/format/formatters/prettier.ts index e56da8fa67..f2969203c7 100644 --- a/dev-infra/format/formatters/prettier.ts +++ b/dev-infra/format/formatters/prettier.ts @@ -7,8 +7,8 @@ */ import {join} from 'path'; -import {exec} from 'shelljs'; +import {spawnSync} from '../../utils/child-process'; import {error} from '../../utils/console'; import {Formatter} from './base-formatter'; @@ -27,21 +27,22 @@ export class Prettier extends Formatter { * The configuration path of the prettier config, obtained during construction to prevent needing * to discover it repeatedly for each execution. */ - private configPath = - this.config['prettier'] ? exec(`${this.binaryFilePath} --find-config-path .`).trim() : ''; + private configPath = this.config['prettier'] ? + spawnSync(this.binaryFilePath, ['--find-config-path', '.']).stdout.trim() : + ''; override actions = { check: { commandFlags: `--config ${this.configPath} --check`, callback: - (_: string, code: number, stdout: string) => { + (_: string, code: number|NodeJS.Signals, stdout: string) => { return code !== 0; }, }, format: { commandFlags: `--config ${this.configPath} --write`, callback: - (file: string, code: number, _: string, stderr: string) => { + (file: string, code: number|NodeJS.Signals, _: string, stderr: string) => { if (code !== 0) { error(`Error running prettier on: ${file}`); error(stderr); diff --git a/dev-infra/format/run-commands-parallel.ts b/dev-infra/format/run-commands-parallel.ts index 5671d9486a..c10a27f9c9 100644 --- a/dev-infra/format/run-commands-parallel.ts +++ b/dev-infra/format/run-commands-parallel.ts @@ -9,8 +9,8 @@ import {Bar} from 'cli-progress'; import * as multimatch from 'multimatch'; import {cpus} from 'os'; -import {exec} from 'shelljs'; +import {spawn, SpawnResult} from '../utils/child-process'; import {info} from '../utils/console'; import {Formatter, FormatterAction, getActiveFormatters} from './formatters/index'; @@ -86,12 +86,11 @@ export function runFormatterInParallel(allFiles: string[], action: FormatterActi // Get the file and formatter for the next command. const {file, formatter} = nextCommand; - exec( - `${formatter.commandFor(action)} ${file}`, - {async: true, silent: true}, - (code, stdout, stderr) => { + const [spawnCmd, ...spawnArgs] = [...formatter.commandFor(action).split(' '), file]; + spawn(spawnCmd, spawnArgs, {suppressErrorOnFailingExitCode: true, mode: 'silent'}) + .then(({stdout, stderr, status}: SpawnResult) => { // Run the provided callback function. - const failed = formatter.callbackFor(action)(file, code, stdout, stderr); + const failed = formatter.callbackFor(action)(file, status, stdout, stderr); if (failed) { failures.push({filePath: file, message: stderr}); } @@ -110,8 +109,7 @@ export function runFormatterInParallel(allFiles: string[], action: FormatterActi progressBar.stop(); resolve(failures); } - }, - ); + }); // Mark the thread as in use as the command execution has been started. threads[thread] = true; } diff --git a/dev-infra/misc/build-and-link/cli.ts b/dev-infra/misc/build-and-link/cli.ts index d32fd4a021..d9144b6a32 100644 --- a/dev-infra/misc/build-and-link/cli.ts +++ b/dev-infra/misc/build-and-link/cli.ts @@ -7,13 +7,13 @@ */ import {green} from 'chalk'; -import {lstatSync, stat, Stats} from 'fs'; -import {isAbsolute, join, resolve} from 'path'; +import {lstatSync} from 'fs'; +import {resolve} from 'path'; import {Arguments, Argv, CommandModule} from 'yargs'; import {buildReleaseOutput} from '../../release/build/index'; +import {spawn} from '../../utils/child-process'; import {error, info, red} from '../../utils/console'; -import {exec} from '../../utils/shelljs'; /** Command line options. */ @@ -52,8 +52,8 @@ async function handler({projectRoot}: Arguments) { info(green(` ✓ Built release output.`)); for (const {outputPath, name} of releaseOutputs) { - exec(`yarn link --cwd ${outputPath}`); - exec(`yarn link --cwd ${projectRoot} ${name}`); + await spawn('yarn', ['link', '--cwd', outputPath]); + await spawn('yarn', ['link', '--cwd', projectRoot, name]); } info(green(` ✓ Linked release packages in provided project.`)); diff --git a/dev-infra/ng-dev.js b/dev-infra/ng-dev.js index ee84e30658..ff363be7b1 100755 --- a/dev-infra/ng-dev.js +++ b/dev-infra/ng-dev.js @@ -22,7 +22,6 @@ var conventionalCommitsParser = require('conventional-commits-parser'); var gitCommits_ = require('git-raw-commits'); var cliProgress = require('cli-progress'); var os = require('os'); -var shelljs = require('shelljs'); var minimatch = require('minimatch'); var ejs = require('ejs'); var ora = require('ora'); @@ -317,6 +316,9 @@ var GitCommandError = /** @class */ (function (_super) { // we sanitize the command that will be part of the error message. _super.call(this, "Command failed: git " + client.sanitizeConsoleOutput(args.join(' '))) || this; _this.args = args; + // Set the prototype explicitly because in ES5, the prototype is accidentally lost due to + // a limitation in down-leveling. + // https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work. Object.setPrototypeOf(_this, GitCommandError.prototype); return _this; } @@ -2440,6 +2442,113 @@ function buildCommitMessageParser(localYargs) { .command(ValidateRangeModule); } +/** + * @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 + */ +/** + * Spawns a given command with the specified arguments inside an interactive shell. All process + * stdin, stdout and stderr output is printed to the current console. + * + * @returns a Promise resolving on success, and rejecting on command failure with the status code. + */ +function spawnInteractive(command, args, options) { + if (options === void 0) { options = {}; } + return new Promise(function (resolve, reject) { + var commandText = command + " " + args.join(' '); + debug("Executing command: " + commandText); + var childProcess = child_process.spawn(command, args, tslib.__assign(tslib.__assign({}, options), { shell: true, stdio: 'inherit' })); + childProcess.on('exit', function (status) { return status === 0 ? resolve() : reject(status); }); + }); +} +/** + * Spawns a given command with the specified arguments inside a shell. All process stdout + * output is captured and returned as resolution on completion. Depending on the chosen + * output mode, stdout/stderr output is also printed to the console, or only on error. + * + * @returns a Promise resolving with captured stdout and stderr on success. The promise + * rejects on command failure. + */ +function spawn(command, args, options) { + if (options === void 0) { options = {}; } + return new Promise(function (resolve, reject) { + var commandText = command + " " + args.join(' '); + var outputMode = options.mode; + debug("Executing command: " + commandText); + var childProcess = child_process.spawn(command, args, tslib.__assign(tslib.__assign({}, options), { shell: true, stdio: 'pipe' })); + var logOutput = ''; + var stdout = ''; + var stderr = ''; + // Capture the stdout separately so that it can be passed as resolve value. + // This is useful if commands return parsable stdout. + childProcess.stderr.on('data', function (message) { + stderr += message; + logOutput += message; + // If console output is enabled, print the message directly to the stderr. Note that + // we intentionally print all output to stderr as stdout should not be polluted. + if (outputMode === undefined || outputMode === 'enabled') { + process.stderr.write(message); + } + }); + childProcess.stdout.on('data', function (message) { + stdout += message; + logOutput += message; + // If console output is enabled, print the message directly to the stderr. Note that + // we intentionally print all output to stderr as stdout should not be polluted. + if (outputMode === undefined || outputMode === 'enabled') { + process.stderr.write(message); + } + }); + childProcess.on('exit', function (exitCode, signal) { + var exitDescription = exitCode !== null ? "exit code \"" + exitCode + "\"" : "signal \"" + signal + "\""; + var printFn = outputMode === 'on-error' ? error : debug; + var status = statusFromExitCodeAndSignal(exitCode, signal); + printFn("Command \"" + commandText + "\" completed with " + exitDescription + "."); + printFn("Process output: \n" + logOutput); + // On success, resolve the promise. Otherwise reject with the captured stderr + // and stdout log output if the output mode was set to `silent`. + if (status === 0 || options.suppressErrorOnFailingExitCode) { + resolve({ stdout: stdout, stderr: stderr, status: status }); + } + else { + reject(outputMode === 'silent' ? logOutput : undefined); + } + }); + }); +} +/** + * Spawns a given command with the specified arguments inside a shell synchronously. + * + * @returns The command's stdout and stderr. + */ +function spawnSync(command, args, options) { + if (options === void 0) { options = {}; } + var commandText = command + " " + args.join(' '); + debug("Executing command: " + commandText); + var _a = child_process.spawnSync(command, args, tslib.__assign(tslib.__assign({}, options), { encoding: 'utf8', shell: true, stdio: 'pipe' })), exitCode = _a.status, signal = _a.signal, stdout = _a.stdout, stderr = _a.stderr; + /** The status of the spawn result. */ + var status = statusFromExitCodeAndSignal(exitCode, signal); + if (status === 0 || options.suppressErrorOnFailingExitCode) { + return { status: status, stdout: stdout, stderr: stderr }; + } + throw new Error(stderr); +} +/** + * Convert the provided exitCode and signal to a single status code. + * + * During `exit` node provides either a `code` or `signal`, one of which is guaranteed to be + * non-null. + * + * For more details see: https://nodejs.org/api/child_process.html#child_process_event_exit + */ +function statusFromExitCodeAndSignal(exitCode, signal) { + var _a; + return (_a = exitCode !== null && exitCode !== void 0 ? exitCode : signal) !== null && _a !== void 0 ? _a : -1; +} + /** * @license * Copyright Google LLC All Rights Reserved. @@ -2644,7 +2753,9 @@ class Prettier extends Formatter { * The configuration path of the prettier config, obtained during construction to prevent needing * to discover it repeatedly for each execution. */ - this.configPath = this.config['prettier'] ? shelljs.exec(`${this.binaryFilePath} --find-config-path .`).trim() : ''; + this.configPath = this.config['prettier'] ? + spawnSync(this.binaryFilePath, ['--find-config-path', '.']).stdout.trim() : + ''; this.actions = { check: { commandFlags: `--config ${this.configPath} --check`, @@ -2747,9 +2858,11 @@ function runFormatterInParallel(allFiles, action) { } // Get the file and formatter for the next command. const { file, formatter } = nextCommand; - shelljs.exec(`${formatter.commandFor(action)} ${file}`, { async: true, silent: true }, (code, stdout, stderr) => { + const [spawnCmd, ...spawnArgs] = [...formatter.commandFor(action).split(' '), file]; + spawn(spawnCmd, spawnArgs, { suppressErrorOnFailingExitCode: true, mode: 'silent' }) + .then(({ stdout, stderr, status }) => { // Run the provided callback function. - const failed = formatter.callbackFor(action)(file, code, stdout, stderr); + const failed = formatter.callbackFor(action)(file, status, stdout, stderr); if (failed) { failures.push({ filePath: file, message: stderr }); } @@ -3376,21 +3489,6 @@ const CheckoutCommandModule = { describe: 'Checkout a PR from the upstream repo', }; -/** - * @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 - */ -/** - * Runs an given command as child process. By default, child process - * output will not be printed. - */ -function exec(cmd, opts) { - return shelljs.exec(cmd, tslib.__assign(tslib.__assign({ silent: true }, opts), { async: false })); -} - /** * @license * Copyright Google LLC All Rights Reserved. @@ -3494,7 +3592,9 @@ function discoverNewConflictsForPr(newPrNumber, updatedAfter) { if (err instanceof GitCommandError) { conflicts.push(pr); } - throw err; + else { + throw err; + } } // Abort any outstanding rebase attempt. git.runGraceful(['rebase', '--abort'], { stdio: 'ignore' }); @@ -3504,7 +3604,7 @@ function discoverNewConflictsForPr(newPrNumber, updatedAfter) { progressBar.stop(); info(); info(`Result:`); - cleanUpGitState(previousBranchOrRevision); + git.checkout(previousBranchOrRevision, true); // If no conflicts are found, exit successfully. if (conflicts.length === 0) { info(`No new conflicting PRs found after #${newPrNumber} merging`); @@ -3519,17 +3619,6 @@ function discoverNewConflictsForPr(newPrNumber, updatedAfter) { process.exit(1); }); } -/** Reset git back to the provided branch or revision. */ -function cleanUpGitState(previousBranchOrRevision) { - // Ensure that any outstanding rebases are aborted. - exec(`git rebase --abort`); - // Ensure that any changes in the current repo state are cleared. - exec(`git reset --hard`); - // Checkout the original branch from before the run began. - exec(`git checkout ${previousBranchOrRevision}`); - // Delete the generated branch. - exec(`git branch -D ${tempWorkingBranch}`); -} /** * @license @@ -5976,88 +6065,6 @@ const ReleaseNotesCommandModule = { describe: 'Generate release notes', }; -/** - * @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 - */ -/** - * Spawns a given command with the specified arguments inside an interactive shell. All process - * stdin, stdout and stderr output is printed to the current console. - * - * @returns a Promise resolving on success, and rejecting on command failure with the status code. - */ -function spawnInteractive(command, args, options) { - if (options === void 0) { options = {}; } - return new Promise(function (resolve, reject) { - var commandText = command + " " + args.join(' '); - debug("Executing command: " + commandText); - var childProcess = child_process.spawn(command, args, tslib.__assign(tslib.__assign({}, options), { shell: true, stdio: 'inherit' })); - childProcess.on('exit', function (status) { return status === 0 ? resolve() : reject(status); }); - }); -} -/** - * Spawns a given command with the specified arguments inside a shell. All process stdout - * output is captured and returned as resolution on completion. Depending on the chosen - * output mode, stdout/stderr output is also printed to the console, or only on error. - * - * @returns a Promise resolving with captured stdout and stderr on success. The promise - * rejects on command failure - */ -function spawn(command, args, options) { - if (options === void 0) { options = {}; } - return new Promise(function (resolve, reject) { - var commandText = command + " " + args.join(' '); - var outputMode = options.mode; - debug("Executing command: " + commandText); - var childProcess = child_process.spawn(command, args, tslib.__assign(tslib.__assign({}, options), { shell: true, stdio: 'pipe' })); - var logOutput = ''; - var stdout = ''; - var stderr = ''; - // Capture the stdout separately so that it can be passed as resolve value. - // This is useful if commands return parsable stdout. - childProcess.stderr.on('data', function (message) { - stderr += message; - logOutput += message; - // If console output is enabled, print the message directly to the stderr. Note that - // we intentionally print all output to stderr as stdout should not be polluted. - if (outputMode === undefined || outputMode === 'enabled') { - process.stderr.write(message); - } - }); - childProcess.stdout.on('data', function (message) { - stdout += message; - logOutput += message; - // If console output is enabled, print the message directly to the stderr. Note that - // we intentionally print all output to stderr as stdout should not be polluted. - if (outputMode === undefined || outputMode === 'enabled') { - process.stderr.write(message); - } - }); - childProcess.on('exit', function (exitCode, signal) { - var exitDescription = exitCode !== null ? "exit code \"" + exitCode + "\"" : "signal \"" + signal + "\""; - var printFn = outputMode === 'on-error' ? error : debug; - var status = statusFromExitCodeAndSignal(exitCode, signal); - printFn("Command \"" + commandText + "\" completed with " + exitDescription + "."); - printFn("Process output: \n" + logOutput); - // On success, resolve the promise. Otherwise reject with the captured stderr - // and stdout log output if the output mode was set to `silent`. - if (status === 0 || options.suppressErrorOnFailingExitCode) { - resolve({ stdout: stdout, stderr: stderr, status: status }); - } - else { - reject(outputMode === 'silent' ? logOutput : undefined); - } - }); - }); -} -/** Convert the provided exitCode and signal to a single status code. */ -function statusFromExitCodeAndSignal(exitCode, signal) { - return exitCode !== null ? exitCode : signal !== null ? signal : -1; -} - /** * @license * Copyright Google LLC All Rights Reserved. @@ -7655,20 +7662,17 @@ const ReleaseSetDistTagCommand = { */ function buildEnvStamp(mode) { console.info(`BUILD_SCM_BRANCH ${getCurrentBranch()}`); - console.info(`BUILD_SCM_COMMIT_SHA ${getCurrentSha()}`); - console.info(`BUILD_SCM_HASH ${getCurrentSha()}`); + console.info(`BUILD_SCM_COMMIT_SHA ${getCurrentBranchOrRevision()}`); + console.info(`BUILD_SCM_HASH ${getCurrentBranchOrRevision()}`); console.info(`BUILD_SCM_LOCAL_CHANGES ${hasLocalChanges()}`); console.info(`BUILD_SCM_USER ${getCurrentGitUser()}`); console.info(`BUILD_SCM_VERSION ${getSCMVersion(mode)}`); - process.exit(0); -} -/** Run the exec command and return the stdout as a trimmed string. */ -function exec$1(cmd) { - return exec(cmd).trim(); + process.exit(); } /** Whether the repo has local changes. */ function hasLocalChanges() { - return !!exec$1(`git status --untracked-files=no --porcelain`); + const git = GitClient.get(); + return git.hasUncommittedChanges(); } /** * Get the version for generated packages. @@ -7677,30 +7681,34 @@ function hasLocalChanges() { * In release mode, the version is based on the base package.json version. */ function getSCMVersion(mode) { + const git = GitClient.get(); if (mode === 'release') { - const git = GitClient.get(); const packageJsonPath = path.join(git.baseDir, 'package.json'); const { version } = require(packageJsonPath); return version; } if (mode === 'snapshot') { - const version = exec$1(`git describe --match [0-9]*.[0-9]*.[0-9]* --abbrev=7 --tags HEAD`); + const version = git.run(['describe', '--match', '[0-9]*.[0-9]*.[0-9]*', '--abbrev=7', '--tags', 'HEAD']) + .stdout.trim(); return `${version.replace(/-([0-9]+)-g/, '+$1.sha-')}${(hasLocalChanges() ? '.with-local-changes' : '')}`; } return '0.0.0'; } -/** Get the current SHA of HEAD. */ -function getCurrentSha() { - return exec$1(`git rev-parse HEAD`); +/** Get the current branch or revision of HEAD. */ +function getCurrentBranchOrRevision() { + const git = GitClient.get(); + return git.getCurrentBranchOrRevision(); } /** Get the currently checked out branch. */ function getCurrentBranch() { - return exec$1(`git symbolic-ref --short HEAD`); + const git = GitClient.get(); + return git.run(['symbolic-ref', '--short', 'HEAD']).stdout.trim(); } /** Get the current git user based on the git config. */ function getCurrentGitUser() { - const userName = exec$1(`git config user.name`); - const userEmail = exec$1(`git config user.email`); + const git = GitClient.get(); + let userName = git.runGraceful(['config', 'user.name']).stdout.trim() || 'Unknown User'; + let userEmail = git.runGraceful(['config', 'user.email']).stdout.trim() || 'unknown_email'; return `${userName} <${userEmail}>`; } @@ -8187,8 +8195,8 @@ function handler$e({ projectRoot }) { } info(chalk.green(` ✓ Built release output.`)); for (const { outputPath, name } of releaseOutputs) { - exec(`yarn link --cwd ${outputPath}`); - exec(`yarn link --cwd ${projectRoot} ${name}`); + yield spawn('yarn', ['link', '--cwd', outputPath]); + yield spawn('yarn', ['link', '--cwd', projectRoot, name]); } info(chalk.green(` ✓ Linked release packages in provided project.`)); }); diff --git a/dev-infra/pr/discover-new-conflicts/BUILD.bazel b/dev-infra/pr/discover-new-conflicts/BUILD.bazel index 3ea331122b..e88ed7eaed 100644 --- a/dev-infra/pr/discover-new-conflicts/BUILD.bazel +++ b/dev-infra/pr/discover-new-conflicts/BUILD.bazel @@ -11,7 +11,6 @@ ts_library( "//dev-infra/utils", "@npm//@types/cli-progress", "@npm//@types/node", - "@npm//@types/shelljs", "@npm//@types/yargs", "@npm//typed-graphqlify", ], diff --git a/dev-infra/pr/discover-new-conflicts/index.ts b/dev-infra/pr/discover-new-conflicts/index.ts index 32138da310..08ee4e01cc 100644 --- a/dev-infra/pr/discover-new-conflicts/index.ts +++ b/dev-infra/pr/discover-new-conflicts/index.ts @@ -13,7 +13,6 @@ import {error, info} from '../../utils/console'; import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client'; import {GitCommandError} from '../../utils/git/git-client'; import {getPendingPrs} from '../../utils/github'; -import {exec} from '../../utils/shelljs'; /* Graphql schema for the response body for each pending PR. */ @@ -125,8 +124,9 @@ export async function discoverNewConflictsForPr(newPrNumber: number, updatedAfte } catch (err) { if (err instanceof GitCommandError) { conflicts.push(pr); + } else { + throw err; } - throw err; } // Abort any outstanding rebase attempt. git.runGraceful(['rebase', '--abort'], {stdio: 'ignore'}); @@ -138,7 +138,7 @@ export async function discoverNewConflictsForPr(newPrNumber: number, updatedAfte info(); info(`Result:`); - cleanUpGitState(previousBranchOrRevision); + git.checkout(previousBranchOrRevision, true); // If no conflicts are found, exit successfully. if (conflicts.length === 0) { @@ -154,15 +154,3 @@ export async function discoverNewConflictsForPr(newPrNumber: number, updatedAfte error.groupEnd(); process.exit(1); } - -/** Reset git back to the provided branch or revision. */ -export function cleanUpGitState(previousBranchOrRevision: string) { - // Ensure that any outstanding rebases are aborted. - exec(`git rebase --abort`); - // Ensure that any changes in the current repo state are cleared. - exec(`git reset --hard`); - // Checkout the original branch from before the run began. - exec(`git checkout ${previousBranchOrRevision}`); - // Delete the generated branch. - exec(`git branch -D ${tempWorkingBranch}`); -} diff --git a/dev-infra/pullapprove/BUILD.bazel b/dev-infra/pullapprove/BUILD.bazel index 94bda2d509..7ab68b7d64 100644 --- a/dev-infra/pullapprove/BUILD.bazel +++ b/dev-infra/pullapprove/BUILD.bazel @@ -17,11 +17,9 @@ ts_library( "//dev-infra/utils", "@npm//@types/minimatch", "@npm//@types/node", - "@npm//@types/shelljs", "@npm//@types/yaml", "@npm//@types/yargs", "@npm//minimatch", - "@npm//shelljs", "@npm//yaml", "@npm//yargs", ], diff --git a/dev-infra/release/stamping/env-stamp.ts b/dev-infra/release/stamping/env-stamp.ts index 45d5221275..e8a7e40a65 100644 --- a/dev-infra/release/stamping/env-stamp.ts +++ b/dev-infra/release/stamping/env-stamp.ts @@ -9,8 +9,6 @@ import {join} from 'path'; import {GitClient} from '../../utils/git/git-client'; -import {exec as _exec} from '../../utils/shelljs'; - export type EnvStampMode = 'snapshot'|'release'; /** @@ -25,22 +23,18 @@ export type EnvStampMode = 'snapshot'|'release'; */ export function buildEnvStamp(mode: EnvStampMode) { console.info(`BUILD_SCM_BRANCH ${getCurrentBranch()}`); - console.info(`BUILD_SCM_COMMIT_SHA ${getCurrentSha()}`); - console.info(`BUILD_SCM_HASH ${getCurrentSha()}`); + console.info(`BUILD_SCM_COMMIT_SHA ${getCurrentBranchOrRevision()}`); + console.info(`BUILD_SCM_HASH ${getCurrentBranchOrRevision()}`); console.info(`BUILD_SCM_LOCAL_CHANGES ${hasLocalChanges()}`); console.info(`BUILD_SCM_USER ${getCurrentGitUser()}`); console.info(`BUILD_SCM_VERSION ${getSCMVersion(mode)}`); - process.exit(0); -} - -/** Run the exec command and return the stdout as a trimmed string. */ -function exec(cmd: string) { - return _exec(cmd).trim(); + process.exit(); } /** Whether the repo has local changes. */ function hasLocalChanges() { - return !!exec(`git status --untracked-files=no --porcelain`); + const git = GitClient.get(); + return git.hasUncommittedChanges(); } /** @@ -50,34 +44,38 @@ function hasLocalChanges() { * In release mode, the version is based on the base package.json version. */ function getSCMVersion(mode: EnvStampMode) { + const git = GitClient.get(); if (mode === 'release') { - const git = GitClient.get(); const packageJsonPath = join(git.baseDir, 'package.json'); const {version} = require(packageJsonPath); return version; } if (mode === 'snapshot') { - const version = exec(`git describe --match [0-9]*.[0-9]*.[0-9]* --abbrev=7 --tags HEAD`); + const version = + git.run(['describe', '--match', '[0-9]*.[0-9]*.[0-9]*', '--abbrev=7', '--tags', 'HEAD']) + .stdout.trim(); return `${version.replace(/-([0-9]+)-g/, '+$1.sha-')}${ (hasLocalChanges() ? '.with-local-changes' : '')}`; } return '0.0.0'; } -/** Get the current SHA of HEAD. */ -function getCurrentSha() { - return exec(`git rev-parse HEAD`); +/** Get the current branch or revision of HEAD. */ +function getCurrentBranchOrRevision() { + const git = GitClient.get(); + return git.getCurrentBranchOrRevision(); } /** Get the currently checked out branch. */ function getCurrentBranch() { - return exec(`git symbolic-ref --short HEAD`); + const git = GitClient.get(); + return git.run(['symbolic-ref', '--short', 'HEAD']).stdout.trim(); } /** Get the current git user based on the git config. */ function getCurrentGitUser() { - const userName = exec(`git config user.name`); - const userEmail = exec(`git config user.email`); - + const git = GitClient.get(); + let userName = git.runGraceful(['config', 'user.name']).stdout.trim() || 'Unknown User'; + let userEmail = git.runGraceful(['config', 'user.email']).stdout.trim() || 'unknown_email'; return `${userName} <${userEmail}>`; } diff --git a/dev-infra/tmpl-package.json b/dev-infra/tmpl-package.json index 3fb79079ff..901b3b9d36 100644 --- a/dev-infra/tmpl-package.json +++ b/dev-infra/tmpl-package.json @@ -33,7 +33,6 @@ "protractor": "", "selenium-webdriver": "", "semver": "", - "shelljs": "", "ts-node": "", "tslib": "", "typed-graphqlify": "", diff --git a/dev-infra/utils/BUILD.bazel b/dev-infra/utils/BUILD.bazel index 09c4f9954b..cc51124606 100644 --- a/dev-infra/utils/BUILD.bazel +++ b/dev-infra/utils/BUILD.bazel @@ -21,12 +21,10 @@ ts_library( "@npm//@types/inquirer", "@npm//@types/node", "@npm//@types/semver", - "@npm//@types/shelljs", "@npm//@types/yargs", "@npm//chalk", "@npm//inquirer", "@npm//semver", - "@npm//shelljs", "@npm//tslib", "@npm//typed-graphqlify", "@npm//yargs", diff --git a/dev-infra/utils/child-process.ts b/dev-infra/utils/child-process.ts index 64fcdcb634..809b39baef 100644 --- a/dev-infra/utils/child-process.ts +++ b/dev-infra/utils/child-process.ts @@ -10,6 +10,7 @@ import {spawn as _spawn, SpawnOptions as _SpawnOptions, spawnSync as _spawnSync, import {debug, error} from './console'; +/** Interface describing the options for spawning a process synchronously. */ export interface SpawnSyncOptions extends Omit<_SpawnSyncOptions, 'shell'|'stdio'> { /** Whether to prevent exit codes being treated as failures. */ suppressErrorOnFailingExitCode?: boolean; @@ -58,7 +59,7 @@ export function spawnInteractive( * output mode, stdout/stderr output is also printed to the console, or only on error. * * @returns a Promise resolving with captured stdout and stderr on success. The promise - * rejects on command failure + * rejects on command failure. */ export function spawn( command: string, args: string[], options: SpawnOptions = {}): Promise { @@ -115,12 +116,12 @@ export function spawn( } /** - * Spawns a given command with the specified arguments inside a shell syncronously. + * Spawns a given command with the specified arguments inside a shell synchronously. * * @returns The command's stdout and stderr. */ export function spawnSync( - command: string, args: string[], options: SpawnOptions = {}): SpawnResult { + command: string, args: string[], options: SpawnSyncOptions = {}): SpawnResult { const commandText = `${command} ${args.join(' ')}`; debug(`Executing command: ${commandText}`); @@ -137,9 +138,14 @@ export function spawnSync( throw new Error(stderr); } - - -/** Convert the provided exitCode and signal to a single status code. */ +/** + * Convert the provided exitCode and signal to a single status code. + * + * During `exit` node provides either a `code` or `signal`, one of which is guaranteed to be + * non-null. + * + * For more details see: https://nodejs.org/api/child_process.html#child_process_event_exit + */ function statusFromExitCodeAndSignal(exitCode: number|null, signal: NodeJS.Signals|null) { - return exitCode !== null ? exitCode : signal !== null ? signal : -1; + return exitCode ?? signal ?? -1; } diff --git a/dev-infra/utils/git/git-client.ts b/dev-infra/utils/git/git-client.ts index 52e803b9a5..e66f85ed1d 100644 --- a/dev-infra/utils/git/git-client.ts +++ b/dev-infra/utils/git/git-client.ts @@ -23,6 +23,10 @@ export class GitCommandError extends Error { // accidentally leak the Github token that might be used in a command, // we sanitize the command that will be part of the error message. super(`Command failed: git ${client.sanitizeConsoleOutput(args.join(' '))}`); + + // Set the prototype explicitly because in ES5, the prototype is accidentally lost due to + // a limitation in down-leveling. + // https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work. Object.setPrototypeOf(this, GitCommandError.prototype); } } diff --git a/dev-infra/utils/shelljs.ts b/dev-infra/utils/shelljs.ts deleted file mode 100644 index 0040b4135b..0000000000 --- a/dev-infra/utils/shelljs.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @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 {exec as _exec, ExecOptions, ShellString} from 'shelljs'; - -/** - * Runs an given command as child process. By default, child process - * output will not be printed. - */ -export function exec(cmd: string, opts?: Omit): ShellString { - return _exec(cmd, {silent: true, ...opts, async: false}); -}