@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
@@ -0,0 +1,109 @@
1
+ import { buildChangelogEntries } from "./buildChangelogEntries.js";
2
+ import { bumpAllVersions } from "./bumpAllVersions.js";
3
+ import { resolveChangelogJsonPath, writeChangelogJson } from "./changelogJsonFile.js";
4
+ import { decideRelease } from "./decideRelease.js";
5
+ import { DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
6
+ import { buildTagPattern, generateChangelog } from "./generateChangelogs.js";
7
+ import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
8
+ import { deriveSectionOrder } from "./resolveReleaseNotesConfig.js";
9
+ import { writeReleaseNotesPreviews } from "./writeReleaseNotesPreviews.js";
10
+ const ROOT_PACKAGE_FILE = "./package.json";
11
+ const ROOT_CHANGELOG_PATH = ".";
12
+ function releasePrepareProject(args) {
13
+ const { config, options, modifiedFiles, tags } = args;
14
+ const { dryRun, bumpOverride, withReleaseNotes, force } = options;
15
+ const project = config.project;
16
+ if (project === void 0) {
17
+ throw new Error("releasePrepareProject called without a configured project block");
18
+ }
19
+ const workTypes = config.workTypes ?? { ...DEFAULT_WORK_TYPES };
20
+ const versionPatterns = config.versionPatterns ?? { ...DEFAULT_VERSION_PATTERNS };
21
+ const contributingPaths = config.workspaces.flatMap((workspace) => workspace.paths);
22
+ const { tag, commits } = getCommitsSinceTarget([project.tagPrefix], contributingPaths);
23
+ const since = tag === void 0 ? "(no previous release found)" : `since ${tag}`;
24
+ const decision = decideRelease({
25
+ commits,
26
+ force,
27
+ bumpOverride,
28
+ workTypes,
29
+ versionPatterns,
30
+ scopeAliases: config.scopeAliases,
31
+ skipReasons: {
32
+ noCommits: `No commits ${since}. Pass --force to release at patch. Skipping.`,
33
+ noBumpWorthy: `No bump-worthy commits ${since}. Pass --force to release at patch (or --force --bump=X for a different level). Skipping.`
34
+ }
35
+ });
36
+ if (decision.outcome === "skip") {
37
+ const skipped = {
38
+ status: "skipped",
39
+ commitCount: commits.length,
40
+ parsedCommitCount: decision.parsedCommitCount,
41
+ skipReason: decision.skipReason
42
+ };
43
+ if (tag !== void 0) {
44
+ skipped.previousTag = tag;
45
+ }
46
+ if (decision.unparseableCommits !== void 0) {
47
+ skipped.unparseableCommits = decision.unparseableCommits;
48
+ }
49
+ return skipped;
50
+ }
51
+ const { releaseType, parsedCommitCount, unparseableCommits } = decision;
52
+ const bump = bumpAllVersions([ROOT_PACKAGE_FILE], releaseType, dryRun);
53
+ const newTag = `${project.tagPrefix}${bump.newVersion}`;
54
+ const tagPattern = buildTagPattern([project.tagPrefix]);
55
+ const changelogFiles = generateChangelog(config, ROOT_CHANGELOG_PATH, newTag, dryRun, {
56
+ tagPattern,
57
+ includePaths: contributingPaths
58
+ });
59
+ const changelogJsonFiles = [];
60
+ if (config.changelogJson.enabled) {
61
+ const changelogJsonPath = resolveChangelogJsonPath(config, ROOT_CHANGELOG_PATH);
62
+ const entries = buildChangelogEntries(config, newTag, {
63
+ tagPattern,
64
+ includePaths: contributingPaths
65
+ });
66
+ if (!dryRun) {
67
+ writeChangelogJson(changelogJsonPath, entries);
68
+ }
69
+ changelogJsonFiles.push(changelogJsonPath);
70
+ }
71
+ const firstChangelogJsonPath = changelogJsonFiles[0];
72
+ if (withReleaseNotes === true && config.changelogJson.enabled && firstChangelogJsonPath !== void 0) {
73
+ const sectionOrder = deriveSectionOrder(workTypes);
74
+ writeReleaseNotesPreviews({
75
+ workspacePath: ROOT_CHANGELOG_PATH,
76
+ tag: newTag,
77
+ changelogJsonPath: firstChangelogJsonPath,
78
+ sectionOrder,
79
+ dryRun
80
+ });
81
+ }
82
+ tags.push(newTag);
83
+ modifiedFiles.push(ROOT_PACKAGE_FILE, ...changelogFiles, ...changelogJsonFiles);
84
+ const result = {
85
+ status: "released",
86
+ commitCount: commits.length,
87
+ parsedCommitCount,
88
+ releaseType,
89
+ currentVersion: bump.currentVersion,
90
+ newVersion: bump.newVersion,
91
+ tag: newTag,
92
+ bumpedFiles: bump.files,
93
+ changelogFiles,
94
+ commits
95
+ };
96
+ if (tag !== void 0) {
97
+ result.previousTag = tag;
98
+ }
99
+ if (unparseableCommits !== void 0) {
100
+ result.unparseableCommits = unparseableCommits;
101
+ }
102
+ if (bumpOverride !== void 0) {
103
+ result.bumpOverride = bumpOverride;
104
+ }
105
+ return result;
106
+ }
107
+ export {
108
+ releasePrepareProject
109
+ };
@@ -2,6 +2,7 @@ import type { ChangelogAudience, ChangelogEntry, ChangelogSection } from './type
2
2
  export interface RenderOptions {
3
3
  filter?: (section: ChangelogSection) => boolean;
4
4
  includeHeading?: boolean;
5
+ sectionOrder?: string[];
5
6
  }
6
7
  export declare function matchesAudience(audience: ChangelogAudience): (section: ChangelogSection) => boolean;
7
8
  export declare function renderReleaseNotesSingle(entry: ChangelogEntry, options?: RenderOptions): string;
@@ -10,7 +10,9 @@ function matchesAudience(audience) {
10
10
  function renderReleaseNotesSingle(entry, options) {
11
11
  const filter = options?.filter;
12
12
  const includeHeading = options?.includeHeading ?? true;
13
- const sections = filter !== void 0 ? entry.sections.filter(filter) : entry.sections;
13
+ const sectionOrder = options?.sectionOrder;
14
+ const filtered = filter !== void 0 ? entry.sections.filter(filter) : entry.sections;
15
+ const sections = sectionOrder !== void 0 ? sortSectionsByOrder(filtered, sectionOrder) : filtered;
14
16
  if (sections.length === 0) {
15
17
  return "";
16
18
  }
@@ -23,12 +25,37 @@ function renderReleaseNotesSingle(entry, options) {
23
25
  lines.push("");
24
26
  }
25
27
  lines.push(`### ${section.title}`, "");
26
- for (const item of section.items) {
28
+ for (const [index, item] of section.items.entries()) {
27
29
  lines.push(`- ${item.description}`);
30
+ if (item.body !== void 0 && item.body.length > 0) {
31
+ lines.push("", ...indentBodyLines(item.body));
32
+ if (index < section.items.length - 1) {
33
+ lines.push("");
34
+ }
35
+ }
28
36
  }
29
37
  }
30
38
  return lines.join("\n") + "\n";
31
39
  }
40
+ function sortSectionsByOrder(sections, order) {
41
+ const priority = /* @__PURE__ */ new Map();
42
+ for (const [index, title] of order.entries()) {
43
+ priority.set(title, index);
44
+ }
45
+ const indexed = sections.map((section, index) => ({ section, index }));
46
+ indexed.sort((a, b) => {
47
+ const priorityA = priority.get(a.section.title) ?? Number.POSITIVE_INFINITY;
48
+ const priorityB = priority.get(b.section.title) ?? Number.POSITIVE_INFINITY;
49
+ if (priorityA !== priorityB) {
50
+ return priorityA - priorityB;
51
+ }
52
+ return a.index - b.index;
53
+ });
54
+ return indexed.map(({ section }) => section);
55
+ }
56
+ function indentBodyLines(body) {
57
+ return body.split("\n").map((line) => line.length === 0 ? "" : ` ${line}`);
58
+ }
32
59
  function renderReleaseNotesMulti(entries, options) {
33
60
  const parts = entries.map((entry) => renderReleaseNotesSingle(entry, options)).filter((part) => part.length > 0);
34
61
  return parts.join("\n");
@@ -1,86 +1,56 @@
1
1
  import { bold, dim, sectionHeader } from "./format.js";
2
2
  function reportPrepare(result) {
3
- const isMultiComponent = result.components.some((c) => c.name !== void 0);
4
- if (isMultiComponent) {
5
- return formatMultiComponent(result);
3
+ const isMultiWorkspace = result.workspaces.some((w) => w.name !== void 0) || result.project !== void 0;
4
+ if (isMultiWorkspace) {
5
+ return formatMultiWorkspace(result);
6
6
  }
7
- return formatSingleComponent(result);
7
+ return formatSingleWorkspace(result);
8
8
  }
9
- function formatSingleComponent(result) {
9
+ function formatSingleWorkspace(result) {
10
10
  const lines = [];
11
- const component = result.components[0];
12
- if (component === void 0) {
11
+ const workspace = result.workspaces[0];
12
+ if (workspace === void 0) {
13
13
  return "";
14
14
  }
15
- const since = component.previousTag === void 0 ? "the beginning" : component.previousTag;
16
- lines.push(dim(`Found ${component.commitCount} commits since ${since}`));
17
- if (component.parsedCommitCount !== void 0) {
18
- lines.push(dim(` Parsed ${component.parsedCommitCount} typed commits`));
15
+ const since = workspace.previousTag === void 0 ? "the beginning" : workspace.previousTag;
16
+ lines.push(dim(`Found ${workspace.commitCount} commits since ${since}`));
17
+ if (workspace.parsedCommitCount !== void 0) {
18
+ lines.push(dim(` Parsed ${workspace.parsedCommitCount} typed commits`));
19
19
  }
20
- formatUnparseableWarning(lines, component);
21
- if (component.status === "skipped") {
22
- lines.push(`\u23ED\uFE0F ${component.skipReason ?? "Skipped"}`);
20
+ formatUnparseableWarning(lines, workspace);
21
+ if (workspace.status === "skipped") {
22
+ lines.push(`\u23ED\uFE0F ${workspace.skipReason}`);
23
23
  return lines.join("\n");
24
24
  }
25
- if (component.parsedCommitCount === void 0 && component.releaseType !== void 0) {
26
- lines.push(` Using bump override: ${component.releaseType}`);
25
+ if (workspace.setVersion !== void 0) {
26
+ lines.push(` Using version override: ${workspace.setVersion}`);
27
+ } else if (workspace.bumpOverride !== void 0) {
28
+ lines.push(` Using bump override: ${workspace.bumpOverride}`);
27
29
  }
28
- if (component.releaseType !== void 0) {
29
- lines.push(dim(`Bumping versions (${component.releaseType})...`));
30
+ if (workspace.releaseType !== void 0) {
31
+ lines.push(dim(`Bumping versions (${workspace.releaseType})...`));
32
+ } else if (workspace.setVersion !== void 0) {
33
+ lines.push(dim(`Bumping versions (version override)...`));
30
34
  }
31
- if (component.currentVersion !== void 0 && component.newVersion !== void 0 && component.releaseType !== void 0) {
32
- lines.push(`\u{1F4E6} ${component.currentVersion} \u2192 ${bold(component.newVersion)} (${component.releaseType})`);
35
+ if (workspace.setVersion !== void 0) {
36
+ lines.push(`\u{1F4E6} ${workspace.currentVersion} \u2192 ${bold(workspace.newVersion)} (version override)`);
37
+ } else if (workspace.releaseType !== void 0) {
38
+ lines.push(`\u{1F4E6} ${workspace.currentVersion} \u2192 ${bold(workspace.newVersion)} (${workspace.releaseType})`);
33
39
  }
34
- formatBumpFiles(lines, component, result.dryRun);
40
+ formatBumpFiles(lines, workspace, result.dryRun);
35
41
  lines.push(dim("Generating changelogs..."));
36
- formatChangelogFiles(lines, component, result.dryRun);
42
+ formatChangelogFiles(lines, workspace, result.dryRun);
37
43
  formatFormatCommand(lines, result);
38
- lines.push(`\u2705 Release preparation complete.`);
39
- if (component.tag !== void 0) {
40
- lines.push(` \u{1F3F7}\uFE0F ${bold(component.tag)}`);
41
- }
44
+ lines.push(`\u2705 Release preparation complete.`, ` \u{1F3F7}\uFE0F ${bold(workspace.tag)}`);
42
45
  return lines.join("\n");
43
46
  }
44
- function formatMultiComponent(result) {
47
+ function formatMultiWorkspace(result) {
45
48
  const lines = [];
46
- for (const component of result.components) {
47
- if (component.name !== void 0) {
48
- lines.push(`
49
- ${sectionHeader(component.name)}`);
50
- }
51
- const since = component.previousTag === void 0 ? "(no previous release found)" : `since ${component.previousTag}`;
52
- lines.push(dim(` Found ${component.commitCount} commits ${since}`));
53
- if (component.status === "skipped") {
54
- lines.push(` \u23ED\uFE0F ${component.skipReason ?? "Skipped"}`);
55
- continue;
56
- }
57
- const { propagatedFrom } = component;
58
- const isPropagatedOnly = propagatedFrom !== void 0 && component.commitCount === 0;
59
- if (isPropagatedOnly) {
60
- const depNames = propagatedFrom.map((p) => p.packageName).join(", ");
61
- lines.push(dim(` 0 commits (bumped via dependency: ${depNames})`));
62
- } else if (component.parsedCommitCount !== void 0) {
63
- lines.push(dim(` Parsed ${component.parsedCommitCount} typed commits`));
64
- }
65
- formatUnparseableWarning(lines, component, " ");
66
- if (component.parsedCommitCount === void 0 && component.releaseType !== void 0 && !isPropagatedOnly) {
67
- lines.push(` Using bump override: ${component.releaseType}`);
68
- }
69
- if (component.releaseType !== void 0) {
70
- lines.push(dim(` Bumping versions (${component.releaseType})...`));
71
- }
72
- if (component.currentVersion !== void 0 && component.newVersion !== void 0 && component.releaseType !== void 0) {
73
- const suffix = isPropagatedOnly ? formatPropagationSuffix(propagatedFrom) : "";
74
- lines.push(
75
- ` \u{1F4E6} ${component.currentVersion} \u2192 ${bold(component.newVersion)} (${component.releaseType}${suffix})`
76
- );
77
- }
78
- formatBumpFiles(lines, component, result.dryRun, " ");
79
- lines.push(dim(" Generating changelogs..."));
80
- formatChangelogFiles(lines, component, result.dryRun, " ");
81
- if (component.tag !== void 0) {
82
- lines.push(` \u{1F3F7}\uFE0F ${bold(component.tag)}`);
83
- }
49
+ for (const workspace of result.workspaces) {
50
+ formatWorkspaceSection(lines, workspace, result.dryRun);
51
+ }
52
+ if (result.project !== void 0) {
53
+ formatProjectSection(lines, result.project, result.dryRun);
84
54
  }
85
55
  formatFormatCommand(lines, result);
86
56
  formatWarnings(lines, result);
@@ -92,12 +62,115 @@ ${sectionHeader(component.name)}`);
92
62
  }
93
63
  } else {
94
64
  lines.push(`
95
- \u23ED\uFE0F No components had release-worthy changes.`);
65
+ \u23ED\uFE0F No workspaces had release-worthy changes.`);
96
66
  }
97
67
  return lines.join("\n");
98
68
  }
99
- function formatBumpFiles(lines, component, dryRun, indent = "") {
100
- for (const file of component.bumpedFiles) {
69
+ function formatProjectSection(lines, project, dryRun) {
70
+ lines.push(`
71
+ ${sectionHeader("project")}`);
72
+ const since = project.previousTag === void 0 ? "(no previous release found)" : `since ${project.previousTag}`;
73
+ lines.push(dim(` Found ${project.commitCount} commits ${since}`));
74
+ if (project.status === "skipped") {
75
+ lines.push(` \u23ED\uFE0F ${project.skipReason}`);
76
+ return;
77
+ }
78
+ const { releaseType, currentVersion, newVersion, tag } = project;
79
+ if (project.parsedCommitCount > 0) {
80
+ lines.push(dim(` Parsed ${project.parsedCommitCount} typed commits`));
81
+ }
82
+ if (project.bumpOverride !== void 0) {
83
+ lines.push(` Using bump override: ${project.bumpOverride}`);
84
+ }
85
+ formatProjectUnparseable(lines, project);
86
+ lines.push(
87
+ dim(` Bumping versions (${releaseType})...`),
88
+ ` \u{1F4E6} ${currentVersion} \u2192 ${bold(newVersion)} (${releaseType})`
89
+ );
90
+ for (const file of project.bumpedFiles) {
91
+ if (dryRun) {
92
+ lines.push(dim(` [dry-run] Would bump ${file}`));
93
+ } else {
94
+ lines.push(dim(` Bumped ${file}`));
95
+ }
96
+ }
97
+ lines.push(dim(" Generating changelogs..."));
98
+ for (const file of project.changelogFiles) {
99
+ if (dryRun) {
100
+ lines.push(dim(` [dry-run] Would run: npx --yes git-cliff ... --output ${file}`));
101
+ } else {
102
+ lines.push(dim(` Generating changelog: ${file}`));
103
+ }
104
+ }
105
+ lines.push(` \u{1F3F7}\uFE0F ${bold(tag)}`);
106
+ }
107
+ function formatProjectUnparseable(lines, project) {
108
+ const unparseable = project.unparseableCommits;
109
+ if (unparseable === void 0 || unparseable.length === 0) {
110
+ return;
111
+ }
112
+ const count = unparseable.length;
113
+ const isPatchFloor = project.parsedCommitCount === 0;
114
+ const suffix = isPatchFloor ? " (defaulting to patch bump)" : "";
115
+ lines.push(` \u26A0\uFE0F ${count} commit${count === 1 ? "" : "s"} could not be parsed${suffix}`);
116
+ for (const commit of unparseable) {
117
+ const shortHash = commit.hash.slice(0, 7);
118
+ const truncatedMessage = commit.message.length > 72 ? `${commit.message.slice(0, 69)}...` : commit.message;
119
+ lines.push(` \xB7 ${shortHash} ${truncatedMessage}`);
120
+ }
121
+ }
122
+ function formatWorkspaceSection(lines, workspace, dryRun) {
123
+ if (workspace.name !== void 0) {
124
+ lines.push(`
125
+ ${sectionHeader(workspace.name)}`);
126
+ }
127
+ const since = workspace.previousTag === void 0 ? "(no previous release found)" : `since ${workspace.previousTag}`;
128
+ lines.push(dim(` Found ${workspace.commitCount} commits ${since}`));
129
+ if (workspace.status === "skipped") {
130
+ lines.push(` \u23ED\uFE0F ${workspace.skipReason}`);
131
+ return;
132
+ }
133
+ const { propagatedFrom } = workspace;
134
+ const isPropagatedOnly = propagatedFrom !== void 0 && workspace.commitCount === 0;
135
+ formatCommitSummary(lines, workspace, propagatedFrom, isPropagatedOnly);
136
+ formatUnparseableWarning(lines, workspace, " ");
137
+ formatBumpLabels(lines, workspace, isPropagatedOnly);
138
+ formatVersionLine(lines, workspace, propagatedFrom, isPropagatedOnly);
139
+ formatBumpFiles(lines, workspace, dryRun, " ");
140
+ lines.push(dim(" Generating changelogs..."));
141
+ formatChangelogFiles(lines, workspace, dryRun, " ");
142
+ lines.push(` \u{1F3F7}\uFE0F ${bold(workspace.tag)}`);
143
+ }
144
+ function formatCommitSummary(lines, workspace, propagatedFrom, isPropagatedOnly) {
145
+ if (isPropagatedOnly && propagatedFrom !== void 0) {
146
+ const depNames = propagatedFrom.map((p) => p.packageName).join(", ");
147
+ lines.push(dim(` 0 commits (bumped via dependency: ${depNames})`));
148
+ } else if (workspace.parsedCommitCount !== void 0 && workspace.parsedCommitCount > 0) {
149
+ lines.push(dim(` Parsed ${workspace.parsedCommitCount} typed commits`));
150
+ }
151
+ }
152
+ function formatBumpLabels(lines, workspace, isPropagatedOnly) {
153
+ if (workspace.setVersion !== void 0) {
154
+ lines.push(` Using version override: ${workspace.setVersion}`);
155
+ } else if (workspace.bumpOverride !== void 0 && !isPropagatedOnly) {
156
+ lines.push(` Using bump override: ${workspace.bumpOverride}`);
157
+ }
158
+ if (workspace.releaseType !== void 0) {
159
+ lines.push(dim(` Bumping versions (${workspace.releaseType})...`));
160
+ } else if (workspace.setVersion !== void 0) {
161
+ lines.push(dim(` Bumping versions (version override)...`));
162
+ }
163
+ }
164
+ function formatVersionLine(lines, workspace, propagatedFrom, isPropagatedOnly) {
165
+ if (workspace.setVersion !== void 0) {
166
+ lines.push(` \u{1F4E6} ${workspace.currentVersion} \u2192 ${bold(workspace.newVersion)} (version override)`);
167
+ } else if (workspace.releaseType !== void 0) {
168
+ const suffix = isPropagatedOnly ? formatPropagationSuffix(propagatedFrom) : "";
169
+ lines.push(` \u{1F4E6} ${workspace.currentVersion} \u2192 ${bold(workspace.newVersion)} (${workspace.releaseType}${suffix})`);
170
+ }
171
+ }
172
+ function formatBumpFiles(lines, workspace, dryRun, indent = "") {
173
+ for (const file of workspace.bumpedFiles) {
101
174
  if (dryRun) {
102
175
  lines.push(dim(`${indent} [dry-run] Would bump ${file}`));
103
176
  } else {
@@ -105,8 +178,8 @@ function formatBumpFiles(lines, component, dryRun, indent = "") {
105
178
  }
106
179
  }
107
180
  }
108
- function formatChangelogFiles(lines, component, dryRun, indent = "") {
109
- for (const file of component.changelogFiles) {
181
+ function formatChangelogFiles(lines, workspace, dryRun, indent = "") {
182
+ for (const file of workspace.changelogFiles) {
110
183
  if (dryRun) {
111
184
  lines.push(dim(`${indent} [dry-run] Would run: npx --yes git-cliff ... --output ${file}`));
112
185
  } else {
@@ -114,13 +187,13 @@ function formatChangelogFiles(lines, component, dryRun, indent = "") {
114
187
  }
115
188
  }
116
189
  }
117
- function formatUnparseableWarning(lines, component, indent = "") {
118
- const unparseable = component.unparseableCommits;
190
+ function formatUnparseableWarning(lines, workspace, indent = "") {
191
+ const unparseable = workspace.unparseableCommits;
119
192
  if (unparseable === void 0 || unparseable.length === 0) {
120
193
  return;
121
194
  }
122
195
  const count = unparseable.length;
123
- const isPatchFloor = component.parsedCommitCount === 0;
196
+ const isPatchFloor = workspace.parsedCommitCount === 0;
124
197
  const suffix = isPatchFloor ? " (defaulting to patch bump)" : "";
125
198
  lines.push(`${indent} \u26A0\uFE0F ${count} commit${count === 1 ? "" : "s"} could not be parsed${suffix}`);
126
199
  for (const commit of unparseable) {
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
- import { findPackageRoot } from "./findPackageRoot.js";
3
+ import { findPackageRoot } from "@williamthorsen/nmr-core";
4
4
  function resolveCliffConfigPath(cliffConfigPath, metaUrl) {
5
5
  if (cliffConfigPath !== void 0) {
6
6
  return cliffConfigPath;
@@ -1,2 +1,2 @@
1
1
  import type { ResolvedTag } from './resolveReleaseTags.ts';
2
- export declare function resolveCommandTags(only: string[] | undefined): Promise<ResolvedTag[]>;
2
+ export declare function resolveCommandTags(tags: string[] | undefined): Promise<ResolvedTag[]>;
@@ -1,7 +1,7 @@
1
- import { basename } from "node:path";
1
+ import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
2
2
  import { discoverWorkspaces } from "./discoverWorkspaces.js";
3
3
  import { resolveReleaseTags } from "./resolveReleaseTags.js";
4
- async function resolveCommandTags(only) {
4
+ async function resolveCommandTags(tags) {
5
5
  let discoveredPaths;
6
6
  try {
7
7
  discoveredPaths = await discoverWorkspaces();
@@ -9,25 +9,29 @@ async function resolveCommandTags(only) {
9
9
  console.error(`Error discovering workspaces: ${error instanceof Error ? error.message : String(error)}`);
10
10
  process.exit(1);
11
11
  }
12
- if (only !== void 0 && discoveredPaths === void 0) {
13
- console.error("Error: --only is only supported for monorepo configurations");
14
- process.exit(1);
12
+ let workspaces;
13
+ if (discoveredPaths !== void 0) {
14
+ try {
15
+ workspaces = discoveredPaths.map((workspacePath) => deriveWorkspaceConfig(workspacePath));
16
+ } catch (error) {
17
+ console.error(`Error resolving workspaces: ${error instanceof Error ? error.message : String(error)}`);
18
+ process.exit(1);
19
+ }
15
20
  }
16
- const workspaceMap = discoveredPaths === void 0 ? void 0 : new Map(discoveredPaths.map((p) => [basename(p), p]));
17
- let resolvedTags = resolveReleaseTags(workspaceMap);
21
+ let resolvedTags = resolveReleaseTags(workspaces);
18
22
  if (resolvedTags.length === 0) {
19
23
  console.error("Error: No release tags found on HEAD. Create tags with `release-kit tag` first.");
20
24
  process.exit(1);
21
25
  }
22
- if (only !== void 0) {
23
- const availableNames = resolvedTags.map((t) => t.dir);
24
- for (const name of only) {
25
- if (!availableNames.includes(name)) {
26
- console.error(`Error: Unknown package "${name}" in --only. Available: ${availableNames.join(", ")}`);
26
+ if (tags !== void 0) {
27
+ const availableTagNames = resolvedTags.map((t) => t.tag);
28
+ for (const name of tags) {
29
+ if (!availableTagNames.includes(name)) {
30
+ console.error(`Error: Unknown tag "${name}" in --tags. Available: ${availableTagNames.join(", ")}`);
27
31
  process.exit(1);
28
32
  }
29
33
  }
30
- resolvedTags = resolvedTags.filter((t) => only.includes(t.dir));
34
+ resolvedTags = resolvedTags.filter((t) => tags.includes(t.tag));
31
35
  }
32
36
  return resolvedTags;
33
37
  }
@@ -2,5 +2,12 @@ import type { ReleaseNotesConfig } from './types.ts';
2
2
  export interface ResolvedReleaseNotesConfig {
3
3
  releaseNotes: ReleaseNotesConfig;
4
4
  changelogJsonOutputPath: string;
5
+ sectionOrder: string[];
5
6
  }
6
- export declare function resolveReleaseNotesConfig(): Promise<ResolvedReleaseNotesConfig>;
7
+ export interface ResolveReleaseNotesConfigOptions {
8
+ strictLoad?: boolean;
9
+ }
10
+ export declare function resolveReleaseNotesConfig(options?: ResolveReleaseNotesConfigOptions): Promise<ResolvedReleaseNotesConfig>;
11
+ export declare function deriveSectionOrder(workTypes: Record<string, {
12
+ header: string;
13
+ }>): string[];
@@ -1,19 +1,24 @@
1
1
  import { DEFAULT_CHANGELOG_JSON_CONFIG, DEFAULT_RELEASE_NOTES_CONFIG } from "./defaults.js";
2
- import { loadConfig } from "./loadConfig.js";
2
+ import { loadConfig, resolveWorkTypes } from "./loadConfig.js";
3
3
  import { validateConfig } from "./validateConfig.js";
4
- async function resolveReleaseNotesConfig() {
4
+ async function resolveReleaseNotesConfig(options = {}) {
5
+ const { strictLoad = false } = options;
5
6
  let rawConfig;
6
7
  try {
7
8
  rawConfig = await loadConfig();
8
9
  } catch (error) {
9
- console.warn(
10
- `Warning: failed to load config; using defaults: ${error instanceof Error ? error.message : String(error)}`
11
- );
10
+ const message = error instanceof Error ? error.message : String(error);
11
+ if (strictLoad) {
12
+ console.error(`Error: failed to load config: ${message}`);
13
+ process.exit(1);
14
+ }
15
+ console.warn(`Warning: failed to load config; using defaults: ${message}`);
12
16
  }
13
17
  if (rawConfig === void 0) {
14
18
  return {
15
19
  releaseNotes: { ...DEFAULT_RELEASE_NOTES_CONFIG },
16
- changelogJsonOutputPath: DEFAULT_CHANGELOG_JSON_CONFIG.outputPath
20
+ changelogJsonOutputPath: DEFAULT_CHANGELOG_JSON_CONFIG.outputPath,
21
+ sectionOrder: deriveSectionOrder(resolveWorkTypes())
17
22
  };
18
23
  }
19
24
  const { config, errors, warnings } = validateConfig(rawConfig);
@@ -29,9 +34,14 @@ async function resolveReleaseNotesConfig() {
29
34
  }
30
35
  return {
31
36
  releaseNotes: { ...DEFAULT_RELEASE_NOTES_CONFIG, ...config.releaseNotes },
32
- changelogJsonOutputPath: config.changelogJson?.outputPath ?? DEFAULT_CHANGELOG_JSON_CONFIG.outputPath
37
+ changelogJsonOutputPath: config.changelogJson?.outputPath ?? DEFAULT_CHANGELOG_JSON_CONFIG.outputPath,
38
+ sectionOrder: deriveSectionOrder(resolveWorkTypes(config.workTypes))
33
39
  };
34
40
  }
41
+ function deriveSectionOrder(workTypes) {
42
+ return Object.values(workTypes).map((entry) => entry.header);
43
+ }
35
44
  export {
45
+ deriveSectionOrder,
36
46
  resolveReleaseNotesConfig
37
47
  };
@@ -1,6 +1,7 @@
1
+ import type { WorkspaceConfig } from './types.ts';
1
2
  export interface ResolvedTag {
2
3
  tag: string;
3
4
  dir: string;
4
5
  workspacePath: string;
5
6
  }
6
- export declare function resolveReleaseTags(workspaceMap?: Map<string, string>): ResolvedTag[];
7
+ export declare function resolveReleaseTags(workspaces?: readonly WorkspaceConfig[]): ResolvedTag[];
@@ -1,12 +1,13 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  const VERSION_PATTERN = /^v\d+\.\d+\.\d+/;
3
- function resolveReleaseTags(workspaceMap) {
3
+ const SEMVER_SUFFIX_PATTERN = /^\d+\.\d+\.\d+/;
4
+ function resolveReleaseTags(workspaces) {
4
5
  const output = execFileSync("git", ["tag", "--points-at", "HEAD"], { encoding: "utf8" });
5
6
  const tags = output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
6
- if (workspaceMap === void 0) {
7
+ if (workspaces === void 0) {
7
8
  return resolveSinglePackageTags(tags);
8
9
  }
9
- return resolveMonorepoTags(tags, workspaceMap);
10
+ return resolveMonorepoTags(tags, workspaces);
10
11
  }
11
12
  function resolveSinglePackageTags(tags) {
12
13
  const matched = tags.filter((tag) => VERSION_PATTERN.test(tag));
@@ -18,24 +19,28 @@ function resolveSinglePackageTags(tags) {
18
19
  }
19
20
  return matched.map((tag) => ({ tag, dir: ".", workspacePath: "." }));
20
21
  }
21
- function resolveMonorepoTags(tags, workspaceMap) {
22
+ function resolveMonorepoTags(tags, workspaces) {
23
+ const sortedWorkspaces = [...workspaces].sort((a, b) => b.tagPrefix.length - a.tagPrefix.length);
22
24
  const resolved = [];
23
25
  for (const tag of tags) {
24
- const dashV = tag.lastIndexOf("-v");
25
- if (dashV === -1) {
26
- continue;
26
+ const match = findMatchingWorkspace(tag, sortedWorkspaces);
27
+ if (match !== void 0) {
28
+ resolved.push({ tag, dir: match.dir, workspacePath: match.workspacePath });
27
29
  }
28
- const dir = tag.slice(0, dashV);
29
- const versionPart = tag.slice(dashV + 1);
30
- if (!VERSION_PATTERN.test(versionPart)) {
30
+ }
31
+ return resolved;
32
+ }
33
+ function findMatchingWorkspace(tag, sortedWorkspaces) {
34
+ for (const workspace of sortedWorkspaces) {
35
+ if (!tag.startsWith(workspace.tagPrefix)) {
31
36
  continue;
32
37
  }
33
- const workspacePath = workspaceMap.get(dir);
34
- if (workspacePath !== void 0) {
35
- resolved.push({ tag, dir, workspacePath });
38
+ const versionSuffix = tag.slice(workspace.tagPrefix.length);
39
+ if (SEMVER_SUFFIX_PATTERN.test(versionSuffix)) {
40
+ return workspace;
36
41
  }
37
42
  }
38
- return resolved;
43
+ return void 0;
39
44
  }
40
45
  export {
41
46
  resolveReleaseTags
@@ -0,0 +1 @@
1
+ export declare function showTagPrefixesCommand(): Promise<number>;