@williamthorsen/release-kit 11.0.0 → 12.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 (69) hide show
  1. package/.meta/changelog.json +62 -2
  2. package/.readyup/kits/default.js +1 -17
  3. package/.readyup/manifest.json +4 -4
  4. package/CHANGELOG.md +40 -0
  5. package/README.md +25 -44
  6. package/dist/esm/attachChangelogDiagnostics.d.ts +4 -0
  7. package/dist/esm/attachChangelogDiagnostics.js +11 -0
  8. package/dist/esm/bin/release-kit.js +4 -3
  9. package/dist/esm/buildChangelogEntries.d.ts +59 -17
  10. package/dist/esm/buildChangelogEntries.js +194 -111
  11. package/dist/esm/buildReleaseSummary.js +2 -2
  12. package/dist/esm/changelogOverrides.d.ts +31 -13
  13. package/dist/esm/changelogOverrides.js +138 -58
  14. package/dist/esm/classifyChangelogCommit.d.ts +38 -0
  15. package/dist/esm/classifyChangelogCommit.js +33 -0
  16. package/dist/esm/decideRelease.d.ts +11 -37
  17. package/dist/esm/decideRelease.js +5 -27
  18. package/dist/esm/defaults.d.ts +1 -1
  19. package/dist/esm/defaults.js +3 -0
  20. package/dist/esm/determineBumpType.d.ts +11 -11
  21. package/dist/esm/determineBumpType.js +6 -7
  22. package/dist/esm/enumerateReleaseWindows.d.ts +50 -0
  23. package/dist/esm/enumerateReleaseWindows.js +124 -0
  24. package/dist/esm/generateChangelogs.d.ts +5 -23
  25. package/dist/esm/generateChangelogs.js +0 -14
  26. package/dist/esm/glyphs.d.ts +1 -1
  27. package/dist/esm/help/prepareHelp.d.ts +1 -1
  28. package/dist/esm/help/prepareHelp.js +4 -6
  29. package/dist/esm/index.d.ts +1 -1
  30. package/dist/esm/init/initCommand.js +1 -1
  31. package/dist/esm/init/scaffold.d.ts +0 -2
  32. package/dist/esm/init/scaffold.js +2 -30
  33. package/dist/esm/loadConfig.d.ts +1 -1
  34. package/dist/esm/loadConfig.js +1 -4
  35. package/dist/esm/parseChangeRecordBlock.d.ts +37 -0
  36. package/dist/esm/parseChangeRecordBlock.js +134 -0
  37. package/dist/esm/parseCommitMessage.d.ts +31 -6
  38. package/dist/esm/parseCommitMessage.js +6 -5
  39. package/dist/esm/prepareCommand.js +4 -12
  40. package/dist/esm/releasePrepare.d.ts +7 -8
  41. package/dist/esm/releasePrepare.js +53 -73
  42. package/dist/esm/releasePrepareMono.js +48 -85
  43. package/dist/esm/releasePrepareProject.d.ts +1 -1
  44. package/dist/esm/releasePrepareProject.js +19 -37
  45. package/dist/esm/reportPrepare.js +39 -28
  46. package/dist/esm/types.d.ts +99 -55
  47. package/dist/esm/types.js +1 -1
  48. package/dist/esm/validateConfig.js +4 -0
  49. package/dist/esm/validateOverridesCommand.d.ts +6 -5
  50. package/dist/esm/validateOverridesCommand.js +16 -23
  51. package/docs/changelogs.md +64 -25
  52. package/docs/configuration.md +2 -3
  53. package/docs/editorial-overrides.md +75 -16
  54. package/docs/project-releases.md +4 -4
  55. package/docs/releasing.md +13 -8
  56. package/docs/work-types.md +13 -9
  57. package/package.json +1 -2
  58. package/schemas/changelog-overrides.json +39 -0
  59. package/cliff.toml.template +0 -65
  60. package/dist/esm/collectPolicyViolations.d.ts +0 -15
  61. package/dist/esm/collectPolicyViolations.js +0 -12
  62. package/dist/esm/determineBumpFromCommits.d.ts +0 -17
  63. package/dist/esm/determineBumpFromCommits.js +0 -28
  64. package/dist/esm/getCommitsSinceTarget.d.ts +0 -21
  65. package/dist/esm/getCommitsSinceTarget.js +0 -66
  66. package/dist/esm/resolveCliffConfigPath.d.ts +0 -11
  67. package/dist/esm/resolveCliffConfigPath.js +0 -20
  68. package/dist/esm/runGitCliff.d.ts +0 -38
  69. package/dist/esm/runGitCliff.js +0 -25
@@ -8,12 +8,11 @@ import type { ReleaseConfig, ReleaseType } from './types.js';
8
8
  */
