feat(dev-infra): add release command for setting NPM dist tag (#38656)
Introduces a new command for `ng-dev release`, so that the NPM dist tag can be set for all configured NPM packages. This command can be useful in case a manual tag needs to be set, but it is primarily used by the release tooling when a new stable version is cut, and when the previous patch branch needs to be set as LTS version through a `v{major}-lts` dist tag. It is necessary to have this as a command so that the release tool can execute it for old branches where other packages might have been configured. This is similar to the separate `ng-dev build` command that we created. Note that we also added logic for spawning a process conveniently with different "console output" modes. This will be useful for other command invocations in the release tool and it's generally better than directly using native `child_process` as that one doesn't log to the dev-infra debug log file. PR Close #38656
This commit is contained in:
parent
b9dce19b3d
commit
372a6cf8d7
|
@ -9,6 +9,7 @@ ts_library(
|
|||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/release/build",
|
||||
"//dev-infra/release/set-dist-tag",
|
||||
"//dev-infra/utils",
|
||||
"@npm//@types/yargs",
|
||||
"@npm//yargs",
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import * as yargs from 'yargs';
|
||||
|
||||
import {ReleaseBuildCommandModule} from './build/cli';
|
||||
import {ReleaseSetDistTagCommand} from './set-dist-tag/cli';
|
||||
import {buildEnvStamp} from './stamping/env-stamp';
|
||||
|
||||
/** Build the parser for the release commands. */
|
||||
|
@ -16,6 +17,7 @@ export function buildReleaseParser(localYargs: yargs.Argv) {
|
|||
.strict()
|
||||
.demandCommand()
|
||||
.command(ReleaseBuildCommandModule)
|
||||
.command(ReleaseSetDistTagCommand)
|
||||
.command(
|
||||
'build-env-stamp', 'Build the environment stamping information', {},
|
||||
() => buildEnvStamp());
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||
load("//tools:defaults.bzl", "jasmine_node_test")
|
||||
|
||||
ts_library(
|
||||
name = "set-dist-tag",
|
||||
srcs = glob(
|
||||
[
|
||||
"**/*.ts",
|
||||
],
|
||||
exclude = ["*.spec.ts"],
|
||||
),
|
||||
module_name = "@angular/dev-infra-private/release/set-dist-tag",
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/release/config",
|
||||
"//dev-infra/release/versioning",
|
||||
"//dev-infra/utils",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/semver",
|
||||
"@npm//@types/yargs",
|
||||
"@npm//ora",
|
||||
"@npm//semver",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
srcs = glob([
|
||||
"*.spec.ts",
|
||||
]),
|
||||
deps = [
|
||||
":set-dist-tag",
|
||||
"//dev-infra/release/config",
|
||||
"//dev-infra/release/versioning",
|
||||
"//dev-infra/utils/testing",
|
||||
"@npm//@types/jasmine",
|
||||
"@npm//@types/node",
|
||||
],
|
||||
)
|
||||
|
||||
jasmine_node_test(
|
||||
name = "test",
|
||||
deps = [":test_lib"],
|
||||
)
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* @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 * as Ora from 'ora';
|
||||
import * as semver from 'semver';
|
||||
import {Arguments, Argv, CommandModule} from 'yargs';
|
||||
|
||||
import {bold, debug, error, green, info, red} from '../../utils/console';
|
||||
import {getReleaseConfig} from '../config/index';
|
||||
import {setNpmTagForPackage} from '../versioning/npm-publish';
|
||||
|
||||
|
||||
/** Command line options for setting a NPM dist tag. */
|
||||
export interface ReleaseSetDistTagOptions {
|
||||
tagName: string;
|
||||
targetVersion: string;
|
||||
}
|
||||
|
||||
function builder(args: Argv): Argv<ReleaseSetDistTagOptions> {
|
||||
return args
|
||||
.positional('tagName', {
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
description: 'Name of the NPM dist tag.',
|
||||
})
|
||||
.positional('targetVersion', {
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
description: 'Version to which the dist tag should be set.'
|
||||
});
|
||||
}
|
||||
|
||||
/** Yargs command handler for building a release. */
|
||||
async function handler(args: Arguments<ReleaseSetDistTagOptions>) {
|
||||
const {targetVersion: rawVersion, tagName} = args;
|
||||
const {npmPackages, publishRegistry} = getReleaseConfig();
|
||||
const version = semver.parse(rawVersion);
|
||||
|
||||
if (version === null) {
|
||||
error(red(`Invalid version specified. Unable to set NPM dist tag.`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const spinner = Ora().start();
|
||||
debug(`Setting "${tagName}" NPM dist tag for release packages to v${version}.`);
|
||||
|
||||
for (const pkgName of npmPackages) {
|
||||
spinner.text = `Setting NPM dist tag for "${pkgName}"`;
|
||||
spinner.render();
|
||||
|
||||
try {
|
||||
await setNpmTagForPackage(pkgName, tagName, version!, publishRegistry);
|
||||
debug(`Successfully set "${tagName}" NPM dist tag for "${pkgName}".`);
|
||||
} catch (e) {
|
||||
spinner.stop();
|
||||
error(e);
|
||||
error(red(` ✘ An error occurred while setting the NPM dist tag for "${pkgName}".`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
spinner.stop();
|
||||
info(green(` ✓ Set NPM dist tag for all release packages.`));
|
||||
info(green(` ${bold(tagName)} will now point to ${bold(`v${version}`)}.`));
|
||||
}
|
||||
|
||||
/** CLI command module for setting a NPM dist tag. */
|
||||
export const ReleaseSetDistTagCommand: CommandModule<{}, ReleaseSetDistTagOptions> = {
|
||||
builder,
|
||||
handler,
|
||||
command: 'set-dist-tag <tag-name> <target-version>',
|
||||
describe: 'Sets a given NPM dist tag for all release packages.',
|
||||
};
|
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* @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 {matchesVersion} from '../../utils/testing/semver-matchers';
|
||||
import * as releaseConfig from '../config/index';
|
||||
import * as npm from '../versioning/npm-publish';
|
||||
|
||||
import {ReleaseSetDistTagCommand} from './cli';
|
||||
|
||||
describe('ng-dev release set-dist-tag', () => {
|
||||
let npmPackages: string[];
|
||||
let publishRegistry: string|undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
npmPackages = ['@angular/pkg1', '@angular/pkg2'];
|
||||
publishRegistry = undefined;
|
||||
|
||||
spyOn(npm, 'setNpmTagForPackage');
|
||||
// We need to stub out the `process.exit` function during tests as the
|
||||
// CLI handler calls those in case of failures.
|
||||
spyOn(process, 'exit');
|
||||
});
|
||||
|
||||
/** Invokes the `set-dist-tag` command handler. */
|
||||
async function invokeCommand(tagName: string, targetVersion: string) {
|
||||
spyOn(releaseConfig, 'getReleaseConfig').and.returnValue({
|
||||
npmPackages,
|
||||
publishRegistry,
|
||||
buildPackages: async () => [],
|
||||
generateReleaseNotesForHead: async () => {}
|
||||
});
|
||||
await ReleaseSetDistTagCommand.handler({tagName, targetVersion, $0: '', _: []});
|
||||
}
|
||||
|
||||
it('should invoke "npm dist-tag" command for all configured packages', async () => {
|
||||
await invokeCommand('latest', '10.0.0');
|
||||
expect(npm.setNpmTagForPackage).toHaveBeenCalledTimes(2);
|
||||
expect(npm.setNpmTagForPackage)
|
||||
.toHaveBeenCalledWith('@angular/pkg1', 'latest', matchesVersion('10.0.0'), undefined);
|
||||
expect(npm.setNpmTagForPackage)
|
||||
.toHaveBeenCalledWith('@angular/pkg2', 'latest', matchesVersion('10.0.0'), undefined);
|
||||
});
|
||||
|
||||
it('should support a configured custom NPM registry', async () => {
|
||||
publishRegistry = 'https://my-custom-registry.angular.io';
|
||||
await invokeCommand('latest', '10.0.0');
|
||||
|
||||
expect(npm.setNpmTagForPackage).toHaveBeenCalledTimes(2);
|
||||
expect(npm.setNpmTagForPackage)
|
||||
.toHaveBeenCalledWith(
|
||||
'@angular/pkg1', 'latest', matchesVersion('10.0.0'),
|
||||
'https://my-custom-registry.angular.io');
|
||||
expect(npm.setNpmTagForPackage)
|
||||
.toHaveBeenCalledWith(
|
||||
'@angular/pkg2', 'latest', matchesVersion('10.0.0'),
|
||||
'https://my-custom-registry.angular.io');
|
||||
});
|
||||
|
||||
it('should error if an invalid version has been specified', async () => {
|
||||
spyOn(console, 'error');
|
||||
await invokeCommand('latest', '10.0');
|
||||
|
||||
expect(console.error)
|
||||
.toHaveBeenCalledWith('Invalid version specified. Unable to set NPM dist tag.');
|
||||
expect(process.exit).toHaveBeenCalledWith(1);
|
||||
expect(process.exit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* @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 * as semver from 'semver';
|
||||
import {spawnWithDebugOutput} from '../../utils/child-process';
|
||||
|
||||
/**
|
||||
* Sets the NPM tag to the specified version for the given package.
|
||||
* @throws With the process log output if the tagging failed.
|
||||
*/
|
||||
export async function setNpmTagForPackage(
|
||||
packageName: string, distTag: string, version: semver.SemVer, registryUrl: string|undefined) {
|
||||
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);
|
||||
}
|
||||
await spawnWithDebugOutput('npm', args, {mode: 'silent'});
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* @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 {spawn, SpawnOptions} from 'child_process';
|
||||
import {debug, error} from './console';
|
||||
|
||||
/** Interface describing the options for spawning a process. */
|
||||
export interface SpawnedProcessOptions extends Omit<SpawnOptions, 'stdio'> {
|
||||
/** Console output mode. Defaults to "enabled". */
|
||||
mode?: 'enabled'|'silent'|'on-error';
|
||||
}
|
||||
|
||||
/** Interface describing the result of a spawned process. */
|
||||
export interface SpawnedProcessResult {
|
||||
/** Captured stdout in string format. */
|
||||
stdout: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export function spawnWithDebugOutput(
|
||||
command: string, args: string[],
|
||||
options: SpawnedProcessOptions = {}): Promise<SpawnedProcessResult> {
|
||||
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: ['inherit', 'pipe', 'pipe']});
|
||||
let logOutput = '';
|
||||
let 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', 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 stderr should not be polluted.
|
||||
if (outputMode === undefined || outputMode === 'enabled') {
|
||||
process.stderr.write(message);
|
||||
}
|
||||
});
|
||||
childProcess.stdout.on('data', 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 stderr should not be polluted.
|
||||
if (outputMode === undefined || outputMode === 'enabled') {
|
||||
process.stderr.write(message);
|
||||
}
|
||||
});
|
||||
|
||||
childProcess.on('exit', (status, signal) => {
|
||||
const exitDescription = status !== null ? `exit code "${status}"` : `signal "${signal}"`;
|
||||
const 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});
|
||||
} else {
|
||||
reject(outputMode === 'silent' ? logOutput : undefined);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue