@williamthorsen/release-kit 4.8.0 → 5.0.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +74 -4
  2. package/README.md +310 -40
  3. package/cliff.toml.template +2 -1
  4. package/dist/esm/.cache +1 -1
  5. package/dist/esm/bin/release-kit.js +44 -13
  6. package/dist/esm/buildDependencyGraph.d.ts +3 -3
  7. package/dist/esm/buildDependencyGraph.js +10 -10
  8. package/dist/esm/buildReleaseSummary.js +4 -4
  9. package/dist/esm/bumpAllVersions.d.ts +1 -0
  10. package/dist/esm/bumpAllVersions.js +16 -2
  11. package/dist/esm/bumpVersion.js +3 -0
  12. package/dist/esm/commitCommand.js +1 -1
  13. package/dist/esm/compareVersions.d.ts +1 -0
  14. package/dist/esm/compareVersions.js +27 -0
  15. package/dist/esm/createGithubRelease.d.ts +6 -2
  16. package/dist/esm/createGithubRelease.js +17 -17
  17. package/dist/esm/createGithubReleaseCommand.d.ts +1 -0
  18. package/dist/esm/createGithubReleaseCommand.js +41 -0
  19. package/dist/esm/defaults.js +5 -3
  20. package/dist/esm/deriveWorkspaceConfig.d.ts +2 -0
  21. package/dist/esm/deriveWorkspaceConfig.js +37 -0
  22. package/dist/esm/detectUndeclaredTagPrefixes.d.ts +7 -0
  23. package/dist/esm/detectUndeclaredTagPrefixes.js +46 -0
  24. package/dist/esm/generateChangelogJson.js +37 -1
  25. package/dist/esm/generateChangelogs.d.ts +1 -1
  26. package/dist/esm/generateChangelogs.js +14 -3
  27. package/dist/esm/getCommitsSinceTarget.d.ts +1 -1
  28. package/dist/esm/getCommitsSinceTarget.js +8 -4
  29. package/dist/esm/index.d.ts +7 -3
  30. package/dist/esm/index.js +10 -3
  31. package/dist/esm/init/initCommand.js +1 -1
  32. package/dist/esm/init/scaffold.d.ts +1 -1
  33. package/dist/esm/init/scaffold.js +8 -5
  34. package/dist/esm/init/templates.d.ts +1 -0
  35. package/dist/esm/init/templates.js +33 -3
  36. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +6 -1
  37. package/dist/esm/injectReleaseNotesIntoReadme.js +20 -7
  38. package/dist/esm/loadConfig.d.ts +2 -1
  39. package/dist/esm/loadConfig.js +65 -12
  40. package/dist/esm/parseRequestedTags.d.ts +1 -0
  41. package/dist/esm/parseRequestedTags.js +10 -0
  42. package/dist/esm/prepareCommand.d.ts +3 -1
  43. package/dist/esm/prepareCommand.js +74 -26
  44. package/dist/esm/previewTagPrefixes.d.ts +30 -0
  45. package/dist/esm/previewTagPrefixes.js +120 -0
  46. package/dist/esm/propagateBumps.d.ts +1 -0
  47. package/dist/esm/propagateBumps.js +1 -1
  48. package/dist/esm/publishCommand.js +8 -13
  49. package/dist/esm/pushCommand.js +5 -4
  50. package/dist/esm/readCurrentVersion.d.ts +1 -0
  51. package/dist/esm/readCurrentVersion.js +21 -0
  52. package/dist/esm/releasePrepare.d.ts +2 -0
  53. package/dist/esm/releasePrepare.js +72 -30
  54. package/dist/esm/releasePrepareMono.js +235 -112
  55. package/dist/esm/renderReleaseNotes.d.ts +1 -0
  56. package/dist/esm/renderReleaseNotes.js +29 -2
  57. package/dist/esm/reportPrepare.js +100 -73
  58. package/dist/esm/resolveCliffConfigPath.js +1 -1
  59. package/dist/esm/resolveCommandTags.d.ts +1 -1
  60. package/dist/esm/resolveCommandTags.js +17 -13
  61. package/dist/esm/resolveReleaseNotesConfig.d.ts +8 -1
  62. package/dist/esm/resolveReleaseNotesConfig.js +17 -7
  63. package/dist/esm/resolveReleaseTags.d.ts +2 -1
  64. package/dist/esm/resolveReleaseTags.js +19 -14
  65. package/dist/esm/showTagPrefixesCommand.d.ts +1 -0
  66. package/dist/esm/showTagPrefixesCommand.js +84 -0
  67. package/dist/esm/sync-labels/initCommand.js +1 -1
  68. package/dist/esm/sync-labels/presets.js +1 -1
  69. package/dist/esm/tagCommand.js +1 -1
  70. package/dist/esm/types.d.ts +22 -7
  71. package/dist/esm/validateConfig.js +179 -36
  72. package/dist/esm/version.d.ts +1 -1
  73. package/dist/esm/version.js +1 -1
  74. package/dist/esm/writeReleaseNotesPreviews.d.ts +18 -0
  75. package/dist/esm/writeReleaseNotesPreviews.js +65 -0
  76. package/package.json +2 -2
  77. package/dist/esm/component.d.ts +0 -2
  78. package/dist/esm/component.js +0 -14
  79. package/dist/esm/findPackageRoot.d.ts +0 -1
  80. package/dist/esm/findPackageRoot.js +0 -17
  81. package/dist/esm/githubReleaseCommand.d.ts +0 -1
  82. package/dist/esm/githubReleaseCommand.js +0 -35
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { parseArgs, translateParseError } from "@williamthorsen/node-monorepo-core";
2
+ import { parseArgs, translateParseError } from "@williamthorsen/nmr-core";
3
3
  import { commitCommand } from "../commitCommand.js";
