@williamthorsen/release-kit 11.0.0 → 13.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 (189) hide show
  1. package/.meta/changelog.json +274 -2
  2. package/.readyup/kits/default.js +18 -33
  3. package/.readyup/kits/npm-auto-publish.js +24 -23
  4. package/.readyup/manifest.json +16 -11
  5. package/CHANGELOG.md +98 -0
  6. package/README.md +57 -69
  7. package/bin/release-kit.js +1 -1
  8. package/dist/esm/assertCleanWorkingTree.d.ts +1 -5
  9. package/dist/esm/assertTaggedBaseline.d.ts +29 -0
  10. package/dist/esm/assertTaggedBaseline.js +41 -0
  11. package/dist/esm/attachChangelogDiagnostics.d.ts +4 -0
  12. package/dist/esm/attachChangelogDiagnostics.js +14 -0
  13. package/dist/esm/bin/release-kit.js +88 -24
  14. package/dist/esm/buildChangelogEntries.d.ts +69 -17
  15. package/dist/esm/buildChangelogEntries.js +222 -120
  16. package/dist/esm/buildDependencyGraph.d.ts +6 -6
  17. package/dist/esm/buildEmptyReleaseEntry.d.ts +1 -8
  18. package/dist/esm/buildReleaseSummary.d.ts +2 -8
  19. package/dist/esm/buildReleaseSummary.js +2 -2
  20. package/dist/esm/buildSyntheticChangelogEntry.d.ts +3 -5
  21. package/dist/esm/buildSyntheticChangelogEntry.js +2 -2
  22. package/dist/esm/bumpVersion.d.ts +1 -8
  23. package/dist/esm/changelogJsonFile.d.ts +5 -7
  24. package/dist/esm/changelogJsonFile.js +2 -17
  25. package/dist/esm/changelogJsonUtils.d.ts +7 -10
  26. package/dist/esm/changelogOverrides.d.ts +79 -100
  27. package/dist/esm/changelogOverrides.js +138 -58
  28. package/dist/esm/classifyChangelogCommit.d.ts +36 -0
  29. package/dist/esm/classifyChangelogCommit.js +34 -0
  30. package/dist/esm/commitCommand.d.ts +2 -4
  31. package/dist/esm/compareVersions.d.ts +10 -4
  32. package/dist/esm/compareVersions.js +16 -0
  33. package/dist/esm/configFlagSchema.d.ts +1 -1
  34. package/dist/esm/createGithubRelease.d.ts +7 -19
  35. package/dist/esm/createGithubReleaseCommand.d.ts +5 -5
  36. package/dist/esm/createGithubReleaseCommand.js +9 -9
  37. package/dist/esm/createTags.d.ts +2 -3
  38. package/dist/esm/decideRelease.d.ts +11 -41
  39. package/dist/esm/decideRelease.js +5 -27
  40. package/dist/esm/defaults.d.ts +5 -24
  41. package/dist/esm/defaults.js +7 -5
  42. package/dist/esm/defineConfig.d.ts +1 -1
  43. package/dist/esm/deleteFileIfExists.d.ts +1 -1
  44. package/dist/esm/deriveReleaseNotesConfig.d.ts +20 -0
  45. package/dist/esm/deriveReleaseNotesConfig.js +21 -0
  46. package/dist/esm/deriveWorkspaceConfig.d.ts +3 -5
  47. package/dist/esm/detectPackageManager.d.ts +2 -4
  48. package/dist/esm/detectUndeclaredTagPrefixes.d.ts +4 -6
  49. package/dist/esm/detectUndeclaredTagPrefixes.js +1 -1
  50. package/dist/esm/determineBumpType.d.ts +11 -11
  51. package/dist/esm/determineBumpType.js +6 -7
  52. package/dist/esm/discoverWorkspaces.d.ts +14 -15
  53. package/dist/esm/discoverWorkspaces.js +12 -12
  54. package/dist/esm/enterRepoRoot.d.ts +12 -0
  55. package/dist/esm/enterRepoRoot.js +16 -0
  56. package/dist/esm/enumerateReleaseWindows.d.ts +50 -0
  57. package/dist/esm/enumerateReleaseWindows.js +124 -0
  58. package/dist/esm/extractMigration.d.ts +6 -8
  59. package/dist/esm/findUnroutedEntryScopes.d.ts +16 -0
  60. package/dist/esm/findUnroutedEntryScopes.js +57 -0
  61. package/dist/esm/formatExcludedSkip.d.ts +3 -0
  62. package/dist/esm/formatExcludedSkip.js +3 -0
  63. package/dist/esm/formatPrivateSkip.d.ts +1 -6
  64. package/dist/esm/generateChangelogs.d.ts +13 -26
  65. package/dist/esm/generateChangelogs.js +0 -14
  66. package/dist/esm/glyphs.d.ts +2 -2
  67. package/dist/esm/help/prepareHelp.d.ts +3 -4
  68. package/dist/esm/help/prepareHelp.js +7 -8
  69. package/dist/esm/index.d.ts +1 -1
  70. package/dist/esm/init/checks.d.ts +3 -3
  71. package/dist/esm/init/checks.js +4 -1
  72. package/dist/esm/init/detectRepoType.d.ts +2 -3
  73. package/dist/esm/init/hasProvenance.d.ts +2 -0
  74. package/dist/esm/init/hasProvenance.js +3 -0
  75. package/dist/esm/init/initCommand.d.ts +3 -6
  76. package/dist/esm/init/initCommand.js +11 -11
  77. package/dist/esm/init/parseJsonRecord.d.ts +1 -4
  78. package/dist/esm/init/scaffold.d.ts +1 -3
  79. package/dist/esm/init/scaffold.js +2 -30
  80. package/dist/esm/init/templates.d.ts +9 -11
  81. package/dist/esm/init/templates.js +12 -11
  82. package/dist/esm/init/updateTemplatesCommand.d.ts +13 -0
  83. package/dist/esm/init/updateTemplatesCommand.js +64 -0
  84. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +12 -17
  85. package/dist/esm/injectSection.d.ts +3 -3
  86. package/dist/esm/loadConfig.d.ts +16 -14
  87. package/dist/esm/loadConfig.js +2 -5
  88. package/dist/esm/loadValidatedConfig.d.ts +16 -19
  89. package/dist/esm/loadValidatedConfig.js +6 -1
  90. package/dist/esm/packsPath.d.ts +9 -9
  91. package/dist/esm/parseChangeRecordBlock.d.ts +37 -0
  92. package/dist/esm/parseChangeRecordBlock.js +134 -0
  93. package/dist/esm/parseCommitMessage.d.ts +42 -32
  94. package/dist/esm/parseCommitMessage.js +33 -41
  95. package/dist/esm/parseRequestedTags.d.ts +3 -3
  96. package/dist/esm/planReleaseNotesPreviews.d.ts +6 -6
  97. package/dist/esm/planVersionBump.d.ts +5 -3
  98. package/dist/esm/planVersionBump.js +3 -0
  99. package/dist/esm/prepareCommand.d.ts +4 -7
  100. package/dist/esm/prepareCommand.js +17 -18
  101. package/dist/esm/previewTagPrefixes.d.ts +8 -10
  102. package/dist/esm/propagateBumps.d.ts +6 -7
  103. package/dist/esm/publish.d.ts +1 -1
  104. package/dist/esm/publishCommand.d.ts +4 -3
  105. package/dist/esm/publishCommand.js +8 -8
  106. package/dist/esm/pushCommand.d.ts +4 -3
  107. package/dist/esm/pushCommand.js +8 -2
  108. package/dist/esm/pushRelease.d.ts +2 -2
  109. package/dist/esm/readChangelogSections.d.ts +33 -0
  110. package/dist/esm/readChangelogSections.js +80 -0
  111. package/dist/esm/releaseFiles.d.ts +5 -7
  112. package/dist/esm/releasePlan.d.ts +11 -13
  113. package/dist/esm/releasePrepare.d.ts +25 -25
  114. package/dist/esm/releasePrepare.js +80 -80
  115. package/dist/esm/releasePrepareMono.d.ts +9 -7
  116. package/dist/esm/releasePrepareMono.js +114 -99
  117. package/dist/esm/releasePrepareProject.d.ts +19 -29
  118. package/dist/esm/releasePrepareProject.js +46 -45
  119. package/dist/esm/renderChangelogMarkdown.d.ts +10 -4
  120. package/dist/esm/renderChangelogMarkdown.js +27 -2
  121. package/dist/esm/renderReleaseNotes.d.ts +3 -11
  122. package/dist/esm/renderReleaseNotes.js +2 -6
  123. package/dist/esm/reportPrepare.d.ts +4 -5
  124. package/dist/esm/reportPrepare.js +67 -42
  125. package/dist/esm/resolveCommandTags.d.ts +15 -11
  126. package/dist/esm/resolveCommandTags.js +35 -5
  127. package/dist/esm/resolveConfigFlag.d.ts +7 -0
  128. package/dist/esm/resolveConfigFlag.js +4 -0
  129. package/dist/esm/resolveReleaseTags.d.ts +6 -8
  130. package/dist/esm/resolveReleaseTags.js +1 -1
  131. package/dist/esm/resolveStylesOrExit.d.ts +2 -2
  132. package/dist/esm/showTagPrefixesCommand.d.ts +7 -10
  133. package/dist/esm/showTagPrefixesCommand.js +4 -4
  134. package/dist/esm/stripEmojiPrefix.d.ts +1 -0
  135. package/dist/esm/stripScope.d.ts +2 -2
  136. package/dist/esm/stripScope.js +3 -3
  137. package/dist/esm/sync-labels/generateCommand.d.ts +11 -10
  138. package/dist/esm/sync-labels/generateCommand.js +11 -8
  139. package/dist/esm/sync-labels/initCommand.d.ts +6 -7
  140. package/dist/esm/sync-labels/initCommand.js +2 -2
  141. package/dist/esm/sync-labels/presets.d.ts +5 -4
  142. package/dist/esm/sync-labels/presets.js +9 -9
  143. package/dist/esm/sync-labels/resolveLabels.d.ts +7 -7
  144. package/dist/esm/sync-labels/resolveLabels.js +1 -1
  145. package/dist/esm/sync-labels/retiredConfig.d.ts +4 -5
  146. package/dist/esm/sync-labels/syncCommand.d.ts +1 -4
  147. package/dist/esm/sync-labels/templates.d.ts +3 -3
  148. package/dist/esm/sync-labels/templates.js +3 -3
  149. package/dist/esm/sync-labels/typeLabels.d.ts +14 -0
  150. package/dist/esm/sync-labels/typeLabels.js +38 -0
  151. package/dist/esm/sync-labels/types.d.ts +1 -1
  152. package/dist/esm/tagCommand.d.ts +1 -1
  153. package/dist/esm/typeGuards.d.ts +2 -2
  154. package/dist/esm/types.d.ts +185 -116
  155. package/dist/esm/types.js +2 -2
  156. package/dist/esm/validateConfig.d.ts +2 -11
  157. package/dist/esm/validateConfig.js +5 -1
  158. package/dist/esm/validateOnlyExcludesStrandedDependents.d.ts +14 -12
  159. package/dist/esm/validateOverridesCommand.d.ts +19 -21
  160. package/dist/esm/validateOverridesCommand.js +16 -23
  161. package/docs/api.md +6 -6
  162. package/docs/changelogs.md +90 -28
  163. package/docs/configuration.md +30 -29
  164. package/docs/editorial-overrides.md +90 -31
  165. package/docs/labels.md +21 -19
  166. package/docs/project-releases.md +14 -14
  167. package/docs/publishing.md +5 -5
  168. package/docs/readiness-checks.md +3 -3
  169. package/docs/releasing.md +54 -24
  170. package/docs/work-types.md +46 -58
  171. package/package.json +7 -7
  172. package/presets/labels/common.yaml +1 -41
  173. package/schemas/changelog-overrides.json +39 -0
  174. package/cliff.toml.template +0 -65
  175. package/dist/esm/collectPolicyViolations.d.ts +0 -15
  176. package/dist/esm/collectPolicyViolations.js +0 -12
  177. package/dist/esm/determineBumpFromCommits.d.ts +0 -17
  178. package/dist/esm/determineBumpFromCommits.js +0 -28
  179. package/dist/esm/getCommitsSinceTarget.d.ts +0 -21
  180. package/dist/esm/getCommitsSinceTarget.js +0 -66
  181. package/dist/esm/resolveCliffConfigPath.d.ts +0 -11
  182. package/dist/esm/resolveCliffConfigPath.js +0 -20
  183. package/dist/esm/resolveReleaseNotesConfig.d.ts +0 -26
  184. package/dist/esm/resolveReleaseNotesConfig.js +0 -29
  185. package/dist/esm/runGitCliff.d.ts +0 -38
  186. package/dist/esm/runGitCliff.js +0 -25
  187. package/dist/esm/workTypesData.d.ts +0 -50
  188. package/dist/esm/workTypesData.js +0 -145
  189. package/schemas/label-map.json +0 -24
