-
Notifications
You must be signed in to change notification settings - Fork 66
Expand file tree
/
Copy pathexternal-commands.ts
More file actions
330 lines (305 loc) · 12.1 KB
/
external-commands.ts
File metadata and controls
330 lines (305 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
/**
* @license
* Copyright Google LLC
*
* 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 {existsSync} from 'fs';
import {join} from 'path';
import semver from 'semver';
import {ChildProcess, SpawnResult, SpawnOptions} from '../../utils/child-process.js';
import {Spinner} from '../../utils/spinner.js';
import {NpmDistTag} from '../versioning/index.js';
import {FatalReleaseActionError} from './actions-error.js';
import {resolveYarnScriptForProject} from '../../utils/resolve-yarn-bin.js';
import {ReleaseBuildJsonStdout} from '../build/cli.js';
import {ReleaseInfoJsonStdout} from '../info/cli.js';
import {ReleasePrecheckJsonStdin} from '../precheck/cli.js';
import {BuiltPackageWithInfo} from '../config/index.js';
import {green, Log} from '../../utils/logging.js';
import {getBazelBin} from '../../utils/bazel-bin.js';
import {PnpmVersioning} from './pnpm-versioning.js';
/*
* ###############################################################
*
* This file contains helpers for invoking external `ng-dev` commands. A subset of actions,
* like building release output or setting aν NPM dist tag for release packages, cannot be
* performed directly as part of the release tool and need to be delegated to external `ng-dev`
* commands that exist across arbitrary version branches.
*
* In a concrete example: Consider a new patch version is released and that a new release
* package has been added to the `next` branch. The patch branch will not contain the new
* release package, so we could not build the release output for it. To work around this, we
* call the ng-dev build command for the patch version branch and expect it to return a list
* of built packages that need to be released as part of this release train.
*
* ###############################################################
*/
/** Class holding method for invoking release action external commands. */
export abstract class ExternalCommands {
/**
* Invokes the `ng-dev release set-dist-tag` command in order to set the specified
* NPM dist tag for all packages in the checked out branch to the given version.
*
* Optionally, the NPM dist tag update can be skipped for experimental packages. This
* is useful when tagging long-term-support packages within NPM.
*/
static async invokeSetNpmDist(
projectDir: string,
npmDistTag: NpmDistTag,
version: semver.SemVer,
options: {skipExperimentalPackages: boolean} = {skipExperimentalPackages: false},
) {
try {
// Note: No progress indicator needed as that is the responsibility of the command.
await this._spawnNpmScript(
[
'ng-dev',
'release',
'set-dist-tag',
npmDistTag,
version.format(),
`--skip-experimental-packages=${options.skipExperimentalPackages}`,
],
projectDir,
);
Log.info(green(` ✓ Set "${npmDistTag}" NPM dist tag for all packages to v${version}.`));
} catch (e) {
Log.error(e);
Log.error(` ✘ An error occurred while setting the NPM dist tag for "${npmDistTag}".`);
throw new FatalReleaseActionError();
}
}
/**
* Invokes the `ng-dev release npm-dist-tag delete` command in order to delete the
* NPM dist tag for all packages in the checked-out version branch.
*/
static async invokeDeleteNpmDistTag(projectDir: string, npmDistTag: NpmDistTag) {
try {
// Note: No progress indicator needed as that is the responsibility of the command.
await this._spawnNpmScript(
['ng-dev', 'release', 'npm-dist-tag', 'delete', npmDistTag],
projectDir,
);
Log.info(green(` ✓ Deleted "${npmDistTag}" NPM dist tag for all packages.`));
} catch (e) {
Log.error(e);
Log.error(` ✘ An error occurred while deleting the NPM dist tag: "${npmDistTag}".`);
throw new FatalReleaseActionError();
}
}
/**
* Invokes the `ng-dev release build` command in order to build the release
* packages for the currently checked out branch.
*/
static async invokeReleaseBuild(projectDir: string): Promise<ReleaseBuildJsonStdout> {
// Note: We explicitly mention that this can take a few minutes, so that it's obvious
// to caretakers that it can take longer than just a few seconds.
const spinner = new Spinner('Building release output. This can take a few minutes.');
try {
const {stdout} = await this._spawnNpmScript(
['ng-dev', 'release', 'build', '--json'],
projectDir,
{
mode: 'silent',
},
);
spinner.complete();
Log.info(green(' ✓ Built release output for all packages.'));
// The `ng-dev release build` command prints a JSON array to stdout
// that represents the built release packages and their output paths.
return JSON.parse(stdout.trim()) as ReleaseBuildJsonStdout;
} catch (e) {
spinner.complete();
Log.error(e);
Log.error(' ✘ An error occurred while building the release packages.');
throw new FatalReleaseActionError();
}
}
/**
* Invokes the `ng-dev release info` command in order to retrieve information
* about the release for the currently checked-out branch.
*
* This is useful to e.g. determine whether a built package is currently
* denoted as experimental or not.
*/
static async invokeReleaseInfo(projectDir: string): Promise<ReleaseInfoJsonStdout> {
try {
const {stdout} = await this._spawnNpmScript(
['ng-dev', 'release', 'info', '--json'],
projectDir,
{mode: 'silent'},
);
// The `ng-dev release info` command prints a JSON object to stdout.
return JSON.parse(stdout.trim()) as ReleaseInfoJsonStdout;
} catch (e) {
Log.error(e);
Log.error(
` ✘ An error occurred while retrieving the release information for ` +
`the currently checked-out branch.`,
);
throw new FatalReleaseActionError();
}
}
/**
* Invokes the `ng-dev release precheck` command in order to validate the
* built packages or run other validations before actually releasing.
*
* This is run as an external command because prechecks can be customized
* through the `ng-dev` configuration, and we wouldn't want to run prechecks
* from the `next` branch for older branches, like patch or an LTS branch.
*/
static async invokeReleasePrecheck(
projectDir: string,
newVersion: semver.SemVer,
builtPackagesWithInfo: BuiltPackageWithInfo[],
): Promise<void> {
const precheckStdin: ReleasePrecheckJsonStdin = {
builtPackagesWithInfo,
newVersion: newVersion.format(),
};
try {
await this._spawnNpmScript(['ng-dev', 'release', 'precheck'], projectDir, {
// Note: We pass the precheck information to the command through `stdin`
// because command line arguments are less reliable and have length limits.
input: JSON.stringify(precheckStdin),
});
Log.info(green(` ✓ Executed release pre-checks for ${newVersion}`));
} catch (e) {
// The `spawn` invocation already prints all stdout/stderr, so we don't need re-print.
// To ease debugging in case of runtime exceptions, we still print the error to `debug`.
Log.debug(e);
Log.error(` ✘ An error occurred while running release pre-checks.`);
throw new FatalReleaseActionError();
}
}
/**
* Invokes the `nvm install` command in order to install the correct Node.js version
* as specified in a `.nvmrc` file, if present.
*/
static async invokeNvmInstall(projectDir: string): Promise<void>;
static async invokeNvmInstall(projectDir: string, quiet: boolean): Promise<void>;
static async invokeNvmInstall(projectDir: string, quiet = false): Promise<void> {
if (!existsSync(join(projectDir, '.nvmrc'))) {
return;
}
try {
// We must source nvm.sh so the shell recognizes the 'nvm' command since nvm is not a binary
// but a shell function. The dot (.) built-in and && operator require shell: true here.
await ChildProcess.spawn('. ~/.nvm/nvm.sh && nvm install', [], {
cwd: projectDir,
mode: 'on-error',
shell: true,
});
if (!quiet) {
const {stdout: nodeVersion} = await ChildProcess.spawn('node', ['--version'], {
mode: 'silent',
cwd: projectDir,
});
Log.info(green(` ✓ Set node version to ${nodeVersion}.`));
}
} catch (e) {
Log.error(e);
Log.error(' ✘ An error occurred while installing Node.js via nvm.');
throw new FatalReleaseActionError();
}
}
/**
* Invokes the `yarn install` command in order to install dependencies for
* the configured project with the currently checked out revision.
*/
static async invokeYarnInstall(projectDir: string): Promise<void> {
// Note: We cannot use `yarn` directly as command because we might operate in
// a different publish branch and the current `PATH` will point to the Yarn version
// that invoked the release tool. More details in the function description.
const yarnCommand = await resolveYarnScriptForProject(projectDir);
try {
// Note: No progress indicator needed as that is the responsibility of the command.
// TODO: Consider using an Ora spinner instead to ensure minimal console output.
await ChildProcess.spawn(
yarnCommand.binary,
[
...yarnCommand.args,
'install',
...(yarnCommand.legacy ? ['--frozen-lockfile', '--non-interactive'] : ['--immutable']),
],
{
cwd: projectDir,
mode: 'on-error',
},
);
Log.info(green(' ✓ Installed project dependencies.'));
} catch (e) {
Log.error(e);
Log.error(' ✘ An error occurred while installing dependencies.');
throw new FatalReleaseActionError();
}
}
/**
* Invokes the `pnpm install` command in order to install dependencies for
* the configured project with the currently checked out revision.
*/
static async invokePnpmInstall(projectDir: string): Promise<void> {
try {
await ChildProcess.spawn(
'pnpm',
[
'install',
'--frozen-lockfile',
// PNPM does not have no interactive,
// See: https://github.com/pnpm/pnpm/issues/6778
'--config.confirmModulesPurge=false',
],
{
cwd: projectDir,
mode: 'on-error',
},
);
Log.info(green(' ✓ Installed project dependencies.'));
} catch (e) {
Log.error(e);
Log.error(' ✘ An error occurred while installing dependencies.');
throw new FatalReleaseActionError();
}
}
private static async _spawnNpmScript(
args: string[],
projectDir: string,
spawnOptions: SpawnOptions = {},
): Promise<SpawnResult> {
if (PnpmVersioning.isUsingPnpm(projectDir)) {
return ChildProcess.spawn('pnpm', ['-s', ...args], {
...spawnOptions,
cwd: projectDir,
});
}
// Note: We cannot use `yarn` directly as command because we might operate in
// a different publish branch and the current `PATH` will point to the Yarn version
// that invoked the release tool. More details in the function description.
const yarnCommand = await resolveYarnScriptForProject(projectDir);
return ChildProcess.spawn(yarnCommand.binary, [...yarnCommand.args, ...args], {
...spawnOptions,
cwd: projectDir,
});
}
/**
* Invokes the `yarn bazel sync --only=repo` command in order
* to refresh Aspect lock files.
*/
static async invokeBazelUpdateAspectLockFiles(projectDir: string): Promise<void> {
// TODO: remove when Angular version 19 is no longer in LTS.
const spinner = new Spinner('Updating Aspect lock files');
try {
await ChildProcess.spawn(getBazelBin(), ['sync', '--only=repo'], {
cwd: projectDir,
mode: 'silent',
});
} catch (e) {
// Note: Gracefully handling these errors because `sync` command
// exits with a non-zero exit code when pnpm-lock.yaml file is updated.
Log.debug(e);
}
spinner.success(green(' Updated Aspect `rules_js` lock files.'));
}
}