@wpelevator/wp-release 0.6.1 → 0.8.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 { formatChangelogList, 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,12 +122,17 @@ 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);
130
+ const changesetDirectory = path.resolve(input.cwd, config.changesets ?? '.changeset');
131
+ const releases = await getVersionedReleases(input.cwd, config, releasePlan.releases, changesetDirectory);
115
132
  if (!input.dryRun) {
116
133
  const journal = new Map();
117
134
  try {
118
- for (const release of releasePlan.releases) {
135
+ for (const release of releases) {
119
136
  const configured = packages.find((pkg) => pkg.name === release.package.name);
120
137
  if (!configured) {
121
138
  throw new ValidationError(`Unknown package "${release.package.name}".`);
@@ -126,11 +143,8 @@ export async function versionChangesets(input) {
126
143
  await verifyVersionTargets(input.cwd, configured, release.nextVersion);
127
144
  await prependChangelog(configured.directory, release, date, config.readmeChangelog ?? false, config.changelogInsertAfter, journal);
128
145
  }
129
- const changesetDirectory = path.resolve(input.cwd, config.changesets ?? '.changeset');
130
146
  for (const changeset of releasePlan.changesets) {
131
- const file = path.join(changesetDirectory, changeset.fileName);
132
- await rememberFile(journal, file);
133
- await rm(file);
147
+ await consumeChangeset(path.join(changesetDirectory, changeset.fileName), changeset, input.packages, journal);
134
148
  }
135
149
  }
136
150
  catch (error) {
@@ -142,11 +156,118 @@ export async function versionChangesets(input) {
142
156
  }
143
157
  }
144
158
  return {
145
- ...releasePlan,
159
+ changesets: releasePlan.changesets,
160
+ releases,
146
161
  date,
147
162
  changed: !input.dryRun && releasePlan.changesets.length > 0,
148
163
  };
149
164
  }
165
+ /**
166
+ * Format the changelog entries of each release, with the configured
167
+ * formatter and the commit that added each changeset.
168
+ *
169
+ * @param cwd Repository root.
170
+ * @param config Repository release configuration.
171
+ * @param releases Package releases.
172
+ * @param changesetDirectory Directory of the changeset files.
173
+ * @return Releases with their changelog entries.
174
+ */
175
+ async function getVersionedReleases(cwd, config, releases, changesetDirectory) {
176
+ const commits = new Map();
177
+ const getCommit = (changeset) => {
178
+ const cached = commits.get(changeset.fileName) ??
179
+ getChangesetCommit(cwd, path.join(changesetDirectory, changeset.fileName));
180
+ commits.set(changeset.fileName, cached);
181
+ return cached;
182
+ };
183
+ return Promise.all(releases.map(async (release) => {
184
+ const entries = [];
185
+ // Format one at a time, so that formatters calling an API aren't rate limited.
186
+ for (const changeset of release.changesets) {
187
+ entries.push(await getChangelogEntry(config, release.package, changeset, getCommit));
188
+ }
189
+ return { ...release, changelog: formatChangelogList(entries) };
190
+ }));
191
+ }
192
+ async function getChangelogEntry(config, pkg, changeset, getCommit) {
193
+ const summary = changeset.summary.trim();
194
+ if (!summary || !config.changelogEntry) {
195
+ return summary;
196
+ }
197
+ const commit = await getCommit(changeset);
198
+ const entry = await config.changelogEntry({
199
+ package: pkg,
200
+ changeset,
201
+ ...(commit ? { commit } : {}),
202
+ });
203
+ if ('string' !== typeof entry) {
204
+ throw new ValidationError(`The changelogEntry formatter didn't return a string for changeset ${changeset.fileName}.`);
205
+ }
206
+ return entry.trim();
207
+ }
208
+ /**
209
+ * Find the commit that added a changeset file.
210
+ *
211
+ * @param cwd Repository root.
212
+ * @param file Changeset file.
213
+ * @return Commit, or undefined when the file isn't committed.
214
+ */
215
+ async function getChangesetCommit(cwd, file) {
216
+ const output = await git(cwd, [
217
+ 'log',
218
+ '--diff-filter=A',
219
+ '--format=%H%x1f%h%x1f%s',
220
+ '-1',
221
+ '--',
222
+ path.relative(cwd, file).split(path.sep).join('/'),
223
+ ]);
224
+ const [hash, shortHash = '', subject = ''] = output
225
+ .trim()
226
+ .split('\x1f');
227
+ return hash ? { hash, shortHash, subject } : undefined;
228
+ }
229
+ /**
230
+ * Limit a release plan to some packages and the changesets that name them.
231
+ *
232
+ * @param plan Release plan of all pending changesets.
233
+ * @param packages Configured packages.
234
+ * @param names Packages to version.
235
+ * @return Release plan of the named packages.
236
+ * @throws {ValidationError} For unknown packages.
237
+ */
238
+ function selectReleasePlan(plan, packages, names) {
239
+ names.forEach((name) => {
240
+ if (!packages.some((pkg) => pkg.name === name)) {
241
+ throw new ValidationError(`Unknown package "${name}".`);
242
+ }
243
+ });
244
+ return {
245
+ releases: plan.releases.filter((release) => names.includes(release.package.name)),
246
+ changesets: plan.changesets.filter((changeset) => names.some((name) => Object.hasOwn(changeset.packages, name))),
247
+ };
248
+ }
249
+ /**
250
+ * Remove an applied changeset, or only the applied packages from a changeset
251
+ * that names other packages too, so that their releases keep its summary.
252
+ *
253
+ * @param file Changeset file.
254
+ * @param changeset Parsed changeset.
255
+ * @param applied Versioned packages, or undefined when all were versioned.
256
+ * @param journal File journal for rolling back.
257
+ */
258
+ async function consumeChangeset(file, changeset, applied, journal) {
259
+ const remaining = Object.entries(changeset.packages).filter(([name]) => undefined !== applied && !applied.includes(name));
260
+ await rememberFile(journal, file);
261
+ if (remaining.length) {
262
+ await writeFile(file, formatChangeset({
263
+ packages: Object.fromEntries(remaining),
264
+ summary: changeset.summary,
265
+ }));
266
+ }
267
+ else {
268
+ await rm(file);
269
+ }
270
+ }
150
271
  /**
151
272
  * Create missing release tags, optionally pushing them to the configured remote.
152
273
  *
@@ -164,19 +285,26 @@ export async function tagChangesets(input) {
164
285
  if (!context) {
165
286
  throw new ValidationError('No wp-release config found. Add wp-release.config.js or pass --config <path>.');
166
287
  }
288
+ const selected = selectPackages(context.packages, input);
167
289
  const packages = input.since
168
- ? (await changedPackagesSince(input.cwd, context.packages, input.since)).packages
169
- : context.packages;
290
+ ? (await changedPackagesSince(input.cwd, selected, input.since))
291
+ .packages
292
+ : selected;
170
293
  const results = [];
171
294
  const dryRun = input.dryRun || input.untagged;
172
295
  const tagFormat = context.config.tagFormat ?? '{path}/{version}';
173
296
  for (const pkg of packages) {
174
- const version = 'git-tag' === pkg.versionConfig.from.kind
175
- ? await getLatestChangelogVersion(pkg.directory, pkg.currentVersion)
176
- : pkg.currentVersion;
297
+ const version = await getTagVersion(pkg, input.version);
298
+ if (undefined !== input.version && input.version !== version) {
299
+ throw new ValidationError(`Package "${pkg.name}" is at version ${version}, not ${input.version}.`);
300
+ }
177
301
  const tag = formatTag(tagFormat, pkg, version);
178
302
  const exists = await gitTagExists(input.cwd, tag);
179
- const notes = await readReleaseNotes(pkg.directory, version);
303
+ if (!exists) {
304
+ await verifyVersionTargets(input.cwd, pkg, version);
305
+ }
306
+ const previousTag = await getPreviousTag(input.cwd, formatTag(tagFormat, pkg, ''), version);
307
+ const notes = await getReleaseNotes(input.cwd, context.config, pkg, { version, tag, previousTag }, exists ? tag : 'HEAD');
180
308
  if (!exists && !dryRun) {
181
309
  await git(input.cwd, ['tag', tag]);
182
310
  }
@@ -184,6 +312,8 @@ export async function tagChangesets(input) {
184
312
  package: pkg.name,
185
313
  version,
186
314
  tag,
315
+ ...(previousTag ? { previousTag } : {}),
316
+ prerelease: isPrerelease(version),
187
317
  exists,
188
318
  created: !exists && !dryRun,
189
319
  notes,
@@ -223,6 +353,7 @@ async function createGitHubRelease(cwd, release) {
223
353
  `${release.package}@${release.version}`,
224
354
  '--notes',
225
355
  release.notes,
356
+ ...(release.prerelease ? ['--prerelease'] : []),
226
357
  ], { cwd });
227
358
  }
228
359
  catch (error) {
@@ -231,6 +362,141 @@ async function createGitHubRelease(cwd, release) {
231
362
  }
232
363
  }
233
364
  }
365
+ /**
366
+ * Get the version to tag. Packages versioned by their git tags have no
367
+ * version file, so the expected version is the version to tag, or else the
368
+ * latest CHANGELOG.md version.
369
+ *
370
+ * @param pkg Configured package.
371
+ * @param expected Expected version from --version, if any.
372
+ * @return Version to tag.
373
+ */
374
+ async function getTagVersion(pkg, expected) {
375
+ if ('git-tag' !== pkg.versionConfig.from.kind) {
376
+ return pkg.currentVersion;
377
+ }
378
+ return (expected ??
379
+ (await getLatestChangelogVersion(pkg.directory, pkg.currentVersion)));
380
+ }
381
+ /**
382
+ * Pick the packages passed with --package, or all packages without it.
383
+ *
384
+ * @param packages Configured packages.
385
+ * @param input Tag options.
386
+ * @return Selected packages in the configured order.
387
+ * @throws {ValidationError} For unknown packages, or --version without a single package.
388
+ */
389
+ function selectPackages(packages, input) {
390
+ const names = input.packages ?? [];
391
+ if (undefined !== input.version && 1 !== names.length) {
392
+ throw new ValidationError('An expected version requires exactly one package.');
393
+ }
394
+ names.forEach((name) => {
395
+ if (!packages.some((pkg) => pkg.name === name)) {
396
+ throw new ValidationError(`Unknown package "${name}".`);
397
+ }
398
+ });
399
+ return names.length
400
+ ? packages.filter((pkg) => names.includes(pkg.name))
401
+ : packages;
402
+ }
403
+ /**
404
+ * Find the tag of the newest release before a version.
405
+ *
406
+ * @param cwd Repository root.
407
+ * @param prefix Tag prefix of the package, like `plugins/alpha/`.
408
+ * @param version Version being released.
409
+ * @return Previous tag, or undefined for the first release.
410
+ */
411
+ async function getPreviousTag(cwd, prefix, version) {
412
+ const output = await git(cwd, ['tag', '--list', `${prefix}*`]);
413
+ return output
414
+ .trim()
415
+ .split(/\r?\n/)
416
+ .filter((tag) => tag.startsWith(prefix))
417
+ .map((tag) => ({ tag, version: tag.slice(prefix.length) }))
418
+ .filter((item) => isVersion(item.version) &&
419
+ compareVersions(item.version, version) < 0)
420
+ .sort((a, b) => compareVersions(b.version, a.version))[0]?.tag;
421
+ }
422
+ /**
423
+ * Build the release notes of a tag with the configured formatter.
424
+ *
425
+ * @param cwd Repository root.
426
+ * @param config Repository release configuration.
427
+ * @param pkg Released package.
428
+ * @param release Version, tag and previous tag of the release.
429
+ * @param release.version Released version.
430
+ * @param release.tag Release tag.
431
+ * @param release.previousTag Tag of the previous release, if any.
432
+ * @param end Commit of the release: the tag, or HEAD before tagging.
433
+ * @return Release notes.
434
+ */
435
+ async function getReleaseNotes(cwd, config, pkg, release, end) {
436
+ const changelog = await readReleaseNotes(pkg.directory, release.version);
437
+ const commits = await getReleaseCommits(cwd, pkg, end, release.previousTag);
438
+ const notes = changelog ||
439
+ commits
440
+ .map((commit) => `- ${commit.subject} (${commit.shortHash})`)
441
+ .join('\n');
442
+ if (!config.releaseNotes) {
443
+ return notes;
444
+ }
445
+ const formatted = await config.releaseNotes({
446
+ package: toChangesetPackage(pkg),
447
+ ...release,
448
+ changelog,
449
+ commits,
450
+ notes,
451
+ });
452
+ if ('string' !== typeof formatted) {
453
+ throw new ValidationError(`The releaseNotes formatter didn't return a string for package "${pkg.name}".`);
454
+ }
455
+ return formatted.trim();
456
+ }
457
+ /**
458
+ * List the commits that changed a package since its previous release.
459
+ *
460
+ * @param cwd Repository root.
461
+ * @param pkg Released package.
462
+ * @param end Commit of the release.
463
+ * @param previousTag Tag of the previous release, if any.
464
+ * @return Commits, newest first.
465
+ */
466
+ async function getReleaseCommits(cwd, pkg, end, previousTag) {
467
+ const packageFromRepo = path.relative(cwd, pkg.directory).split(path.sep).join('/') ||
468
+ '.';
469
+ const output = await git(cwd, [
470
+ 'log',
471
+ '--format=%H%x1f%h%x1f%s',
472
+ previousTag ? `${previousTag}..${end}` : end,
473
+ '--',
474
+ packageFromRepo,
475
+ ]);
476
+ return output
477
+ .trim()
478
+ .split(/\r?\n/)
479
+ .filter(Boolean)
480
+ .map((line) => {
481
+ const [hash = '', shortHash = '', subject = ''] = line.split('\x1f');
482
+ return { hash, shortHash, subject };
483
+ });
484
+ }
485
+ /**
486
+ * Remove the release configuration from a package passed to config callbacks.
487
+ *
488
+ * @param pkg Configured package.
489
+ * @return Package without its group configuration.
490
+ */
491
+ function toChangesetPackage(pkg) {
492
+ return {
493
+ name: pkg.name,
494
+ directory: pkg.directory,
495
+ path: pkg.path,
496
+ type: pkg.type,
497
+ currentVersion: pkg.currentVersion,
498
+ };
499
+ }
234
500
  /**
235
501
  * Check whether changed files belong to packages without a changeset.
236
502
  *
@@ -549,9 +815,7 @@ async function prependChangelog(directory, release, date, readmeChangelog, chang
549
815
  const summaries = release.changesets
550
816
  .map((item) => item.summary.trim())
551
817
  .filter(Boolean);
552
- const section = `## ${release.nextVersion} (${date})\n\n${summaries
553
- .map((item) => `- ${item.replaceAll(/\r?\n/g, '\n ')}`)
554
- .join('\n')}\n\n`;
818
+ const section = `## ${release.nextVersion} (${date})\n\n${release.changelog}\n\n`;
555
819
  const changelog = path.join(directory, 'CHANGELOG.md');
556
820
  let previous = '';
557
821
  try {
@@ -826,10 +1090,60 @@ function packageType(group) {
826
1090
  }
827
1091
  return 'other';
828
1092
  }
1093
+ const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?$/;
1094
+ function isVersion(version) {
1095
+ return VERSION_PATTERN.test(version);
1096
+ }
1097
+ function isPrerelease(version) {
1098
+ return Boolean(VERSION_PATTERN.exec(version)?.[4]);
1099
+ }
1100
+ /**
1101
+ * Compare two versions with SemVer precedence, where a pre-release like
1102
+ * 1.0.0-rc.1 comes before 1.0.0.
1103
+ *
1104
+ * @param left Version.
1105
+ * @param right Version to compare with.
1106
+ * @return Negative when left is lower, positive when higher, otherwise 0.
1107
+ */
829
1108
  function compareVersions(left, right) {
830
- const a = left.split('.').map(Number);
831
- const b = right.split('.').map(Number);
832
- return ((a[0] ?? 0) - (b[0] ?? 0) ||
1109
+ const [leftCore = '', leftPre] = splitPrerelease(left);
1110
+ const [rightCore = '', rightPre] = splitPrerelease(right);
1111
+ const a = leftCore.split('.').map(Number);
1112
+ const b = rightCore.split('.').map(Number);
1113
+ const core = (a[0] ?? 0) - (b[0] ?? 0) ||
833
1114
  (a[1] ?? 0) - (b[1] ?? 0) ||
834
- (a[2] ?? 0) - (b[2] ?? 0));
1115
+ (a[2] ?? 0) - (b[2] ?? 0);
1116
+ if (core || leftPre === rightPre) {
1117
+ return core;
1118
+ }
1119
+ if (undefined === leftPre || undefined === rightPre) {
1120
+ return undefined === leftPre ? 1 : -1;
1121
+ }
1122
+ return comparePrerelease(leftPre.split('.'), rightPre.split('.'));
1123
+ }
1124
+ function splitPrerelease(version) {
1125
+ const index = version.indexOf('-');
1126
+ return -1 === index
1127
+ ? [version]
1128
+ : [version.slice(0, index), version.slice(index + 1)];
1129
+ }
1130
+ function comparePrerelease(left, right) {
1131
+ const [a, ...leftRest] = left;
1132
+ const [b, ...rightRest] = right;
1133
+ if (undefined === a || undefined === b) {
1134
+ return (undefined === a ? 0 : 1) - (undefined === b ? 0 : 1);
1135
+ }
1136
+ const order = compareIdentifier(a, b);
1137
+ return order || comparePrerelease(leftRest, rightRest);
1138
+ }
1139
+ function compareIdentifier(a, b) {
1140
+ const aNumeric = /^\d+$/.test(a);
1141
+ const bNumeric = /^\d+$/.test(b);
1142
+ if (aNumeric && bNumeric) {
1143
+ return Number(a) - Number(b);
1144
+ }
1145
+ if (aNumeric || bNumeric) {
1146
+ return aNumeric ? -1 : 1;
1147
+ }
1148
+ return Number(a > b) - Number(a < b);
835
1149
  }
@@ -32,6 +32,11 @@ export interface PackageRelease {
32
32
  nextVersion: string;
33
33
  changesets: Changeset[];
34
34
  }
35
+ /** A package release with its formatted changelog entries, once versioned. */
36
+ export interface VersionedRelease extends PackageRelease {
37
+ /** Changelog entries as a Markdown list, as added to CHANGELOG.md. */
38
+ changelog: string;
39
+ }
35
40
  /** Deterministic release plan assembled from pending changesets. */
36
41
  export interface ChangesetReleasePlan {
37
42
  changesets: Changeset[];
@@ -53,6 +58,20 @@ export declare function parseChangeset(contents: string, fileName?: string): Cha
53
58
  * @return Markdown contents with a trailing newline.
54
59
  */
55
60
  export declare function formatChangeset(changeset: Omit<Changeset, 'fileName'>): string;
61
+ /**
62
+ * Format the summaries of changesets as a Markdown list of changelog entries.
63
+ *
64
+ * @param changesets Changesets of a release.
65
+ * @return Markdown list, empty without summaries.
66
+ */
67
+ export declare function formatChangelogEntries(changesets: Changeset[]): string;
68
+ /**
69
+ * Format changelog entries as a Markdown list, indenting their extra lines.
70
+ *
71
+ * @param entries Changelog entries; empty ones are left out.
72
+ * @return Markdown list, empty without entries.
73
+ */
74
+ export declare function formatChangelogList(entries: string[]): string;
56
75
  /**
57
76
  * Return the strongest bump requested for a package across changesets.
58
77
  *
@@ -64,6 +64,27 @@ 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 formatChangelogList(changesets.map((item) => item.summary.trim()));
75
+ }
76
+ /**
77
+ * Format changelog entries as a Markdown list, indenting their extra lines.
78
+ *
79
+ * @param entries Changelog entries; empty ones are left out.
80
+ * @return Markdown list, empty without entries.
81
+ */
82
+ export function formatChangelogList(entries) {
83
+ return entries
84
+ .filter(Boolean)
85
+ .map((item) => `- ${item.replaceAll(/\r?\n/g, '\n ')}`)
86
+ .join('\n');
87
+ }
67
88
  /**
68
89
  * Return the strongest bump requested for a package across changesets.
69
90
  *
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import sign from "./commands/sign.js";
6
6
  import verify from "./commands/verify.js";
7
7
  import versionCheck from "./commands/version-check.js";
8
8
  import deploySvn from "./commands/deploy-svn.js";
9
+ import deployGit from "./commands/deploy-git.js";
9
10
  import { Logger, TextWriter } from "./logger.js";
10
11
  import { getBoolean, getString } from "./commands/command.js";
11
12
  import { ValidationError } from "./errors.js";
@@ -25,6 +26,7 @@ export const commands = [
25
26
  verify,
26
27
  versionCheck,
27
28
  deploySvn,
29
+ deployGit,
28
30
  changesetAdd,
29
31
  changesetStatus,
30
32
  changesetVersion,
@@ -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.
@@ -100,6 +105,13 @@ export declare const changesetTagArgs: {
100
105
  readonly since: {
101
106
  readonly type: "string";
102
107
  };
108
+ readonly package: {
109
+ readonly type: "string";
110
+ readonly multiple: true;
111
+ };
112
+ readonly version: {
113
+ readonly type: "string";
114
+ };
103
115
  };
104
116
  /** Parsed input for `changeset-tag`. */
105
117
  export interface ChangesetTagArgs {
@@ -108,6 +120,8 @@ export interface ChangesetTagArgs {
108
120
  untagged: boolean;
109
121
  githubRelease: boolean;
110
122
  since?: string;
123
+ packages: string[];
124
+ version?: string;
111
125
  }
112
126
  /**
113
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,9 +105,14 @@ 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`. */
@@ -116,6 +122,8 @@ export const changesetTagArgs = {
116
122
  untagged: { type: 'boolean' },
117
123
  'github-release': { type: 'boolean' },
118
124
  since: { type: 'string' },
125
+ package: { type: 'string', multiple: true },
126
+ version: { type: 'string' },
119
127
  };
120
128
  /**
121
129
  * Parse the `changeset-tag` options.
@@ -130,10 +138,21 @@ export function parseChangesetTagArgs(values) {
130
138
  untagged: getBoolean(values, 'untagged'),
131
139
  githubRelease: getBoolean(values, 'github-release'),
132
140
  since: getString(values, 'since'),
141
+ packages: getStrings(values, 'package'),
142
+ version: getString(values, 'version')?.trim(),
133
143
  };
134
144
  if ('' === args.since?.trim()) {
135
145
  throw new ValidationError('--since <ref> must not be empty.');
136
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
+ }
137
156
  if (args.githubRelease && !args.push) {
138
157
  throw new ValidationError('--github-release requires --push.');
139
158
  }
@@ -19,6 +19,13 @@ declare const _default: {
19
19
  readonly since: {
20
20
  readonly type: "string";
21
21
  };
22
+ readonly package: {
23
+ readonly type: "string";
24
+ readonly multiple: true;
25
+ };
26
+ readonly version: {
27
+ readonly type: "string";
28
+ };
22
29
  };
23
30
  run({ values, cwd, configFile, logger }: import("./command.ts").CommandContext): Promise<{
24
31
  exitCode: number;
@@ -1,18 +1,25 @@
1
1
  import { tagChangesets } from "../changeset-release.js";
2
2
  import { parseChangesetTagArgs, changesetTagArgs } from "./changeset-args.js";
3
+ import { getChangesetTagSummary, getTagStatus } from "../summary.js";
3
4
  export default {
4
5
  name: 'changeset-tag',
5
- usage: 'changeset-tag [--dry-run] [--untagged] [--since <ref>] [--push] [--github-release]',
6
+ usage: 'changeset-tag [--dry-run] [--untagged] [--since <ref>] [--package <name>] [--version <version>] [--push] [--github-release]',
6
7
  summary: 'Create missing release tags for configured package versions.',
7
8
  help: `Options:
8
- --dry-run List missing tags without creating them
9
+ --dry-run List missing tags and their release notes without
10
+ creating them
9
11
  --untagged List only releases without a tag, without creating tags
10
12
  --since <ref> Tag packages changed since this git ref
13
+ --package <name> Tag only this package (repeatable)
14
+ --version <version> Fail unless the single --package is at this version;
15
+ the version to tag for packages versioned by git tags
11
16
  --push Push newly created tags to origin
12
- --github-release Create a GitHub Release for each tag
17
+ --github-release Create a GitHub Release for each tag, marked as a
18
+ pre-release for versions like 1.0.0-rc.1
13
19
 
14
20
  Tags are created only when absent, and GitHub Releases are created only when
15
- missing, so rerunning after a partial failure is safe. --github-release also
21
+ missing, so rerunning after a partial failure is safe. Before a tag is created,
22
+ every configured version target must match the version. --github-release also
16
23
  requires --push. With --json or --github-output, --untagged lists the releases
17
24
  that still need a tag, for example to release each of them with a separate
18
25
  workflow.`,
@@ -24,7 +31,17 @@ workflow.`,
24
31
  ...(configFile ? { configFile } : {}),
25
32
  ...args,
26
33
  });
27
- result.tags.forEach((item) => logger.info(`${item.created ? 'Created' : 'Exists'} ${item.tag}`));
34
+ result.tags.forEach((item) => {
35
+ logger.info(`${getTagStatus(item)} ${item.tag}`);
36
+ // A dry run is for previewing the release notes.
37
+ if (args.dryRun) {
38
+ logger.info(item.notes || 'No release notes.');
39
+ }
40
+ else {
41
+ logger.debug(item.notes);
42
+ }
43
+ });
44
+ logger.summary(() => getChangesetTagSummary(result));
28
45
  return { exitCode: 0, result };
29
46
  },
30
47
  };