4
- import { githubReleaseCommand } from "../githubReleaseCommand.js";
4
+ import { createGithubReleaseCommand } from "../createGithubReleaseCommand.js";
5
5
  import { initCommand } from "../init/initCommand.js";
6
6
  import { prepareCommand } from "../prepareCommand.js";
7
7
  import { publishCommand } from "../publishCommand.js";
8
8
  import { pushCommand } from "../pushCommand.js";
9
+ import { showTagPrefixesCommand } from "../showTagPrefixesCommand.js";
9
10
  import { generateCommand } from "../sync-labels/generateCommand.js";
10
11
  import { syncLabelsInitCommand } from "../sync-labels/initCommand.js";
11
12
  import { syncLabelsCommand } from "../sync-labels/syncCommand.js";
@@ -21,7 +22,8 @@ Commands:
21
22
  tag Create annotated git tags from the tags file
22
23
  push Push release commit and tags (one push per tag)
23
24
  publish Publish packages with release tags on HEAD
24
- github-release Create GitHub Releases from changelog.json for tags on HEAD
25
+ create-github-release Create GitHub Releases from changelog.json for tags on HEAD
26
+ show-tag-prefixes Show derived and declared legacy tag prefixes per workspace
25
27
  init Initialize release-kit in the current repository
26
28
  sync-labels Manage GitHub label synchronization
27
29
 
@@ -100,9 +102,13 @@ Run release preparation with automatic workspace discovery.
100
102
 
101
103
  Options:
102
104
  --dry-run Run without modifying any files
103
- --bump=major|minor|patch Override the bump type for all components
105
+ --bump=major|minor|patch Override the bump type for all workspaces
106
+ --set-version=X.Y.Z Set an explicit version; bypasses commit-derived bumps. Requires --only in monorepo mode.
104
107
  --no-git-checks, -n Skip the clean-working-tree check