9
9
  export interface ReleasePrepareOptions {
10
10
  /**
11
- * Release even when no commits or no bump-worthy commits exist since the last tag
12
- * (monorepo only). Orthogonal to `bumpOverride`: when `bumpOverride` is not given,
13
- * the release falls back to `patch`.
11
+ * Release even when no commits or no bump-worthy commits exist since the last tag.
12
+ * Orthogonal to `bumpOverride`: when `bumpOverride` is not given, the release falls back to `patch`.
14
13
  */
15
14
  force?: boolean;
16
- /** Override the bump type instead of determining it from commits. */
15
+ /** Choose the level of a release instead of the level that the changelog items call for; triggers no release. */
17
16
  bumpOverride?: ReleaseType;
18
17
  /**
19
18
  * Explicit target version (canonical `N.N.N`) that bypasses commit-derived bump logic.
@@ -39,11 +38,11 @@ export interface ReleasePrepareOptions {
39
38
  /**
40
39
  * Orchestrate the release preparation workflow for a single package.
41
40
  *
42
- * 1. Gets commits since the last tag.
43
- * 2. Determines the bump type from commits (or uses the override).
41
+ * 1. Reads the release history once.
42
+ * 2. Decides the release from the history's bump, `--force`, and `--bump` (or takes `--set-version`).
44
43
  * 3. Bumps all configured package.json version fields.
45
- * 4. Generates changelogs via git-cliff.
46
- * 5. Runs the optional format command.
44
+ * 4. Generates changelogs from the same history.
45
+ * 5. Renders the optional format command.
47
46
  *
48
47
  * Returns a structured `PrepareResult` with all data needed for presentation.
49
48
  */
@@ -1,13 +1,11 @@
1
1
  import { join as joinPath } from 'node:path';
2
- import { buildChangelogEntries } from "./buildChangelogEntries.js";
2
+ import { attachChangelogDiagnostics } from "./attachChangelogDiagnostics.js";
3
+ import { readReleaseHistory, toChangelogEntries } from "./buildChangelogEntries.js";
3
4
  import { buildEmptyReleaseEntry } from "./buildEmptyReleaseEntry.js";
4
5
  import { buildReleaseSummary } from "./buildReleaseSummary.js";
5
6
  import { mergeChangelogEntriesWithDisk, renderChangelogJson, resolveChangelogJsonPath } from "./changelogJsonFile.js";
6
7
  import { applyChangelogOverrides, formatStaleOverrideKeyWarning, loadOverridesForScopes, } from "./changelogOverrides.js";
7
- import { createPolicyViolationCollector } from "./collectPolicyViolations.js";
8
- import { DEFAULT_BREAKING_POLICIES, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
9
- import { determineBumpFromCommits } from "./determineBumpFromCommits.js";
10
- import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
8
+ import { decideRelease } from "./decideRelease.js";
11
9
  import { hasPrettierConfig } from "./hasPrettierConfig.js";
12
10
  import { resolveWorkTypes } from "./loadConfig.js";
13
11
  import { planReleaseNotesPreviews } from "./planReleaseNotesPreviews.js";
@@ -15,46 +13,32 @@ import { planVersionBump, planVersionSet } from "./planVersionBump.js";
15
13
  import { renderChangelogMarkdown } from "./renderChangelogMarkdown.js";
16
14
  import { deriveSectionOrder } from "./resolveReleaseNotesConfig.js";
17
15
  export function releasePrepare(config, options) {
18
- const { bumpOverride, setVersion, withReleaseNotes } = options;
16
+ const { bumpOverride, force, setVersion, withReleaseNotes } = options;
19
17
  const writes = [];
20
- const workTypes = config.workTypes ?? { ...DEFAULT_WORK_TYPES };
21
- const versionPatterns = config.versionPatterns ?? { ...DEFAULT_VERSION_PATTERNS };
22
- const breakingPolicies = config.breakingPolicies ?? DEFAULT_BREAKING_POLICIES;
23
18
  const overridesResult = loadOverridesForScopes({ project: '.' });
24
19
  if (overridesResult.errors.length > 0) {
25
20
  throw new Error(`Failed to load changelog overrides:\n - ${overridesResult.errors.join('\n - ')}`);
26
21
  }
27
22
  const overrides = overridesResult.project;
28
- const { tag, commits } = getCommitsSinceTarget([config.tagPrefix]);
23
+ const history = readReleaseHistory(config, { tagPrefixes: [config.tagPrefix] });
24
+ const { commits } = history.unreleased;
25
+ const tag = history.previousTag;
26
+ const since = tag === undefined ? '(no previous release found)' : `since ${tag}`;
29
27
  let releaseType;
30
- let parsedCommitCount;
31
- let unparseableCommits;
32
- const collector = createPolicyViolationCollector();
33
28
  let bump;
34
- if (setVersion !== undefined) {
35
- bump = planVersionSet(config.packageFiles, setVersion);
36
- }
37
- else {
38
- if (bumpOverride === undefined) {
39
- const determination = determineBumpFromCommits(commits, workTypes, versionPatterns, config.scopeAliases, {
40
- breakingPolicies,
41
- onPolicyViolation: collector.onPolicyViolation,
42
- });
43
- parsedCommitCount = determination.parsedCommitCount;
44
- unparseableCommits = determination.unparseableCommits;
45
- releaseType = determination.releaseType;
46
- }
47
- else {
48
- releaseType = bumpOverride;
49
- }
50
- if (releaseType === undefined) {
51
- const skipped = buildSkippedSinglePackage({
52
- commitCount: commits.length,
53
- previousTag: tag,
54
- parsedCommitCount,
55
- unparseableCommits,
56
- policyViolations: collector.violations.length > 0 ? collector.violations : undefined,
57
- });
29
+ if (setVersion === undefined) {
30
+ const decision = decideRelease({
31
+ naturalBump: history.unreleased.bump,
32
+ commitCount: commits.length,
33
+ force,
34
+ bumpOverride,
35
+ skipReasons: {
36
+ noCommits: `No commits ${since}. Pass --force to release at patch. Skipping.`,
37
+ noBumpWorthy: `No bump-worthy commits ${since}. Pass --force to release at patch (or --force --bump=X for a different level). Skipping.`,
38
+ },
39
+ });
40
+ if (decision.outcome === 'skip') {
41
+ const skipped = buildSkippedSinglePackage(history, decision.skipReason);
58
42
  return {
59
43
  workspaces: [skipped],
60
44
  tags: [],
@@ -63,14 +47,18 @@ export function releasePrepare(config, options) {
63
47
  formatCommand: undefined,
64
48
  };
65
49
  }
50
+ releaseType = decision.releaseType;
66
51
  bump = planVersionBump(config.packageFiles, releaseType);
67
52
  }
53
+ else {
54
+ bump = planVersionSet(config.packageFiles, setVersion);
55
+ }
68
56
  writes.push(...bump.writes);
69
57
  const newTag = `${config.tagPrefix}${bump.newVersion}`;
70
58
  const planWarnings = [];
71
59
  const changelogs = planSinglePackageChangelogs({
72
60
  config,
73
- commits,
61
+ history,
74
62
  newTag,
75
63
  newVersion: bump.newVersion,
76
64
  overrides,
@@ -90,15 +78,12 @@ export function releasePrepare(config, options) {
90
78
  formatCommand = { command: `${formatCommandStr} ${modifiedFiles.join(' ')}`, files: modifiedFiles };
91
79
  }
92
80
  const released = buildReleasedSinglePackage({
93
- commits,
81
+ history,
94
82
  bump,
95
83
  newTag,
96
84
  changelogFiles,
97
- previousTag: tag,
98
- parsedCommitCount,
99
85
  releaseType,
100
- unparseableCommits,
101
- policyViolations: collector.violations.length > 0 ? collector.violations : undefined,
86
+ bumpOverride: setVersion === undefined ? bumpOverride : undefined,
102
87
  setVersion,
103
88
  previewFiles: previewWrites.map((write) => write.path),
104
89
  });
@@ -114,28 +99,26 @@ export function releasePrepare(config, options) {
114
99
  }
115
100
  return plan;
116
101
  }
117
- function buildSkippedSinglePackage(args) {
102
+ function buildSkippedSinglePackage(history, skipReason) {
103
+ const { unreleased } = history;
118
104
  const skipped = {
119
105
  status: 'skipped',
120
- commitCount: args.commitCount,
121
- skipReason: 'No release-worthy changes found. Skipping.',
106
+ commitCount: unreleased.commits.length,
107
+ parsedCommitCount: unreleased.parsedCommitCount,
108
+ skipReason,
122
109
  };
123
- if (args.previousTag !== undefined) {
124
- skipped.previousTag = args.previousTag;
125
- }
126
- if (args.parsedCommitCount !== undefined) {
127
- skipped.parsedCommitCount = args.parsedCommitCount;
110
+ if (history.previousTag !== undefined) {
111
+ skipped.previousTag = history.previousTag;
128
112
  }
129
- if (args.unparseableCommits !== undefined) {
130
- skipped.unparseableCommits = args.unparseableCommits;
131
- }
132
- if (args.policyViolations !== undefined) {
133
- skipped.policyViolations = args.policyViolations;
113
+ if (unreleased.unparseableCommits !== undefined) {
114
+ skipped.unparseableCommits = unreleased.unparseableCommits;
134
115
  }
116
+ attachChangelogDiagnostics(skipped, unreleased.diagnostics);
135
117
  return skipped;
136
118
  }
137
119
  function buildReleasedSinglePackage(args) {
138
- const { commits, bump, newTag, changelogFiles, previousTag, parsedCommitCount, releaseType, unparseableCommits, policyViolations, setVersion, } = args;
120
+ const { history, bump, newTag, changelogFiles, releaseType, bumpOverride, setVersion } = args;
121
+ const { commits, diagnostics, parsedCommitCount, unparseableCommits } = history.unreleased;
139
122
  const released = {
140
123
  status: 'released',
141
124
  commitCount: commits.length,
@@ -146,21 +129,20 @@ function buildReleasedSinglePackage(args) {
146
129
  changelogFiles,
147
130
  commits,
148
131
  };
149
- if (previousTag !== undefined) {
150
- released.previousTag = previousTag;
151
- }
152
- if (parsedCommitCount !== undefined) {
153
- released.parsedCommitCount = parsedCommitCount;
132
+ if (history.previousTag !== undefined) {
133
+ released.previousTag = history.previousTag;
154
134
  }
155
135
  if (releaseType !== undefined) {
156
136
  released.releaseType = releaseType;
137
+ released.parsedCommitCount = parsedCommitCount;
138
+ if (unparseableCommits !== undefined) {
139
+ released.unparseableCommits = unparseableCommits;
140
+ }
157
141
  }
158
- if (unparseableCommits !== undefined) {
159
- released.unparseableCommits = unparseableCommits;
160
- }
161
- if (policyViolations !== undefined) {
162
- released.policyViolations = policyViolations;
142
+ if (bumpOverride !== undefined) {
143
+ released.bumpOverride = bumpOverride;
163
144
  }
145
+ attachChangelogDiagnostics(released, diagnostics);
164
146
  if (setVersion !== undefined) {
165
147
  released.setVersion = setVersion;
166
148
  }
@@ -170,13 +152,11 @@ function buildReleasedSinglePackage(args) {
170
152
  return released;
171
153
  }
172
154
  function planSinglePackageChangelogs(args) {
173
- const { config, commits, newTag, newVersion, overrides, overrideWarnings } = args;
174
- const isEmptyRange = commits.length === 0;
155
+ const { config, history, newTag, newVersion, overrides, overrideWarnings } = args;
156
+ const isEmptyRange = history.unreleased.commits.length === 0;
175
157
  const today = new Date().toISOString().slice(0, 10);
176
- const baseEntries = isEmptyRange
177
- ? [buildEmptyReleaseEntry(newVersion, today)]
178
- : buildChangelogEntries(config, newTag);
179
- const applied = applyChangelogOverrides(baseEntries, overrides);
158
+ const builtEntries = isEmptyRange ? [buildEmptyReleaseEntry(newVersion, today)] : toChangelogEntries(history, newTag);
159
+ const applied = applyChangelogOverrides(builtEntries, overrides);
180
160
  if (applied.errors.length > 0) {
181
161
  throw new Error(`Changelog override application failed:\n - ${applied.errors.join('\n - ')}`);
182
162
  }
@@ -1,18 +1,16 @@
1
1
  import { join as joinPath } from 'node:path';
2
2
  import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
3
- import { buildChangelogEntries } from "./buildChangelogEntries.js";
3
+ import { attachChangelogDiagnostics } from "./attachChangelogDiagnostics.js";
4
+ import { readReleaseHistory, toChangelogEntries } from "./buildChangelogEntries.js";
4
5
  import { buildDependencyGraph } from "./buildDependencyGraph.js";
5
6
  import { buildEmptyReleaseEntry } from "./buildEmptyReleaseEntry.js";
6
7
  import { buildReleaseSummary } from "./buildReleaseSummary.js";
7
8
  import { buildSyntheticChangelogEntry } from "./buildSyntheticChangelogEntry.js";
8
9
  import { mergeChangelogEntriesWithDisk, renderChangelogJson, resolveChangelogJsonPath } from "./changelogJsonFile.js";
9
10
  import { applyWorkspaceOverrides, createOverrideContext, formatStaleOverrideKeyWarning, } from "./changelogOverrides.js";
10
- import { createPolicyViolationCollector } from "./collectPolicyViolations.js";
11
11
  import { decideRelease } from "./decideRelease.js";
12
- import { DEFAULT_BREAKING_POLICIES, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
13
12
  import { detectUndeclaredTagPrefixes } from "./detectUndeclaredTagPrefixes.js";
14
- import { buildTagPattern, getAllTagPrefixes } from "./generateChangelogs.js";
15
- import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
13
+ import { getAllTagPrefixes } from "./generateChangelogs.js";
16
14
  import { hasPrettierConfig } from "./hasPrettierConfig.js";
17
15
  import { resolveWorkTypes } from "./loadConfig.js";
18
16
  import { planReleaseNotesPreviews } from "./planReleaseNotesPreviews.js";
@@ -33,10 +31,10 @@ export function releasePrepareMono(config, options) {
33
31
  const { directBumps, directResults, skippedResults } = determineDirectBumps(config, options);
34
32
  const previousTags = new Map();
35
33
  for (const result of directResults.values()) {
36
- previousTags.set(result.workspace.dir, result.tag);
34
+ previousTags.set(result.workspace.dir, result.history.previousTag);
37
35
  }
38
36
  for (const skipped of skippedResults) {
39
- previousTags.set(skipped.workspace.dir, skipped.tag);
37
+ previousTags.set(skipped.workspace.dir, skipped.history.previousTag);
40
38
  }
41
39
  const graph = buildDependencyGraph(config.workspaces);
42
40
  const fullReleaseSet = propagateBumps(directBumps, graph);
@@ -111,9 +109,6 @@ function determineDirectBumps(config, options) {
111
109
  if (setVersion !== undefined && config.workspaces.length !== 1) {
112
110
  throw new Error(`--set-version requires exactly one workspace; received ${config.workspaces.length}`);
113
111
  }
114
- const workTypes = config.workTypes ?? { ...DEFAULT_WORK_TYPES };
115
- const versionPatterns = config.versionPatterns ?? { ...DEFAULT_VERSION_PATTERNS };
116
- const breakingPolicies = config.breakingPolicies ?? DEFAULT_BREAKING_POLICIES;
117
112
  const directBumps = new Map();
118
113
  const directResults = new Map();
119
114
  const skippedResults = [];
@@ -122,7 +117,8 @@ function determineDirectBumps(config, options) {
122
117
  for (const workspace of config.workspaces) {
123
118
  const name = workspace.dir;
124
119
  const stageLabel = workspaceStageLabel(workspace.dir);
125
- const { tag, commits } = tryStage(stageLabel, () => getCommitsSinceTarget(getAllTagPrefixes(workspace), workspace.paths));
120
+ const history = tryStage(stageLabel, () => readReleaseHistory(config, { tagPrefixes: getAllTagPrefixes(workspace), paths: workspace.paths }));
121
+ const tag = history.previousTag;
126
122
  const since = tag === undefined ? '(no previous release found)' : `since ${tag}`;
127
123
  if (tag === undefined) {
128
124
  maybeEmitBaselineHint(workspace, knownPrefixes, hintState);
@@ -131,54 +127,32 @@ function determineDirectBumps(config, options) {
131
127
  directBumps.set(workspace.dir, { releaseType: 'patch', newVersionOverride: setVersion });
132
128
  directResults.set(workspace.dir, {
133
129
  workspace,
134
- tag,
135
- commits,
130
+ history,
136
131
  releaseType: undefined,
137
- parsedCommitCount: undefined,
138
- unparseableCommits: undefined,
139
- policyViolations: undefined,
140
132
  bumpOverride: undefined,
141
133
  setVersion,
142
134
  });
143
135
  continue;
144
136
  }
145
- const collector = createPolicyViolationCollector();
146
- const decision = tryStage(stageLabel, () => decideRelease({
147
- commits,
137
+ const decision = decideRelease({
138
+ naturalBump: history.unreleased.bump,
139
+ commitCount: history.unreleased.commits.length,
148
140
  force,
149
141
  bumpOverride,
150
- workTypes,
151
- versionPatterns,
152
- scopeAliases: config.scopeAliases,
153
- breakingPolicies,
154
- onPolicyViolation: collector.onPolicyViolation,
155
142
  skipReasons: {
156
143
  noCommits: `No commits for ${name} ${since}. Pass --force to release at patch. Skipping.`,
157
144
  noBumpWorthy: `No bump-worthy commits for ${name} ${since}. Pass --force to release at patch (or --force --bump=X for a different level). Skipping.`,
158
145
  },
159
- }));
160
- const policyViolations = collector.violations.length > 0 ? collector.violations : undefined;
146
+ });
161
147
  if (decision.outcome === 'skip') {
162
- skippedResults.push({
163
- workspace,
164
- tag,
165
- commitCount: commits.length,
166
- parsedCommitCount: decision.parsedCommitCount,
167
- unparseableCommits: decision.unparseableCommits,
168
- policyViolations,
169
- skipReason: decision.skipReason,
170
- });
148
+ skippedResults.push({ workspace, history, skipReason: decision.skipReason });
171
149
  continue;
172
150
  }
173
151
  directBumps.set(workspace.dir, { releaseType: decision.releaseType });
174
152
  directResults.set(workspace.dir, {
175
153
  workspace,
176
- tag,
177
- commits,
154
+ history,
178
155
  releaseType: decision.releaseType,
179
- parsedCommitCount: decision.parsedCommitCount,
180
- unparseableCommits: decision.unparseableCommits,
181
- policyViolations,
182
156
  bumpOverride,
183
157
  });
184
158
  }
@@ -190,24 +164,21 @@ function collectSkippedWorkspaces(skippedResults, fullReleaseSet) {
190
164
  if (fullReleaseSet.has(skipped.workspace.dir)) {
191
165
  continue;
192
166
  }
167
+ const { previousTag, unreleased } = skipped.history;
193
168
  const result = {
194
169
  name: skipped.workspace.dir,
195
170
  status: 'skipped',
196
- commitCount: skipped.commitCount,
171
+ commitCount: unreleased.commits.length,
172
+ parsedCommitCount: unreleased.parsedCommitCount,
197
173
  skipReason: skipped.skipReason,
198
174
  };
199
- if (skipped.tag !== undefined) {
200
- result.previousTag = skipped.tag;
201
- }
202
- if (skipped.parsedCommitCount !== undefined) {
203
- result.parsedCommitCount = skipped.parsedCommitCount;
175
+ if (previousTag !== undefined) {
176
+ result.previousTag = previousTag;
204
177
  }
205
- if (skipped.unparseableCommits !== undefined) {
206
- result.unparseableCommits = skipped.unparseableCommits;
207
- }
208
- if (skipped.policyViolations !== undefined) {
209
- result.policyViolations = skipped.policyViolations;
178
+ if (unreleased.unparseableCommits !== undefined) {
179
+ result.unparseableCommits = unreleased.unparseableCommits;
210
180
  }
181
+ attachChangelogDiagnostics(result, unreleased.diagnostics);
211
182
  workspaces.push(result);
212
183
  }
213
184
  return workspaces;
@@ -256,15 +227,13 @@ function executeWorkspaceRelease(args) {
256
227
  tags.push(newTag);
257
228
  writes.push(...bump.writes);
258
229
  modifiedFiles.push(...workspace.packageFiles, ...workspace.changelogPaths.map((changelogPath) => joinPath(changelogPath, 'CHANGELOG.md')));
259
- const isPropagationOnly = directResult === undefined;
260
- const isEmptyRange = directResult !== undefined && directResult.commits.length === 0;
230
+ const directCommits = directResult?.history.unreleased.commits;
261
231
  const { changelogFiles, previewFiles } = generateWorkspaceChangelogs({
262
232
  workspace,
263
233
  releaseEntry,
264
234
  newTag,
265
235
  newVersion: bump.newVersion,
266
- isPropagationOnly,
267
- isEmptyRange,
236
+ history: directResult?.history,
268
237
  config,
269
238
  today,
270
239
  modifiedFiles,
@@ -277,7 +246,7 @@ function executeWorkspaceRelease(args) {
277
246
  const released = {
278
247
  name: dir,
279
248
  status: 'released',
280
- commitCount: directResult?.commits.length ?? 0,
249
+ commitCount: directCommits?.length ?? 0,
281
250
  currentVersion: bump.currentVersion,
282
251
  newVersion: bump.newVersion,
283
252
  tag: newTag,
@@ -286,7 +255,7 @@ function executeWorkspaceRelease(args) {
286
255
  ...(previewFiles.length > 0 && { previewFiles }),
287
256
  };
288
257
  attachReleasedWorkspaceOptionals(released, {
289
- previousTag: directResult?.tag ?? previousTags.get(dir),
258
+ previousTag: directResult?.history.previousTag ?? previousTags.get(dir),
290
259
  directResult,
291
260
  releaseEntry,
292
261
  setVersionTarget,
@@ -298,20 +267,11 @@ function attachReleasedWorkspaceOptionals(released, args) {
298
267
  if (previousTag !== undefined) {
299
268
  released.previousTag = previousTag;
300
269
  }
301
- if (directResult?.parsedCommitCount !== undefined) {
302
- released.parsedCommitCount = directResult.parsedCommitCount;
303
- }
304
270
  if (setVersionTarget === undefined) {
305
271
  released.releaseType = releaseEntry.releaseType;
306
272
  }
307
- if (directResult?.commits !== undefined) {
308
- released.commits = directResult.commits;
309
- }
310
- if (directResult?.unparseableCommits !== undefined) {
311
- released.unparseableCommits = directResult.unparseableCommits;
312
- }
313
- if (directResult?.policyViolations !== undefined) {
314
- released.policyViolations = directResult.policyViolations;
273
+ if (directResult !== undefined) {
274
+ attachDirectHistory(released, directResult.history.unreleased, setVersionTarget === undefined);
315
275
  }
316
276
  if (releaseEntry.propagatedFrom !== undefined) {
317
277
  released.propagatedFrom = releaseEntry.propagatedFrom;
@@ -323,19 +283,20 @@ function attachReleasedWorkspaceOptionals(released, args) {
323
283
  released.setVersion = setVersionTarget;
324
284
  }
325
285
  }
286
+ function attachDirectHistory(released, unreleased, hasDecidedBump) {
287
+ released.commits = unreleased.commits;
288
+ if (hasDecidedBump) {
289
+ released.parsedCommitCount = unreleased.parsedCommitCount;
290
+ if (unreleased.unparseableCommits !== undefined) {
291
+ released.unparseableCommits = unreleased.unparseableCommits;
292
+ }
293
+ }
294
+ attachChangelogDiagnostics(released, unreleased.diagnostics);
295
+ }
326
296
  function generateWorkspaceChangelogs(args) {
327
- const { workspace, releaseEntry, newTag, newVersion, isPropagationOnly, isEmptyRange, config, today, modifiedFiles, writes, warnings, previewOptions, overrideContext, sectionOrder, } = args;
328
- const newEntries = buildWorkspaceEntries({
329
- workspace,
330
- releaseEntry,
331
- newTag,
332
- newVersion,
333
- isPropagationOnly,
334
- isEmptyRange,
335
- config,
336
- today,
337
- });
338
- const applied = applyWorkspaceOverrides(newEntries, workspace.workspacePath, overrideContext);
297
+ const { workspace, releaseEntry, newTag, newVersion, history, config, today, modifiedFiles, writes, warnings, previewOptions, overrideContext, sectionOrder, } = args;
298
+ const entries = buildWorkspaceEntries({ workspace, releaseEntry, newTag, newVersion, history, today });
299
+ const applied = applyWorkspaceOverrides(entries, workspace.workspacePath, overrideContext);
339
300
  const changelogFiles = [];
340
301
  let firstMergedEntries;
341
302
  for (const changelogPath of workspace.changelogPaths) {
@@ -355,15 +316,17 @@ function generateWorkspaceChangelogs(args) {
355
316
  return { changelogFiles, previewFiles: previews.map((write) => write.path) };
356
317
  }
357
318
  function buildWorkspaceEntries(args) {
358
- const { workspace, releaseEntry, newTag, newVersion, isPropagationOnly, isEmptyRange, config, today } = args;
359
- if (isPropagationOnly && releaseEntry.propagatedFrom !== undefined) {
319
+ const { workspace, releaseEntry, newTag, newVersion, history, today } = args;
320
+ if (history === undefined) {
321
+ if (releaseEntry.propagatedFrom === undefined) {
322
+ throw new Error(`Workspace '${workspace.dir}' has neither a direct release nor a propagation source`);
323
+ }
360
324
  return [buildSyntheticChangelogEntry(releaseEntry.propagatedFrom, newVersion, today)];
361
325
  }
362
- if (isEmptyRange) {
326
+ if (history.unreleased.commits.length === 0) {
363
327
  return [buildEmptyReleaseEntry(newVersion, today)];
364
328
  }
365
- const tagPattern = buildTagPattern(getAllTagPrefixes(workspace));
366
- return buildChangelogEntries(config, newTag, { tagPattern, includePaths: workspace.paths });
329
+ return toChangelogEntries(history, newTag);
367
330
  }
368
331
  function planPreviews(workspace, newTag, entries, previewOptions, warnings) {
369
332
  if (!previewOptions.enabled || entries === undefined) {
@@ -40,7 +40,7 @@ export interface ReleasePrepareProjectArgs {
40
40
  /**
41
41
  * Run the project-level release stage.
42
42
  *
43
- * Mirrors the per-workspace pipeline shape — find baseline tag → derive bump → bump version →
43
+ * Mirrors the per-workspace pipeline shape — read the history → decide the bump → bump version →
44
44
  * regenerate CHANGELOG → optionally emit changelog.json and release-notes previews — but
45
45
  * targets the root `package.json` and the root `CHANGELOG.md`. Contributing paths come from
46
46
  * the resolved `project.paths`, which defaults to the union of every (already-filtered)
@@ -1,13 +1,11 @@
1
1
  import { join as joinPath } from 'node:path';
2
- import { buildChangelogEntries } from "./buildChangelogEntries.js";
2
+ import { attachChangelogDiagnostics } from "./attachChangelogDiagnostics.js";
3
+ import { readReleaseHistory, toChangelogEntries } from "./buildChangelogEntries.js";
3
4
  import { buildEmptyReleaseEntry } from "./buildEmptyReleaseEntry.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";
11
9
  import { planReleaseNotesPreviews } from "./planReleaseNotesPreviews.js";
12
10
  import { planVersionBump } from "./planVersionBump.js";
13
11
  import { renderChangelogMarkdown } from "./renderChangelogMarkdown.js";
@@ -23,54 +21,43 @@ export function releasePrepareProject(args) {
23
21
  throw new Error('releasePrepareProject called without a configured project block');
24
22
  }
25
23
  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);
24
+ const history = readReleaseHistory(config, { tagPrefixes: [project.tagPrefix], paths: project.paths });
25
+ const { commits, diagnostics, parsedCommitCount, unparseableCommits } = history.unreleased;
26
+ const tag = history.previousTag;
30
27
  const since = tag === undefined ? '(no previous release found)' : `since ${tag}`;
31
- const collector = createPolicyViolationCollector();
32
28
  const decision = decideRelease({
33
- commits,
29
+ naturalBump: history.unreleased.bump,
30
+ commitCount: commits.length,
34
31
  force,
35
32
  bumpOverride,
36
- workTypes,
37
- versionPatterns,
38
- scopeAliases: config.scopeAliases,
39
- breakingPolicies,
40
- onPolicyViolation: collector.onPolicyViolation,
41
33
  skipReasons: {
42
34
  noCommits: `No commits ${since}. Pass --force to release at patch. Skipping.`,
43
35
  noBumpWorthy: `No bump-worthy commits ${since}. Pass --force to release at patch (or --force --bump=X for a different level). Skipping.`,
44
36
  },
45
37
  });
46
- const policyViolations = collector.violations.length > 0 ? collector.violations : undefined;
47
38
  if (decision.outcome === 'skip') {
48
39
  const skipped = {
49
40
  status: 'skipped',
50
41
  commitCount: commits.length,
51
- parsedCommitCount: decision.parsedCommitCount,
42
+ parsedCommitCount,
52
43
  skipReason: decision.skipReason,
53
44
  };
54
45
  if (tag !== undefined) {
55
46
  skipped.previousTag = tag;
56
47
  }
57
- if (decision.unparseableCommits !== undefined) {
58
- skipped.unparseableCommits = decision.unparseableCommits;
59
- }
60
- if (policyViolations !== undefined) {
61
- skipped.policyViolations = policyViolations;
48
+ if (unparseableCommits !== undefined) {
49
+ skipped.unparseableCommits = unparseableCommits;
62
50
  }
51
+ attachChangelogDiagnostics(skipped, diagnostics);
63
52
  return skipped;
64
53
  }
65
- const { releaseType, parsedCommitCount, unparseableCommits } = decision;
54
+ const { releaseType } = decision;
66
55
  const bump = planVersionBump([ROOT_PACKAGE_FILE], releaseType);
67
56
  writes.push(...bump.writes);
68
57
  const newTag = `${project.tagPrefix}${bump.newVersion}`;
69
58
  const changelogs = planProjectChangelogs({
70
59
  config,
71
- project,
72
- commits,
73
- contributingPaths,
60
+ history,
74
61
  newTag,
75
62
  newVersion: bump.newVersion,
76
63
  rootOverrides,
@@ -114,12 +101,10 @@ export function releasePrepareProject(args) {
114
101
  if (unparseableCommits !== undefined) {
115
102
  result.unparseableCommits = unparseableCommits;
116
103
  }
117
- if (policyViolations !== undefined) {
118
- result.policyViolations = policyViolations;
119
- }
120
104
  if (bumpOverride !== undefined) {
121
105
  result.bumpOverride = bumpOverride;
122
106
  }
107
+ attachChangelogDiagnostics(result, diagnostics);
123
108
  return result;
124
109
  }
125
110
  function resolveOptionalOverrideArgs(args) {
@@ -131,14 +116,11 @@ function resolveOptionalOverrideArgs(args) {
131
116
  };
132
117
  }
133
118
  function planProjectChangelogs(args) {
134
- const { config, project, commits, contributingPaths, newTag, newVersion, rootOverrides, overrideWarnings, globalMatchedRootKeys, } = args;
135
- const isEmptyRange = commits.length === 0;
119
+ const { config, history, newTag, newVersion, rootOverrides, overrideWarnings, globalMatchedRootKeys } = args;
120
+ const isEmptyRange = history.unreleased.commits.length === 0;
136
121
  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);
122
+ const builtEntries = isEmptyRange ? [buildEmptyReleaseEntry(newVersion, today)] : toChangelogEntries(history, newTag);
123
+ const applied = applyChangelogOverrides(builtEntries, rootOverrides);
142
124
  if (applied.errors.length > 0) {
143
125
  throw new Error(`Changelog override application failed:\n - ${applied.errors.join('\n - ')}`);
144
126
  }