Revert "refactor(dev-infra): add spawnSync to child process utils, normalize naming of child-process utils (#42394)" (#42829)

This reverts commit 08444c6679.

PR Close #42829
This commit is contained in:
Andrew Kushnir 2021-07-12 14:58:53 -07:00
parent 3d668162d9
commit c1c1cda866
8 changed files with 76 additions and 144 deletions

View File

@ -144,7 +144,6 @@ var GitCommandError = /** @class */ (function (_super) {
// we sanitize the command that will be part of the error message. // we sanitize the command that will be part of the error message.
_super.call(this, "Command failed: git " + client.sanitizeConsoleOutput(args.join(' '))) || this; _super.call(this, "Command failed: git " + client.sanitizeConsoleOutput(args.join(' '))) || this;
_this.args = args; _this.args = args;
Object.setPrototypeOf(_this, GitCommandError.prototype);
return _this; return _this;
} }
return GitCommandError; return GitCommandError;

View File

@ -316,7 +316,6 @@ var GitCommandError = /** @class */ (function (_super) {
// we sanitize the command that will be part of the error message. // we sanitize the command that will be part of the error message.
_super.call(this, "Command failed: git " + client.sanitizeConsoleOutput(args.join(' '))) || this; _super.call(this, "Command failed: git " + client.sanitizeConsoleOutput(args.join(' '))) || this;
_this.args = args; _this.args = args;
Object.setPrototypeOf(_this, GitCommandError.prototype);
return _this; return _this;
} }
return GitCommandError; return GitCommandError;
@ -3310,40 +3309,30 @@ function discoverNewConflictsForPr(newPrNumber, updatedAfter) {
info(`Retrieved ${allPendingPRs.length} total pending PRs`); info(`Retrieved ${allPendingPRs.length} total pending PRs`);
info(`Checking ${pendingPrs.length} PRs for conflicts after a merge of #${newPrNumber}`); info(`Checking ${pendingPrs.length} PRs for conflicts after a merge of #${newPrNumber}`);
// Fetch and checkout the PR being checked. // Fetch and checkout the PR being checked.
git.run(['fetch', '-q', requestedPr.headRef.repository.url, requestedPr.headRef.name]); exec(`git fetch ${requestedPr.headRef.repository.url} ${requestedPr.headRef.name}`);
git.run(['checkout', '-q', '-B', tempWorkingBranch, 'FETCH_HEAD']); exec(`git checkout -B ${tempWorkingBranch} FETCH_HEAD`);
// Rebase the PR against the PRs target branch. // Rebase the PR against the PRs target branch.
git.run(['fetch', '-q', requestedPr.baseRef.repository.url, requestedPr.baseRef.name]); exec(`git fetch ${requestedPr.baseRef.repository.url} ${requestedPr.baseRef.name}`);
try { const result = exec(`git rebase FETCH_HEAD`);
git.run(['rebase', 'FETCH_HEAD'], { stdio: 'ignore' }); if (result.code) {
} error('The requested PR currently has conflicts');
catch (err) { cleanUpGitState(previousBranchOrRevision);
if (err instanceof GitCommandError) { process.exit(1);
error('The requested PR currently has conflicts');
git.checkout(previousBranchOrRevision, true);
process.exit(1);
}
throw err;
} }
// Start the progress bar // Start the progress bar
progressBar.start(pendingPrs.length, 0); progressBar.start(pendingPrs.length, 0);
// Check each PR to determine if it can merge cleanly into the repo after the target PR. // Check each PR to determine if it can merge cleanly into the repo after the target PR.
for (const pr of pendingPrs) { for (const pr of pendingPrs) {
// Fetch and checkout the next PR // Fetch and checkout the next PR
git.run(['fetch', '-q', pr.headRef.repository.url, pr.headRef.name]); exec(`git fetch ${pr.headRef.repository.url} ${pr.headRef.name}`);
git.run(['checkout', '-q', '--detach', 'FETCH_HEAD']); exec(`git checkout --detach FETCH_HEAD`);
// Check if the PR cleanly rebases into the repo after the target PR. // Check if the PR cleanly rebases into the repo after the target PR.
try { const result = exec(`git rebase ${tempWorkingBranch}`);
git.run(['rebase', tempWorkingBranch], { stdio: 'ignore' }); if (result.code !== 0) {
} conflicts.push(pr);
catch (err) {
if (err instanceof GitCommandError) {
conflicts.push(pr);
}
throw err;
} }
// Abort any outstanding rebase attempt. // Abort any outstanding rebase attempt.
git.runGraceful(['rebase', '--abort'], { stdio: 'ignore' }); exec(`git rebase --abort`);
progressBar.increment(1); progressBar.increment(1);
} }
// End the progress bar as all PRs have been processed. // 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. * @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 = {}; } if (options === void 0) { options = {}; }
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
var commandText = command + " " + args.join(' '); 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. * 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 * @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 = {}; } if (options === void 0) { options = {}; }
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
var commandText = command + " " + args.join(' '); var commandText = command + " " + args.join(' ');
@ -5882,16 +5871,15 @@ function spawn(command, args, options) {
process.stderr.write(message); process.stderr.write(message);
} }
}); });
childProcess.on('exit', function (exitCode, signal) { childProcess.on('exit', function (status, signal) {
var exitDescription = exitCode !== null ? "exit code \"" + exitCode + "\"" : "signal \"" + signal + "\""; var exitDescription = status !== null ? "exit code \"" + status + "\"" : "signal \"" + signal + "\"";
var printFn = outputMode === 'on-error' ? error : debug; var printFn = outputMode === 'on-error' ? error : debug;
var status = statusFromExitCodeAndSignal(exitCode, signal);
printFn("Command \"" + commandText + "\" completed with " + exitDescription + "."); printFn("Command \"" + commandText + "\" completed with " + exitDescription + ".");
printFn("Process output: \n" + logOutput); printFn("Process output: \n" + logOutput);
// On success, resolve the promise. Otherwise reject with the captured stderr // On success, resolve the promise. Otherwise reject with the captured stderr
// and stdout log output if the output mode was set to `silent`. // and stdout log output if the output mode was set to `silent`.
if (status === 0 || options.suppressErrorOnFailingExitCode) { if (status === 0) {
resolve({ stdout: stdout, stderr: stderr, status: status }); resolve({ stdout: stdout, stderr: stderr });
} }
else { else {
reject(outputMode === 'silent' ? logOutput : undefined); 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 * @license
@ -5922,7 +5906,7 @@ function runNpmPublish(packagePath, distTag, registryUrl) {
if (registryUrl !== undefined) { if (registryUrl !== undefined) {
args.push('--registry', registryUrl); 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) { if (registryUrl !== undefined) {
args.push('--registry', registryUrl); 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); args.push('--registry', registryUrl);
} }
try { try {
yield spawn('npm', args, { mode: 'silent' }); yield spawnWithDebugOutput('npm', args, { mode: 'silent' });
} }
catch (e) { catch (e) {
return false; return false;
@ -5974,7 +5958,7 @@ function npmLogin(registryUrl) {
} }
// The login command prompts for username, password and other profile information. Hence // The login command prompts for username, password and other profile information. Hence
// the process needs to be interactive (i.e. respecting current TTYs stdin). // 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); args.splice(1, 0, '--registry', registryUrl);
} }
try { try {
yield spawn('npm', args, { mode: 'silent' }); yield spawnWithDebugOutput('npm', args, { mode: 'silent' });
} }
finally { finally {
return npmIsLoggedIn(registryUrl); return npmIsLoggedIn(registryUrl);
@ -6113,7 +6097,7 @@ function invokeSetNpmDistCommand(npmDistTag, version) {
return tslib.__awaiter(this, void 0, void 0, function* () { return tslib.__awaiter(this, void 0, void 0, function* () {
try { try {
// Note: No progress indicator needed as that is the responsibility of the command. // 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}.`)); info(green(` ✓ Set "${npmDistTag}" NPM dist tag for all packages to v${version}.`));
} }
catch (e) { catch (e) {
@ -6133,7 +6117,7 @@ function invokeReleaseBuildCommand() {
try { try {
// Since we expect JSON to be printed from the `ng-dev release build` command, // 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. // 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(); spinner.stop();
info(green(' ✓ Built release output for all packages.')); info(green(' ✓ Built release output for all packages.'));
// The `ng-dev release build` command prints a JSON array to stdout // The `ng-dev release build` command prints a JSON array to stdout
@ -6157,7 +6141,7 @@ function invokeYarnInstallCommand(projectDir) {
try { try {
// Note: No progress indicator needed as that is the responsibility of the command. // 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. // 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.')); info(green(' ✓ Installed project dependencies.'));
} }
catch (e) { catch (e) {
@ -7295,7 +7279,7 @@ class ReleaseTool {
try { try {
// Note: We do not rely on `/usr/bin/env` but rather access the `env` binary directly as it // 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. // 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(); const version = pyVersion.stdout.trim() || pyVersion.stderr.trim();
if (version.startsWith('Python 3.')) { if (version.startsWith('Python 3.')) {
debug(`Local python version: ${version}`); debug(`Local python version: ${version}`);

View File

@ -11,7 +11,7 @@ import {types as graphqlTypes} from 'typed-graphqlify';
import {error, info} from '../../utils/console'; import {error, info} from '../../utils/console';
import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client'; 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 {getPendingPrs} from '../../utils/github';
import {exec} from '../../utils/shelljs'; 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}`); info(`Checking ${pendingPrs.length} PRs for conflicts after a merge of #${newPrNumber}`);
// Fetch and checkout the PR being checked. // Fetch and checkout the PR being checked.
git.run(['fetch', '-q', requestedPr.headRef.repository.url, requestedPr.headRef.name]); exec(`git fetch ${requestedPr.headRef.repository.url} ${requestedPr.headRef.name}`);
git.run(['checkout', '-q', '-B', tempWorkingBranch, 'FETCH_HEAD']); exec(`git checkout -B ${tempWorkingBranch} FETCH_HEAD`);
// Rebase the PR against the PRs target branch. // Rebase the PR against the PRs target branch.
git.run(['fetch', '-q', requestedPr.baseRef.repository.url, requestedPr.baseRef.name]); exec(`git fetch ${requestedPr.baseRef.repository.url} ${requestedPr.baseRef.name}`);
try { const result = exec(`git rebase FETCH_HEAD`);
git.run(['rebase', 'FETCH_HEAD'], {stdio: 'ignore'}); if (result.code) {
} catch (err) { error('The requested PR currently has conflicts');
if (err instanceof GitCommandError) { cleanUpGitState(previousBranchOrRevision);
error('The requested PR currently has conflicts'); process.exit(1);
git.checkout(previousBranchOrRevision, true);
process.exit(1);
}
throw err;
} }
// Start the progress bar // 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. // Check each PR to determine if it can merge cleanly into the repo after the target PR.
for (const pr of pendingPrs) { for (const pr of pendingPrs) {
// Fetch and checkout the next PR // Fetch and checkout the next PR
git.run(['fetch', '-q', pr.headRef.repository.url, pr.headRef.name]); exec(`git fetch ${pr.headRef.repository.url} ${pr.headRef.name}`);
git.run(['checkout', '-q', '--detach', 'FETCH_HEAD']); exec(`git checkout --detach FETCH_HEAD`);
// Check if the PR cleanly rebases into the repo after the target PR. // Check if the PR cleanly rebases into the repo after the target PR.
try { const result = exec(`git rebase ${tempWorkingBranch}`);
git.run(['rebase', tempWorkingBranch], {stdio: 'ignore'}); if (result.code !== 0) {
} catch (err) { conflicts.push(pr);
if (err instanceof GitCommandError) {
conflicts.push(pr);
}
throw err;
} }
// Abort any outstanding rebase attempt. // Abort any outstanding rebase attempt.
git.runGraceful(['rebase', '--abort'], {stdio: 'ignore'}); exec(`git rebase --abort`);
progressBar.increment(1); progressBar.increment(1);
} }

View File

@ -9,7 +9,7 @@
import * as ora from 'ora'; import * as ora from 'ora';
import * as semver from 'semver'; 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 {error, green, info, red} from '../../utils/console';
import {BuiltPackage} from '../config/index'; import {BuiltPackage} from '../config/index';
import {NpmDistTag} from '../versioning'; import {NpmDistTag} from '../versioning';
@ -40,7 +40,7 @@ import {FatalReleaseActionError} from './actions-error';
export async function invokeSetNpmDistCommand(npmDistTag: NpmDistTag, version: semver.SemVer) { export async function invokeSetNpmDistCommand(npmDistTag: NpmDistTag, version: semver.SemVer) {
try { try {
// Note: No progress indicator needed as that is the responsibility of the command. // 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()]); 'yarn', ['--silent', 'ng-dev', 'release', 'set-dist-tag', npmDistTag, version.format()]);
info(green(` ✓ Set "${npmDistTag}" NPM dist tag for all packages to v${version}.`)); info(green(` ✓ Set "${npmDistTag}" NPM dist tag for all packages to v${version}.`));
} catch (e) { } catch (e) {
@ -59,8 +59,8 @@ export async function invokeReleaseBuildCommand(): Promise<BuiltPackage[]> {
try { try {
// Since we expect JSON to be printed from the `ng-dev release build` command, // 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. // we spawn the process in silent mode. We have set up an Ora progress spinner.
const {stdout} = const {stdout} = await spawnWithDebugOutput(
await spawn('yarn', ['--silent', 'ng-dev', 'release', 'build', '--json'], {mode: 'silent'}); 'yarn', ['--silent', 'ng-dev', 'release', 'build', '--json'], {mode: 'silent'});
spinner.stop(); spinner.stop();
info(green(' ✓ Built release output for all packages.')); info(green(' ✓ Built release output for all packages.'));
// The `ng-dev release build` command prints a JSON array to stdout // The `ng-dev release build` command prints a JSON array to stdout
@ -82,7 +82,8 @@ export async function invokeYarnInstallCommand(projectDir: string): Promise<void
try { try {
// Note: No progress indicator needed as that is the responsibility of the command. // 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. // TODO: Consider using an Ora spinner instead to ensure minimal console output.
await spawn('yarn', ['install', '--frozen-lockfile', '--non-interactive'], {cwd: projectDir}); await spawnWithDebugOutput(
'yarn', ['install', '--frozen-lockfile', '--non-interactive'], {cwd: projectDir});
info(green(' ✓ Installed project dependencies.')); info(green(' ✓ Installed project dependencies.'));
} catch (e) { } catch (e) {
error(e); error(e);

View File

@ -8,7 +8,7 @@
import {ListChoiceOptions, prompt} from 'inquirer'; import {ListChoiceOptions, prompt} from 'inquirer';
import {spawn} from '../../utils/child-process'; import {spawnWithDebugOutput} from '../../utils/child-process';
import {GithubConfig} from '../../utils/config'; import {GithubConfig} from '../../utils/config';
import {debug, error, info, log, promptConfirm, red, yellow} from '../../utils/console'; import {debug, error, info, log, promptConfirm, red, yellow} from '../../utils/console';
import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client'; import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client';
@ -138,7 +138,8 @@ export class ReleaseTool {
try { try {
// Note: We do not rely on `/usr/bin/env` but rather access the `env` binary directly as it // 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. // should be part of the shell's `$PATH`. This is necessary for compatibility with Windows.
const pyVersion = await spawn('env', ['python', '--version'], {mode: 'silent'}); const pyVersion =
await spawnWithDebugOutput('env', ['python', '--version'], {mode: 'silent'});
const version = pyVersion.stdout.trim() || pyVersion.stderr.trim(); const version = pyVersion.stdout.trim() || pyVersion.stderr.trim();
if (version.startsWith('Python 3.')) { if (version.startsWith('Python 3.')) {
debug(`Local python version: ${version}`); debug(`Local python version: ${version}`);

View File

@ -7,9 +7,7 @@
*/ */
import * as semver from 'semver'; import * as semver from 'semver';
import {spawnInteractiveCommand, spawnWithDebugOutput} from '../../utils/child-process';
import {spawn, spawnInteractive} from '../../utils/child-process';
import {NpmDistTag} from './npm-registry'; import {NpmDistTag} from './npm-registry';
/** /**
@ -23,7 +21,7 @@ export async function runNpmPublish(
if (registryUrl !== undefined) { if (registryUrl !== undefined) {
args.push('--registry', registryUrl); args.push('--registry', registryUrl);
} }
await spawn('npm', args, {cwd: packagePath, mode: 'silent'}); await spawnWithDebugOutput('npm', args, {cwd: packagePath, mode: 'silent'});
} }
/** /**
@ -37,7 +35,7 @@ export async function setNpmTagForPackage(
if (registryUrl !== undefined) { if (registryUrl !== undefined) {
args.push('--registry', registryUrl); args.push('--registry', registryUrl);
} }
await spawn('npm', args, {mode: 'silent'}); await spawnWithDebugOutput('npm', args, {mode: 'silent'});
} }
/** /**
@ -51,7 +49,7 @@ export async function npmIsLoggedIn(registryUrl: string|undefined): Promise<bool
args.push('--registry', registryUrl); args.push('--registry', registryUrl);
} }
try { try {
await spawn('npm', args, {mode: 'silent'}); await spawnWithDebugOutput('npm', args, {mode: 'silent'});
} catch (e) { } catch (e) {
return false; return false;
} }
@ -72,7 +70,7 @@ export async function npmLogin(registryUrl: string|undefined) {
} }
// The login command prompts for username, password and other profile information. Hence // The login command prompts for username, password and other profile information. Hence
// the process needs to be interactive (i.e. respecting current TTYs stdin). // the process needs to be interactive (i.e. respecting current TTYs stdin).
await spawnInteractive('npm', args); await spawnInteractiveCommand('npm', args);
} }
/** /**
@ -88,7 +86,7 @@ export async function npmLogout(registryUrl: string|undefined): Promise<boolean>
args.splice(1, 0, '--registry', registryUrl); args.splice(1, 0, '--registry', registryUrl);
} }
try { try {
await spawn('npm', args, {mode: 'silent'}); await spawnWithDebugOutput('npm', args, {mode: 'silent'});
} finally { } finally {
return npmIsLoggedIn(registryUrl); return npmIsLoggedIn(registryUrl);
} }

View File

@ -6,34 +6,21 @@
* found in the LICENSE file at https://angular.io/license * 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'; 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. */ /** Interface describing the options for spawning a process. */
export interface SpawnOptions extends Omit<_SpawnOptions, 'shell'|'stdio'> { export interface SpawnedProcessOptions extends Omit<SpawnOptions, 'stdio'> {
/** Console output mode. Defaults to "enabled". */ /** Console output mode. Defaults to "enabled". */
mode?: 'enabled'|'silent'|'on-error'; 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. */ /** Interface describing the result of a spawned process. */
export interface SpawnResult { export interface SpawnedProcessResult {
/** Captured stdout in string format. */ /** Captured stdout in string format. */
stdout: string; stdout: string;
/** Captured stderr in string format. */ /** Captured stderr in string format. */
stderr: string; 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. * @returns a Promise resolving on success, and rejecting on command failure with the status code.
*/ */
export function spawnInteractive( export function spawnInteractiveCommand(
command: string, args: string[], options: SpawnInteractiveCommandOptions = {}) { command: string, args: string[], options: Omit<SpawnOptions, 'stdio'> = {}) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const commandText = `${command} ${args.join(' ')}`; const commandText = `${command} ${args.join(' ')}`;
debug(`Executing command: ${commandText}`); 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)); 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. * 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 * @returns a Promise resolving with captured stdout and stderr on success. The promise
* rejects on command failure * rejects on command failure.
*/ */
export function spawn( export function spawnWithDebugOutput(
command: string, args: string[], options: SpawnOptions = {}): Promise<SpawnResult> { command: string, args: string[],
options: SpawnedProcessOptions = {}): Promise<SpawnedProcessResult> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const commandText = `${command} ${args.join(' ')}`; const commandText = `${command} ${args.join(' ')}`;
const outputMode = options.mode; const outputMode = options.mode;
debug(`Executing command: ${commandText}`); 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 logOutput = '';
let stdout = ''; let stdout = '';
let stderr = ''; let stderr = '';
@ -95,51 +83,20 @@ export function spawn(
} }
}); });
childProcess.on('exit', (exitCode, signal) => { childProcess.on('exit', (status, signal) => {
const exitDescription = exitCode !== null ? `exit code "${exitCode}"` : `signal "${signal}"`; const exitDescription = status !== null ? `exit code "${status}"` : `signal "${signal}"`;
const printFn = outputMode === 'on-error' ? error : debug; const printFn = outputMode === 'on-error' ? error : debug;
const status = statusFromExitCodeAndSignal(exitCode, signal);
printFn(`Command "${commandText}" completed with ${exitDescription}.`); printFn(`Command "${commandText}" completed with ${exitDescription}.`);
printFn(`Process output: \n${logOutput}`); printFn(`Process output: \n${logOutput}`);
// On success, resolve the promise. Otherwise reject with the captured stderr // On success, resolve the promise. Otherwise reject with the captured stderr
// and stdout log output if the output mode was set to `silent`. // and stdout log output if the output mode was set to `silent`.
if (status === 0 || options.suppressErrorOnFailingExitCode) { if (status === 0) {
resolve({stdout, stderr, status}); resolve({stdout, stderr});
} else { } else {
reject(outputMode === 'silent' ? logOutput : undefined); 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;
}

View File

@ -9,6 +9,7 @@
import {spawnSync, SpawnSyncOptions, SpawnSyncReturns} from 'child_process'; import {spawnSync, SpawnSyncOptions, SpawnSyncReturns} from 'child_process';
import {Options as SemVerOptions, parse, SemVer} from 'semver'; import {Options as SemVerOptions, parse, SemVer} from 'semver';
import {spawnWithDebugOutput} from '../child-process';
import {getConfig, GithubConfig, NgDevConfig} from '../config'; import {getConfig, GithubConfig, NgDevConfig} from '../config';
import {debug, info} from '../console'; import {debug, info} from '../console';
import {DryRunError, isDryRun} from '../dry-run'; 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, // accidentally leak the Github token that might be used in a command,
// we sanitize the command that will be part of the error message. // we sanitize the command that will be part of the error message.
super(`Command failed: git ${client.sanitizeConsoleOutput(args.join(' '))}`); super(`Command failed: git ${client.sanitizeConsoleOutput(args.join(' '))}`);
Object.setPrototypeOf(this, GitCommandError.prototype);
} }
} }