feat(dev-infra): add command for building release output (#38656)
Adds a command for building all release packages. This command is primarily used by the release tool for building release output in version branches. The release tool cannot build the release packages configured in `master` as those packages could differ from the packages available in a given version branch. Also, the build process could have changed, so we want to have an API for building release packages that is guaranteed to be consistent across branches. PR Close #38656
This commit is contained in:
parent
964ac1542a
commit
b9dce19b3d
|
@ -8,6 +8,7 @@ ts_library(
|
|||
module_name = "@angular/dev-infra-private/release",
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/release/build",
|
||||
"//dev-infra/utils",
|
||||
"@npm//@types/yargs",
|
||||
"@npm//yargs",
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||
load("//tools:defaults.bzl", "jasmine_node_test")
|
||||
|
||||
ts_library(
|
||||
name = "build",
|
||||
srcs = glob(
|
||||
[
|
||||
"**/*.ts",
|
||||
],
|
||||
exclude = ["*.spec.ts"],
|
||||
),
|
||||
module_name = "@angular/dev-infra-private/release/build",
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/release/config",
|
||||
"//dev-infra/utils",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/yargs",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
srcs = glob([
|
||||
"*.spec.ts",
|
||||
]),
|
||||
deps = [
|
||||
":build",
|
||||
"//dev-infra/release/config",
|
||||
"@npm//@types/jasmine",
|
||||
"@npm//@types/node",
|
||||
],
|
||||
)
|
||||
|
||||
jasmine_node_test(
|
||||
name = "test",
|
||||
deps = [":test_lib"],
|
||||
)
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file will be spawned as a separate process when the `ng-dev release build` command is
|
||||
* invoked. A separate process allows us to hide any superfluous stdout output from arbitrary
|
||||
* build commands that we cannot control. This is necessary as the `ng-dev release build` command
|
||||
* supports stdout JSON output that should be parsable and not polluted from other stdout messages.
|
||||
*/
|
||||
|
||||
import {getReleaseConfig} from '../config/index';
|
||||
|
||||
// Start the release package building.
|
||||
main();
|
||||
|
||||
/** Main function for building the release packages. */
|
||||
async function main() {
|
||||
if (process.send === undefined) {
|
||||
throw Error('This script needs to be invoked as a NodeJS worker.');
|
||||
}
|
||||
|
||||
const config = getReleaseConfig();
|
||||
const builtPackages = await config.buildPackages();
|
||||
|
||||
// Transfer the built packages back to the parent process.
|
||||
process.send(builtPackages);
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* @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 releaseConfig from '../config/index';
|
||||
import {ReleaseBuildCommandModule} from './cli';
|
||||
import * as index from './index';
|
||||
|
||||
describe('ng-dev release build', () => {
|
||||
let npmPackages: string[];
|
||||
let buildPackages: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
npmPackages = ['@angular/pkg1', '@angular/pkg2'];
|
||||
buildPackages = jasmine.createSpy('buildPackages').and.resolveTo([
|
||||
{name: '@angular/pkg1', outputPath: 'dist/pkg1'},
|
||||
{name: '@angular/pkg2', outputPath: 'dist/pkg2'},
|
||||
]);
|
||||
|
||||
// We cannot test the worker process, so we fake the worker function and
|
||||
// directly call the package build function.
|
||||
spyOn(index, 'buildReleaseOutput').and.callFake(() => buildPackages());
|
||||
// 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 build command handler. */
|
||||
async function invokeBuild({json}: {json?: boolean} = {}) {
|
||||
spyOn(releaseConfig, 'getReleaseConfig')
|
||||
.and.returnValue({npmPackages, buildPackages, generateReleaseNotesForHead: async () => {}});
|
||||
await ReleaseBuildCommandModule.handler({json: !!json, $0: '', _: []});
|
||||
}
|
||||
|
||||
it('should invoke configured build packages function', async () => {
|
||||
await invokeBuild();
|
||||
expect(buildPackages).toHaveBeenCalledTimes(1);
|
||||
expect(process.exit).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should print built packages as JSON if `--json` is specified', async () => {
|
||||
const writeSpy = spyOn(process.stdout, 'write');
|
||||
await invokeBuild({json: true});
|
||||
|
||||
expect(buildPackages).toHaveBeenCalledTimes(1);
|
||||
expect(writeSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
const jsonText = writeSpy.calls.mostRecent().args[0] as string;
|
||||
const parsed = JSON.parse(jsonText);
|
||||
|
||||
expect(parsed).toEqual([
|
||||
{name: '@angular/pkg1', outputPath: 'dist/pkg1'},
|
||||
{name: '@angular/pkg2', outputPath: 'dist/pkg2'}
|
||||
]);
|
||||
expect(process.exit).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should error if package has not been built', async () => {
|
||||
// Set up a NPM package that is not built.
|
||||
npmPackages.push('@angular/non-existent');
|
||||
|
||||
spyOn(console, 'error');
|
||||
await invokeBuild();
|
||||
|
||||
expect(console.error).toHaveBeenCalledTimes(2);
|
||||
expect(console.error)
|
||||
.toHaveBeenCalledWith(
|
||||
jasmine.stringMatching(`Release output missing for the following packages`));
|
||||
expect(console.error).toHaveBeenCalledWith(jasmine.stringMatching(`- @angular/non-existent`));
|
||||
expect(process.exit).toHaveBeenCalledTimes(1);
|
||||
expect(process.exit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* @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 {Arguments, Argv, CommandModule} from 'yargs';
|
||||
|
||||
import {getConfig} from '../../utils/config';
|
||||
import {error, green, info, red, warn, yellow} from '../../utils/console';
|
||||
import {BuiltPackage, getReleaseConfig} from '../config/index';
|
||||
|
||||
import {buildReleaseOutput} from './index';
|
||||
|
||||
/** Command line options for building a release. */
|
||||
export interface ReleaseBuildOptions {
|
||||
json: boolean;
|
||||
}
|
||||
|
||||
/** Yargs command builder for configuring the `ng-dev release build` command. */
|
||||
function builder(argv: Argv): Argv<ReleaseBuildOptions> {
|
||||
return argv.option('json', {
|
||||
type: 'boolean',
|
||||
description: 'Whether the built packages should be printed to stdout as JSON.',
|
||||
default: false,
|
||||
});
|
||||
}
|
||||
|
||||
/** Yargs command handler for building a release. */
|
||||
async function handler(args: Arguments<ReleaseBuildOptions>) {
|
||||
const {npmPackages} = getReleaseConfig();
|
||||
let builtPackages = await buildReleaseOutput();
|
||||
|
||||
// If package building failed, print an error and exit with an error code.
|
||||
if (builtPackages === null) {
|
||||
error(red(` ✘ Could not build release output. Please check output above.`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// If no packages have been built, we assume that this is never correct
|
||||
// and exit with an error code.
|
||||
if (builtPackages.length === 0) {
|
||||
error(red(` ✘ No release packages have been built. Please ensure that the`));
|
||||
error(red(` build script is configured correctly in ".ng-dev".`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const missingPackages =
|
||||
npmPackages.filter(pkgName => !builtPackages!.find(b => b.name === pkgName));
|
||||
|
||||
// Check for configured release packages which have not been built. We want to
|
||||
// error and exit if any configured package has not been built.
|
||||
if (missingPackages.length > 0) {
|
||||
error(red(` ✘ Release output missing for the following packages:`));
|
||||
missingPackages.forEach(pkgName => error(red(` - ${pkgName}`)));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (args.json) {
|
||||
process.stdout.write(JSON.stringify(builtPackages, null, 2));
|
||||
} else {
|
||||
info(green(' ✓ Built release packages.'));
|
||||
builtPackages.forEach(({name}) => info(green(` - ${name}`)));
|
||||
}
|
||||
}
|
||||
|
||||
/** CLI command module for building release output. */
|
||||
export const ReleaseBuildCommandModule: CommandModule<{}, ReleaseBuildOptions> = {
|
||||
builder,
|
||||
handler,
|
||||
command: 'build',
|
||||
describe: 'Builds the release output for the current branch.',
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* @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 {fork} from 'child_process';
|
||||
import {BuiltPackage} from '../config/index';
|
||||
|
||||
/**
|
||||
* Builds the release output without polluting the process stdout. Build scripts commonly
|
||||
* print messages to stderr or stdout. This is fine in most cases, but sometimes other tooling
|
||||
* reserves stdout for data transfer (e.g. when `ng release build --json` is invoked). To not
|
||||
* pollute the stdout in such cases, we launch a child process for building the release packages
|
||||
* and redirect all stdout output to the stderr channel (which can be read in the terminal).
|
||||
*/
|
||||
export async function buildReleaseOutput(): Promise<BuiltPackage[]|null> {
|
||||
return new Promise(resolve => {
|
||||
const buildProcess = fork(require.resolve('./build-worker'), [], {
|
||||
// The stdio option is set to redirect any "stdout" output directly to the "stderr" file
|
||||
// descriptor. An additional "ipc" file descriptor is created to support communication with
|
||||
// the build process. https://nodejs.org/api/child_process.html#child_process_options_stdio.
|
||||
stdio: ['inherit', 2, 2, 'ipc'],
|
||||
});
|
||||
let builtPackages: BuiltPackage[]|null = null;
|
||||
|
||||
// The child process will pass the `buildPackages()` output through the
|
||||
// IPC channel. We keep track of it so that we can use it as resolve value.
|
||||
buildProcess.on('message', buildResponse => builtPackages = buildResponse);
|
||||
|
||||
// On child process exit, resolve the promise with the received output.
|
||||
buildProcess.on('exit', () => resolve(builtPackages));
|
||||
});
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
import * as yargs from 'yargs';
|
||||
|
||||
import {ReleaseBuildCommandModule} from './build/cli';
|
||||
import {buildEnvStamp} from './stamping/env-stamp';
|
||||
|
||||
/** Build the parser for the release commands. */
|
||||
|
@ -14,6 +15,7 @@ export function buildReleaseParser(localYargs: yargs.Argv) {
|
|||
return localYargs.help()
|
||||
.strict()
|
||||
.demandCommand()
|
||||
.command(ReleaseBuildCommandModule)
|
||||
.command(
|
||||
'build-env-stamp', 'Build the environment stamping information', {},
|
||||
() => buildEnvStamp());
|
||||
|
|
Loading…
Reference in New Issue