@williamthorsen/release-kit 4.8.0 → 5.1.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 (100) hide show
  1. package/CHANGELOG.md +134 -4
  2. package/README.md +404 -40
  3. package/cliff.toml.template +2 -1
  4. package/dist/esm/.cache +1 -1
  5. package/dist/esm/assertCleanWorkingTree.js +1 -1
  6. package/dist/esm/bin/release-kit.js +45 -14
  7. package/dist/esm/buildChangelogEntries.d.ts +3 -0
  8. package/dist/esm/{generateChangelogJson.js → buildChangelogEntries.js} +40 -77
  9. package/dist/esm/buildDependencyGraph.d.ts +4 -3
  10. package/dist/esm/buildDependencyGraph.js +18 -11
  11. package/dist/esm/buildReleaseSummary.js +12 -4
  12. package/dist/esm/buildSyntheticChangelogEntry.d.ts +5 -0
  13. package/dist/esm/buildSyntheticChangelogEntry.js +13 -0
  14. package/dist/esm/bumpAllVersions.d.ts +1 -0
  15. package/dist/esm/bumpAllVersions.js +16 -2
  16. package/dist/esm/bumpVersion.js +3 -0
  17. package/dist/esm/changelogJsonFile.d.ts +4 -0
  18. package/dist/esm/changelogJsonFile.js +68 -0
  19. package/dist/esm/commitCommand.js +1 -1
  20. package/dist/esm/compareVersions.d.ts +1 -0
  21. package/dist/esm/compareVersions.js +27 -0
  22. package/dist/esm/createGithubRelease.d.ts +6 -2
  23. package/dist/esm/createGithubRelease.js +17 -17
  24. package/dist/esm/createGithubReleaseCommand.d.ts +1 -0
  25. package/dist/esm/createGithubReleaseCommand.js +41 -0
  26. package/dist/esm/decideRelease.d.ts +25 -0
  27. package/dist/esm/decideRelease.js +28 -0
  28. package/dist/esm/defaults.d.ts +1 -0
  29. package/dist/esm/defaults.js +7 -3
  30. package/dist/esm/deriveWorkspaceConfig.d.ts +2 -0
  31. package/dist/esm/deriveWorkspaceConfig.js +37 -0
  32. package/dist/esm/detectUndeclaredTagPrefixes.d.ts +7 -0
  33. package/dist/esm/detectUndeclaredTagPrefixes.js +46 -0
  34. package/dist/esm/generateChangelogs.d.ts +1 -1
  35. package/dist/esm/generateChangelogs.js +14 -3
  36. package/dist/esm/getCommitsSinceTarget.d.ts +1 -1
  37. package/dist/esm/getCommitsSinceTarget.js +8 -4
  38. package/dist/esm/index.d.ts +2 -39
  39. package/dist/esm/index.js +0 -75
  40. package/dist/esm/init/initCommand.js +1 -1
  41. package/dist/esm/init/scaffold.d.ts +1 -1
  42. package/dist/esm/init/scaffold.js +8 -5
  43. package/dist/esm/init/templates.d.ts +1 -0
  44. package/dist/esm/init/templates.js +35 -5
  45. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +6 -1
  46. package/dist/esm/injectReleaseNotesIntoReadme.js +20 -7
  47. package/dist/esm/loadConfig.d.ts +12 -2
  48. package/dist/esm/loadConfig.js +161 -14
  49. package/dist/esm/parseRequestedTags.d.ts +1 -0
  50. package/dist/esm/parseRequestedTags.js +10 -0
  51. package/dist/esm/prepareCommand.d.ts +3 -1
  52. package/dist/esm/prepareCommand.js +121 -31
  53. package/dist/esm/previewTagPrefixes.d.ts +30 -0
  54. package/dist/esm/previewTagPrefixes.js +120 -0
  55. package/dist/esm/propagateBumps.d.ts +1 -0
  56. package/dist/esm/propagateBumps.js +1 -1
  57. package/dist/esm/publish.d.ts +0 -1
  58. package/dist/esm/publish.js +3 -3
  59. package/dist/esm/publishCommand.js +18 -14
  60. package/dist/esm/pushCommand.js +5 -4
  61. package/dist/esm/readCurrentVersion.d.ts +1 -0
  62. package/dist/esm/readCurrentVersion.js +21 -0
  63. package/dist/esm/releasePrepare.d.ts +2 -0
  64. package/dist/esm/releasePrepare.js +140 -54
  65. package/dist/esm/releasePrepareMono.js +312 -143
  66. package/dist/esm/releasePrepareProject.d.ts +9 -0
  67. package/dist/esm/releasePrepareProject.js +109 -0
  68. package/dist/esm/renderReleaseNotes.d.ts +1 -0
  69. package/dist/esm/renderReleaseNotes.js +29 -2
  70. package/dist/esm/reportPrepare.js +146 -73
  71. package/dist/esm/resolveCliffConfigPath.js +1 -1
  72. package/dist/esm/resolveCommandTags.d.ts +1 -1
  73. package/dist/esm/resolveCommandTags.js +17 -13
  74. package/dist/esm/resolveReleaseNotesConfig.d.ts +8 -1
  75. package/dist/esm/resolveReleaseNotesConfig.js +17 -7
  76. package/dist/esm/resolveReleaseTags.d.ts +2 -1
  77. package/dist/esm/resolveReleaseTags.js +19 -14
  78. package/dist/esm/showTagPrefixesCommand.d.ts +1 -0
  79. package/dist/esm/showTagPrefixesCommand.js +84 -0
  80. package/dist/esm/sync-labels/initCommand.js +1 -1
  81. package/dist/esm/sync-labels/presets.js +1 -1
  82. package/dist/esm/tagCommand.js +1 -1
  83. package/dist/esm/types.d.ts +77 -19
  84. package/dist/esm/validateConfig.js +205 -36
  85. package/dist/esm/validateOnlyExcludesStrandedDependents.d.ts +14 -0
  86. package/dist/esm/validateOnlyExcludesStrandedDependents.js +109 -0
  87. package/dist/esm/version.d.ts +1 -1
  88. package/dist/esm/version.js +1 -1
  89. package/dist/esm/writeReleaseNotesPreviews.d.ts +18 -0
  90. package/dist/esm/writeReleaseNotesPreviews.js +65 -0
  91. package/package.json +5 -2
  92. package/presets/labels/common.yaml +9 -6
  93. package/schemas/label-map.json +24 -0
  94. package/dist/esm/component.d.ts +0 -2
  95. package/dist/esm/component.js +0 -14
  96. package/dist/esm/findPackageRoot.d.ts +0 -1
  97. package/dist/esm/findPackageRoot.js +0 -17
  98. package/dist/esm/generateChangelogJson.d.ts +0 -7
  99. package/dist/esm/githubReleaseCommand.d.ts +0 -1
  100. 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]