105
- --only=name1,name2 Only process the named components (comma-separated, monorepo only)
108
+ --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only)
109
+ --with-release-notes Also write per-workspace release-notes previews under {workspacePath}/docs/
110
+ (docs/README.v{version}.md and docs/RELEASE_NOTES.v{version}.md).
111
+ Recommended .gitignore entry: packages/*/docs/*.v*.md (or docs/*.v*.md).
106
112
  --help, -h Show this help message
107
113
  `);
108
114
  }
@@ -139,23 +145,36 @@ fires a separate workflow run per tag.
139
145
 
140
146
  Options:
141
147
  --dry-run Preview without pushing
142
- --only=name1,name2 Only push tags for the named packages (comma-separated, monorepo only)
148
+ --tags=tag1,tag2 Only push the named tags (comma-separated, full tag names)
143
149
  --tags-only Skip the branch push (push tags only)
144
150
  --help, -h Show this help message
145
151
  `);
146
152
  }
147
- function showGithubReleaseHelp() {
153
+ function showCreateGithubReleaseHelp() {
148
154
  console.info(`
149
- Usage: release-kit github-release [options]
155
+ Usage: release-kit create-github-release [options]
150
156
 
151
157
  Create GitHub Releases from changelog.json for tags on HEAD.
152
158
 
153
159
  Options:
154
160
  --dry-run Preview without creating releases
155
- --only=name1,name2 Only create releases for the named packages (comma-separated, monorepo only)
161
+ --tags=tag1,tag2 Only create releases for the named tags (comma-separated, full tag names)
156
162
  --help, -h Show this help message
157
163
  `);
158
164
  }
165
+ function showShowTagPrefixesHelp() {
166
+ console.info(`
167
+ Usage: release-kit show-tag-prefixes [options]
168
+
169
+ Print a per-workspace table of derived tag prefixes, tag counts, and declared
170
+ legacy tag prefixes. Surfaces any release-shaped tags whose prefix is neither a
171
+ derived prefix nor declared in \`legacyIdentities\`, with a copy-pasteable
172
+ config snippet.
173
+
174
+ Options:
175
+ --help, -h Show this help message
176
+ `);
177
+ }
159
178
  function showPublishHelp() {
160
179
  console.info(`
161
180
  Usage: release-kit publish [options]
@@ -165,7 +184,7 @@ Publish packages that have release tags on HEAD.
165
184
  Options:
166
185
  --dry-run Preview without publishing
167
186
  --no-git-checks Skip git checks (pnpm only)
168
- --only=name1,name2 Only publish the named packages (comma-separated, monorepo only)
187
+ --tags=tag1,tag2 Only publish the named tags (comma-separated, full tag names)
169
188
  --provenance Generate provenance statement (requires OIDC, not supported by classic yarn)
170
189
  --help, -h Show this help message
171
190
  `);
@@ -218,12 +237,12 @@ if (command === "push") {
218
237
  await pushCommand(flags);
219
238
  process.exit(0);
220
239
  }
221
- if (command === "github-release") {
240
+ if (command === "create-github-release") {
222
241
  if (flags.some((f) => f === "--help" || f === "-h")) {
223
- showGithubReleaseHelp();
242
+ showCreateGithubReleaseHelp();
224
243
  process.exit(0);
225
244
  }
226
- await githubReleaseCommand(flags);
245
+ await createGithubReleaseCommand(flags);
227
246
  process.exit(0);
228
247
  }
229
248
  if (command === "publish") {
@@ -234,6 +253,18 @@ if (command === "publish") {
234
253
  await publishCommand(flags);
235
254
  process.exit(0);
236
255
  }
256
+ if (command === "show-tag-prefixes") {
257
+ if (flags.some((f) => f === "--help" || f === "-h")) {
258
+ showShowTagPrefixesHelp();
259
+ process.exit(0);
260
+ }
261
+ if (flags.length > 0) {
262
+ console.error(`Error: Unknown option: ${flags[0]}`);
263
+ process.exit(1);
264
+ }
265
+ const exitCode = await showTagPrefixesCommand();
266
+ process.exit(exitCode);
267
+ }
237
268
  if (command === "init") {
238
269
  if (flags.some((f) => f === "--help" || f === "-h")) {
239
270
  showInitHelp();
@@ -1,7 +1,7 @@
1
- import type { ComponentConfig } from './types.ts';
1
+ import type { WorkspaceConfig } from './types.ts';
2
2
  export interface DependencyGraph {
3
3
  packageNameToDir: Map<string, string>;
4
4
  dirToPackageName: Map<string, string>;
5
- dependentsOf: Map<string, ComponentConfig[]>;
5
+ dependentsOf: Map<string, WorkspaceConfig[]>;
6
6
  }
7
- export declare function buildDependencyGraph(components: readonly ComponentConfig[]): DependencyGraph;
7
+ export declare function buildDependencyGraph(workspaces: readonly WorkspaceConfig[]): DependencyGraph;
@@ -2,25 +2,25 @@ import { readFileSync } from "node:fs";
2
2
  function isPackageJsonSubset(value) {
3
3
  return typeof value === "object" && value !== null;
4
4
  }
5
- function buildDependencyGraph(components) {
5
+ function buildDependencyGraph(workspaces) {
6
6
  const packageNameToDir = /* @__PURE__ */ new Map();
7
7
  const dirToPackageName = /* @__PURE__ */ new Map();
8
8
  const dependentsOf = /* @__PURE__ */ new Map();
