diff --git a/dev-infra/build-worker.js b/dev-infra/build-worker.js index 9654f967cd..5d8477e5e0 100644 --- a/dev-infra/build-worker.js +++ b/dev-infra/build-worker.js @@ -144,7 +144,6 @@ 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; - Object.setPrototypeOf(_this, GitCommandError.prototype); return _this; } return GitCommandError; diff --git a/dev-infra/ng-dev.js b/dev-infra/ng-dev.js index c05c96d301..4dcd9b1082 100755 --- a/dev-infra/ng-dev.js +++ b/dev-infra/ng-dev.js @@ -316,7 +316,6 @@ 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; - Object.setPrototypeOf(_this, GitCommandError.prototype); return _this; } return GitCommandError; @@ -3310,40 +3309,30 @@ function discoverNewConflictsForPr(newPrNumber, updatedAfter) { info(`Retrieved ${allPendingPRs.length} total pending PRs`); info(`Checking ${pendingPrs.length} PRs for conflicts after a merge of #${newPrNumber}`); // Fetch and checkout the PR being checked. - git.run(['fetch', '-q', requestedPr.headRef.repository.url, requestedPr.headRef.name]); - git.run(['checkout', '-q', '-B', tempWorkingBranch, 'FETCH_HEAD']); + exec(`git fetch ${requestedPr.headRef.repository.url} ${requestedPr.headRef.name}`); + exec(`git checkout -B ${tempWorkingBranch} FETCH_HEAD`); // Rebase the PR against the PRs target branch. - git.run(['fetch', '-q', requestedPr.baseRef.repository.url, requestedPr.baseRef.name]); - try { - git.run(['rebase', 'FETCH_HEAD'], { stdio: 'ignore' }); - } - catch (err) { - if (err instanceof GitCommandError) { - error('The requested PR currently has conflicts'); - git.checkout(previousBranchOrRevision, true); - process.exit(1); - } - throw err; + exec(`git fetch ${requestedPr.baseRef.repository.url} ${requestedPr.baseRef.name}`); + const result = exec(`git rebase FETCH_HEAD`); + if (result.code) { + error('The requested PR currently has conflicts'); + cleanUpGitState(previousBranchOrRevision); + process.exit(1); } // Start the progress bar progressBar.start(pendingPrs.length, 0); // Check each PR to determine if it can merge cleanly into the repo after the target PR. for (const pr of pendingPrs) { // Fetch and checkout the next PR - git.run(['fetch', '-q', pr.headRef.repository.url, pr.headRef.name]); - git.run(['checkout', '-q', '--detach', 'FETCH_HEAD']); + exec(`git fetch ${pr.headRef.repository.url} ${pr.headRef.name}`); + exec(`git checkout --detach FETCH_HEAD`); // Check if the PR cleanly rebases into the repo after the target PR. - try { - git.run(['rebase', tempWorkingBranch], { stdio: 'ignore' }); - } - catch (err) { - if (err instanceof GitCommandError) { - conflicts.push(pr); - } - throw err; + const result = exec(`git rebase ${tempWorkingBranch}`); + if (result.code !== 0) { + conflicts.push(pr); } // Abort any outstanding rebase attempt. - git.runGraceful(['rebase', '--abort'], { stdio: 'ignore' }); + exec(`git rebase --abort`); progressBar.increment(1); } // End the progress bar as all PRs have been processed. @@ -5835,7 +5824,7 @@ const ReleaseNotesCommandModule = { * * @returns a Promise resolving on success, and rejecting on command failure with the status code. */ -function spawnInteractive(command, args, options) { +function spawnInteractiveCommand(command, args, options) { if (options === void 0) { options = {}; } return new Promise(function (resolve, reject) { var commandText = command + " " + args.join(' '); @@ -5850,9 +5839,9 @@ function spawnInteractive(command, args, options) { * 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. */ -function spawn(command, args, options) { +function spawnWithDebugOutput(command, args, options) { if (options === void 0) { options = {}; } return new Promise(function (resolve, reject) { var commandText = command + " " + args.join(' '); @@ -5882,16 +5871,15 @@ function spawn(command, args, options) { process.stderr.write(message); } }); - childProcess.on('exit', function (exitCode, signal) { - var exitDescription = exitCode !== null ? "exit code \"" + exitCode + "\"" : "signal \"" + signal + "\""; + childProcess.on('exit', function (status, signal) { + var exitDescription = status !== null ? "exit code \"" + status + "\"" : "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 }); + if (status === 0) { + resolve({ stdout: stdout, stderr: stderr }); } else { reject(outputMode === 'silent' ? logOutput : undefined); @@ -5899,10 +5887,6 @@ function spawn(command, args, options) { }); }); } -/** Convert the provided exitCode and signal to a single status code. */ -function statusFromExitCodeAndSignal(exitCode, signal) { - return exitCode !== null ? exitCode : signal !== null ? signal : -1; -} /** * @license @@ -5922,7 +5906,7 @@ function runNpmPublish(packagePath, distTag, registryUrl) { if (registryUrl !== undefined) { args.push('--registry', registryUrl); } - yield spawn('npm', args, { cwd: packagePath, mode: 'silent' }); + yield spawnWithDebugOutput('npm', args, { cwd: packagePath, mode: 'silent' }); }); } /** @@ -5936,7 +5920,7 @@ function setNpmTagForPackage(packageName, distTag, version, registryUrl) { if (registryUrl !== undefined) { args.push('--registry', registryUrl); } - yield spawn('npm', args, { mode: 'silent' }); + yield spawnWithDebugOutput('npm', args, { mode: 'silent' }); }); } /** @@ -5951,7 +5935,7 @@ function npmIsLoggedIn(registryUrl) { args.push('--registry', registryUrl); } try { - yield spawn('npm', args, { mode: 'silent' }); + yield spawnWithDebugOutput('npm', args, { mode: 'silent' }); } catch (e) { return false; @@ -5974,7 +5958,7 @@ function npmLogin(registryUrl) { } // The login command prompts for username, password and other profile information. Hence // the process needs to be interactive (i.e. respecting current TTYs stdin). - yield spawnInteractive('npm', args); + yield spawnInteractiveCommand('npm', args); }); } /** @@ -5991,7 +5975,7 @@ function npmLogout(registryUrl) { args.splice(1, 0, '--registry', registryUrl); } try { - yield spawn('npm', args, { mode: 'silent' }); + yield spawnWithDebugOutput('npm', args, { mode: 'silent' }); } finally { return npmIsLoggedIn(registryUrl); @@ -6113,7 +6097,7 @@ function invokeSetNpmDistCommand(npmDistTag, version) { return tslib.__awaiter(this, void 0, void 0, function* () { try { // Note: No progress indicator needed as that is the responsibility of the command. - yield spawn('yarn', ['--silent', 'ng-dev', 'release', 'set-dist-tag', npmDistTag, version.format()]); + yield spawnWithDebugOutput('yarn', ['--silent', 'ng-dev', 'release', 'set-dist-tag', npmDistTag, version.format()]); info(green(` ✓ Set "${npmDistTag}" NPM dist tag for all packages to v${version}.`)); } catch (e) { @@ -6133,7 +6117,7 @@ function invokeReleaseBuildCommand() { try { // Since we expect JSON to be printed from the `ng-dev release build` command, // we spawn the process in silent mode. We have set up an Ora progress spinner. - const { stdout } = yield spawn('yarn', ['--silent', 'ng-dev', 'release', 'build', '--json'], { mode: 'silent' }); + const { stdout } = yield spawnWithDebugOutput('yarn', ['--silent', 'ng-dev', 'release', 'build', '--json'], { mode: 'silent' }); spinner.stop(); info(green(' ✓ Built release output for all packages.')); // The `ng-dev release build` command prints a JSON array to stdout @@ -6157,7 +6141,7 @@ function invokeYarnInstallCommand(projectDir) { try { // Note: No progress indicator needed as that is the responsibility of the command. // TODO: Consider using an Ora spinner instead to ensure minimal console output. - yield spawn('yarn', ['install', '--frozen-lockfile', '--non-interactive'], { cwd: projectDir }); + yield spawnWithDebugOutput('yarn', ['install', '--frozen-lockfile', '--non-interactive'], { cwd: projectDir }); info(green(' ✓ Installed project dependencies.')); } catch (e) { @@ -7295,7 +7279,7 @@ class ReleaseTool { try { // Note: We do not rely on `/usr/bin/env` but rather access the `env` binary directly as it // should be part of the shell's `$PATH`. This is necessary for compatibility with Windows. - const pyVersion = yield spawn('env', ['python', '--version'], { mode: 'silent' }); + const pyVersion = yield spawnWithDebugOutput('env', ['python', '--version'], { mode: 'silent' }); const version = pyVersion.stdout.trim() || pyVersion.stderr.trim(); if (version.startsWith('Python 3.')) { debug(`Local python version: ${version}`); diff --git a/dev-infra/pr/discover-new-conflicts/index.ts b/dev-infra/pr/discover-new-conflicts/index.ts index 32138da310..b164155566 100644 --- a/dev-infra/pr/discover-new-conflicts/index.ts +++ b/dev-infra/pr/discover-new-conflicts/index.ts @@ -11,7 +11,7 @@ import {types as graphqlTypes} from 'typed-graphqlify'; import {error, info} from '../../utils/console'; import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client'; -import {GitCommandError} from '../../utils/git/git-client'; +import {GitClient} from '../../utils/git/git-client'; import {getPendingPrs} from '../../utils/github'; import {exec} from '../../utils/shelljs'; @@ -95,20 +95,16 @@ export async function discoverNewConflictsForPr(newPrNumber: number, updatedAfte info(`Checking ${pendingPrs.length} PRs for conflicts after a merge of #${newPrNumber}`); // Fetch and checkout the PR being checked. - git.run(['fetch', '-q', requestedPr.headRef.repository.url, requestedPr.headRef.name]); - git.run(['checkout', '-q', '-B', tempWorkingBranch, 'FETCH_HEAD']); + exec(`git fetch ${requestedPr.headRef.repository.url} ${requestedPr.headRef.name}`); + exec(`git checkout -B ${tempWorkingBranch} FETCH_HEAD`); // Rebase the PR against the PRs target branch. - git.run(['fetch', '-q', requestedPr.baseRef.repository.url, requestedPr.baseRef.name]); - try { - git.run(['rebase', 'FETCH_HEAD'], {stdio: 'ignore'}); - } catch (err) { - if (err instanceof GitCommandError) { - error('The requested PR currently has conflicts'); - git.checkout(previousBranchOrRevision, true); - process.exit(1); - } - throw err; + exec(`git fetch ${requestedPr.baseRef.repository.url} ${requestedPr.baseRef.name}`); + const result = exec(`git rebase FETCH_HEAD`); + if (result.code) { + error('The requested PR currently has conflicts'); + cleanUpGitState(previousBranchOrRevision); + process.exit(1); } // Start the progress bar @@ -117,19 +113,15 @@ export async function discoverNewConflictsForPr(newPrNumber: number, updatedAfte // Check each PR to determine if it can merge cleanly into the repo after the target PR. for (const pr of pendingPrs) { // Fetch and checkout the next PR - git.run(['fetch', '-q', pr.headRef.repository.url, pr.headRef.name]); - git.run(['checkout', '-q', '--detach', 'FETCH_HEAD']); + exec(`git fetch ${pr.headRef.repository.url} ${pr.headRef.name}`); + exec(`git checkout --detach FETCH_HEAD`); // Check if the PR cleanly rebases into the repo after the target PR. - try { - git.run(['rebase', tempWorkingBranch], {stdio: 'ignore'}); - } catch (err) { - if (err instanceof GitCommandError) { - conflicts.push(pr); - } - throw err; + const result = exec(`git rebase ${tempWorkingBranch}`); + if (result.code !== 0) { + conflicts.push(pr); } // Abort any outstanding rebase attempt. - git.runGraceful(['rebase', '--abort'], {stdio: 'ignore'}); + exec(`git rebase --abort`); progressBar.increment(1); } diff --git a/dev-infra/release/publish/external-commands.ts b/dev-infra/release/publish/external-commands.ts index f0b9edf366..2683e3ce46 100644 --- a/dev-infra/release/publish/external-commands.ts +++ b/dev-infra/release/publish/external-commands.ts @@ -9,7 +9,7 @@ import * as ora from 'ora'; import * as semver from 'semver'; -import {spawn} from '../../utils/child-process'; +import {spawnWithDebugOutput} from '../../utils/child-process'; import {error, green, info, red} from '../../utils/console'; import {BuiltPackage} from '../config/index'; import {NpmDistTag} from '../versioning'; @@ -40,7 +40,7 @@ import {FatalReleaseActionError} from './actions-error'; export async function invokeSetNpmDistCommand(npmDistTag: NpmDistTag, version: semver.SemVer) { try { // Note: No progress indicator needed as that is the responsibility of the command. - await spawn( + await spawnWithDebugOutput( 'yarn', ['--silent', 'ng-dev', 'release', 'set-dist-tag', npmDistTag, version.format()]); info(green(` ✓ Set "${npmDistTag}" NPM dist tag for all packages to v${version}.`)); } catch (e) { @@ -59,8 +59,8 @@ export async function invokeReleaseBuildCommand(): Promise { try { // Since we expect JSON to be printed from the `ng-dev release build` command, // we spawn the process in silent mode. We have set up an Ora progress spinner. - const {stdout} = - await spawn('yarn', ['--silent', 'ng-dev', 'release', 'build', '--json'], {mode: 'silent'}); + const {stdout} = await spawnWithDebugOutput( + 'yarn', ['--silent', 'ng-dev', 'release', 'build', '--json'], {mode: 'silent'}); spinner.stop(); info(green(' ✓ Built release output for all packages.')); // The `ng-dev release build` command prints a JSON array to stdout @@ -82,7 +82,8 @@ export async function invokeYarnInstallCommand(projectDir: string): Promise args.splice(1, 0, '--registry', registryUrl); } try { - await spawn('npm', args, {mode: 'silent'}); + await spawnWithDebugOutput('npm', args, {mode: 'silent'}); } finally { return npmIsLoggedIn(registryUrl); } diff --git a/dev-infra/utils/child-process.ts b/dev-infra/utils/child-process.ts index 64fcdcb634..1a1950f3d5 100644 --- a/dev-infra/utils/child-process.ts +++ b/dev-infra/utils/child-process.ts @@ -6,34 +6,21 @@ * found in the LICENSE file at https://angular.io/license */ -import {spawn as _spawn, SpawnOptions as _SpawnOptions, spawnSync as _spawnSync, SpawnSyncOptions as _SpawnSyncOptions} from 'child_process'; +import {spawn, SpawnOptions} from 'child_process'; import {debug, error} from './console'; - -export interface SpawnSyncOptions extends Omit<_SpawnSyncOptions, 'shell'|'stdio'> { - /** Whether to prevent exit codes being treated as failures. */ - suppressErrorOnFailingExitCode?: boolean; -} - /** Interface describing the options for spawning a process. */ -export interface SpawnOptions extends Omit<_SpawnOptions, 'shell'|'stdio'> { +export interface SpawnedProcessOptions extends Omit { /** Console output mode. Defaults to "enabled". */ mode?: 'enabled'|'silent'|'on-error'; - /** Whether to prevent exit codes being treated as failures. */ - suppressErrorOnFailingExitCode?: boolean; } -/** Interface describing the options for spawning an interactive process. */ -export type SpawnInteractiveCommandOptions = Omit<_SpawnOptions, 'shell'|'stdio'>; - /** Interface describing the result of a spawned process. */ -export interface SpawnResult { +export interface SpawnedProcessResult { /** Captured stdout in string format. */ stdout: string; /** Captured stderr in string format. */ stderr: string; - /** The exit code or signal of the process. */ - status: number|NodeJS.Signals; } /** @@ -42,12 +29,12 @@ export interface SpawnResult { * * @returns a Promise resolving on success, and rejecting on command failure with the status code. */ -export function spawnInteractive( - command: string, args: string[], options: SpawnInteractiveCommandOptions = {}) { +export function spawnInteractiveCommand( + command: string, args: string[], options: Omit = {}) { return new Promise((resolve, reject) => { const commandText = `${command} ${args.join(' ')}`; debug(`Executing command: ${commandText}`); - const childProcess = _spawn(command, args, {...options, shell: true, stdio: 'inherit'}); + const childProcess = spawn(command, args, {...options, shell: true, stdio: 'inherit'}); childProcess.on('exit', status => status === 0 ? resolve() : reject(status)); }); } @@ -58,17 +45,18 @@ 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 { +export function spawnWithDebugOutput( + command: string, args: string[], + options: SpawnedProcessOptions = {}): Promise { return new Promise((resolve, reject) => { const commandText = `${command} ${args.join(' ')}`; const outputMode = options.mode; debug(`Executing command: ${commandText}`); - const childProcess = _spawn(command, args, {...options, shell: true, stdio: 'pipe'}); + const childProcess = spawn(command, args, {...options, shell: true, stdio: 'pipe'}); let logOutput = ''; let stdout = ''; let stderr = ''; @@ -95,51 +83,20 @@ export function spawn( } }); - childProcess.on('exit', (exitCode, signal) => { - const exitDescription = exitCode !== null ? `exit code "${exitCode}"` : `signal "${signal}"`; + childProcess.on('exit', (status, signal) => { + const exitDescription = status !== null ? `exit code "${status}"` : `signal "${signal}"`; const printFn = outputMode === 'on-error' ? error : debug; - const 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, stderr, status}); + if (status === 0) { + resolve({stdout, stderr}); } else { reject(outputMode === 'silent' ? logOutput : undefined); } }); }); } - -/** - * Spawns a given command with the specified arguments inside a shell syncronously. - * - * @returns The command's stdout and stderr. - */ -export function spawnSync( - command: string, args: string[], options: SpawnOptions = {}): SpawnResult { - const commandText = `${command} ${args.join(' ')}`; - debug(`Executing command: ${commandText}`); - - const {status: exitCode, signal, stdout, stderr} = - _spawnSync(command, args, {...options, encoding: 'utf8', shell: true, stdio: 'pipe'}); - - /** The status of the spawn result. */ - const status = statusFromExitCodeAndSignal(exitCode, signal); - - if (status === 0 || options.suppressErrorOnFailingExitCode) { - return {status, stdout, stderr}; - } - - throw new Error(stderr); -} - - - -/** Convert the provided exitCode and signal to a single status code. */ -function statusFromExitCodeAndSignal(exitCode: number|null, signal: NodeJS.Signals|null) { - return exitCode !== null ? exitCode : signal !== null ? signal : -1; -} diff --git a/dev-infra/utils/git/git-client.ts b/dev-infra/utils/git/git-client.ts index 3f9f6042b0..2faef84805 100644 --- a/dev-infra/utils/git/git-client.ts +++ b/dev-infra/utils/git/git-client.ts @@ -9,6 +9,7 @@ import {spawnSync, SpawnSyncOptions, SpawnSyncReturns} from 'child_process'; import {Options as SemVerOptions, parse, SemVer} from 'semver'; +import {spawnWithDebugOutput} from '../child-process'; import {getConfig, GithubConfig, NgDevConfig} from '../config'; import {debug, info} from '../console'; import {DryRunError, isDryRun} from '../dry-run'; @@ -23,7 +24,6 @@ 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(' '))}`); - Object.setPrototypeOf(this, GitCommandError.prototype); } }