@@ -164,8 +183,8 @@ Publish packages that have release tags on HEAD.
164
183
 
165
184
  Options:
166
185
  --dry-run Preview without publishing
167
- --no-git-checks Skip git checks (pnpm only)
168
- --only=name1,name2 Only publish the named packages (comma-separated, monorepo only)
186
+ --no-git-checks Skip the clean-working-tree check
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();
@@ -0,0 +1,3 @@
1
+ import type { GenerateChangelogOptions } from './generateChangelogs.ts';
2
+ import type { ChangelogEntry, ReleaseConfig } from './types.ts';
3
+ export declare function buildChangelogEntries(config: Pick<ReleaseConfig, 'cliffConfigPath' | 'changelogJson'>, tag: string, options?: GenerateChangelogOptions): ChangelogEntry[];
@@ -1,16 +1,11 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { copyFileSync, mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
- import { dirname, join } from "node:path";
5
- import stringify from "json-stringify-pretty-compact";
6
- import { extractVersion, isChangelogEntry } from "./changelogJsonUtils.js";
4
+ import { join } from "node:path";
5
+ import { extractVersion } from "./changelogJsonUtils.js";
7
6
  import { resolveCliffConfigPath } from "./resolveCliffConfigPath.js";
8
7
  import { isRecord, isUnknownArray } from "./typeGuards.js";
9
- function generateChangelogJson(config, changelogPath, tag, dryRun, options) {
10
- const outputFile = join(changelogPath, config.changelogJson.outputPath);
11
- if (dryRun) {
12
- return [outputFile];
13
- }
8
+ function buildChangelogEntries(config, tag, options) {
14
9
  const resolvedConfigPath = resolveCliffConfigPath(config.cliffConfigPath, import.meta.url);
15
10
  let cliffConfigPath = resolvedConfigPath;
16
11
  let tempDir;
@@ -33,15 +28,10 @@ function generateChangelogJson(config, changelogPath, tag, dryRun, options) {
33
28
  });
34
29
  const releases = parseCliffContext(contextJson);
35
30
  const devOnlySections = new Set(config.changelogJson.devOnlySections);
36
- const entries = transformReleases(releases, devOnlySections);
37
- const existingEntries = readExistingEntries(outputFile);
38
- const merged = mergeEntries(entries, existingEntries);
39
- mkdirSync(dirname(outputFile), { recursive: true });
40
- writeFileSync(outputFile, stringify(merged, { maxLength: 100 }) + "\n", "utf8");
41
- return [outputFile];
31
+ return transformReleases(releases, devOnlySections);
42
32
  } catch (error) {
43
33
  throw new Error(
44
- `Failed to generate changelog JSON for ${outputFile}: ${error instanceof Error ? error.message : String(error)}`
34
+ `Failed to build changelog entries for tag ${tag}: ${error instanceof Error ? error.message : String(error)}`
45
35
  );
46
36
  } finally {
47
37
  if (tempDir !== void 0) {
@@ -49,25 +39,6 @@ function generateChangelogJson(config, changelogPath, tag, dryRun, options) {
49
39
  }
50
40
  }
51
41
  }
52
- function generateSyntheticChangelogJson(config, changelogPath, newVersion, date, propagatedFrom, dryRun) {
53
- const outputFile = join(changelogPath, config.changelogJson.outputPath);
54
- if (dryRun) {
55
- return [outputFile];
56
- }
57
- const items = propagatedFrom.map((dep) => ({
58
- description: `Bumped \`${dep.packageName}\` to ${dep.newVersion}`
59
- }));
60
- const entry = {
61
- version: newVersion,
62
- date,
63
- sections: [{ title: "Dependency updates", audience: "dev", items }]
64
- };
65
- const existingEntries = readExistingEntries(outputFile);
66
- const merged = mergeEntries([entry], existingEntries);
67
- mkdirSync(dirname(outputFile), { recursive: true });
68
- writeFileSync(outputFile, stringify(merged, { maxLength: 100 }) + "\n", "utf8");
69
- return [outputFile];
70
- }
71
42
  function parseCliffContext(json) {
72
43
  const parsed = JSON.parse(json);
73
44
  if (!isUnknownArray(parsed)) {
@@ -115,12 +86,17 @@ function transformReleases(releases, devOnlySections) {
115
86
  for (const commit of release.commits ?? []) {
116
87
  const group = commit.group ?? "Other";
117
88
  const description = extractDescription(commit.message);
89
+ const body = extractBody(commit.message);
118
90
  let items = sectionMap.get(group);
119
91
  if (items === void 0) {
120
92
  items = [];
121
93
  sectionMap.set(group, items);
122
94
  }
123
- items.push({ description });
95
+ const item = { description };
96
+ if (body !== void 0) {
97
+ item.body = body;
98
+ }
99
+ items.push(item);
124
100
  }
125
101
  const sections = [];
126
102
  for (const [title, items] of sectionMap) {
@@ -147,50 +123,37 @@ function extractDescription(message) {
147
123
  }
148
124
  return firstLine;
149
125
  }
150
- function readExistingEntries(filePath) {
151
- if (!existsSync(filePath)) {
152
- return [];
153
- }
154
- try {
155
- const content = readFileSync(filePath, "utf8");
156
- const parsed = JSON.parse(content);
157
- if (!isUnknownArray(parsed)) {
158
- return [];
126
+ const TRAILER_PATTERNS = [
127
+ /^Signed-off-by:/i,
128
+ /^Co-authored-by:/i,
129
+ /^(Closes|Fixes|Resolves)\s+#\d+\s*$/i,
130
+ /^https?:\/\/\S+\/pull\/\d+\/?\s*$/
131
+ ];
132
+ function extractBody(message) {
133
+ const lines = message.split("\n").slice(1);
134
+ let start = 0;
135
+ while (start < lines.length && (lines[start] ?? "").trim() === "") {
136
+ start += 1;
137
+ }
138
+ let end = lines.length;
139
+ while (end > start) {
140
+ const line = lines[end - 1] ?? "";
141
+ const trimmed = line.trim();
142
+ if (trimmed === "") {
143
+ end -= 1;
144
+ continue;
159
145
  }
160
- return parsed.filter(isChangelogEntry);
161
- } catch (error) {
162
- console.warn(
163
- `Warning: could not parse existing ${filePath}: ${error instanceof Error ? error.message : String(error)}; treating as empty`
164
- );
165
- return [];
166
- }
167
- }
168
- function mergeEntries(newEntries, existingEntries) {
169
- const versionMap = /* @__PURE__ */ new Map();
170
- for (const entry of existingEntries) {
171
- versionMap.set(entry.version, entry);
146
+ if (TRAILER_PATTERNS.some((pattern) => pattern.test(trimmed))) {
147
+ end -= 1;
148
+ continue;
149
+ }
150
+ break;
172
151
  }
173
- for (const entry of newEntries) {
174
- versionMap.set(entry.version, entry);
152
+ if (end <= start) {
153
+ return void 0;
175
154
  }
176
- return [...versionMap.values()].sort((a, b) => compareVersionsDescending(a.version, b.version));
177
- }
178
- function parseVersionParts(version) {
179
- return version.split(".").map((s) => {
180
- const n = Number(s);
181
- return Number.isNaN(n) ? 0 : n;
182
- });
183
- }
184
- function compareVersionsDescending(a, b) {
185
- const partsA = parseVersionParts(a);
186
- const partsB = parseVersionParts(b);
187
- for (let i = 0; i < 3; i++) {
188
- const diff = (partsB[i] ?? 0) - (partsA[i] ?? 0);
189
- if (diff !== 0) return diff;
190
- }
191
- return 0;
155
+ return lines.slice(start, end).join("\n").trim();
192
156
  }
193
157
  export {
194
- generateChangelogJson,
195
- generateSyntheticChangelogJson
158
+ buildChangelogEntries
196
159
  };
@@ -1,7 +1,8 @@
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
+ dependenciesOf: Map<string, Set<string>>;
6
7
  }
7
- export declare function buildDependencyGraph(components: readonly ComponentConfig[]): DependencyGraph;
8
+ export declare function buildDependencyGraph(workspaces: readonly WorkspaceConfig[]): DependencyGraph;
@@ -2,25 +2,26 @@ 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 dependenciesOf = /* @__PURE__ */ new Map();
10
+ const workspacePackages = /* @__PURE__ */ new Map();
11
+ for (const workspace of workspaces) {
12
+ const primaryPackageFile = workspace.packageFiles[0];
12
13
  if (primaryPackageFile === void 0) {
13
14
  continue;
14
15
  }
15
16
  const pkg = readPackageJsonSubset(primaryPackageFile);
16
- componentPackages.set(component, pkg);
17
+ workspacePackages.set(workspace, pkg);
17
18
  if (pkg.name === void 0) {
18
19
  continue;
19
20
  }
20
- packageNameToDir.set(pkg.name, component.dir);
21
- dirToPackageName.set(component.dir, pkg.name);
21
+ packageNameToDir.set(pkg.name, workspace.dir);
22
+ dirToPackageName.set(workspace.dir, pkg.name);
22
23
  }
23
- for (const [component, pkg] of componentPackages) {
24
+ for (const [workspace, pkg] of workspacePackages) {
24
25
  const allDeps = { ...pkg.dependencies, ...pkg.peerDependencies };
25
26
  for (const [depName, depVersion] of Object.entries(allDeps)) {
26
27
  if (typeof depVersion !== "string" || !depVersion.startsWith("workspace:")) {
@@ -28,13 +29,19 @@ function buildDependencyGraph(components) {
28
29
  }
29
30
  const existing = dependentsOf.get(depName);
30
31
  if (existing === void 0) {
31
- dependentsOf.set(depName, [component]);
32
+ dependentsOf.set(depName, [workspace]);
32
33
  } else {
33
- existing.push(component);
34
+ existing.push(workspace);
35
+ }
36
+ const forward = dependenciesOf.get(workspace.dir);
37
+ if (forward === void 0) {
38
+ dependenciesOf.set(workspace.dir, /* @__PURE__ */ new Set([depName]));
39
+ } else {
40
+ forward.add(depName);
34
41
  }
35
42
  }
36
43
  }
37
- return { packageNameToDir, dirToPackageName, dependentsOf };
44
+ return { packageNameToDir, dirToPackageName, dependentsOf, dependenciesOf };
38
45
  }
39
46
  function readPackageJsonSubset(filePath) {
40
47
  let content;
@@ -1,20 +1,28 @@
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") {
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
  }
16
16
  sections.push(lines.join("\n"));
17
17
  }
18
+ const project = result.project;
19
+ if (project !== void 0 && project.status === "released" && project.commits.length > 0) {
20
+ const lines = [project.tag];
21
+ for (const commit of project.commits) {
22
+ lines.push(`- ${stripScope(commit.message)}`);
23
+ }
24
+ sections.push(lines.join("\n"));
25
+ }
18
26
  return sections.join("\n\n");
19
27
  }
20
28
  export {
@@ -0,0 +1,5 @@
1
+ import type { ChangelogEntry } from './types.ts';
2
+ export declare function buildSyntheticChangelogEntry(propagatedFrom: ReadonlyArray<{
3
+ packageName: string;
4
+ newVersion: string;
5
+ }>, version: string, date: string): ChangelogEntry;
@@ -0,0 +1,13 @@
1
+ function buildSyntheticChangelogEntry(propagatedFrom, version, date) {
2
+ const items = propagatedFrom.map((dep) => ({
3
+ description: `Bumped \`${dep.packageName}\` to ${dep.newVersion}`
4
+ }));
5
+ return {
6
+ version,
7
+ date,
8
+ sections: [{ title: "Dependency updates", audience: "dev", items }]
9
+ };
10
+ }
11
+ export {
12
+ buildSyntheticChangelogEntry
13
+ };
@@ -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`;
@@ -0,0 +1,4 @@
1
+ import type { ChangelogEntry, ReleaseConfig } from './types.ts';
2
+ export declare function resolveChangelogJsonPath(config: Pick<ReleaseConfig, 'changelogJson'>, changelogPath: string): string;
3
+ export declare function writeChangelogJson(filePath: string, entries: ChangelogEntry[]): string;
4
+ export declare function upsertChangelogJson(filePath: string, entries: ChangelogEntry[]): string;
@@ -0,0 +1,68 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import stringify from "json-stringify-pretty-compact";
4
+ import semver from "semver";
5
+ import { isChangelogEntry } from "./changelogJsonUtils.js";
6
+ import { isUnknownArray } from "./typeGuards.js";
7
+ function resolveChangelogJsonPath(config, changelogPath) {
8
+ return join(changelogPath, config.changelogJson.outputPath);
9
+ }
10
+ function writeChangelogJson(filePath, entries) {
11
+ const sorted = sortNewestFirst(entries);
12
+ mkdirSync(dirname(filePath), { recursive: true });
13
+ writeFileSync(filePath, stringify(sorted, { maxLength: 100 }) + "\n", "utf8");
14
+ return filePath;
15
+ }
16
+ function upsertChangelogJson(filePath, entries) {
17
+ const existing = readExistingEntries(filePath);
18
+ const merged = mergeEntries(entries, existing);
19
+ mkdirSync(dirname(filePath), { recursive: true });
20
+ writeFileSync(filePath, stringify(merged, { maxLength: 100 }) + "\n", "utf8");
21
+ return filePath;
22
+ }
23
+ function sortNewestFirst(entries) {
24
+ return [...entries].sort((a, b) => compareVersionsDescending(a.version, b.version));
25
+ }
26
+ function readExistingEntries(filePath) {
27
+ if (!existsSync(filePath)) {
28
+ return [];
29
+ }
30
+ try {
31
+ const content = readFileSync(filePath, "utf8");
32
+ const parsed = JSON.parse(content);
33
+ if (!isUnknownArray(parsed)) {
34
+ return [];
35
+ }
36
+ return parsed.filter(isChangelogEntry);
37
+ } catch (error) {
38
+ console.warn(
39
+ `Warning: could not parse existing ${filePath}: ${error instanceof Error ? error.message : String(error)}; treating as empty`
40
+ );
41
+ return [];
42
+ }
43
+ }
44
+ function mergeEntries(newEntries, existingEntries) {
45
+ const versionMap = /* @__PURE__ */ new Map();
46
+ for (const entry of existingEntries) {
47
+ versionMap.set(entry.version, entry);
48
+ }
49
+ for (const entry of newEntries) {
50
+ versionMap.set(entry.version, entry);
51
+ }
52
+ return sortNewestFirst(versionMap.values());
53
+ }
54
+ function compareVersionsDescending(a, b) {
55
+ const aValid = semver.valid(a);
56
+ const bValid = semver.valid(b);
57
+ if (aValid && bValid) return semver.rcompare(aValid, bValid);
58
+ if (aValid) return -1;
59
+ if (bValid) return 1;
60
+ if (a > b) return -1;
61
+ if (a < b) return 1;
62
+ return 0;
63
+ }
64
+ export {
65
+ resolveChangelogJsonPath,
66
+ upsertChangelogJson,
67
+ writeChangelogJson
68
+ };
@@ -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,