@@ -1,17 +1,16 @@
1
1
  import { join as joinPath } from 'node:path';
2
- import { buildChangelogEntries } from "./buildChangelogEntries.js";
3
- import { buildEmptyReleaseEntry } from "./buildEmptyReleaseEntry.js";
2
+ import { assertTaggedBaseline, findUntaggedBaseline } from "./assertTaggedBaseline.js";
3
+ import { attachChangelogDiagnostics } from "./attachChangelogDiagnostics.js";
4
+ import { readReleaseHistory, toReleaseEntries } from "./buildChangelogEntries.js";
4
5
  import { mergeChangelogEntriesWithDisk, renderChangelogJson, resolveChangelogJsonPath } from "./changelogJsonFile.js";
5
6
  import { applyChangelogOverrides } from "./changelogOverrides.js";
6
- import { createPolicyViolationCollector } from "./collectPolicyViolations.js";
7
7
  import { decideRelease } from "./decideRelease.js";
8
- import { DEFAULT_BREAKING_POLICIES, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
9
- import { buildTagPattern } from "./generateChangelogs.js";
10
- import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
8
+ import { DEFAULT_WORK_TYPES } from "./defaults.js";
9
+ import { deriveSectionOrder } from "./deriveReleaseNotesConfig.js";
11
10
  import { planReleaseNotesPreviews } from "./planReleaseNotesPreviews.js";
12
11
  import { planVersionBump } from "./planVersionBump.js";
12
+ import { planPreservedSections } from "./readChangelogSections.js";
13
13
  import { renderChangelogMarkdown } from "./renderChangelogMarkdown.js";
14
- import { deriveSectionOrder } from "./resolveReleaseNotesConfig.js";
15
14
  const ROOT_PACKAGE_FILE = './package.json';
16
15
  const ROOT_CHANGELOG_PATH = '.';
17
16
  export function releasePrepareProject(args) {
@@ -23,56 +22,53 @@ export function releasePrepareProject(args) {
23
22
  throw new Error('releasePrepareProject called without a configured project block');
24
23
  }
25
24
  const workTypes = config.workTypes ?? { ...DEFAULT_WORK_TYPES };
26
- const versionPatterns = config.versionPatterns ?? { ...DEFAULT_VERSION_PATTERNS };
27
- const breakingPolicies = config.breakingPolicies ?? DEFAULT_BREAKING_POLICIES;
28
- const contributingPaths = project.paths;
29
- const { tag, commits } = getCommitsSinceTarget([project.tagPrefix], contributingPaths);
25
+ const history = readReleaseHistory(config, { tagPrefixes: [project.tagPrefix], paths: project.paths });
26
+ assertTaggedBaseline([
27
+ findUntaggedBaseline({
28
+ label: 'project',
29
+ packageFiles: [ROOT_PACKAGE_FILE],
30
+ changelogPaths: [ROOT_CHANGELOG_PATH],
31
+ tagPrefixes: [project.tagPrefix],
32
+ previousTag: history.previousTag,
33
+ }, config),
34
+ ]);
35
+ const { commits, diagnostics, parsedCommitCount, unparseableCommits } = history.unreleased;
36
+ const tag = history.previousTag;
30
37
  const since = tag === undefined ? '(no previous release found)' : `since ${tag}`;
31
- const collector = createPolicyViolationCollector();
32
38
  const decision = decideRelease({
33
- commits,
39
+ naturalBump: history.unreleased.bump,
40
+ commitCount: commits.length,
34
41
  force,
35
42
  bumpOverride,
36
- workTypes,
37
- versionPatterns,
38
- scopeAliases: config.scopeAliases,
39
- breakingPolicies,
40
- onPolicyViolation: collector.onPolicyViolation,
41
43
  skipReasons: {
42
44
  noCommits: `No commits ${since}. Pass --force to release at patch. Skipping.`,
43
45
  noBumpWorthy: `No bump-worthy commits ${since}. Pass --force to release at patch (or --force --bump=X for a different level). Skipping.`,
44
46
  },
45
47
  });
46
- const policyViolations = collector.violations.length > 0 ? collector.violations : undefined;
47
48
  if (decision.outcome === 'skip') {
48
49
  const skipped = {
49
50
  status: 'skipped',
50
51
  commitCount: commits.length,
51
- parsedCommitCount: decision.parsedCommitCount,
52
+ parsedCommitCount,
52
53
  skipReason: decision.skipReason,
53
54
  };
54
55
  if (tag !== undefined) {
55
56
  skipped.previousTag = tag;
56
57
  }
57
- if (decision.unparseableCommits !== undefined) {
58
- skipped.unparseableCommits = decision.unparseableCommits;
59
- }
60
- if (policyViolations !== undefined) {
61
- skipped.policyViolations = policyViolations;
58
+ if (unparseableCommits !== undefined) {
59
+ skipped.unparseableCommits = unparseableCommits;
62
60
  }
61
+ attachChangelogDiagnostics(skipped, diagnostics);
63
62
  return skipped;
64
63
  }
65
- const { releaseType, parsedCommitCount, unparseableCommits } = decision;
64
+ const { releaseType } = decision;
66
65
  const bump = planVersionBump([ROOT_PACKAGE_FILE], releaseType);
67
66
  writes.push(...bump.writes);
68
67
  const newTag = `${project.tagPrefix}${bump.newVersion}`;
69
68
  const changelogs = planProjectChangelogs({
70
69
  config,
71
- project,
72
- commits,
73
- contributingPaths,
70
+ history,
74
71
  newTag,
75
- newVersion: bump.newVersion,
76
72
  rootOverrides,
77
73
  overrideWarnings,
78
74
  globalMatchedRootKeys,
@@ -108,18 +104,19 @@ export function releasePrepareProject(args) {
108
104
  if (previewFiles.length > 0) {
109
105
  result.previewFiles = previewFiles;
110
106
  }
107
+ if (changelogs.changelogPreservation.length > 0) {
108
+ result.changelogPreservation = changelogs.changelogPreservation;
109
+ }
111
110
  if (tag !== undefined) {
112
111
  result.previousTag = tag;
113
112
  }
114
113
  if (unparseableCommits !== undefined) {
115
114
  result.unparseableCommits = unparseableCommits;
116
115
  }
117
- if (policyViolations !== undefined) {
118
- result.policyViolations = policyViolations;
119
- }
120
116
  if (bumpOverride !== undefined) {
121
117
  result.bumpOverride = bumpOverride;
122
118
  }
119
+ attachChangelogDiagnostics(result, diagnostics);
123
120
  return result;
124
121
  }
125
122
  function resolveOptionalOverrideArgs(args) {
@@ -131,14 +128,10 @@ function resolveOptionalOverrideArgs(args) {
131
128
  };
132
129
  }
133
130
  function planProjectChangelogs(args) {
134
- const { config, project, commits, contributingPaths, newTag, newVersion, rootOverrides, overrideWarnings, globalMatchedRootKeys, } = args;
135
- const isEmptyRange = commits.length === 0;
131
+ const { config, history, newTag, rootOverrides, overrideWarnings, globalMatchedRootKeys } = args;
136
132
  const today = new Date().toISOString().slice(0, 10);
137
- const tagPattern = buildTagPattern([project.tagPrefix]);
138
- const newEntries = isEmptyRange
139
- ? [buildEmptyReleaseEntry(newVersion, today)]
140
- : buildChangelogEntries(config, newTag, { tagPattern, includePaths: contributingPaths });
141
- const applied = applyChangelogOverrides(newEntries, rootOverrides);
133
+ const builtEntries = toReleaseEntries(history, newTag, today);
134
+ const applied = applyChangelogOverrides(builtEntries, rootOverrides);
142
135
  if (applied.errors.length > 0) {
143
136
  throw new Error(`Changelog override application failed:\n - ${applied.errors.join('\n - ')}`);
144
137
  }
@@ -148,9 +141,7 @@ function planProjectChangelogs(args) {
148
141
  }
149
142
  const changelogJsonPath = resolveChangelogJsonPath(config, ROOT_CHANGELOG_PATH);
150
143
  const sectionOrder = deriveSectionOrder(config.workTypes ?? { ...DEFAULT_WORK_TYPES });
151
- const renderEntries = isEmptyRange
152
- ? mergeChangelogEntriesWithDisk(changelogJsonPath, applied.entries)
153
- : applied.entries;
144
+ const renderEntries = mergeChangelogEntriesWithDisk(changelogJsonPath, applied.entries);
154
145
  const writes = [];
155
146
  const changelogJsonFiles = [];
156
147
  if (config.changelogJson.enabled) {
@@ -158,6 +149,16 @@ function planProjectChangelogs(args) {
158
149
  changelogJsonFiles.push(changelogJsonPath);
159
150
  }
160
151
  const changelogFile = joinPath(ROOT_CHANGELOG_PATH, 'CHANGELOG.md');
161
- writes.push({ path: changelogFile, content: renderChangelogMarkdown(renderEntries, { sectionOrder }) });
162
- return { changelogFiles: [changelogFile], changelogJsonFiles, entries: renderEntries, writes };
152
+ const preserved = planPreservedSections(changelogFile, renderEntries);
153
+ writes.push({
154
+ path: changelogFile,
155
+ content: renderChangelogMarkdown(renderEntries, { sectionOrder, preservedSections: preserved.sections }),
156
+ });
157
+ return {
158
+ changelogFiles: [changelogFile],
159
+ changelogJsonFiles,
160
+ changelogPreservation: preserved.preservation === undefined ? [] : [preserved.preservation],
161
+ entries: renderEntries,
162
+ writes,
163
+ };
163
164
  }
@@ -1,22 +1,28 @@
1
+ import type { VersionedMarkdownSection } from './readChangelogSections.js';
1
2
  import type { ChangelogEntry } from './types.js';
2
3
  /** Options for `renderChangelogMarkdown`. */
3
4
  export interface RenderChangelogMarkdownOptions {
4
- /** Section titles in canonical order; passed through to `renderReleaseNotesMulti`. */
5
+ /** Section titles in canonical order; passed through to `renderReleaseNotesSingle`. */
5
6
  sectionOrder?: string[];
7
+ /**
8
+ * Sections of the existing `CHANGELOG.md` to keep verbatim, interleaved by version among the rendered entries. A
9
+ * section's version must not also be an entry's version.
10
+ */
11
+ preservedSections?: readonly VersionedMarkdownSection[];
6
12
  }
7
13
  /**
8
- * Render `ChangelogEntry[]` into a complete `CHANGELOG.md`-shaped string.
14
+ * Renders `ChangelogEntry[]` into a complete `CHANGELOG.md`-shaped string.
9
15
  *
10
16
  * Format:
11
17
  * `# Changelog`
12
18
  * blank line
13
19
  * `All notable changes …`
14
20
  * blank line
15
- * per-entry sections (delegated to `renderReleaseNotesMulti`)
21
+ * per-entry sections (delegated to `renderReleaseNotesSingle`), with preserved sections interleaved by version
16
22
  * blank line
17
23
  * footer comment
18
24
  *
19
25
  * Entries are rendered in the order provided; the caller sorts (typically newest-first via
20
- * `changelogJsonFile`'s SemVer-aware comparator).
26
+ * `compareVersionsDescending`).
21
27
  */
22
28
  export declare function renderChangelogMarkdown(entries: ChangelogEntry[], options?: RenderChangelogMarkdownOptions): string;
@@ -1,10 +1,15 @@
1
- import { renderReleaseNotesMulti } from "./renderReleaseNotes.js";
1
+ import { compareVersionsDescending } from "./compareVersions.js";
2
+ import { renderReleaseNotesSingle } from "./renderReleaseNotes.js";
2
3
  const CHANGELOG_HEADER = `# Changelog
3
4
 
4
5
  All notable changes to this project will be documented in this file.`;
5
6
  const CHANGELOG_FOOTER = `<!-- Generated by release-kit. Do not edit this file. Use .meta/changelog-overrides.json to override entries. -->`;
6
7
  export function renderChangelogMarkdown(entries, options) {
7
- const body = renderReleaseNotesMulti(entries, options);
8
+ const sections = interleavePreservedSections(entries.map((entry) => ({ version: entry.version, text: renderReleaseNotesSingle(entry, options) })), options?.preservedSections ?? []);
9
+ const body = sections
10
+ .map((part) => part.text)
11
+ .filter((text) => text.length > 0)
12
+ .join('\n');
8
13
  const trimmedBody = body.trimEnd();
9
14
  const parts = [CHANGELOG_HEADER];
10
15
  if (trimmedBody.length > 0) {
@@ -13,3 +18,23 @@ export function renderChangelogMarkdown(entries, options) {
13
18
  parts.push(CHANGELOG_FOOTER);
14
19
  return parts.join('\n\n') + '\n';
15
20
  }
21
+ function interleavePreservedSections(rendered, preserved) {
22
+ const sorted = preserved.toSorted((a, b) => compareVersionsDescending(a.version, b.version));
23
+ const pending = sorted.map(withTrailingNewline);
24
+ const result = [];
25
+ let index = 0;
26
+ for (const part of rendered) {
27
+ let section = pending[index];
28
+ while (section !== undefined && compareVersionsDescending(section.version, part.version) < 0) {
29
+ result.push(section);
30
+ index++;
31
+ section = pending[index];
32
+ }
33
+ result.push(part);
34
+ }
35
+ result.push(...pending.slice(index));
36
+ return result;
37
+ }
38
+ function withTrailingNewline(section) {
39
+ return { version: section.version, text: `${section.text}\n` };
40
+ }
@@ -13,22 +13,14 @@ export interface RenderOptions {
13
13
  sectionOrder?: string[];
14
14
  }
15
15
  /**
16
- * Create a predicate that matches sections visible to the given audience.
16
+ * Creates a predicate that matches sections visible to the given audience.
17
17
  *
18
18
  * `"all"` matches only sections with `audience: "all"` (public-facing).
19
19
  * `"dev"` matches all sections (developers see everything).
20
20
  */
21
21
  export declare function matchesAudience(audience: ChangelogAudience): (section: ChangelogSection) => boolean;
22
22
  /**
23
- * Render a single `ChangelogEntry` to markdown.
24
- *
25
- * Output format mirrors the existing CHANGELOG.md style: an H2 version heading followed by
26
- * H3 section headings with bulleted items.
23
+ * Renders a single `ChangelogEntry` to markdown: an H2 version heading followed by H3 section
24
+ * headings with bulleted items.
27
25
  */
28
26
  export declare function renderReleaseNotesSingle(entry: ChangelogEntry, options?: RenderOptions): string;
29
- /**
30
- * Render multiple `ChangelogEntry` values to a single markdown document.
31
- *
32
- * Entries are rendered in the order provided; the caller is responsible for sorting.
33
- */
34
- export declare function renderReleaseNotesMulti(entries: ChangelogEntry[], options?: RenderOptions): string;
@@ -1,6 +1,6 @@
1
- import { WORK_TYPES_DATA } from "./workTypesData.js";
1
+ import { CANONICAL_TAXONOMY } from '@williamthorsen/change-grammar';
2
2
  function getBreakingPrefix() {
3
- const { emoji, label } = WORK_TYPES_DATA.markers.breaking;
3
+ const { emoji, label } = CANONICAL_TAXONOMY.markers.breaking;
4
4
  return `${emoji} **${label}:** `;
5
5
  }
6
6
  function allSections() {
@@ -62,7 +62,3 @@ function sortSectionsByOrder(sections, order) {
62
62
  function indentBodyLines(body) {
63
63
  return body.split('\n').map((line) => (line.length === 0 ? '' : ` ${line}`));
64
64
  }
65
- export function renderReleaseNotesMulti(entries, options) {
66
- const parts = entries.map((entry) => renderReleaseNotesSingle(entry, options)).filter((part) => part.length > 0);
67
- return parts.join('\n');
68
- }
@@ -1,6 +1,6 @@
1
1
  import { type OutputStyle } from '@williamthorsen/nmr-core';
2
2
  import type { PrepareResult } from './types.js';
3
- /** How the plan was carried out, which decides the tense the report is rendered in. */
3
+ /** How the plan was carried out, which decides the report's tense. */
4
4
  export interface ReportPrepareOptions {
5
5
  /** True when the plan was written to disk; false for a dry run. */
6
6
  applied: boolean;
@@ -9,14 +9,13 @@ export interface ReportPrepareOptions {
9
9
  style: OutputStyle;
10
10
  }
11
11
  /**
12
- * Format a release plan into styled terminal output.
12
+ * Formats a release plan into styled terminal output, without writing it.
13
13
  *
14
- * Pure function: accepts structured data, returns a string. Never writes to stdout.
15
- * Single-workspace mode (no `name` field) renders flat output; multi-workspace mode
14
+ * Single-workspace mode (without a `name` field) renders flat output; multi-workspace mode
16
15
  * renders section headers per workspace, an optional project section, and a tag summary
17
16
  * at the end.
18
17
  *
19
- * Whether the plan was applied is a property of the render, not of the plan: the same plan
18
+ * Whether the plan was applied is a property of the render, not of the plan: The same plan
20
19
  * produces the dry-run report and the applied report.
21
20
  */
22
21
  export declare function reportPrepare(plan: PrepareResult, options: ReportPrepareOptions): string;
@@ -1,8 +1,6 @@
1
1
  import { formatGlyphLine, formatStatusLine, measureGlyphColumn, STATUS_GLYPHS, truncateToWidth, } from '@williamthorsen/nmr-core';
2
2
  import { bold, dim, sectionHeader } from "./format.js";
3
3
  import { RELEASE_GLYPHS } from "./glyphs.js";
4
- import { GIT_CLIFF_NPX_ARGS } from "./runGitCliff.js";
5
- const CLIFF_DRY_RUN_COMMAND = `npx ${GIT_CLIFF_NPX_ARGS.join(' ')} ...`;
6
4
  const SUBJECT_COLUMN_BUDGET = 72;
7
5
  export function reportPrepare(plan, options) {
8
6
  const isMultiWorkspace = plan.workspaces.some((w) => w.name !== undefined) || plan.project !== undefined;
@@ -27,6 +25,7 @@ function formatSingleWorkspace(result, options) {
27
25
  formatUnparseableWarning(lines, workspace, style);
28
26
  formatPolicyViolations(lines, workspace.policyViolations, style);
29
27
  if (workspace.status === 'skipped') {
28
+ formatChangeRecordWarnings(lines, workspace, style);
30
29
  lines.push(formatStatusLine(style, 'skipped', workspace.skipReason));
31
30
  formatWarnings(lines, result, style);
32
31
  return lines.join('\n');
@@ -49,9 +48,10 @@ function formatSingleWorkspace(result, options) {
49
48
  else if (workspace.releaseType !== undefined) {
50
49
  lines.push(formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${workspace.currentVersion} → ${bold(workspace.newVersion)} (${workspace.releaseType})`));
51
50
  }
51
+ formatChangeRecordWarnings(lines, workspace, style);
52
52
  formatBumpFiles(lines, workspace, dryRun);
53
53
  lines.push(dim('Generating changelogs...'));
54
- formatChangelogFiles(lines, workspace, dryRun);
54
+ formatChangelogFiles(lines, workspace, dryRun, style);
55
55
  formatPreviewFiles(lines, workspace, dryRun);
56
56
  formatFormatCommand(lines, result, options);
57
57
  formatWarnings(lines, result, style);
@@ -77,7 +77,7 @@ function formatMultiWorkspace(result, options) {
77
77
  }
78
78
  }
79
79
  else {
80
- lines.push(`\n${formatStatusLine(style, 'skipped', 'No workspaces had release-worthy changes.')}`);
80
+ lines.push(`\n${formatStatusLine(style, 'skipped', 'The run did not find release-worthy changes in any workspace.')}`);
81
81
  }
82
82
  return lines.join('\n');
83
83
  }
@@ -87,6 +87,8 @@ function formatProjectSection(lines, project, dryRun, style) {
87
87
  lines.push(dim(` Found ${project.commitCount} commits ${since}`));
88
88
  formatPolicyViolations(lines, project.policyViolations, style, ' ');
89
89
  if (project.status === 'skipped') {
90
+ formatUnparseableWarning(lines, project, style, ' ');
91
+ formatChangeRecordWarnings(lines, project, style, ' ');
90
92
  lines.push(` ${formatStatusLine(style, 'skipped', project.skipReason)}`);
91
93
  return;
92
94
  }
@@ -97,7 +99,8 @@ function formatProjectSection(lines, project, dryRun, style) {
97
99
  if (project.bumpOverride !== undefined) {
98
100
  lines.push(` Using bump override: ${project.bumpOverride}`);
99
101
  }
100
- formatProjectUnparseable(lines, project, style);
102
+ formatUnparseableWarning(lines, project, style, ' ');
103
+ formatChangeRecordWarnings(lines, project, style, ' ');
101
104
  lines.push(dim(` Bumping versions (${releaseType})...`), ` ${formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${currentVersion} → ${bold(newVersion)} (${releaseType})`)}`);
102
105
  for (const file of project.bumpedFiles) {
103
106
  if (dryRun) {
@@ -108,32 +111,10 @@ function formatProjectSection(lines, project, dryRun, style) {
108
111
  }
109
112
  }
110
113
  lines.push(dim(' Generating changelogs...'));
111
- for (const file of project.changelogFiles) {
112
- if (dryRun) {
113
- lines.push(dim(` [dry-run] Would run: ${CLIFF_DRY_RUN_COMMAND} --output ${file}`));
114
- }
115
- else {
116
- lines.push(dim(` Generating changelog: ${file}`));
117
- }
118
- }
114
+ formatChangelogFiles(lines, project, dryRun, style, ' ');
119
115
  formatPreviewFiles(lines, project, dryRun, ' ');
120
116
  lines.push(` ${formatGlyphLine(RELEASE_GLYPHS, style, 'tag', bold(tag))}`);
121
117
  }
122
- function formatProjectUnparseable(lines, project, style) {
123
- const unparseable = project.unparseableCommits;
124
- if (unparseable === undefined || unparseable.length === 0) {
125
- return;
126
- }
127
- const count = unparseable.length;
128
- const isPatchFloor = project.parsedCommitCount === 0;
129
- const suffix = isPatchFloor ? ' (defaulting to patch bump)' : '';
130
- lines.push(` ${formatStatusLine(style, 'warning', `${count} commit${count === 1 ? '' : 's'} could not be parsed${suffix}`)}`);
131
- for (const commit of unparseable) {
132
- const shortHash = commit.hash.slice(0, 7);
133
- const truncatedMessage = truncateSubject(commit.message);
134
- lines.push(` · ${shortHash} ${truncatedMessage}`);
135
- }
136
- }
137
118
  function formatWorkspaceSection(lines, workspace, dryRun, style) {
138
119
  if (workspace.name !== undefined) {
139
120
  lines.push(`\n${sectionHeader(workspace.name)}`);
@@ -141,26 +122,30 @@ function formatWorkspaceSection(lines, workspace, dryRun, style) {
141
122
  const since = workspace.previousTag === undefined ? '(no previous release found)' : `since ${workspace.previousTag}`;
142
123
  lines.push(dim(` Found ${workspace.commitCount} commits ${since}`));
143
124
  if (workspace.status === 'skipped') {
125
+ formatUnparseableWarning(lines, workspace, style, ' ');
126
+ formatPolicyViolations(lines, workspace.policyViolations, style, ' ');
127
+ formatChangeRecordWarnings(lines, workspace, style, ' ');
144
128
  lines.push(` ${formatStatusLine(style, 'skipped', workspace.skipReason)}`);
145
129
  return;
146
130
  }
147
131
  const { propagatedFrom } = workspace;
148
- const isPropagatedOnly = propagatedFrom !== undefined && workspace.commitCount === 0;
132
+ const isPropagatedOnly = workspace.propagatedOnly === true;
149
133
  formatCommitSummary(lines, workspace, propagatedFrom, isPropagatedOnly);
150
134
  formatUnparseableWarning(lines, workspace, style, ' ');
151
135
  formatPolicyViolations(lines, workspace.policyViolations, style, ' ');
136
+ formatChangeRecordWarnings(lines, workspace, style, ' ');
152
137
  formatBumpLabels(lines, workspace, isPropagatedOnly);
153
138
  formatVersionLine(lines, workspace, propagatedFrom, isPropagatedOnly, style);
154
139
  formatBumpFiles(lines, workspace, dryRun, ' ');
155
140
  lines.push(dim(' Generating changelogs...'));
156
- formatChangelogFiles(lines, workspace, dryRun, ' ');
141
+ formatChangelogFiles(lines, workspace, dryRun, style, ' ');
157
142
  formatPreviewFiles(lines, workspace, dryRun, ' ');
158
143
  lines.push(` ${formatGlyphLine(RELEASE_GLYPHS, style, 'tag', bold(workspace.tag))}`);
159
144
  }
160
145
  function formatCommitSummary(lines, workspace, propagatedFrom, isPropagatedOnly) {
161
146
  if (isPropagatedOnly && propagatedFrom !== undefined) {
162
147
  const depNames = propagatedFrom.map((p) => p.packageName).join(', ');
163
- lines.push(dim(` 0 commits (bumped via dependency: ${depNames})`));
148
+ lines.push(dim(` Bumped via dependency: ${depNames}`));
164
149
  }
165
150
  else if (workspace.parsedCommitCount !== undefined && workspace.parsedCommitCount > 0) {
166
151
  lines.push(dim(` Parsed ${workspace.parsedCommitCount} typed commits`));
@@ -200,14 +185,26 @@ function formatBumpFiles(lines, workspace, dryRun, indent = '') {
200
185
  }
201
186
  }
202
187
  }
203
- function formatChangelogFiles(lines, workspace, dryRun, indent = '') {
204
- for (const file of workspace.changelogFiles) {
188
+ function formatChangelogFiles(lines, release, dryRun, style, indent = '') {
189
+ for (const file of release.changelogFiles) {
205
190
  if (dryRun) {
206
- lines.push(dim(`${indent} [dry-run] Would run: ${CLIFF_DRY_RUN_COMMAND} --output ${file}`));
191
+ lines.push(dim(`${indent} [dry-run] Would generate changelog: ${file}`));
207
192
  }
208
193
  else {
209
194
  lines.push(dim(`${indent} Generating changelog: ${file}`));
210
195
  }
196
+ const preservation = release.changelogPreservation?.find((candidate) => candidate.file === file);
197
+ if (preservation === undefined) {
198
+ continue;
199
+ }
200
+ if (preservation.preservedVersions.length > 0) {
201
+ lines.push(dim(`${indent} Kept from the existing file: ${preservation.preservedVersions.join(', ')}`));
202
+ }
203
+ const dropped = preservation.droppedUnversionedHeadings;
204
+ if (dropped.length > 0) {
205
+ const message = `Dropped ${dropped.length} section${dropped.length === 1 ? '' : 's'} without a version: ${dropped.join(', ')}`;
206
+ lines.push(`${indent} ${formatStatusLine(style, 'warning', message)}`);
207
+ }
211
208
  }
212
209
  }
213
210
  function formatPreviewFiles(lines, release, dryRun, indent = '') {
@@ -221,19 +218,17 @@ function formatPreviewFiles(lines, release, dryRun, indent = '') {
221
218
  }
222
219
  }
223
220
  }
224
- function formatUnparseableWarning(lines, workspace, style, indent = '') {
225
- const unparseable = workspace.unparseableCommits;
221
+ function formatUnparseableWarning(lines, release, style, indent = '') {
222
+ const unparseable = release.unparseableCommits;
226
223
  if (unparseable === undefined || unparseable.length === 0) {
227
224
  return;
228
225
  }
229
226
  const count = unparseable.length;
230
- const isPatchFloor = workspace.parsedCommitCount === 0;
231
- const suffix = isPatchFloor ? ' (defaulting to patch bump)' : '';
232
- lines.push(`${indent} ${formatStatusLine(style, 'warning', `${count} commit${count === 1 ? '' : 's'} could not be parsed${suffix}`)}`);
227
+ lines.push(`${indent} ${formatStatusLine(style, 'warning', `${count} commit${count === 1 ? '' : 's'} could not be parsed`)}`);
233
228
  for (const commit of unparseable) {
234
229
  const shortHash = commit.hash.slice(0, 7);
235
- const truncatedMessage = truncateSubject(commit.message);
236
- lines.push(`${indent} · ${shortHash} ${truncatedMessage}`);
230
+ const truncatedSubject = truncateSubject(commit.subject);
231
+ lines.push(`${indent} · ${shortHash} ${truncatedSubject}`);
237
232
  }
238
233
  }
239
234
  function formatPolicyViolations(lines, violations, style, indent = '') {
@@ -246,7 +241,37 @@ function formatPolicyViolations(lines, violations, style, indent = '') {
246
241
  const shortHash = violation.commitHash.slice(0, 7);
247
242
  const subject = violation.commitSubject;
248
243
  const truncatedSubject = truncateSubject(subject);
249
- lines.push(`${indent} · ${shortHash} '${truncatedSubject}' — type '${violation.type}' at ${violation.surface} surface`);
244
+ const location = violation.surface === 'entry' ? `entry ${violation.entryPosition ?? '?'}` : `${violation.surface} surface`;
245
+ lines.push(`${indent} · ${shortHash} '${truncatedSubject}': type '${violation.type}' at ${location}`);
246
+ }
247
+ }
248
+ function formatChangeRecordWarnings(lines, release, style, indent = '') {
249
+ const { malformedBlocks = [], undeclaredEntryTypes = [], unroutedEntryScopes = [] } = release;
250
+ if (malformedBlocks.length > 0) {
251
+ const count = malformedBlocks.length;
252
+ const header = `${count} change-record block${count === 1 ? '' : 's'} could not be read (item taken from the title)`;
253
+ lines.push(`${indent} ${formatStatusLine(style, 'warning', `${header}:`)}`);
254
+ for (const block of malformedBlocks) {
255
+ lines.push(`${indent} · ${block.commitHash.slice(0, 7)} '${truncateSubject(block.commitSubject)}': ${block.reason}`);
256
+ }
257
+ }
258
+ if (undeclaredEntryTypes.length > 0) {
259
+ const count = undeclaredEntryTypes.length;
260
+ const header = `${count} change-record entr${count === 1 ? 'y has an' : 'ies have'} undeclared type${count === 1 ? '' : 's'} (no item)`;
261
+ lines.push(`${indent} ${formatStatusLine(style, 'warning', `${header}:`)}`);
262
+ for (const entry of undeclaredEntryTypes) {
263
+ const shortHash = entry.commitHash.slice(0, 7);
264
+ lines.push(`${indent} · ${shortHash} '${truncateSubject(entry.commitSubject)}': type '${entry.type}' at entry ${entry.entryPosition}`);
265
+ }
266
+ }
267
+ if (unroutedEntryScopes.length > 0) {
268
+ const count = unroutedEntryScopes.length;
269
+ const header = `${count} change-record entry scope${count === 1 ? ' does not match' : 's do not match'} any workspace in the commit's window (no item there)`;
270
+ lines.push(`${indent} ${formatStatusLine(style, 'warning', `${header}:`)}`);
271
+ for (const finding of unroutedEntryScopes) {
272
+ const shortHash = finding.commitHash.slice(0, 7);
273
+ lines.push(`${indent} · ${shortHash} '${truncateSubject(finding.commitSubject)}': scope '${finding.scope}' at entry ${finding.entryPosition}`);
274
+ }
250
275
  }
251
276
  }
252
277
  function formatPropagationSuffix(propagatedFrom) {
@@ -1,15 +1,19 @@
1
1
  import { type ResolvedTag } from './resolveReleaseTags.js';
2
+ import type { ReleaseKitConfig } from './types.js';
2
3
  /**
3
- * Discover workspaces, resolve release tags from HEAD, validate `--tags` names against the
4
- * full resolved tag names (e.g., `nmr-core-v1.3.0`), and return the filtered tag list.
5
- * Works in both single-package and monorepo modes. Exits with an error message on any validation
6
- * failure — including `deriveWorkspaceConfig()` throws for workspaces missing a `package.json` `name` field,
7
- * and a workspace declaring patterns that resolve to no package, which is not single-package mode.
4
+ * Discovers workspaces, resolves release tags from HEAD, validates `--tags` names against the full resolved tag
5
+ * names (e.g., `nmr-core-v1.3.0`), and returns the filtered tag list. Exits with an error message on any validation
6
+ * failure, including a workspace whose `package.json` does not have a `name` field, a config that
7
+ * `mergeMonorepoConfig` rejects, and a workspace that does not resolve to any package, which is not single-package
8
+ * mode.
8
9
  *
9
- * In both modes `deriveWorkspaceConfig` is called so `WorkspaceConfig.isPublishable` (read
10
- * from `package.json#private`) propagates onto each `ResolvedTag`. Single-package mode now
11
- * derives `./package.json`'s workspace config too — consumers that previously relied on
12
- * single-package mode skipping the derive step need a `name` field on `./package.json`,
13
- * but in practice every npm/pnpm package already has one.
10
+ * In monorepo mode the workspace list is `userConfig` merged over the discovered workspaces, as `prepare` builds it.
11
+ * A tag on HEAD whose workspace the config excludes still counts as a tag on HEAD, so `--tags` may name it and a
12
+ * HEAD whose tags are all excluded returns an empty list rather than failing. Each excluded tag that the run
13
+ * selects, every one without `--tags` or the named ones with it, is dropped with a warning.
14
+ * Single-package mode ignores `userConfig`.
15
+ *
16
+ * In both modes the workspace configs come from `deriveWorkspaceConfig`, so that `WorkspaceConfig.isPublishable`
17
+ * (read from `package.json#private`) propagates onto each `ResolvedTag`.
14
18
  */
15
- export declare function resolveCommandTags(tags: string[] | undefined): ResolvedTag[];
19
+ export declare function resolveCommandTags(tags: string[] | undefined, userConfig: ReleaseKitConfig | undefined): ResolvedTag[];
@@ -1,9 +1,12 @@
1
+ import { basename } from 'node:path';
1
2
  import { reportError } from '@williamthorsen/nmr-core';
2
3
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
4
  import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
4
5
  import { describeEmptyWorkspace, discoverWorkspaces } from "./discoverWorkspaces.js";
6
+ import { formatExcludedSkip } from "./formatExcludedSkip.js";
7
+ import { mergeMonorepoConfig, readRootPackageVersion } from "./loadConfig.js";
5
8
  import { resolveReleaseTags } from "./resolveReleaseTags.js";
6
- export function resolveCommandTags(tags) {
9
+ export function resolveCommandTags(tags, userConfig) {
7
10
  let workspace;
8
11
  try {
9
12
  workspace = discoverWorkspaces();
@@ -18,12 +21,13 @@ export function resolveCommandTags(tags) {
18
21
  }
19
22
  let workspaces;
20
23
  let singleWorkspace;
24
+ const excludedDirs = workspace.kind === 'packages' ? collectExcludedDirs(userConfig) : new Set();
21
25
  try {
22
26
  if (workspace.kind === 'single-package') {
23
27
  singleWorkspace = deriveWorkspaceConfig('.');
24
28
  }
25
29
  else {
26
- workspaces = workspace.packageDirs.map((workspacePath) => deriveWorkspaceConfig(workspacePath));
30
+ workspaces = buildMonorepoWorkspaces(workspace.packageDirs, userConfig, excludedDirs);
27
31
  }
28
32
  }
29
33
  catch (error) {
@@ -38,10 +42,10 @@ export function resolveCommandTags(tags) {
38
42
  resolvedTags = resolveReleaseTags({ singleWorkspace });
39
43
  }
40
44
  else {
41
- throw new Error('resolveCommandTags: invariant violated — neither workspaces nor singleWorkspace was derived');
45
+ throw new Error('resolveCommandTags: invariant violated; neither workspaces nor singleWorkspace was derived');
42
46
  }
43
47
  if (resolvedTags.length === 0) {
44
- reportError('No release tags found on HEAD. Create tags with `release-kit tag` first.');
48
+ reportError('HEAD does not have any release tags. Create tags with `release-kit tag` first.');
45
49
  process.exit(1);
46
50
  }
47
51
  if (tags !== undefined) {
@@ -55,5 +59,31 @@ export function resolveCommandTags(tags) {
55
59
  }
56
60
  resolvedTags = resolvedTags.filter((t) => tags.includes(t.tag));
57
61
  }
58
- return resolvedTags;
62
+ return dropExcludedTags(resolvedTags, excludedDirs);
63
+ }
64
+ function buildMonorepoWorkspaces(packageDirs, userConfig, excludedDirs) {
65
+ const { workspaces } = mergeMonorepoConfig(packageDirs, userConfig, readRootPackageVersion());
66
+ if (excludedDirs.size === 0) {
67
+ return workspaces;
68
+ }
69
+ const excluded = packageDirs
70
+ .filter((packageDir) => excludedDirs.has(basename(packageDir)))
71
+ .map((packageDir) => deriveWorkspaceConfig(packageDir));
72
+ return [...workspaces, ...excluded];
73
+ }
74
+ function collectExcludedDirs(userConfig) {
75
+ const overrides = userConfig?.workspaces ?? [];
76
+ return new Set(overrides.filter((override) => override.shouldExclude === true).map((override) => override.dir));
77
+ }
78
+ function dropExcludedTags(resolvedTags, excludedDirs) {
79
+ const retained = [];
80
+ for (const resolvedTag of resolvedTags) {
81
+ if (excludedDirs.has(resolvedTag.dir)) {
82
+ console.warn(formatExcludedSkip(resolvedTag));
83
+ }
84
+ else {
85
+ retained.push(resolvedTag);
86
+ }
87
+ }
88
+ return retained;
59
89
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Resolves a `--config` value against the directory from which `release-kit` was invoked, which differs from the
3
+ * working directory once the process has moved to the repo root.
4
+ *
5
+ * Returns `undefined` when the flag is absent, so that `loadConfig` falls back to the default path under the root.
6
+ */
7
+ export declare function resolveConfigFlag(config: string | undefined, invocationDir: string): string | undefined;