@williamthorsen/release-kit 4.7.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 (87) hide show
  1. package/CHANGELOG.md +80 -0
  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 +67 -12
  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 +9 -3
  30. package/dist/esm/index.js +12 -3
  31. package/dist/esm/init/detectRepoType.js +1 -2
  32. package/dist/esm/init/initCommand.js +1 -1
  33. package/dist/esm/init/scaffold.d.ts +1 -1
  34. package/dist/esm/init/scaffold.js +8 -5
  35. package/dist/esm/init/templates.d.ts +1 -0
  36. package/dist/esm/init/templates.js +40 -10
  37. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +6 -1
  38. package/dist/esm/injectReleaseNotesIntoReadme.js +20 -7
  39. package/dist/esm/loadConfig.d.ts +2 -1
  40. package/dist/esm/loadConfig.js +65 -12
  41. package/dist/esm/parseRequestedTags.d.ts +1 -0
  42. package/dist/esm/parseRequestedTags.js +10 -0
  43. package/dist/esm/prepareCommand.d.ts +3 -1
  44. package/dist/esm/prepareCommand.js +75 -27
  45. package/dist/esm/previewTagPrefixes.d.ts +30 -0
  46. package/dist/esm/previewTagPrefixes.js +120 -0
  47. package/dist/esm/propagateBumps.d.ts +1 -0
  48. package/dist/esm/propagateBumps.js +1 -1
  49. package/dist/esm/publishCommand.js +8 -13
  50. package/dist/esm/pushCommand.d.ts +1 -0
  51. package/dist/esm/pushCommand.js +47 -0
  52. package/dist/esm/pushRelease.d.ts +11 -0
  53. package/dist/esm/pushRelease.js +26 -0
  54. package/dist/esm/readCurrentVersion.d.ts +1 -0
  55. package/dist/esm/readCurrentVersion.js +21 -0
  56. package/dist/esm/releasePrepare.d.ts +2 -0
  57. package/dist/esm/releasePrepare.js +72 -30
  58. package/dist/esm/releasePrepareMono.js +235 -112
  59. package/dist/esm/renderReleaseNotes.d.ts +1 -0
  60. package/dist/esm/renderReleaseNotes.js +29 -2
  61. package/dist/esm/reportPrepare.js +100 -73
  62. package/dist/esm/resolveCliffConfigPath.js +1 -1
  63. package/dist/esm/resolveCommandTags.d.ts +1 -1
  64. package/dist/esm/resolveCommandTags.js +17 -13
  65. package/dist/esm/resolveReleaseNotesConfig.d.ts +8 -1
  66. package/dist/esm/resolveReleaseNotesConfig.js +17 -7
  67. package/dist/esm/resolveReleaseTags.d.ts +2 -1
  68. package/dist/esm/resolveReleaseTags.js +19 -14
  69. package/dist/esm/showTagPrefixesCommand.d.ts +1 -0
  70. package/dist/esm/showTagPrefixesCommand.js +84 -0
  71. package/dist/esm/sync-labels/initCommand.js +1 -1
  72. package/dist/esm/sync-labels/presets.js +1 -1
  73. package/dist/esm/sync-labels/templates.js +1 -1
  74. package/dist/esm/tagCommand.js +1 -1
  75. package/dist/esm/types.d.ts +22 -7
  76. package/dist/esm/validateConfig.js +179 -36
  77. package/dist/esm/version.d.ts +1 -1
  78. package/dist/esm/version.js +1 -1
  79. package/dist/esm/writeReleaseNotesPreviews.d.ts +18 -0
  80. package/dist/esm/writeReleaseNotesPreviews.js +65 -0
  81. package/package.json +2 -2
  82. package/dist/esm/component.d.ts +0 -2
  83. package/dist/esm/component.js +0 -14
  84. package/dist/esm/findPackageRoot.d.ts +0 -1
  85. package/dist/esm/findPackageRoot.js +0 -17
  86. package/dist/esm/githubReleaseCommand.d.ts +0 -1
  87. package/dist/esm/githubReleaseCommand.js +0 -35
