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__"],
|
visibility = ["//dev-infra:__subpackages__"],
|
||||||
deps = [
|
deps = [
|
||||||
"//dev-infra/release/build",
|
"//dev-infra/release/build",
|
||||||
|
"//dev-infra/release/set-dist-tag",
|
||||||
"//dev-infra/utils",
|
"//dev-infra/utils",
|
||||||
"@npm//@types/yargs",
|
"@npm//@types/yargs",
|
||||||
"@npm//yargs",
|
"@npm//yargs",
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import * as yargs from 'yargs';
|
import * as yargs from 'yargs';
|
||||||
|
|
||||||
import {ReleaseBuildCommandModule} from './build/cli';
|
import {ReleaseBuildCommandModule} from './build/cli';
|
||||||
|
import {ReleaseSetDistTagCommand} from './set-dist-tag/cli';
|
||||||
import {buildEnvStamp} from './stamping/env-stamp';
|
import {buildEnvStamp} from './stamping/env-stamp';
|
||||||
|
|
||||||
/** Build the parser for the release commands. */
|
/** Build the parser for the release commands. */
|
||||||
|
@ -16,6 +17,7 @@ export function buildReleaseParser(localYargs: yargs.Argv) {
|
||||||
.strict()
|
.strict()
|
||||||
.demandCommand()
|
.demandCommand()
|
||||||
.command(ReleaseBuildCommandModule)
|
.command(ReleaseBuildCommandModule)
|
||||||
|
.command(ReleaseSetDistTagCommand)
|
||||||
.command(
|
.command(
|
||||||
'build-env-stamp', 'Build the environment stamping information', {},
|
'build-env-stamp', 'Build the environment stamping information', {},
|
||||||
() => buildEnvStamp());
|
() => 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