/
command-object.ts
373 lines (356 loc) · 11.9 KB
/
command-object.ts
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
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
import { Argv, CommandModule, showHelp } from 'yargs';
import { readNxJson } from '../../project-graph/file-utils';
import { logger } from '../../utils/logger';
import {
OutputStyle,
RunManyOptions,
parseCSV,
withOutputStyleOption,
withOverrides,
withRunManyOptions,
} from '../yargs-utils/shared-options';
import { VersionData } from './utils/shared';
export interface NxReleaseArgs {
groups?: string[];
projects?: string[];
dryRun?: boolean;
verbose?: boolean;
firstRelease?: boolean;
}
interface GitCommitAndTagOptions {
stageChanges?: boolean;
gitCommit?: boolean;
gitCommitMessage?: string;
gitCommitArgs?: string;
gitTag?: boolean;
gitTagMessage?: string;
gitTagArgs?: string;
}
export type VersionOptions = NxReleaseArgs &
GitCommitAndTagOptions & {
specifier?: string;
preid?: string;
stageChanges?: boolean;
generatorOptionsOverrides?: Record<string, unknown>;
};
export type ChangelogOptions = NxReleaseArgs &
GitCommitAndTagOptions & {
// version and/or versionData must be set
version?: string | null;
versionData?: VersionData;
to?: string;
from?: string;
interactive?: string;
gitRemote?: string;
createRelease?: false | 'github';
};
export type PublishOptions = NxReleaseArgs &
Partial<RunManyOptions> & { outputStyle?: OutputStyle } & {
registry?: string;
tag?: string;
otp?: number;
};
export type ReleaseOptions = NxReleaseArgs & {
yes?: boolean;
skipPublish?: boolean;
};
export const yargsReleaseCommand: CommandModule<
Record<string, unknown>,
NxReleaseArgs
> = {
command: 'release',
describe:
'Orchestrate versioning and publishing of applications and libraries',
builder: (yargs) =>
yargs
.command(releaseCommand)
.command(versionCommand)
.command(changelogCommand)
.command(publishCommand)
.demandCommand()
// Error on typos/mistyped CLI args, there is no reason to support arbitrary unknown args for these commands
.strictOptions()
.option('groups', {
description:
'One or more release groups to target with the current command.',
type: 'string',
coerce: parseCSV,
alias: ['group', 'g'],
})
.option('projects', {
type: 'string',
alias: 'p',
coerce: parseCSV,
describe:
'Projects to run. (comma/space delimited project names and/or patterns)',
})
.option('dry-run', {
describe:
'Preview the changes without updating files/creating releases',
alias: 'd',
type: 'boolean',
default: false,
})
.option('verbose', {
type: 'boolean',
describe:
'Prints additional information about the commands (e.g., stack traces)',
})
.option('first-release', {
type: 'boolean',
description:
'Indicates that this is the first release for the selected release group. If the current version cannot be determined as usual, the version on disk will be used as a fallback. This is useful when using git or the registry to determine the current version of packages, since those sources are only available after the first release. Also indicates that changelog generation should not assume a previous git tag exists and that publishing should not check for the existence of the package before running.',
})
.check((argv) => {
if (argv.groups && argv.projects) {
throw new Error(
'The --projects and --groups options are mutually exclusive, please use one or the other.'
);
}
const nxJson = readNxJson();
if ((argv.groups as string[] | string)?.length) {
for (const group of argv.groups as string[] | string) {
if (!nxJson.release?.groups?.[group]) {
throw new Error(
`The specified release group "${group}" was not found in nx.json`
);
}
}
}
return true;
}) as any, // the type: 'string' and coerce: parseCSV combo isn't enough to produce the string[] type for projects and groups
handler: async () => {
showHelp();
process.exit(1);
},
};
const releaseCommand: CommandModule<NxReleaseArgs, ReleaseOptions> = {
command: '$0 [specifier]',
describe:
'Create a version and release for the workspace, generate a changelog, and optionally publish the packages',
builder: (yargs) =>
yargs
.positional('specifier', {
type: 'string',
describe:
'Exact version or semver keyword to apply to the selected release group.',
})
.option('yes', {
type: 'boolean',
alias: 'y',
description:
'Automatically answer yes to the confirmation prompt for publishing',
})
.option('skip-publish', {
type: 'boolean',
description:
'Skip publishing by automatically answering no to the confirmation prompt for publishing',
})
.check((argv) => {
if (argv.yes !== undefined && argv.skipPublish !== undefined) {
throw new Error(
'The --yes and --skip-publish options are mutually exclusive, please use one or the other.'
);
}
return true;
}),
handler: async (args) => {
const release = await import('./release');
const result = await release.releaseCLIHandler(args);
if (args.dryRun) {
logger.warn(`\nNOTE: The "dryRun" flag means no changes were made.`);
}
if (typeof result === 'number') {
process.exit(result);
}
process.exit(0);
},
};
const versionCommand: CommandModule<NxReleaseArgs, VersionOptions> = {
command: 'version [specifier]',
aliases: ['v'],
describe:
'Create a version and release for one or more applications and libraries',
builder: (yargs) =>
withGitCommitAndGitTagOptions(
yargs
.positional('specifier', {
type: 'string',
describe:
'Exact version or semver keyword to apply to the selected release group.',
})
.option('preid', {
type: 'string',
describe:
'The optional prerelease identifier to apply to the version, in the case that specifier has been set to prerelease.',
default: '',
})
.option('stage-changes', {
type: 'boolean',
describe:
'Whether or not to stage the changes made by this command. Useful when combining this command with changelog generation.',
})
),
handler: async (args) => {
const release = await import('./version');
const result = await release.releaseVersionCLIHandler(args);
if (args.dryRun) {
logger.warn(`\nNOTE: The "dryRun" flag means no changes were made.`);
}
if (typeof result === 'number') {
process.exit(result);
}
process.exit(0);
},
};
const changelogCommand: CommandModule<NxReleaseArgs, ChangelogOptions> = {
command: 'changelog [version]',
aliases: ['c'],
describe:
'Generate a changelog for one or more projects, and optionally push to Github',
builder: (yargs) =>
withGitCommitAndGitTagOptions(
yargs
// Disable default meaning of yargs version for this command
.version(false)
.positional('version', {
type: 'string',
description:
'The version to create a Github release and changelog for',
})
.option('from', {
type: 'string',
description:
'The git reference to use as the start of the changelog. If not set it will attempt to resolve the latest tag and use that',
})
.option('to', {
type: 'string',
description: 'The git reference to use as the end of the changelog',
default: 'HEAD',
})
.option('interactive', {
alias: 'i',
type: 'string',
description:
'Interactively modify changelog markdown contents in your code editor before applying the changes. You can set it to be interactive for all changelogs, or only the workspace level, or only the project level',
choices: ['all', 'workspace', 'projects'],
})
.option('git-remote', {
type: 'string',
description:
'Alternate git remote in the form {user}/{repo} on which to create the Github release (useful for testing)',
default: 'origin',
})
.check((argv) => {
if (!argv.version) {
throw new Error(
'An explicit target version must be specified when using the changelog command directly'
);
}
return true;
})
),
handler: async (args) => {
const release = await import('./changelog');
const result = await release.releaseChangelogCLIHandler(args);
if (args.dryRun) {
logger.warn(`\nNOTE: The "dryRun" flag means no changes were made.`);
}
if (typeof result === 'number') {
process.exit(result);
}
process.exit(0);
},
};
const publishCommand: CommandModule<NxReleaseArgs, PublishOptions> = {
command: 'publish',
aliases: ['p'],
describe: 'Publish a versioned project to a registry',
builder: (yargs) =>
withRunManyOptions(withOutputStyleOption(yargs))
.option('registry', {
type: 'string',
description: 'The registry to publish to',
})
.option('tag', {
type: 'string',
description: 'The distribution tag to apply to the published package',
})
.option('otp', {
type: 'number',
description:
'A one-time password for publishing to a registry that requires 2FA',
}),
handler: async (args) => {
const status = await (
await import('./publish')
).releasePublishCLIHandler(coerceParallelOption(withOverrides(args, 2)));
if (args.dryRun) {
logger.warn(`\nNOTE: The "dryRun" flag means no changes were made.`);
}
process.exit(status);
},
};
function coerceParallelOption(args: any) {
if (args['parallel'] === 'false' || args['parallel'] === false) {
return {
...args,
parallel: 1,
};
} else if (
args['parallel'] === 'true' ||
args['parallel'] === true ||
args['parallel'] === ''
) {
return {
...args,
parallel: Number(args['maxParallel'] || args['max-parallel'] || 3),
};
} else if (args['parallel'] !== undefined) {
return {
...args,
parallel: Number(args['parallel']),
};
}
return args;
}
function withGitCommitAndGitTagOptions<T>(
yargs: Argv<T>
): Argv<T & GitCommitAndTagOptions> {
return yargs
.option('git-commit', {
describe:
'Whether or not to automatically commit the changes made by this command',
type: 'boolean',
})
.option('git-commit-message', {
describe:
'Custom git commit message to use when committing the changes made by this command. {version} will be dynamically interpolated when performing fixed releases, interpolated tags will be appended to the commit body when performing independent releases.',
type: 'string',
})
.option('git-commit-args', {
describe:
'Additional arguments (added after the --message argument, which may or may not be customized with --git-commit-message) to pass to the `git commit` command invoked behind the scenes',
type: 'string',
})
.option('git-tag', {
describe:
'Whether or not to automatically tag the changes made by this command',
type: 'boolean',
})
.option('git-tag-message', {
describe:
'Custom git tag message to use when tagging the changes made by this command. This defaults to be the same value as the tag itself.',
type: 'string',
})
.option('git-tag-args', {
describe:
'Additional arguments to pass to the `git tag` command invoked behind the scenes',
type: 'string',
})
.option('stage-changes', {
describe:
'Whether or not to stage the changes made by this command. Always treated as true if git-commit is true.',
type: 'boolean',
});
}