@@ -1,10 +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
+ import { pushCommand } from "../pushCommand.js";
9
+ import { showTagPrefixesCommand } from "../showTagPrefixesCommand.js";
8
10
  import { generateCommand } from "../sync-labels/generateCommand.js";
9
11
  import { syncLabelsInitCommand } from "../sync-labels/initCommand.js";
10
12
  import { syncLabelsCommand } from "../sync-labels/syncCommand.js";
@@ -18,8 +20,10 @@ Commands:
18
20
  prepare Run release preparation (auto-discovers workspaces)
19
21
  commit Stage changes and create the release commit
20
22
  tag Create annotated git tags from the tags file
23
+ push Push release commit and tags (one push per tag)
21
24
  publish Publish packages with release tags on HEAD
22
- 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
23
27
  init Initialize release-kit in the current repository
24
28
  sync-labels Manage GitHub label synchronization
25
29
 
@@ -98,9 +102,13 @@ Run release preparation with automatic workspace discovery.
98
102
 
99
103
  Options:
100
104
  --dry-run Run without modifying any files
101
- --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.
102
107
  --no-git-checks, -n Skip the clean-working-tree check
103
- --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).
104
112
  --help, -h Show this help message
105
113
  `);
106
114
  }
@@ -128,18 +136,45 @@ Options:
128
136
  --help, -h Show this help message
129
137
  `);
130
138
  }
131
- function showGithubReleaseHelp() {
139
+ function showPushHelp() {
132
140
  console.info(`
133
- Usage: release-kit github-release [options]
141
+ Usage: release-kit push [options]
142
+
143
+ Push the release commit and each tag individually, ensuring GitHub Actions
144
+ fires a separate workflow run per tag.
145
+
146
+ Options:
147
+ --dry-run Preview without pushing
148
+ --tags=tag1,tag2 Only push the named tags (comma-separated, full tag names)
149
+ --tags-only Skip the branch push (push tags only)
150
+ --help, -h Show this help message
151
+ `);
152
+ }
153
+ function showCreateGithubReleaseHelp() {
154
+ console.info(`
155
+ Usage: release-kit create-github-release [options]
134
156
 
135
157
  Create GitHub Releases from changelog.json for tags on HEAD.
136
158
 
137
159
  Options:
138
160
  --dry-run Preview without creating releases
139
- --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)
140
162
  --help, -h Show this help message
141
163
  `);
142
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
+ }
143
178
  function showPublishHelp() {
144
179
  console.info(`
145
180
  Usage: release-kit publish [options]
@@ -149,7 +184,7 @@ Publish packages that have release tags on HEAD.
149
184
  Options:
150
185
  --dry-run Preview without publishing
151
186
  --no-git-checks Skip git checks (pnpm only)
152
- --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)
153
188
  --provenance Generate provenance statement (requires OIDC, not supported by classic yarn)
154
189
  --help, -h Show this help message
155
190
  `);
@@ -194,12 +229,20 @@ if (command === "tag") {
194
229
  tagCommand(flags);
195
230
  process.exit(0);
196
231
  }
197
- if (command === "github-release") {
232
+ if (command === "push") {
198
233
  if (flags.some((f) => f === "--help" || f === "-h")) {
199
- showGithubReleaseHelp();
234
+ showPushHelp();
200
235
  process.exit(0);
201
236
  }
202
- await githubReleaseCommand(flags);
237
+ await pushCommand(flags);
238
+ process.exit(0);
239
+ }
240
+ if (command === "create-github-release") {
241
+ if (flags.some((f) => f === "--help" || f === "-h")) {
242
+ showCreateGithubReleaseHelp();
243
+ process.exit(0);
244
+ }
245
+ await createGithubReleaseCommand(flags);
203
246
  process.exit(0);
204
247
  }
205
248
  if (command === "publish") {
@@ -210,6 +253,18 @@ if (command === "publish") {
210
253
  await publishCommand(flags);
211
254
  process.exit(0);
212
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
+ }
213
268
  if (command === "init") {
214
269
  if (flags.some((f) => f === "--help" || f === "-h")) {
215
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
  };