fix(dev-infra): check and validate npm login state for release publishing (#40485)
Check and require logging into NPM before beginning the release process. PR Close #40485
This commit is contained in:
parent
0712d52e83
commit
27a818a0e7
|
@ -5008,6 +5008,159 @@ const ReleaseBuildCommandModule = {
|
|||
describe: 'Builds the release output for the current branch.',
|
||||
};
|
||||
|
||||
/**
|
||||
* @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 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 on success. The promise
|
||||
* rejects on command failure.
|
||||
*/
|
||||
function spawnWithDebugOutput(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: ['inherit', 'pipe', 'pipe'] }));
|
||||
var logOutput = '';
|
||||
var stdout = '';
|
||||
// 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) {
|
||||
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 (status, signal) {
|
||||
var exitDescription = status !== null ? "exit code \"" + status + "\"" : "signal \"" + signal + "\"";
|
||||
var printFn = outputMode === 'on-error' ? error : debug;
|
||||
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) {
|
||||
resolve({ stdout: stdout });
|
||||
}
|
||||
else {
|
||||
reject(outputMode === 'silent' ? logOutput : undefined);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 NPM publish within a specified package directory.
|
||||
* @throws With the process log output if the publish failed.
|
||||
*/
|
||||
function runNpmPublish(packagePath, distTag, registryUrl) {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
const args = ['publish', '--access', 'public', '--tag', distTag];
|
||||
// If a custom registry URL has been specified, add the `--registry` flag.
|
||||
if (registryUrl !== undefined) {
|
||||
args.push('--registry', registryUrl);
|
||||
}
|
||||
yield spawnWithDebugOutput('npm', args, { cwd: packagePath, mode: 'silent' });
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Sets the NPM tag to the specified version for the given package.
|
||||
* @throws With the process log output if the tagging failed.
|
||||
*/
|
||||
function setNpmTagForPackage(packageName, distTag, version, registryUrl) {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
const args = ['dist-tag', 'add', `${packageName}@${version}`, distTag];
|
||||
// If a custom registry URL has been specified, add the `--registry` flag.
|
||||
if (registryUrl !== undefined) {
|
||||
args.push('--registry', registryUrl);
|
||||
}
|
||||
yield spawnWithDebugOutput('npm', args, { mode: 'silent' });
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Checks whether the user is currently logged into NPM.
|
||||
* @returns Whether the user is currently logged into NPM.
|
||||
*/
|
||||
function npmIsLoggedIn(registryUrl) {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
const args = ['whoami'];
|
||||
// If a custom registry URL has been specified, add the `--registry` flag.
|
||||
if (registryUrl !== undefined) {
|
||||
args.push('--registry', registryUrl);
|
||||
}
|
||||
try {
|
||||
yield spawnWithDebugOutput('npm', args, { mode: 'silent' });
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Log into NPM at a provided registry.
|
||||
* @throws With the process log output if the login fails.
|
||||
*/
|
||||
function npmLogin(registryUrl) {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
const args = ['login', '--no-browser'];
|
||||
// If a custom registry URL has been specified, add the `--registry` flag. The `--registry` flag
|
||||
// must be spliced into the correct place in the command as npm expects it to be the flag
|
||||
// immediately following the login subcommand.
|
||||
if (registryUrl !== undefined) {
|
||||
args.splice(1, 0, '--registry', registryUrl);
|
||||
}
|
||||
yield spawnWithDebugOutput('npm', args);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Log out of NPM at a provided registry.
|
||||
* @returns Whether the user was logged out of NPM.
|
||||
*/
|
||||
function npmLogout(registryUrl) {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
const args = ['logout'];
|
||||
// If a custom registry URL has been specified, add the `--registry` flag. The `--registry` flag
|
||||
// must be spliced into the correct place in the command as npm expects it to be the flag
|
||||
// immediately following the logout subcommand.
|
||||
if (registryUrl !== undefined) {
|
||||
args.splice(1, 0, '--registry', registryUrl);
|
||||
}
|
||||
try {
|
||||
yield spawnWithDebugOutput('npm', args, { mode: 'silent' });
|
||||
}
|
||||
finally {
|
||||
return npmIsLoggedIn(registryUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
|
@ -5115,102 +5268,6 @@ function semverInc(version, release, identifier) {
|
|||
return clone.inc(release, identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 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 on success. The promise
|
||||
* rejects on command failure.
|
||||
*/
|
||||
function spawnWithDebugOutput(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: ['inherit', 'pipe', 'pipe'] }));
|
||||
var logOutput = '';
|
||||
var stdout = '';
|
||||
// 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) {
|
||||
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 (status, signal) {
|
||||
var exitDescription = status !== null ? "exit code \"" + status + "\"" : "signal \"" + signal + "\"";
|
||||
var printFn = outputMode === 'on-error' ? error : debug;
|
||||
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) {
|
||||
resolve({ stdout: stdout });
|
||||
}
|
||||
else {
|
||||
reject(outputMode === 'silent' ? logOutput : undefined);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 NPM publish within a specified package directory.
|
||||
* @throws With the process log output if the publish failed.
|
||||
*/
|
||||
function runNpmPublish(packagePath, distTag, registryUrl) {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
const args = ['publish', '--access', 'public', '--tag', distTag];
|
||||
// If a custom registry URL has been specified, add the `--registry` flag.
|
||||
if (registryUrl !== undefined) {
|
||||
args.push('--registry', registryUrl);
|
||||
}
|
||||
yield spawnWithDebugOutput('npm', args, { cwd: packagePath, mode: 'silent' });
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Sets the NPM tag to the specified version for the given package.
|
||||
* @throws With the process log output if the tagging failed.
|
||||
*/
|
||||
function setNpmTagForPackage(packageName, distTag, version, registryUrl) {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
const args = ['dist-tag', 'add', `${packageName}@${version}`, distTag];
|
||||
// If a custom registry URL has been specified, add the `--registry` flag.
|
||||
if (registryUrl !== undefined) {
|
||||
args.push('--registry', registryUrl);
|
||||
}
|
||||
yield spawnWithDebugOutput('npm', args, { mode: 'silent' });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
|
@ -6383,6 +6440,8 @@ class ReleaseTool {
|
|||
this._projectRoot = _projectRoot;
|
||||
/** Client for interacting with the Github API and the local Git command. */
|
||||
this._git = new GitClient(this._githubToken, { github: this._github }, this._projectRoot);
|
||||
/** The previous git commit to return back to after the release tool runs. */
|
||||
this.previousGitBranchOrRevision = this._git.getCurrentBranchOrRevision();
|
||||
}
|
||||
/** Runs the interactive release tool. */
|
||||
run() {
|
||||
|
@ -6395,6 +6454,9 @@ class ReleaseTool {
|
|||
if (!(yield this._verifyNoUncommittedChanges()) || !(yield this._verifyRunningFromNextBranch())) {
|
||||
return CompletionState.FATAL_ERROR;
|
||||
}
|
||||
if (!(yield this._verifyNpmLoginState())) {
|
||||
return CompletionState.MANUALLY_ABORTED;
|
||||
}
|
||||
const { owner, name } = this._github;
|
||||
const repo = { owner, name, api: this._git.github };
|
||||
const releaseTrains = yield fetchActiveReleaseTrains(repo);
|
||||
|
@ -6402,7 +6464,6 @@ class ReleaseTool {
|
|||
// the current project branching state without switching context.
|
||||
yield printActiveReleaseTrains(releaseTrains, this._config);
|
||||
const action = yield this._promptForReleaseAction(releaseTrains);
|
||||
const previousGitBranchOrRevision = this._git.getCurrentBranchOrRevision();
|
||||
try {
|
||||
yield action.perform();
|
||||
}
|
||||
|
@ -6418,11 +6479,20 @@ class ReleaseTool {
|
|||
return CompletionState.FATAL_ERROR;
|
||||
}
|
||||
finally {
|
||||
this._git.checkout(previousGitBranchOrRevision, true);
|
||||
yield this.cleanup();
|
||||
}
|
||||
return CompletionState.SUCCESS;
|
||||
});
|
||||
}
|
||||
/** Run post release tool cleanups. */
|
||||
cleanup() {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
// Return back to the git state from before the release tool ran.
|
||||
this._git.checkout(this.previousGitBranchOrRevision, true);
|
||||
// Ensure log out of NPM.
|
||||
yield npmLogout(this._config.publishRegistry);
|
||||
});
|
||||
}
|
||||
/** Prompts the caretaker for a release action that should be performed. */
|
||||
_promptForReleaseAction(activeTrains) {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
|
@ -6473,6 +6543,33 @@ class ReleaseTool {
|
|||
return true;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Verifies that the user is logged into NPM at the correct registry, if defined for the release.
|
||||
* @returns a boolean indicating whether the user is logged into NPM.
|
||||
*/
|
||||
_verifyNpmLoginState() {
|
||||
var _a;
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
const registry = `NPM at the ${(_a = this._config.publishRegistry) !== null && _a !== void 0 ? _a : 'default NPM'} registry`;
|
||||
if (yield npmIsLoggedIn(this._config.publishRegistry)) {
|
||||
debug(`Already logged into ${registry}.`);
|
||||
return true;
|
||||
}
|
||||
error(red(` ✘ Not currently logged into ${registry}.`));
|
||||
const shouldLogin = yield promptConfirm('Would you like to log into NPM now?');
|
||||
if (shouldLogin) {
|
||||
debug('Starting NPM login.');
|
||||
try {
|
||||
yield npmLogin(this._config.publishRegistry);
|
||||
}
|
||||
catch (_b) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -9,10 +9,11 @@
|
|||
import {ListChoiceOptions, prompt} from 'inquirer';
|
||||
|
||||
import {GithubConfig} from '../../utils/config';
|
||||
import {error, info, log, red, yellow} from '../../utils/console';
|
||||
import {debug, error, info, log, promptConfirm, red, yellow} from '../../utils/console';
|
||||
import {GitClient} from '../../utils/git/index';
|
||||
import {ReleaseConfig} from '../config';
|
||||
import {ActiveReleaseTrains, fetchActiveReleaseTrains, nextBranchName} from '../versioning/active-release-trains';
|
||||
import {npmIsLoggedIn, npmLogin, npmLogout} from '../versioning/npm-publish';
|
||||
import {printActiveReleaseTrains} from '../versioning/print-active-trains';
|
||||
import {GithubRepoWithApi} from '../versioning/version-branches';
|
||||
|
||||
|
@ -29,6 +30,8 @@ export enum CompletionState {
|
|||
export class ReleaseTool {
|
||||
/** Client for interacting with the Github API and the local Git command. */
|
||||
private _git = new GitClient(this._githubToken, {github: this._github}, this._projectRoot);
|
||||
/** The previous git commit to return back to after the release tool runs. */
|
||||
private previousGitBranchOrRevision = this._git.getCurrentBranchOrRevision();
|
||||
|
||||
constructor(
|
||||
protected _config: ReleaseConfig, protected _github: GithubConfig,
|
||||
|
@ -46,6 +49,10 @@ export class ReleaseTool {
|
|||
return CompletionState.FATAL_ERROR;
|
||||
}
|
||||
|
||||
if (!await this._verifyNpmLoginState()) {
|
||||
return CompletionState.MANUALLY_ABORTED;
|
||||
}
|
||||
|
||||
const {owner, name} = this._github;
|
||||
const repo: GithubRepoWithApi = {owner, name, api: this._git.github};
|
||||
const releaseTrains = await fetchActiveReleaseTrains(repo);
|
||||
|
@ -55,7 +62,6 @@ export class ReleaseTool {
|
|||
await printActiveReleaseTrains(releaseTrains, this._config);
|
||||
|
||||
const action = await this._promptForReleaseAction(releaseTrains);
|
||||
const previousGitBranchOrRevision = this._git.getCurrentBranchOrRevision();
|
||||
|
||||
try {
|
||||
await action.perform();
|
||||
|
@ -70,12 +76,20 @@ export class ReleaseTool {
|
|||
}
|
||||
return CompletionState.FATAL_ERROR;
|
||||
} finally {
|
||||
this._git.checkout(previousGitBranchOrRevision, true);
|
||||
await this.cleanup();
|
||||
}
|
||||
|
||||
return CompletionState.SUCCESS;
|
||||
}
|
||||
|
||||
/** Run post release tool cleanups. */
|
||||
private async cleanup(): Promise<void> {
|
||||
// Return back to the git state from before the release tool ran.
|
||||
this._git.checkout(this.previousGitBranchOrRevision, true);
|
||||
// Ensure log out of NPM.
|
||||
await npmLogout(this._config.publishRegistry);
|
||||
}
|
||||
|
||||
/** Prompts the caretaker for a release action that should be performed. */
|
||||
private async _promptForReleaseAction(activeTrains: ActiveReleaseTrains) {
|
||||
const choices: ListChoiceOptions[] = [];
|
||||
|
@ -129,4 +143,28 @@ export class ReleaseTool {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the user is logged into NPM at the correct registry, if defined for the release.
|
||||
* @returns a boolean indicating whether the user is logged into NPM.
|
||||
*/
|
||||
private async _verifyNpmLoginState(): Promise<boolean> {
|
||||
const registry = `NPM at the ${this._config.publishRegistry ?? 'default NPM'} registry`;
|
||||
if (await npmIsLoggedIn(this._config.publishRegistry)) {
|
||||
debug(`Already logged into ${registry}.`);
|
||||
return true;
|
||||
}
|
||||
error(red(` ✘ Not currently logged into ${registry}.`));
|
||||
const shouldLogin = await promptConfirm('Would you like to log into NPM now?');
|
||||
if (shouldLogin) {
|
||||
debug('Starting NPM login.');
|
||||
try {
|
||||
await npmLogin(this._config.publishRegistry);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,3 +36,55 @@ export async function setNpmTagForPackage(
|
|||
}
|
||||
await spawnWithDebugOutput('npm', args, {mode: 'silent'});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the user is currently logged into NPM.
|
||||
* @returns Whether the user is currently logged into NPM.
|
||||
*/
|
||||
export async function npmIsLoggedIn(registryUrl: string|undefined): Promise<boolean> {
|
||||
const args = ['whoami'];
|
||||
// If a custom registry URL has been specified, add the `--registry` flag.
|
||||
if (registryUrl !== undefined) {
|
||||
args.push('--registry', registryUrl);
|
||||
}
|
||||
try {
|
||||
await spawnWithDebugOutput('npm', args, {mode: 'silent'});
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log into NPM at a provided registry.
|
||||
* @throws With the process log output if the login fails.
|
||||
*/
|
||||
export async function npmLogin(registryUrl: string|undefined) {
|
||||
const args = ['login', '--no-browser'];
|
||||
// If a custom registry URL has been specified, add the `--registry` flag. The `--registry` flag
|
||||
// must be spliced into the correct place in the command as npm expects it to be the flag
|
||||
// immediately following the login subcommand.
|
||||
if (registryUrl !== undefined) {
|
||||
args.splice(1, 0, '--registry', registryUrl);
|
||||
}
|
||||
await spawnWithDebugOutput('npm', args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log out of NPM at a provided registry.
|
||||
* @returns Whether the user was logged out of NPM.
|
||||
*/
|
||||
export async function npmLogout(registryUrl: string|undefined): Promise<boolean> {
|
||||
const args = ['logout'];
|
||||
// If a custom registry URL has been specified, add the `--registry` flag. The `--registry` flag
|
||||
// must be spliced into the correct place in the command as npm expects it to be the flag
|
||||
// immediately following the logout subcommand.
|
||||
if (registryUrl !== undefined) {
|
||||
args.splice(1, 0, '--registry', registryUrl);
|
||||
}
|
||||
try {
|
||||
await spawnWithDebugOutput('npm', args, {mode: 'silent'});
|
||||
} finally {
|
||||
return npmIsLoggedIn(registryUrl);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue