@wpelevator/wp-release 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ import { promisify } from 'node:util';
6
6
  import { loadChangesetConfig } from "./changeset-config.js";
7
7
  import { findMainPluginFile } from "./package-meta.js";
8
8
  import { findConstantValues } from "./version-check.js";
9
- import { getChangesetBump, getNextVersion, parseChangeset, } from "./changesets.js";
9
+ import { formatChangelogEntries, formatChangeset, getChangesetBump, getNextVersion, parseChangeset, } from "./changesets.js";
10
10
  import { ValidationError } from "./errors.js";
11
11
  const execFileAsync = promisify(execFile);
12
12
  /**
@@ -19,6 +19,18 @@ export async function getChangesetReleasePlan(input) {
19
19
  const context = await getReleaseContext(input);
20
20
  return context?.plan;
21
21
  }
22
+ /**
23
+ * Discover the packages of the repository configuration.
24
+ *
25
+ * @param input Repository root and optional explicit config file.
26
+ * @return Packages in the configured order with their current versions.
27
+ * @throws {ValidationError} Without a config, or for an invalid one.
28
+ */
29
+ export async function getChangesetPackages(input) {
30
+ const loaded = await requireConfig(input);
31
+ const packages = await discoverPackages(input.cwd, loaded.config);
32
+ return packages.map(toChangesetPackage);
33
+ }
22
34
  async function getReleaseContext(input) {
23
35
  const loaded = await loadChangesetConfig(input);
24
36
  if (!loaded) {
@@ -110,7 +122,10 @@ export async function versionChangesets(input) {
110
122
  if (!context) {
111
123
  throw new ValidationError('No wp-release config found. Add wp-release.config.js or pass --config <path>.');
112
124
  }
113
- const { config, packages, plan: releasePlan } = context;
125
+ const { config, packages } = context;
126
+ const releasePlan = input.packages
127
+ ? selectReleasePlan(context.plan, packages, input.packages)
128
+ : context.plan;
114
129
  const date = input.date ?? new Date().toISOString().slice(0, 10);
115
130
  if (!input.dryRun) {
116
131
  const journal = new Map();
@@ -128,9 +143,7 @@ export async function versionChangesets(input) {
128
143
  }
129
144
  const changesetDirectory = path.resolve(input.cwd, config.changesets ?? '.changeset');
130
145
  for (const changeset of releasePlan.changesets) {
131
- const file = path.join(changesetDirectory, changeset.fileName);
132
- await rememberFile(journal, file);
133
- await rm(file);
146
+ await consumeChangeset(path.join(changesetDirectory, changeset.fileName), changeset, input.packages, journal);
134
147
  }
135
148
  }
136
149
  catch (error) {
@@ -147,6 +160,48 @@ export async function versionChangesets(input) {
147
160
  changed: !input.dryRun && releasePlan.changesets.length > 0,
148
161
  };
149
162
  }
163
+ /**
164
+ * Limit a release plan to some packages and the changesets that name them.
165
+ *
166
+ * @param plan Release plan of all pending changesets.
167
+ * @param packages Configured packages.
168
+ * @param names Packages to version.
169
+ * @return Release plan of the named packages.
170
+ * @throws {ValidationError} For unknown packages.
171
+ */
172
+ function selectReleasePlan(plan, packages, names) {
173
+ names.forEach((name) => {
174
+ if (!packages.some((pkg) => pkg.name === name)) {
175
+ throw new ValidationError(`Unknown package "${name}".`);
176
+ }
177
+ });
178
+ return {
179
+ releases: plan.releases.filter((release) => names.includes(release.package.name)),
180
+ changesets: plan.changesets.filter((changeset) => names.some((name) => Object.hasOwn(changeset.packages, name))),
181
+ };
182
+ }
183
+ /**
184
+ * Remove an applied changeset, or only the applied packages from a changeset
185
+ * that names other packages too, so that their releases keep its summary.
186
+ *
187
+ * @param file Changeset file.
188
+ * @param changeset Parsed changeset.
189
+ * @param applied Versioned packages, or undefined when all were versioned.
190
+ * @param journal File journal for rolling back.
191
+ */
192
+ async function consumeChangeset(file, changeset, applied, journal) {
193
+ const remaining = Object.entries(changeset.packages).filter(([name]) => undefined !== applied && !applied.includes(name));
194
+ await rememberFile(journal, file);
195
+ if (remaining.length) {
196
+ await writeFile(file, formatChangeset({
197
+ packages: Object.fromEntries(remaining),
198
+ summary: changeset.summary,
199
+ }));
200
+ }
201
+ else {
202
+ await rm(file);
203
+ }
204
+ }
150
205
  /**
151
206
  * Create missing release tags, optionally pushing them to the configured remote.
152
207
  *
@@ -157,49 +212,64 @@ export async function tagChangesets(input) {
157
212
  if (input.githubRelease && !input.push) {
158
213
  throw new ValidationError('GitHub Releases require pushing the tags.');
159
214
  }
215
+ if (input.untagged && input.push) {
216
+ throw new ValidationError('Listing untagged releases cannot be combined with pushing tags.');
217
+ }
160
218
  const context = await getReleaseContext(input);
161
219
  if (!context) {
162
220
  throw new ValidationError('No wp-release config found. Add wp-release.config.js or pass --config <path>.');
163
221
  }
222
+ const selected = selectPackages(context.packages, input);
164
223
  const packages = input.since
165
- ? (await changedPackagesSince(input.cwd, context.packages, input.since)).packages
166
- : context.packages;
224
+ ? (await changedPackagesSince(input.cwd, selected, input.since))
225
+ .packages
226
+ : selected;
167
227
  const results = [];
228
+ const dryRun = input.dryRun || input.untagged;
168
229
  const tagFormat = context.config.tagFormat ?? '{path}/{version}';
169
230
  for (const pkg of packages) {
170
- const version = 'git-tag' === pkg.versionConfig.from.kind
171
- ? await getLatestChangelogVersion(pkg.directory, pkg.currentVersion)
172
- : pkg.currentVersion;
231
+ const version = await getTagVersion(pkg, input.version);
232
+ if (undefined !== input.version && input.version !== version) {
233
+ throw new ValidationError(`Package "${pkg.name}" is at version ${version}, not ${input.version}.`);
234
+ }
173
235
  const tag = formatTag(tagFormat, pkg, version);
174
236
  const exists = await gitTagExists(input.cwd, tag);
175
- const notes = await readReleaseNotes(pkg.directory, version);
176
- if (!exists && !input.dryRun) {
237
+ if (!exists) {
238
+ await verifyVersionTargets(input.cwd, pkg, version);
239
+ }
240
+ const previousTag = await getPreviousTag(input.cwd, formatTag(tagFormat, pkg, ''), version);
241
+ const notes = await getReleaseNotes(input.cwd, context.config, pkg, { version, tag, previousTag }, exists ? tag : 'HEAD');
242
+ if (!exists && !dryRun) {
177
243
  await git(input.cwd, ['tag', tag]);
178
244
  }
179
245
  results.push({
180
246
  package: pkg.name,
181
247
  version,
182
248
  tag,
183
- created: !exists && !input.dryRun,
249
+ ...(previousTag ? { previousTag } : {}),
250
+ prerelease: isPrerelease(version),
251
+ exists,
252
+ created: !exists && !dryRun,
184
253
  notes,
185
254
  });
186
255
  }
187
- if (input.push && !input.dryRun) {
188
- const created = results.filter((result) => result.created);
189
- if (created.length) {
190
- await git(input.cwd, [
191
- 'push',
192
- 'origin',
193
- ...created.map((item) => item.tag),
194
- ]);
256
+ // Push one tag at a time because GitHub doesn't create push events for
257
+ // tags when more than three are pushed at once.
258
+ if (input.push && !dryRun) {
259
+ for (const item of results.filter((result) => result.created)) {
260
+ await git(input.cwd, ['push', 'origin', item.tag]);
195
261
  }
196
262
  }
197
- if (input.githubRelease && !input.dryRun) {
263
+ if (input.githubRelease && !dryRun) {
198
264
  for (const release of results) {
199
265
  await createGitHubRelease(input.cwd, release);
200
266
  }
201
267
  }
202
- return { tags: results };
268
+ return {
269
+ tags: input.untagged
270
+ ? results.filter((result) => !result.exists)
271
+ : results,
272
+ };
203
273
  }
204
274
  async function createGitHubRelease(cwd, release) {
205
275
  try {
@@ -217,6 +287,7 @@ async function createGitHubRelease(cwd, release) {
217
287
  `${release.package}@${release.version}`,
218
288
  '--notes',
219
289
  release.notes,
290
+ ...(release.prerelease ? ['--prerelease'] : []),
220
291
  ], { cwd });
221
292
  }
222
293
  catch (error) {
@@ -225,6 +296,141 @@ async function createGitHubRelease(cwd, release) {
225
296
  }
226
297
  }
227
298
  }
299
+ /**
300
+ * Get the version to tag. Packages versioned by their git tags have no
301
+ * version file, so the expected version is the version to tag, or else the
302
+ * latest CHANGELOG.md version.
303
+ *
304
+ * @param pkg Configured package.
305
+ * @param expected Expected version from --version, if any.
306
+ * @return Version to tag.
307
+ */
308
+ async function getTagVersion(pkg, expected) {
309
+ if ('git-tag' !== pkg.versionConfig.from.kind) {
310
+ return pkg.currentVersion;
311
+ }
312
+ return (expected ??
313
+ (await getLatestChangelogVersion(pkg.directory, pkg.currentVersion)));
314
+ }
315
+ /**
316
+ * Pick the packages passed with --package, or all packages without it.
317
+ *
318
+ * @param packages Configured packages.
319
+ * @param input Tag options.
320
+ * @return Selected packages in the configured order.
321
+ * @throws {ValidationError} For unknown packages, or --version without a single package.
322
+ */
323
+ function selectPackages(packages, input) {
324
+ const names = input.packages ?? [];
325
+ if (undefined !== input.version && 1 !== names.length) {
326
+ throw new ValidationError('An expected version requires exactly one package.');
327
+ }
328
+ names.forEach((name) => {
329
+ if (!packages.some((pkg) => pkg.name === name)) {
330
+ throw new ValidationError(`Unknown package "${name}".`);
331
+ }
332
+ });
333
+ return names.length
334
+ ? packages.filter((pkg) => names.includes(pkg.name))
335
+ : packages;
336
+ }
337
+ /**
338
+ * Find the tag of the newest release before a version.
339
+ *
340
+ * @param cwd Repository root.
341
+ * @param prefix Tag prefix of the package, like `plugins/alpha/`.
342
+ * @param version Version being released.
343
+ * @return Previous tag, or undefined for the first release.
344
+ */
345
+ async function getPreviousTag(cwd, prefix, version) {
346
+ const output = await git(cwd, ['tag', '--list', `${prefix}*`]);
347
+ return output
348
+ .trim()
349
+ .split(/\r?\n/)
350
+ .filter((tag) => tag.startsWith(prefix))
351
+ .map((tag) => ({ tag, version: tag.slice(prefix.length) }))
352
+ .filter((item) => isVersion(item.version) &&
353
+ compareVersions(item.version, version) < 0)
354
+ .sort((a, b) => compareVersions(b.version, a.version))[0]?.tag;
355
+ }
356
+ /**
357
+ * Build the release notes of a tag with the configured formatter.
358
+ *
359
+ * @param cwd Repository root.
360
+ * @param config Repository release configuration.
361
+ * @param pkg Released package.
362
+ * @param release Version, tag and previous tag of the release.
363
+ * @param release.version Released version.
364
+ * @param release.tag Release tag.
365
+ * @param release.previousTag Tag of the previous release, if any.
366
+ * @param end Commit of the release: the tag, or HEAD before tagging.
367
+ * @return Release notes.
368
+ */
369
+ async function getReleaseNotes(cwd, config, pkg, release, end) {
370
+ const changelog = await readReleaseNotes(pkg.directory, release.version);
371
+ const commits = await getReleaseCommits(cwd, pkg, end, release.previousTag);
372
+ const notes = changelog ||
373
+ commits
374
+ .map((commit) => `- ${commit.subject} (${commit.shortHash})`)
375
+ .join('\n');
376
+ if (!config.releaseNotes) {
377
+ return notes;
378
+ }
379
+ const formatted = await config.releaseNotes({
380
+ package: toChangesetPackage(pkg),
381
+ ...release,
382
+ changelog,
383
+ commits,
384
+ notes,
385
+ });
386
+ if ('string' !== typeof formatted) {
387
+ throw new ValidationError(`The releaseNotes formatter didn't return a string for package "${pkg.name}".`);
388
+ }
389
+ return formatted.trim();
390
+ }
391
+ /**
392
+ * List the commits that changed a package since its previous release.
393
+ *
394
+ * @param cwd Repository root.
395
+ * @param pkg Released package.
396
+ * @param end Commit of the release.
397
+ * @param previousTag Tag of the previous release, if any.
398
+ * @return Commits, newest first.
399
+ */
400
+ async function getReleaseCommits(cwd, pkg, end, previousTag) {
401
+ const packageFromRepo = path.relative(cwd, pkg.directory).split(path.sep).join('/') ||
402
+ '.';
403
+ const output = await git(cwd, [
404
+ 'log',
405
+ '--format=%H%x1f%h%x1f%s',
406
+ previousTag ? `${previousTag}..${end}` : end,
407
+ '--',
408
+ packageFromRepo,
409
+ ]);
410
+ return output
411
+ .trim()
412
+ .split(/\r?\n/)
413
+ .filter(Boolean)
414
+ .map((line) => {
415
+ const [hash = '', shortHash = '', subject = ''] = line.split('\x1f');
416
+ return { hash, shortHash, subject };
417
+ });
418
+ }
419
+ /**
420
+ * Remove the release configuration from a package passed to config callbacks.
421
+ *
422
+ * @param pkg Configured package.
423
+ * @return Package without its group configuration.
424
+ */
425
+ function toChangesetPackage(pkg) {
426
+ return {
427
+ name: pkg.name,
428
+ directory: pkg.directory,
429
+ path: pkg.path,
430
+ type: pkg.type,
431
+ currentVersion: pkg.currentVersion,
432
+ };
433
+ }
228
434
  /**
229
435
  * Check whether changed files belong to packages without a changeset.
230
436
  *
@@ -543,9 +749,7 @@ async function prependChangelog(directory, release, date, readmeChangelog, chang
543
749
  const summaries = release.changesets
544
750
  .map((item) => item.summary.trim())
545
751
  .filter(Boolean);
546
- const section = `## ${release.nextVersion} (${date})\n\n${summaries
547
- .map((item) => `- ${item.replaceAll(/\r?\n/g, '\n ')}`)
548
- .join('\n')}\n\n`;
752
+ const section = `## ${release.nextVersion} (${date})\n\n${formatChangelogEntries(release.changesets)}\n\n`;
549
753
  const changelog = path.join(directory, 'CHANGELOG.md');
550
754
  let previous = '';
551
755
  try {
@@ -820,10 +1024,60 @@ function packageType(group) {
820
1024
  }
821
1025
  return 'other';
822
1026
  }
1027
+ const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?$/;
1028
+ function isVersion(version) {
1029
+ return VERSION_PATTERN.test(version);
1030
+ }
1031
+ function isPrerelease(version) {
1032
+ return Boolean(VERSION_PATTERN.exec(version)?.[4]);
1033
+ }
1034
+ /**
1035
+ * Compare two versions with SemVer precedence, where a pre-release like
1036
+ * 1.0.0-rc.1 comes before 1.0.0.
1037
+ *
1038
+ * @param left Version.
1039
+ * @param right Version to compare with.
1040
+ * @return Negative when left is lower, positive when higher, otherwise 0.
1041
+ */
823
1042
  function compareVersions(left, right) {
824
- const a = left.split('.').map(Number);
825
- const b = right.split('.').map(Number);
826
- return ((a[0] ?? 0) - (b[0] ?? 0) ||
1043
+ const [leftCore = '', leftPre] = splitPrerelease(left);
1044
+ const [rightCore = '', rightPre] = splitPrerelease(right);
1045
+ const a = leftCore.split('.').map(Number);
1046
+ const b = rightCore.split('.').map(Number);
1047
+ const core = (a[0] ?? 0) - (b[0] ?? 0) ||
827
1048
  (a[1] ?? 0) - (b[1] ?? 0) ||
828
- (a[2] ?? 0) - (b[2] ?? 0));
1049
+ (a[2] ?? 0) - (b[2] ?? 0);
1050
+ if (core || leftPre === rightPre) {
1051
+ return core;
1052
+ }
1053
+ if (undefined === leftPre || undefined === rightPre) {
1054
+ return undefined === leftPre ? 1 : -1;
1055
+ }
1056
+ return comparePrerelease(leftPre.split('.'), rightPre.split('.'));
1057
+ }
1058
+ function splitPrerelease(version) {
1059
+ const index = version.indexOf('-');
1060
+ return -1 === index
1061
+ ? [version]
1062
+ : [version.slice(0, index), version.slice(index + 1)];
1063
+ }
1064
+ function comparePrerelease(left, right) {
1065
+ const [a, ...leftRest] = left;
1066
+ const [b, ...rightRest] = right;
1067
+ if (undefined === a || undefined === b) {
1068
+ return (undefined === a ? 0 : 1) - (undefined === b ? 0 : 1);
1069
+ }
1070
+ const order = compareIdentifier(a, b);
1071
+ return order || comparePrerelease(leftRest, rightRest);
1072
+ }
1073
+ function compareIdentifier(a, b) {
1074
+ const aNumeric = /^\d+$/.test(a);
1075
+ const bNumeric = /^\d+$/.test(b);
1076
+ if (aNumeric && bNumeric) {
1077
+ return Number(a) - Number(b);
1078
+ }
1079
+ if (aNumeric || bNumeric) {
1080
+ return aNumeric ? -1 : 1;
1081
+ }
1082
+ return Number(a > b) - Number(a < b);
829
1083
  }
@@ -53,6 +53,13 @@ export declare function parseChangeset(contents: string, fileName?: string): Cha
53
53
  * @return Markdown contents with a trailing newline.
54
54
  */
55
55
  export declare function formatChangeset(changeset: Omit<Changeset, 'fileName'>): string;
56
+ /**
57
+ * Format the summaries of changesets as a Markdown list of changelog entries.
58
+ *
59
+ * @param changesets Changesets of a release.
60
+ * @return Markdown list, empty without summaries.
61
+ */
62
+ export declare function formatChangelogEntries(changesets: Changeset[]): string;
56
63
  /**
57
64
  * Return the strongest bump requested for a package across changesets.
58
65
  *
@@ -64,6 +64,19 @@ export function formatChangeset(changeset) {
64
64
  .join('\n');
65
65
  return `---\n${frontmatter}\n---\n\n${changeset.summary.trim()}\n`;
66
66
  }
67
+ /**
68
+ * Format the summaries of changesets as a Markdown list of changelog entries.
69
+ *
70
+ * @param changesets Changesets of a release.
71
+ * @return Markdown list, empty without summaries.
72
+ */
73
+ export function formatChangelogEntries(changesets) {
74
+ return changesets
75
+ .map((item) => item.summary.trim())
76
+ .filter(Boolean)
77
+ .map((item) => `- ${item.replaceAll(/\r?\n/g, '\n ')}`)
78
+ .join('\n');
79
+ }
67
80
  /**
68
81
  * Return the strongest bump requested for a package across changesets.
69
82
  *
package/dist/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { appendFileSync } from 'node:fs';
1
2
  import { parseArgs } from 'node:util';
2
3
  import zip from "./commands/zip.js";
3
4
  import checksums from "./commands/checksums.js";
@@ -5,6 +6,7 @@ import sign from "./commands/sign.js";
5
6
  import verify from "./commands/verify.js";
6
7
  import versionCheck from "./commands/version-check.js";
7
8
  import deploySvn from "./commands/deploy-svn.js";
9
+ import deployGit from "./commands/deploy-git.js";
8
10
  import { Logger, TextWriter } from "./logger.js";
9
11
  import { getBoolean, getString } from "./commands/command.js";
10
12
  import { ValidationError } from "./errors.js";
@@ -24,6 +26,7 @@ export const commands = [
24
26
  verify,
25
27
  versionCheck,
26
28
  deploySvn,
29
+ deployGit,
27
30
  changesetAdd,
28
31
  changesetStatus,
29
32
  changesetVersion,
@@ -36,6 +39,7 @@ const globalArgs = {
36
39
  'log-format': { type: 'string' },
37
40
  ci: { type: 'boolean' },
38
41
  json: { type: 'boolean' },
42
+ 'github-output': { type: 'boolean' },
39
43
  config: { type: 'string' },
40
44
  };
41
45
  const globalOptions = `Global options:
@@ -47,6 +51,8 @@ const globalOptions = `Global options:
47
51
  when GITHUB_ACTIONS=true, otherwise text)
48
52
  --ci Same as --log-format github
49
53
  --json Print the result as JSON
54
+ --github-output Set the result as JSON to the "result" output of
55
+ the GitHub Actions step
50
56
  --config <path> Path to a JavaScript release config file`;
51
57
  /**
52
58
  * Get the top-level help text listing all commands.
@@ -143,6 +149,21 @@ function createLogWriter(values, streams) {
143
149
  }
144
150
  return create(streams);
145
151
  }
152
+ /**
153
+ * Get the GitHub Actions step output file for --github-output, checked before
154
+ * the command runs.
155
+ *
156
+ * @param values Parsed option values.
157
+ * @return Output file path, or undefined without --github-output.
158
+ * @throws {ValidationError} For --github-output without GITHUB_OUTPUT.
159
+ */
160
+ function getGitHubOutputFile(values) {
161
+ const file = process.env.GITHUB_OUTPUT;
162
+ if (getBoolean(values, 'github-output') && !file) {
163
+ throw new ValidationError('--github-output requires the GITHUB_OUTPUT environment variable of GitHub Actions.');
164
+ }
165
+ return getBoolean(values, 'github-output') ? file : undefined;
166
+ }
146
167
  /**
147
168
  * Parse the arguments of a command and run it.
148
169
  *
@@ -169,6 +190,7 @@ async function runCommand(command, args, io) {
169
190
  if ('' === configFile?.trim()) {
170
191
  throw new ValidationError('--config <path> must not be empty.');
171
192
  }
193
+ const githubOutputFile = getGitHubOutputFile(values);
172
194
  logger = new Logger({
173
195
  writer: createLogWriter(values, { stdout, stderr }),
174
196
  quiet: getBoolean(values, 'quiet') || getBoolean(values, 'json'),
@@ -184,6 +206,10 @@ async function runCommand(command, args, io) {
184
206
  if (getBoolean(values, 'json')) {
185
207
  stdout.write(JSON.stringify(result, null, '\t') + '\n');
186
208
  }
209
+ if (githubOutputFile) {
210
+ // Compact JSON has no line breaks, so it fits a single output line.
211
+ appendFileSync(githubOutputFile, `result=${JSON.stringify(result)}\n`);
212
+ }
187
213
  return exitCode;
188
214
  }
189
215
  catch (error) {
@@ -61,11 +61,16 @@ export declare const changesetVersionArgs: {
61
61
  readonly date: {
62
62
  readonly type: "string";
63
63
  };
64
+ readonly package: {
65
+ readonly type: "string";
66
+ readonly multiple: true;
67
+ };
64
68
  };
65
69
  /** Parsed input for `changeset-version`. */
66
70
  export interface ChangesetVersionArgs {
67
71
  dryRun: boolean;
68
72
  date?: string;
73
+ packages?: string[];
69
74
  }
70
75
  /**
71
76
  * Validate and normalize a release date option.
@@ -91,19 +96,32 @@ export declare const changesetTagArgs: {
91
96
  readonly 'dry-run': {
92
97
  readonly type: "boolean";
93
98
  };
99
+ readonly untagged: {
100
+ readonly type: "boolean";
101
+ };
94
102
  readonly 'github-release': {
95
103
  readonly type: "boolean";
96
104
  };
97
105
  readonly since: {
98
106
  readonly type: "string";
99
107
  };
108
+ readonly package: {
109
+ readonly type: "string";
110
+ readonly multiple: true;
111
+ };
112
+ readonly version: {
113
+ readonly type: "string";
114
+ };
100
115
  };
101
116
  /** Parsed input for `changeset-tag`. */
102
117
  export interface ChangesetTagArgs {
103
118
  push: boolean;
104
119
  dryRun: boolean;
120
+ untagged: boolean;
105
121
  githubRelease: boolean;
106
122
  since?: string;
123
+ packages: string[];
124
+ version?: string;
107
125
  }
108
126
  /**
109
127
  * Parse the `changeset-tag` options.
@@ -74,6 +74,7 @@ export function parseChangesetStatusArgs(values) {
74
74
  export const changesetVersionArgs = {
75
75
  'dry-run': { type: 'boolean' },
76
76
  date: { type: 'string' },
77
+ package: { type: 'string', multiple: true },
77
78
  };
78
79
  /**
79
80
  * Validate and normalize a release date option.
@@ -104,17 +105,25 @@ export function parseChangesetDate(value) {
104
105
  */
105
106
  export function parseChangesetVersionArgs(values) {
106
107
  const date = parseChangesetDate(getString(values, 'date'));
108
+ const packages = getStrings(values, 'package');
109
+ if (packages.some((name) => !name.trim())) {
110
+ throw new ValidationError('--package <name> must not be empty.');
111
+ }
107
112
  return {
108
113
  dryRun: getBoolean(values, 'dry-run'),
109
114
  ...(date ? { date } : {}),
115
+ ...(packages.length ? { packages } : {}),
110
116
  };
111
117
  }
112
118
  /** Parse-args options for `changeset-tag`. */
113
119
  export const changesetTagArgs = {
114
120
  push: { type: 'boolean' },
115
121
  'dry-run': { type: 'boolean' },
122
+ untagged: { type: 'boolean' },
116
123
  'github-release': { type: 'boolean' },
117
124
  since: { type: 'string' },
125
+ package: { type: 'string', multiple: true },
126
+ version: { type: 'string' },
118
127
  };
119
128
  /**
120
129
  * Parse the `changeset-tag` options.
@@ -126,14 +135,29 @@ export function parseChangesetTagArgs(values) {
126
135
  const args = {
127
136
  push: getBoolean(values, 'push'),
128
137
  dryRun: getBoolean(values, 'dry-run'),
138
+ untagged: getBoolean(values, 'untagged'),
129
139
  githubRelease: getBoolean(values, 'github-release'),
130
140
  since: getString(values, 'since'),
141
+ packages: getStrings(values, 'package'),
142
+ version: getString(values, 'version')?.trim(),
131
143
  };
132
144
  if ('' === args.since?.trim()) {
133
145
  throw new ValidationError('--since <ref> must not be empty.');
134
146
  }
147
+ if (args.packages.some((name) => !name.trim())) {
148
+ throw new ValidationError('--package <name> must not be empty.');
149
+ }
150
+ if ('' === args.version) {
151
+ throw new ValidationError('--version <version> must not be empty.');
152
+ }
153
+ if (undefined !== args.version && 1 !== args.packages.length) {
154
+ throw new ValidationError('--version <version> requires exactly one --package <name>.');
155
+ }
135
156
  if (args.githubRelease && !args.push) {
136
157
  throw new ValidationError('--github-release requires --push.');
137
158
  }
159
+ if (args.untagged && args.push) {
160
+ throw new ValidationError('--untagged cannot be combined with --push.');
161
+ }
138
162
  return args;
139
163
  }
@@ -10,12 +10,22 @@ declare const _default: {
10
10
  readonly 'dry-run': {
11
11
  readonly type: "boolean";
12
12
  };
13
+ readonly untagged: {
14
+ readonly type: "boolean";
15
+ };
13
16
  readonly 'github-release': {
14
17
  readonly type: "boolean";
15
18
  };
16
19
  readonly since: {
17
20
  readonly type: "string";
18
21
  };
22
+ readonly package: {
23
+ readonly type: "string";
24
+ readonly multiple: true;
25
+ };
26
+ readonly version: {
27
+ readonly type: "string";
28
+ };
19
29
  };
20
30
  run({ values, cwd, configFile, logger }: import("./command.ts").CommandContext): Promise<{
21
31
  exitCode: number;