Skip to content

Commit b9dce19

Browse files
devversionalxhub
authored andcommitted
feat(dev-infra): add command for building release output (angular#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 angular#38656
1 parent 964ac15 commit b9dce19

File tree

7 files changed

+261
-0
lines changed

7 files changed

+261
-0
lines changed

dev-infra/release/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ ts_library(
88
module_name = "@angular/dev-infra-private/release",
99
visibility = ["//dev-infra:__subpackages__"],
1010
deps = [
11+
"//dev-infra/release/build",
1112
"//dev-infra/utils",
1213
"@npm//@types/yargs",
1314
"@npm//yargs",
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
load("@npm_bazel_typescript//:index.bzl", "ts_library")
2+
load("//tools:defaults.bzl", "jasmine_node_test")
3+
4+
ts_library(
5+
name = "build",
6+
srcs = glob(
7+
[
8+
"**/*.ts",
9+
],
10+
exclude = ["*.spec.ts"],
11+
),
12+
module_name = "@angular/dev-infra-private/release/build",
13+
visibility = ["//dev-infra:__subpackages__"],
14+
deps = [
15+
"//dev-infra/release/config",
16+
"//dev-infra/utils",
17+
"@npm//@types/node",
18+
"@npm//@types/yargs",
19+
],
20+
)
21+
22+
ts_library(
23+
name = "test_lib",
24+
srcs = glob([
25+
"*.spec.ts",
26+
]),
27+
deps = [
28+
":build",
29+
"//dev-infra/release/config",
30+
"@npm//@types/jasmine",
31+
"@npm//@types/node",
32+
],
33+
)
34+
35+
jasmine_node_test(
36+
name = "test",
37+
deps = [":test_lib"],
38+
)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
/*
10+
* This file will be spawned as a separate process when the `ng-dev release build` command is
11+
* invoked. A separate process allows us to hide any superfluous stdout output from arbitrary
12+
* build commands that we cannot control. This is necessary as the `ng-dev release build` command
13+
* supports stdout JSON output that should be parsable and not polluted from other stdout messages.
14+
*/
15+
16+
import {getReleaseConfig} from '../config/index';
17+
18+
// Start the release package building.
19+
main();
20+
21+
/** Main function for building the release packages. */
22+
async function main() {
23+
if (process.send === undefined) {
24+
throw Error('This script needs to be invoked as a NodeJS worker.');
25+
}
26+
27+
const config = getReleaseConfig();
28+
const builtPackages = await config.buildPackages();
29+
30+
// Transfer the built packages back to the parent process.
31+
process.send(builtPackages);
32+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import * as releaseConfig from '../config/index';
10+
import {ReleaseBuildCommandModule} from './cli';
11+
import * as index from './index';
12+
13+
describe('ng-dev release build', () => {
14+
let npmPackages: string[];
15+
let buildPackages: jasmine.Spy;
16+
17+
beforeEach(() => {
18+
npmPackages = ['@angular/pkg1', '@angular/pkg2'];
19+
buildPackages = jasmine.createSpy('buildPackages').and.resolveTo([
20+
{name: '@angular/pkg1', outputPath: 'dist/pkg1'},
21+
{name: '@angular/pkg2', outputPath: 'dist/pkg2'},
22+
]);
23+
24+
// We cannot test the worker process, so we fake the worker function and
25+
// directly call the package build function.
26+
spyOn(index, 'buildReleaseOutput').and.callFake(() => buildPackages());
27+
// We need to stub out the `process.exit` function during tests as the CLI
28+
// handler calls those in case of failures.
29+
spyOn(process, 'exit');
30+
});
31+
32+
/** Invokes the build command handler. */
33+
async function invokeBuild({json}: {json?: boolean} = {}) {
34+
spyOn(releaseConfig, 'getReleaseConfig')
35+
.and.returnValue({npmPackages, buildPackages, generateReleaseNotesForHead: async () => {}});
36+
await ReleaseBuildCommandModule.handler({json: !!json, $0: '', _: []});
37+
}
38+
39+
it('should invoke configured build packages function', async () => {
40+
await invokeBuild();
41+
expect(buildPackages).toHaveBeenCalledTimes(1);
42+
expect(process.exit).toHaveBeenCalledTimes(0);
43+
});
44+
45+
it('should print built packages as JSON if `--json` is specified', async () => {
46+
const writeSpy = spyOn(process.stdout, 'write');
47+
await invokeBuild({json: true});
48+
49+
expect(buildPackages).toHaveBeenCalledTimes(1);
50+
expect(writeSpy).toHaveBeenCalledTimes(1);
51+
52+
const jsonText = writeSpy.calls.mostRecent().args[0] as string;
53+
const parsed = JSON.parse(jsonText);
54+
55+
expect(parsed).toEqual([
56+
{name: '@angular/pkg1', outputPath: 'dist/pkg1'},
57+
{name: '@angular/pkg2', outputPath: 'dist/pkg2'}
58+
]);
59+
expect(process.exit).toHaveBeenCalledTimes(0);
60+
});
61+
62+
it('should error if package has not been built', async () => {
63+
// Set up a NPM package that is not built.
64+
npmPackages.push('@angular/non-existent');
65+
66+
spyOn(console, 'error');
67+
await invokeBuild();
68+
69+
expect(console.error).toHaveBeenCalledTimes(2);
70+
expect(console.error)
71+
.toHaveBeenCalledWith(
72+
jasmine.stringMatching(`Release output missing for the following packages`));
73+
expect(console.error).toHaveBeenCalledWith(jasmine.stringMatching(`- @angular/non-existent`));
74+
expect(process.exit).toHaveBeenCalledTimes(1);
75+
expect(process.exit).toHaveBeenCalledWith(1);
76+
});
77+
});

dev-infra/release/build/cli.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Arguments, Argv, CommandModule} from 'yargs';
10+
11+
import {getConfig} from '../../utils/config';
12+
import {error, green, info, red, warn, yellow} from '../../utils/console';
13+
import {BuiltPackage, getReleaseConfig} from '../config/index';
14+
15+
import {buildReleaseOutput} from './index';
16+
17+
/** Command line options for building a release. */
18+
export interface ReleaseBuildOptions {
19+
json: boolean;
20+
}
21+
22+
/** Yargs command builder for configuring the `ng-dev release build` command. */
23+
function builder(argv: Argv): Argv<ReleaseBuildOptions> {
24+
return argv.option('json', {
25+
type: 'boolean',
26+
description: 'Whether the built packages should be printed to stdout as JSON.',
27+
default: false,
28+
});
29+
}
30+
31+
/** Yargs command handler for building a release. */
32+
async function handler(args: Arguments<ReleaseBuildOptions>) {
33+
const {npmPackages} = getReleaseConfig();
34+
let builtPackages = await buildReleaseOutput();
35+
36+
// If package building failed, print an error and exit with an error code.
37+
if (builtPackages === null) {
38+
error(red(` ✘ Could not build release output. Please check output above.`));
39+
process.exit(1);
40+
}
41+
42+
// If no packages have been built, we assume that this is never correct
43+
// and exit with an error code.
44+
if (builtPackages.length === 0) {
45+
error(red(` ✘ No release packages have been built. Please ensure that the`));
46+
error(red(` build script is configured correctly in ".ng-dev".`));
47+
process.exit(1);
48+
}
49+
50+
const missingPackages =
51+
npmPackages.filter(pkgName => !builtPackages!.find(b => b.name === pkgName));
52+
53+
// Check for configured release packages which have not been built. We want to
54+
// error and exit if any configured package has not been built.
55+
if (missingPackages.length > 0) {
56+
error(red(` ✘ Release output missing for the following packages:`));
57+
missingPackages.forEach(pkgName => error(red(` - ${pkgName}`)));
58+
process.exit(1);
59+
}
60+
61+
if (args.json) {
62+
process.stdout.write(JSON.stringify(builtPackages, null, 2));
63+
} else {
64+
info(green(' ✓ Built release packages.'));
65+
builtPackages.forEach(({name}) => info(green(` - ${name}`)));
66+
}
67+
}
68+
69+
/** CLI command module for building release output. */
70+
export const ReleaseBuildCommandModule: CommandModule<{}, ReleaseBuildOptions> = {
71+
builder,
72+
handler,
73+
command: 'build',
74+
describe: 'Builds the release output for the current branch.',
75+
};

dev-infra/release/build/index.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {fork} from 'child_process';
10+
import {BuiltPackage} from '../config/index';
11+
12+
/**
13+
* Builds the release output without polluting the process stdout. Build scripts commonly
14+
* print messages to stderr or stdout. This is fine in most cases, but sometimes other tooling
15+
* reserves stdout for data transfer (e.g. when `ng release build --json` is invoked). To not
16+
* pollute the stdout in such cases, we launch a child process for building the release packages
17+
* and redirect all stdout output to the stderr channel (which can be read in the terminal).
18+
*/
19+
export async function buildReleaseOutput(): Promise<BuiltPackage[]|null> {
20+
return new Promise(resolve => {
21+
const buildProcess = fork(require.resolve('./build-worker'), [], {
22+
// The stdio option is set to redirect any "stdout" output directly to the "stderr" file
23+
// descriptor. An additional "ipc" file descriptor is created to support communication with
24+
// the build process. https://nodejs.org/api/child_process.html#child_process_options_stdio.
25+
stdio: ['inherit', 2, 2, 'ipc'],
26+
});
27+
let builtPackages: BuiltPackage[]|null = null;
28+
29+
// The child process will pass the `buildPackages()` output through the
30+
// IPC channel. We keep track of it so that we can use it as resolve value.
31+
buildProcess.on('message', buildResponse => builtPackages = buildResponse);
32+
33+
// On child process exit, resolve the promise with the received output.
34+
buildProcess.on('exit', () => resolve(builtPackages));
35+
});
36+
}

dev-infra/release/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
*/
88
import * as yargs from 'yargs';
99

10+
import {ReleaseBuildCommandModule} from './build/cli';
1011
import {buildEnvStamp} from './stamping/env-stamp';
1112

1213
/** Build the parser for the release commands. */
1314
export function buildReleaseParser(localYargs: yargs.Argv) {
1415
return localYargs.help()
1516
.strict()
1617
.demandCommand()
18+
.command(ReleaseBuildCommandModule)
1719
.command(
1820
'build-env-stamp', 'Build the environment stamping information', {},
1921
() => buildEnvStamp());

0 commit comments

Comments
 (0)