9
- const componentPackages = /* @__PURE__ */ new Map();
10
- for (const component of components) {
11
- const primaryPackageFile = component.packageFiles[0];
9
+ const workspacePackages = /* @__PURE__ */ new Map();
10
+ for (const workspace of workspaces) {
11
+ const primaryPackageFile = workspace.packageFiles[0];
12
12
  if (primaryPackageFile === void 0) {
13
13
  continue;
14
14
  }
15
15
  const pkg = readPackageJsonSubset(primaryPackageFile);
16
- componentPackages.set(component, pkg);
16
+ workspacePackages.set(workspace, pkg);
17
17
  if (pkg.name === void 0) {
18
18
  continue;
19
19
  }
20
- packageNameToDir.set(pkg.name, component.dir);
21
- dirToPackageName.set(component.dir, pkg.name);
20
+ packageNameToDir.set(pkg.name, workspace.dir);
21
+ dirToPackageName.set(workspace.dir, pkg.name);
22
22
  }
23
- for (const [component, pkg] of componentPackages) {
23
+ for (const [workspace, pkg] of workspacePackages) {
24
24
  const allDeps = { ...pkg.dependencies, ...pkg.peerDependencies };
25
25
  for (const [depName, depVersion] of Object.entries(allDeps)) {
26
26
  if (typeof depVersion !== "string" || !depVersion.startsWith("workspace:")) {
@@ -28,9 +28,9 @@ function buildDependencyGraph(components) {
28
28
  }
29
29
  const existing = dependentsOf.get(depName);
30
30
  if (existing === void 0) {
31
- dependentsOf.set(depName, [component]);
31
+ dependentsOf.set(depName, [workspace]);
32
32
  } else {
33
- existing.push(component);
33
+ existing.push(workspace);
34
34
  }
35
35
  }
36
36
  }
@@ -1,15 +1,15 @@
1
1
  import { stripScope } from "./stripScope.js";
2
2
  function buildReleaseSummary(result) {
3
3
  const sections = [];
4
- for (const component of result.components) {
5
- if (component.status !== "released" || component.tag === void 0) {
4
+ for (const workspace of result.workspaces) {
5
+ if (workspace.status !== "released" || workspace.tag === void 0) {
6
6
  continue;
7
7
  }
8
- const commits = component.commits;
8
+ const commits = workspace.commits;
9
9
  if (commits === void 0 || commits.length === 0) {
10
10
  continue;
11
11
  }
12
- const lines = [component.tag];
12
+ const lines = [workspace.tag];
13
13
  for (const commit of commits) {
14
14
  lines.push(`- ${stripScope(commit.message)}`);
15
15
  }
@@ -1,2 +1,3 @@
1
1
  import type { BumpResult, ReleaseType } from './types.ts';
2
2
  export declare function bumpAllVersions(packageFiles: readonly string[], releaseType: ReleaseType, dryRun: boolean): BumpResult;
3
+ export declare function setAllVersions(packageFiles: readonly string[], newVersion: string, dryRun: boolean): BumpResult;
@@ -11,6 +11,20 @@ function bumpAllVersions(packageFiles, releaseType, dryRun) {
11
11
  const firstPkg = readPackageJson(firstFile);
12
12
  const currentVersion = firstPkg.version;
13
13
  const newVersion = bumpVersion(currentVersion, releaseType);
14
+ writeVersionToAllFiles(packageFiles, firstFile, firstPkg, newVersion, dryRun);
15
+ return { currentVersion, newVersion, files: [...packageFiles] };
16
+ }
17
+ function setAllVersions(packageFiles, newVersion, dryRun) {
18
+ const firstFile = packageFiles[0];
19
+ if (firstFile === void 0) {
20
+ throw new Error("No package files specified");
21
+ }
22
+ const firstPkg = readPackageJson(firstFile);
23
+ const currentVersion = firstPkg.version;
24
+ writeVersionToAllFiles(packageFiles, firstFile, firstPkg, newVersion, dryRun);
25
+ return { currentVersion, newVersion, files: [...packageFiles] };
26
+ }
27
+ function writeVersionToAllFiles(packageFiles, firstFile, firstPkg, newVersion, dryRun) {
14
28
  for (const filePath of packageFiles) {
15
29
  if (dryRun) {
16
30
  continue;
@@ -23,7 +37,6 @@ function bumpAllVersions(packageFiles, releaseType, dryRun) {
23
37
  throw new Error(`Failed to write ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
24
38
  }
25
39
  }
26
- return { currentVersion, newVersion, files: [...packageFiles] };
27
40
  }
28
41
  function readPackageJson(filePath) {
29
42
  let content;
@@ -44,5 +57,6 @@ function readPackageJson(filePath) {
44
57
  return parsed;
45
58
  }
46
59
  export {
47
- bumpAllVersions
60
+ bumpAllVersions,
61
+ setAllVersions
48
62
  };
@@ -14,6 +14,9 @@ function bumpVersion(version, releaseType) {
14
14
  const patchNum = Number.parseInt(patch, 10);
15
15
  switch (releaseType) {
16
16
  case "major":
17
+ if (majorNum === 0) {
18
+ return `0.${minorNum + 1}.0`;
19
+ }
17
20
  return `${majorNum + 1}.0.0`;
18
21
  case "minor":
19
22
  return `${majorNum}.${minorNum + 1}.0`;
@@ -1,6 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { readFileSync } from "node:fs";
3
- import { parseArgs, translateParseError } from "@williamthorsen/node-monorepo-core";
3
+ import { parseArgs, translateParseError } from "@williamthorsen/nmr-core";
4
4
  import { RELEASE_SUMMARY_FILE, RELEASE_TAGS_FILE } from "./prepareCommand.js";
5
5
  const commitFlagSchema = {
6
6
  dryRun: { long: "--dry-run", type: "boolean" }
@@ -0,0 +1 @@
1
+ export declare function isForwardVersion(current: string, target: string): boolean;
@@ -0,0 +1,27 @@
1
+ const CANONICAL_SEMVER_PATTERN = /^(\d+)\.(\d+)\.(\d+)$/;
2
+ function parseCanonicalSemver(version) {
3
+ const match = version.match(CANONICAL_SEMVER_PATTERN);
4
+ if (!match) {
5
+ throw new Error(`Invalid semver version: '${version}'`);
6
+ }
7
+ const [, major = "", minor = "", patch = ""] = match;
8
+ return {
9
+ major: Number.parseInt(major, 10),
10
+ minor: Number.parseInt(minor, 10),
11
+ patch: Number.parseInt(patch, 10)
12
+ };
13
+ }
14
+ function isForwardVersion(current, target) {
15
+ const currentParsed = parseCanonicalSemver(current);
16
+ const targetParsed = parseCanonicalSemver(target);
17
+ if (targetParsed.major !== currentParsed.major) {
18
+ return targetParsed.major > currentParsed.major;
19
+ }
20
+ if (targetParsed.minor !== currentParsed.minor) {
21
+ return targetParsed.minor > currentParsed.minor;
22
+ }
23
+ return targetParsed.patch > currentParsed.patch;
24
+ }
25
+ export {
26
+ isForwardVersion
27
+ };
@@ -1,11 +1,15 @@
1
- import type { ReleaseNotesConfig } from './types.ts';
2
1
  export interface CreateGithubReleaseOptions {
3
2
  tag: string;
4
3
  changelogJsonPath: string;
5
4
  dryRun: boolean;
5
+ sectionOrder?: string[];
6
6
  }
7
7
  export declare function createGithubRelease(options: CreateGithubReleaseOptions): boolean;
8
+ export interface CreateGithubReleasesOutcome {
9
+ created: string[];
10
+ skipped: string[];
11
+ }
8
12
  export declare function createGithubReleases(tags: Array<{
9
13
  tag: string;
10
14
  workspacePath: string;
11
- }>, releaseNotes: ReleaseNotesConfig, changelogJsonOutputPath: string, dryRun: boolean): void;
15
+ }>, changelogJsonOutputPath: string, dryRun: boolean, sectionOrder?: string[]): CreateGithubReleasesOutcome;
@@ -4,7 +4,7 @@ import { join } from "node:path";
4
4
  import { extractVersion, readChangelogEntries } from "./changelogJsonUtils.js";
5
5
  import { matchesAudience, renderReleaseNotesSingle } from "./renderReleaseNotes.js";
6
6
  function createGithubRelease(options) {
7
- const { tag, changelogJsonPath, dryRun } = options;
7
+ const { tag, changelogJsonPath, dryRun, sectionOrder } = options;
8
8
  if (!existsSync(changelogJsonPath)) {
9
9
  console.warn(`Warning: ${changelogJsonPath} not found; skipping GitHub Release creation`);
10
10
  return false;
@@ -25,7 +25,8 @@ function createGithubRelease(options) {
25
25
  }
26
26
  const body = renderReleaseNotesSingle(entry, {
27
27
  filter: matchesAudience("all"),
28
- includeHeading: false
28
+ includeHeading: false,
29
+ ...sectionOrder === void 0 ? {} : { sectionOrder }
29
30
  });
30
31
  if (body.trim() === "") {
31
32
  return false;
@@ -35,24 +36,23 @@ function createGithubRelease(options) {
35
36
  console.info(`[dry-run] Would run: gh ${args.join(" ")}`);
36
37
  return true;
37
38
  }
38
- try {
39
- execFileSync("gh", args, { stdio: "inherit" });
40
- return true;
41
- } catch (error) {
42
- console.warn(
43
- `Warning: failed to create GitHub Release for ${tag}: ${error instanceof Error ? error.message : String(error)}`
44
- );
45
- return false;
46
- }
39
+ execFileSync("gh", args, { stdio: "inherit" });
40
+ return true;
47
41
  }
48
- function createGithubReleases(tags, releaseNotes, changelogJsonOutputPath, dryRun) {
49
- if (!releaseNotes.shouldCreateGithubRelease) {
50
- return;
51
- }
42
+ function createGithubReleases(tags, changelogJsonOutputPath, dryRun, sectionOrder) {
43
+ const created = [];
44
+ const skipped = [];
52
45
  for (const { tag, workspacePath } of tags) {
53
46
  const changelogJsonPath = join(workspacePath, changelogJsonOutputPath);
54
- createGithubRelease({ tag, changelogJsonPath, dryRun });
55
- }
47
+ const result = createGithubRelease({
48
+ tag,
49
+ changelogJsonPath,
50
+ dryRun,
51
+ ...sectionOrder === void 0 ? {} : { sectionOrder }
52
+ });
53
+ (result ? created : skipped).push(tag);
54
+ }
55
+ return { created, skipped };
56
56
  }
57
57
  export {
58
58
  createGithubRelease,
@@ -0,0 +1 @@
1
+ export declare function createGithubReleaseCommand(argv: string[]): Promise<void>;
@@ -0,0 +1,41 @@
1
+ import { parseArgs, translateParseError } from "@williamthorsen/nmr-core";
2
+ import { createGithubReleases } from "./createGithubRelease.js";
3
+ import { parseRequestedTags } from "./parseRequestedTags.js";
4
+ import { resolveCommandTags } from "./resolveCommandTags.js";
5
+ import { resolveReleaseNotesConfig } from "./resolveReleaseNotesConfig.js";
6
+ const createGithubReleaseFlagSchema = {
7
+ dryRun: { long: "--dry-run", type: "boolean" },
8
+ tags: { long: "--tags", type: "string" }
9
+ };
10
+ async function createGithubReleaseCommand(argv) {
11
+ let parsed;
12
+ try {
13
+ parsed = parseArgs(argv, createGithubReleaseFlagSchema);
14
+ } catch (error) {
15
+ console.error(`Error: ${translateParseError(error)}`);
16
+ process.exit(1);
17
+ }
18
+ const { dryRun } = parsed.flags;
19
+ const requestedTags = parseRequestedTags(parsed.flags.tags);
20
+ const resolvedTags = await resolveCommandTags(requestedTags);
21
+ const { changelogJsonOutputPath, sectionOrder } = await resolveReleaseNotesConfig({ strictLoad: true });
22
+ let outcome;
23
+ try {
24
+ outcome = createGithubReleases(resolvedTags, changelogJsonOutputPath, dryRun, sectionOrder);
25
+ } catch (error) {
26
+ console.error(`Error creating GitHub Releases: ${error instanceof Error ? error.message : String(error)}`);
27
+ process.exit(1);
28
+ }
29
+ if (requestedTags !== void 0 && outcome.created.length === 0) {
30
+ console.error(
31
+ `Error: no GitHub Releases were created for requested tags: ${outcome.skipped.join(", ")}. Each was skipped (missing changelog entry, no all-audience content, or empty rendered body).`
32
+ );
33
+ process.exit(1);
34
+ }
35
+ if (outcome.skipped.length > 0) {
36
+ console.info(`Skipped ${outcome.skipped.length} tag(s) with no releasable content: ${outcome.skipped.join(", ")}.`);
37
+ }
38
+ }
39
+ export {
40
+ createGithubReleaseCommand
41
+ };
@@ -11,6 +11,9 @@ const DEFAULT_WORK_TYPES = {
11
11
  ci: { header: "CI" },
12
12
  deps: { header: "Dependencies", aliases: ["dep"] },
13
13
  docs: { header: "Documentation", aliases: ["doc"] },
14
+ ai: { header: "Agentic support" },
15
+ // `fmt` is retained for bump-determination (`parseCommitMessage`), even though
16
+ // `cliff.toml.template` skips `fmt:` commits so they never enter the changelog.
14
17
  fmt: { header: "Formatting" }
15
18
  };
16
19
  const DEFAULT_VERSION_PATTERNS = {
@@ -20,11 +23,10 @@ const DEFAULT_VERSION_PATTERNS = {
20
23
  const DEFAULT_CHANGELOG_JSON_CONFIG = {
21
24
  enabled: true,
22
25
  outputPath: ".meta/changelog.json",
23
- devOnlySections: ["CI", "Dependencies", "Formatting", "Internal", "Tests", "Tooling"]
26
+ devOnlySections: ["Agentic support", "CI", "Dependencies", "Internal", "Refactoring", "Tests", "Tooling"]
24
27
  };
25
28
  const DEFAULT_RELEASE_NOTES_CONFIG = {
26
- shouldInjectIntoReadme: false,
27
- shouldCreateGithubRelease: false
29
+ shouldInjectIntoReadme: false
28
30
  };
29
31
  export {
30
32
  DEFAULT_CHANGELOG_JSON_CONFIG,
@@ -0,0 +1,2 @@
1
+ import type { WorkspaceConfig } from './types.ts';
2
+ export declare function deriveWorkspaceConfig(workspacePath: string): WorkspaceConfig;
@@ -0,0 +1,37 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { basename } from "node:path";
3
+ import { isRecord } from "./typeGuards.js";
4
+ function deriveWorkspaceConfig(workspacePath) {
5
+ const dir = basename(workspacePath);
6
+ const packageJsonPath = `${workspacePath}/package.json`;
7
+ let parsed;
8
+ try {
9
+ parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
10
+ } catch (error) {
11
+ const message = error instanceof Error ? error.message : String(error);
12
+ throw new Error(`Failed to read ${packageJsonPath}: ${message}`);
13
+ }
14
+ const name = isRecord(parsed) ? parsed.name : void 0;
15
+ if (typeof name !== "string" || name.length === 0) {
16
+ throw new Error(`${packageJsonPath} is missing a 'name' field (required for tag derivation).`);
17
+ }
18
+ const unscopedName = stripNpmScope(name);
19
+ return {
20
+ dir,
21
+ name,
22
+ tagPrefix: `${unscopedName}-v`,
23
+ workspacePath,
24
+ packageFiles: [packageJsonPath],
25
+ changelogPaths: [workspacePath],
26
+ paths: [`${workspacePath}/**`]
27
+ };
28
+ }
29
+ function stripNpmScope(name) {
30
+ if (name.startsWith("@") && name.includes("/")) {
31
+ return name.slice(name.indexOf("/") + 1);
32
+ }
33
+ return name;
34
+ }
35
+ export {
36
+ deriveWorkspaceConfig
37
+ };
@@ -0,0 +1,7 @@
1
+ export interface UndeclaredTagPrefix {
2
+ prefix: string;
3
+ tagCount: number;
4
+ exampleTags: string[];
5
+ suggestedDir: string;
6
+ }
7
+ export declare function detectUndeclaredTagPrefixes(knownPrefixes: readonly string[]): UndeclaredTagPrefix[];
@@ -0,0 +1,46 @@
1
+ import { execFileSync } from "node:child_process";
2
+ const EXAMPLE_TAG_LIMIT = 3;
3
+ const CANDIDATE_TAG_PATTERN = /^(?<prefix>[a-z][a-z0-9-]*-v)\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/;
4
+ function detectUndeclaredTagPrefixes(knownPrefixes) {
5
+ const known = new Set(knownPrefixes);
6
+ let rawOutput;
7
+ try {
8
+ rawOutput = execFileSync("git", ["tag", "--list"], {
9
+ encoding: "utf8",
10
+ stdio: ["pipe", "pipe", "pipe"]
11
+ });
12
+ } catch {
13
+ return [];
14
+ }
15
+ const grouped = /* @__PURE__ */ new Map();
16
+ for (const line of rawOutput.split("\n")) {
17
+ const tag = line.trim();
18
+ if (tag === "") continue;
19
+ const match = CANDIDATE_TAG_PATTERN.exec(tag);
20
+ if (match === null) continue;
21
+ const prefix = match.groups?.prefix ?? "";
22
+ if (prefix === "" || known.has(prefix)) continue;
23
+ let tags = grouped.get(prefix);
24
+ if (tags === void 0) {
25
+ tags = [];
26
+ grouped.set(prefix, tags);
27
+ }
28
+ tags.push(tag);
29
+ }
30
+ const results = [];
31
+ for (const [prefix, tags] of grouped) {
32
+ results.push({
33
+ prefix,
34
+ tagCount: tags.length,
35
+ exampleTags: tags.slice(0, EXAMPLE_TAG_LIMIT),
36
+ suggestedDir: stripTrailingTagMarker(prefix)
37
+ });
38
+ }
39
+ return results.sort((a, b) => a.prefix.localeCompare(b.prefix));
40
+ }
41
+ function stripTrailingTagMarker(prefix) {
42
+ return prefix.endsWith("-v") ? prefix.slice(0, -2) : prefix;
43
+ }
44
+ export {
45
+ detectUndeclaredTagPrefixes
46
+ };
@@ -115,12 +115,17 @@ function transformReleases(releases, devOnlySections) {
115
115
  for (const commit of release.commits ?? []) {
116
116
  const group = commit.group ?? "Other";
117
117
  const description = extractDescription(commit.message);
118
+ const body = extractBody(commit.message);
118
119
  let items = sectionMap.get(group);
119
120
  if (items === void 0) {
120
121
  items = [];
121
122
  sectionMap.set(group, items);
122
123
  }
123
- items.push({ description });
124
+ const item = { description };
125
+ if (body !== void 0) {
126
+ item.body = body;
127
+ }
128
+ items.push(item);
124
129
  }
125
130
  const sections = [];
126
131
  for (const [title, items] of sectionMap) {
@@ -147,6 +152,37 @@ function extractDescription(message) {
147
152
  }
148
153
  return firstLine;
149
154
  }
155
+ const TRAILER_PATTERNS = [
156
+ /^Signed-off-by:/i,
157
+ /^Co-authored-by:/i,
158
+ /^(Closes|Fixes|Resolves)\s+#\d+\s*$/i,
159
+ /^https?:\/\/\S+\/pull\/\d+\/?\s*$/
160
+ ];
161
+ function extractBody(message) {
162
+ const lines = message.split("\n").slice(1);
163
+ let start = 0;
164
+ while (start < lines.length && (lines[start] ?? "").trim() === "") {
165
+ start += 1;
166
+ }
167
+ let end = lines.length;
168
+ while (end > start) {
169
+ const line = lines[end - 1] ?? "";
170
+ const trimmed = line.trim();
171
+ if (trimmed === "") {
172
+ end -= 1;
173
+ continue;
174
+ }
175
+ if (TRAILER_PATTERNS.some((pattern) => pattern.test(trimmed))) {
176
+ end -= 1;
177
+ continue;
178
+ }
179
+ break;
180
+ }
181
+ if (end <= start) {
182
+ return void 0;
183
+ }
184
+ return lines.slice(start, end).join("\n").trim();
185
+ }
150
186
  function readExistingEntries(filePath) {
151
187
  if (!existsSync(filePath)) {
152
188
  return [];
@@ -1,5 +1,5 @@
1
1
  import type { ReleaseConfig } from './types.ts';
2
- export declare function buildTagPattern(tagPrefix: string): string;
2
+ export declare function buildTagPattern(tagPrefixes: readonly string[]): string;
3
3
  export interface GenerateChangelogOptions {
4
4
  tagPattern?: string;
5
5
  includePaths?: string[];
@@ -3,8 +3,19 @@ import { copyFileSync, mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { resolveCliffConfigPath } from "./resolveCliffConfigPath.js";
6
- function buildTagPattern(tagPrefix) {
7
- return `${tagPrefix}[0-9].*`;
6
+ function buildTagPattern(tagPrefixes) {
7
+ if (tagPrefixes.length === 0) {
8
+ throw new Error("buildTagPattern: tagPrefixes must contain at least one entry");
9
+ }
10
+ if (tagPrefixes.length === 1) {
11
+ const single = tagPrefixes[0] ?? "";
12
+ return `${single}[0-9].*`;
13
+ }
14
+ const escaped = tagPrefixes.map(escapeRegex);
15
+ return `(${escaped.join("|")})[0-9].*`;
16
+ }
17
+ function escapeRegex(value) {
18
+ return value.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
8
19
  }
9
20
  function generateChangelog(config, changelogPath, tag, dryRun, options) {
10
21
  const resolvedConfigPath = resolveCliffConfigPath(config.cliffConfigPath, import.meta.url);
@@ -41,7 +52,7 @@ function generateChangelog(config, changelogPath, tag, dryRun, options) {
41
52
  }
42
53
  }
43
54
  function generateChangelogs(config, tag, dryRun) {
44
- const tagPattern = buildTagPattern(config.tagPrefix);
55
+ const tagPattern = buildTagPattern([config.tagPrefix]);
45
56
  const results = [];
46
57
  for (const changelogPath of config.changelogPaths) {
47
58
  results.push(...generateChangelog(config, changelogPath, tag, dryRun, { tagPattern }));
@@ -1,5 +1,5 @@
1
1
  import type { Commit } from './types.ts';
2
- export declare function getCommitsSinceTarget(tagPrefix: string, paths?: string[]): {
2
+ export declare function getCommitsSinceTarget(tagPrefixes: readonly string[], paths?: string[]): {
3
3
  tag: string | undefined;
4
4
  commits: Commit[];
5
5
  };