@williamthorsen/release-kit 5.2.1 → 5.3.1

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 (46) hide show
  1. package/CHANGELOG.md +76 -26
  2. package/README.md +223 -31
  3. package/cliff.toml.template +4 -39
  4. package/dist/esm/.cache +1 -1
  5. package/dist/esm/bin/release-kit.js +59 -0
  6. package/dist/esm/buildChangelogEntries.js +6 -0
  7. package/dist/esm/buildEmptyReleaseEntry.d.ts +2 -0
  8. package/dist/esm/buildEmptyReleaseEntry.js +16 -0
  9. package/dist/esm/changelogJsonFile.d.ts +2 -0
  10. package/dist/esm/changelogJsonFile.js +11 -1
  11. package/dist/esm/changelogJsonUtils.d.ts +2 -1
  12. package/dist/esm/changelogJsonUtils.js +9 -0
  13. package/dist/esm/changelogOverrides.d.ts +53 -0
  14. package/dist/esm/changelogOverrides.js +424 -0
  15. package/dist/esm/checkWorkTypesDrift.js +3 -2
  16. package/dist/esm/defaults.js +1 -1
  17. package/dist/esm/generateChangelogs.d.ts +2 -3
  18. package/dist/esm/generateChangelogs.js +4 -34
  19. package/dist/esm/index.d.ts +2 -0
  20. package/dist/esm/index.js +8 -0
  21. package/dist/esm/loadConfig.d.ts +1 -1
  22. package/dist/esm/releasePrepare.js +68 -11
  23. package/dist/esm/releasePrepareMono.js +103 -59
  24. package/dist/esm/releasePrepareProject.d.ts +4 -1
  25. package/dist/esm/releasePrepareProject.js +74 -18
  26. package/dist/esm/renderChangelogMarkdown.d.ts +12 -0
  27. package/dist/esm/renderChangelogMarkdown.js +32 -0
  28. package/dist/esm/renderReleaseNotes.js +6 -1
  29. package/dist/esm/resolveReleaseNotesConfig.js +3 -1
  30. package/dist/esm/runGitCliff.d.ts +1 -0
  31. package/dist/esm/runGitCliff.js +7 -0
  32. package/dist/esm/syncWorkTypes.js +3 -2
  33. package/dist/esm/types.d.ts +98 -31
  34. package/dist/esm/types.js +60 -0
  35. package/dist/esm/validateConfig.js +84 -345
  36. package/dist/esm/validateOverridesCommand.d.ts +14 -0
  37. package/dist/esm/validateOverridesCommand.js +136 -0
  38. package/dist/esm/work-types.json +23 -17
  39. package/dist/esm/work-types.schema.json +28 -1
  40. package/dist/esm/workTypesData.d.ts +8 -0
  41. package/dist/esm/workTypesData.js +20 -17
  42. package/dist/esm/workTypesUtils.d.ts +1 -0
  43. package/dist/esm/workTypesUtils.js +8 -0
  44. package/package.json +5 -4
  45. package/dist/esm/writeSyntheticChangelog.d.ts +0 -9
  46. package/dist/esm/writeSyntheticChangelog.js +0 -27
@@ -0,0 +1,424 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { isRecord } from "./typeGuards.js";
4
+ const OVERRIDES_FILENAME = ".meta/changelog-overrides.json";
5
+ const VALID_AUDIENCE_VALUES = /* @__PURE__ */ new Set(["all", "dev", "skip"]);
6
+ const V1_SUPPORTED_AUDIENCE_VALUES = /* @__PURE__ */ new Set(["skip"]);
7
+ const KNOWN_OVERRIDE_FIELDS = /* @__PURE__ */ new Set(["audience", "description", "body", "breaking"]);
8
+ function loadChangelogOverrides(path2) {
9
+ if (!existsSync(path2)) {
10
+ return { overrides: /* @__PURE__ */ new Map() };
11
+ }
12
+ let content;
13
+ try {
14
+ content = readFileSync(path2, "utf8");
15
+ } catch (error) {
16
+ return {
17
+ errors: [`Failed to read override file ${path2}: ${error instanceof Error ? error.message : String(error)}`]
18
+ };
19
+ }
20
+ let parsed;
21
+ try {
22
+ parsed = JSON.parse(content);
23
+ } catch (error) {
24
+ return {
25
+ errors: [`Failed to parse override file ${path2}: ${error instanceof Error ? error.message : String(error)}`]
26
+ };
27
+ }
28
+ const result = validateChangelogOverrides(parsed);
29
+ if (result.errors.length > 0) {
30
+ return { errors: result.errors };
31
+ }
32
+ return { overrides: result.overrides };
33
+ }
34
+ function validateChangelogOverrides(raw) {
35
+ const overrides = /* @__PURE__ */ new Map();
36
+ const errors = [];
37
+ if (!isRecord(raw)) {
38
+ errors.push("Override file: top-level value must be an object keyed by commit hash");
39
+ return { overrides, errors };
40
+ }
41
+ for (const [key, rawEntry] of Object.entries(raw)) {
42
+ if (key === "") {
43
+ errors.push("Override file: empty-string key is not a valid commit hash");
44
+ continue;
45
+ }
46
+ const validated = validateSingleOverride(key, rawEntry, errors);
47
+ if (validated !== void 0) {
48
+ overrides.set(key, validated);
49
+ }
50
+ }
51
+ return { overrides, errors };
52
+ }
53
+ function validateSingleOverride(key, rawEntry, errors) {
54
+ if (!isRecord(rawEntry)) {
55
+ errors.push(`overrides['${key}']: must be an object`);
56
+ return void 0;
57
+ }
58
+ let entryValid = true;
59
+ for (const fieldName of Object.keys(rawEntry)) {
60
+ if (!KNOWN_OVERRIDE_FIELDS.has(fieldName)) {
61
+ errors.push(`overrides['${key}']: unknown field '${fieldName}'`);
62
+ entryValid = false;
63
+ }
64
+ }
65
+ const result = {};
66
+ if (rawEntry.audience !== void 0) {
67
+ const audienceResult = validateAudience(key, rawEntry.audience, errors);
68
+ if (audienceResult === void 0) {
69
+ entryValid = false;
70
+ } else {
71
+ result.audience = audienceResult;
72
+ }
73
+ }
74
+ if (rawEntry.description !== void 0) {
75
+ if (typeof rawEntry.description !== "string") {
76
+ errors.push(`overrides['${key}']: 'description' must be a string`);
77
+ entryValid = false;
78
+ } else {
79
+ result.description = rawEntry.description;
80
+ }
81
+ }
82
+ if (rawEntry.body !== void 0) {
83
+ if (typeof rawEntry.body !== "string") {
84
+ errors.push(`overrides['${key}']: 'body' must be a string`);
85
+ entryValid = false;
86
+ } else {
87
+ result.body = rawEntry.body;
88
+ }
89
+ }
90
+ if (rawEntry.breaking !== void 0) {
91
+ if (typeof rawEntry.breaking !== "boolean") {
92
+ errors.push(`overrides['${key}']: 'breaking' must be a boolean`);
93
+ entryValid = false;
94
+ } else {
95
+ result.breaking = rawEntry.breaking;
96
+ }
97
+ }
98
+ if (Object.keys(result).length === 0 && entryValid) {
99
+ errors.push(`overrides['${key}']: at least one override field must be set`);
100
+ return void 0;
101
+ }
102
+ if (!entryValid) {
103
+ return void 0;
104
+ }
105
+ return result;
106
+ }
107
+ function validateAudience(key, value, errors) {
108
+ if (typeof value !== "string" || !VALID_AUDIENCE_VALUES.has(value)) {
109
+ errors.push(`overrides['${key}']: 'audience' must be one of 'all' | 'dev' | 'skip'`);
110
+ return void 0;
111
+ }
112
+ if (!V1_SUPPORTED_AUDIENCE_VALUES.has(value)) {
113
+ errors.push(`overrides['${key}']: audience '${value}' is not yet supported; only 'skip' is currently accepted`);
114
+ return void 0;
115
+ }
116
+ return "skip";
117
+ }
118
+ function formatStaleOverrideKeyWarning(key) {
119
+ return `Override key '${key}' did not match any commit hash in the changelog (likely a stale reference)`;
120
+ }
121
+ function applyChangelogOverrides(entries, overrides) {
122
+ const warnings = [];
123
+ const errors = [];
124
+ const matchedKeys = [];
125
+ if (overrides.size === 0) {
126
+ return { entries: entries.map(cloneEntry), warnings, errors, matchedKeys };
127
+ }
128
+ const allHashes = [];
129
+ for (const entry of entries) {
130
+ for (const section of entry.sections) {
131
+ for (const item of section.items) {
132
+ if (item.hash !== void 0) {
133
+ allHashes.push(item.hash);
134
+ }
135
+ }
136
+ }
137
+ }
138
+ const keyToMatchedHashes = /* @__PURE__ */ new Map();
139
+ for (const overrideKey of overrides.keys()) {
140
+ const matches = allHashes.filter((hash) => hash.startsWith(overrideKey));
141
+ if (matches.length === 0) {
142
+ continue;
143
+ }
144
+ if (matches.length > 1) {
145
+ errors.push(
146
+ `Override key '${overrideKey}' is ambiguous: matches multiple commits (${matches.join(", ")}). Use a longer prefix or the full commit hash.`
147
+ );
148
+ continue;
149
+ }
150
+ keyToMatchedHashes.set(overrideKey, matches);
151
+ matchedKeys.push(overrideKey);
152
+ }
153
+ const hashToOverride = /* @__PURE__ */ new Map();
154
+ for (const [overrideKey, matchedHashes] of keyToMatchedHashes) {
155
+ const override = overrides.get(overrideKey);
156
+ if (override === void 0) continue;
157
+ for (const hash of matchedHashes) {
158
+ hashToOverride.set(hash, override);
159
+ }
160
+ }
161
+ const transformedEntries = [];
162
+ for (const entry of entries) {
163
+ const transformedSections = [];
164
+ for (const section of entry.sections) {
165
+ const transformedItems = applyOverridesToItems(section.items, hashToOverride);
166
+ if (transformedItems.length === 0) {
167
+ continue;
168
+ }
169
+ transformedSections.push({ ...section, items: transformedItems });
170
+ }
171
+ transformedEntries.push({ ...entry, sections: transformedSections });
172
+ }
173
+ return { entries: transformedEntries, warnings, errors, matchedKeys };
174
+ }
175
+ function applyOverridesToItems(items, hashToOverride) {
176
+ const result = [];
177
+ for (const item of items) {
178
+ if (item.hash === void 0) {
179
+ result.push(cloneItem(item));
180
+ continue;
181
+ }
182
+ const override = hashToOverride.get(item.hash);
183
+ if (override === void 0) {
184
+ result.push(cloneItem(item));
185
+ continue;
186
+ }
187
+ if (override.audience === "skip") {
188
+ continue;
189
+ }
190
+ result.push(applyOverrideToItem(item, override));
191
+ }
192
+ return result;
193
+ }
194
+ function applyOverrideToItem(item, override) {
195
+ const result = { ...item };
196
+ if (override.description !== void 0) {
197
+ result.description = override.description;
198
+ }
199
+ if (override.body !== void 0) {
200
+ result.body = override.body;
201
+ }
202
+ if (override.breaking !== void 0) {
203
+ result.breaking = override.breaking;
204
+ }
205
+ return result;
206
+ }
207
+ function cloneItem(item) {
208
+ return { ...item };
209
+ }
210
+ function cloneEntry(entry) {
211
+ return {
212
+ ...entry,
213
+ sections: entry.sections.map((section) => ({ ...section, items: section.items.map(cloneItem) }))
214
+ };
215
+ }
216
+ function resolveOverridePath(scopeRoot) {
217
+ return path.posix.join(scopeRoot, OVERRIDES_FILENAME);
218
+ }
219
+ function loadOverridesForScopes(scopes) {
220
+ const errors = [];
221
+ let project = /* @__PURE__ */ new Map();
222
+ const perWorkspace = /* @__PURE__ */ new Map();
223
+ if (scopes.project !== void 0) {
224
+ const result = loadChangelogOverrides(resolveOverridePath(scopes.project));
225
+ if ("errors" in result) {
226
+ errors.push(...result.errors);
227
+ } else {
228
+ project = result.overrides;
229
+ }
230
+ }
231
+ for (const workspacePath of scopes.workspaces ?? []) {
232
+ const result = loadChangelogOverrides(resolveOverridePath(workspacePath));
233
+ if ("errors" in result) {
234
+ errors.push(...result.errors);
235
+ } else if (result.overrides.size > 0) {
236
+ perWorkspace.set(workspacePath, result.overrides);
237
+ }
238
+ }
239
+ return { project, perWorkspace, errors };
240
+ }
241
+ function composeOverrides(rootEntries, workspaceEntries) {
242
+ const composed = new Map(rootEntries);
243
+ if (workspaceEntries !== void 0) {
244
+ for (const [key, value] of workspaceEntries) {
245
+ composed.set(key, value);
246
+ }
247
+ }
248
+ return composed;
249
+ }
250
+ function createOverrideContext(workspaces) {
251
+ const result = loadOverridesForScopes({
252
+ project: ".",
253
+ workspaces: workspaces.map((workspace) => workspace.workspacePath)
254
+ });
255
+ if (result.errors.length > 0) {
256
+ throw new Error(`Failed to load changelog overrides:
257
+ - ${result.errors.join("\n - ")}`);
258
+ }
259
+ return {
260
+ project: result.project,
261
+ perWorkspace: result.perWorkspace,
262
+ overrideWarnings: [],
263
+ globalMatchedRootKeys: /* @__PURE__ */ new Set()
264
+ };
265
+ }
266
+ function validateAllChangelogOverrides(inputs) {
267
+ const errors = [];
268
+ const warnings = [];
269
+ const projectFilePath = inputs.project?.filePath;
270
+ const projectMap = loadScopeMap(projectFilePath, errors);
271
+ const workspaceMaps = (inputs.workspaces ?? []).map((scope) => ({
272
+ filePath: scope.filePath,
273
+ hashes: scope.hashes,
274
+ map: loadScopeMap(scope.filePath, errors)
275
+ }));
276
+ const globalMatchedRootKeys = /* @__PURE__ */ new Set();
277
+ for (const workspace of workspaceMaps) {
278
+ processWorkspaceScope({
279
+ workspace,
280
+ projectFilePath,
281
+ projectMap,
282
+ errors,
283
+ warnings,
284
+ globalMatchedRootKeys
285
+ });
286
+ }
287
+ const projectHashes = inputs.project?.hashes;
288
+ if (projectFilePath !== void 0 && projectHashes !== void 0) {
289
+ processProjectScope({ projectFilePath, projectMap, projectHashes, errors, globalMatchedRootKeys });
290
+ }
291
+ if (projectFilePath !== void 0) {
292
+ collectRootStaleWarnings(projectFilePath, projectMap, globalMatchedRootKeys, warnings);
293
+ }
294
+ return { errors, warnings };
295
+ }
296
+ function processWorkspaceScope(args) {
297
+ const { workspace, projectFilePath, projectMap, errors, warnings, globalMatchedRootKeys } = args;
298
+ const { filePath, hashes, map } = workspace;
299
+ const workspaceApplied = applyChangelogOverrides(makeValidationEntries(hashes), map);
300
+ for (const message of workspaceApplied.errors) {
301
+ errors.push(prefixWithFilePath(filePath, message));
302
+ }
303
+ if (projectFilePath !== void 0 && projectMap.size > 0) {
304
+ const projectMinusShadowed = filterShadowedKeys(projectMap, map);
305
+ const projectApplied = applyChangelogOverrides(makeValidationEntries(hashes), projectMinusShadowed);
306
+ for (const message of projectApplied.errors) {
307
+ errors.push(prefixWithFilePath(projectFilePath, message));
308
+ }
309
+ }
310
+ for (const key of map.keys()) {
311
+ if (!hasAnyMatch(key, hashes)) {
312
+ warnings.push(formatWorkspaceStaleWarning(filePath, key));
313
+ }
314
+ }
315
+ for (const key of projectMap.keys()) {
316
+ if (map.has(key)) continue;
317
+ if (hasAnyMatch(key, hashes)) {
318
+ globalMatchedRootKeys.add(key);
319
+ }
320
+ }
321
+ }
322
+ function processProjectScope(args) {
323
+ const { projectFilePath, projectMap, projectHashes, errors, globalMatchedRootKeys } = args;
324
+ const applied = applyChangelogOverrides(makeValidationEntries(projectHashes), projectMap);
325
+ for (const message of applied.errors) {
326
+ errors.push(prefixWithFilePath(projectFilePath, message));
327
+ }
328
+ for (const key of projectMap.keys()) {
329
+ if (hasAnyMatch(key, projectHashes)) {
330
+ globalMatchedRootKeys.add(key);
331
+ }
332
+ }
333
+ }
334
+ function collectRootStaleWarnings(projectFilePath, projectMap, globalMatchedRootKeys, warnings) {
335
+ for (const key of projectMap.keys()) {
336
+ if (!globalMatchedRootKeys.has(key)) {
337
+ warnings.push(formatRootStaleWarning(projectFilePath, key));
338
+ }
339
+ }
340
+ }
341
+ function hasAnyMatch(key, hashes) {
342
+ return hashes.some((hash) => hash.startsWith(key));
343
+ }
344
+ function filterShadowedKeys(projectMap, workspaceMap) {
345
+ const result = /* @__PURE__ */ new Map();
346
+ for (const [key, value] of projectMap) {
347
+ if (workspaceMap.has(key)) continue;
348
+ result.set(key, value);
349
+ }
350
+ return result;
351
+ }
352
+ function loadScopeMap(filePath, errors) {
353
+ if (filePath === void 0) {
354
+ return /* @__PURE__ */ new Map();
355
+ }
356
+ const result = loadChangelogOverrides(filePath);
357
+ if ("errors" in result) {
358
+ for (const message of result.errors) {
359
+ errors.push(prefixWithFilePath(filePath, message));
360
+ }
361
+ return /* @__PURE__ */ new Map();
362
+ }
363
+ return result.overrides;
364
+ }
365
+ function makeValidationEntries(hashes) {
366
+ return [
367
+ {
368
+ version: "0.0.0",
369
+ date: "0000-00-00",
370
+ sections: [
371
+ {
372
+ title: "Validation",
373
+ audience: "all",
374
+ items: hashes.map((hash) => ({ description: "", hash }))
375
+ }
376
+ ]
377
+ }
378
+ ];
379
+ }
380
+ function prefixWithFilePath(filePath, message) {
381
+ return `${filePath}: ${message}`;
382
+ }
383
+ function formatWorkspaceStaleWarning(filePath, key) {
384
+ return `${filePath}: Override key '${key}' did not match any commit in this workspace's history (likely a stale reference)`;
385
+ }
386
+ function formatRootStaleWarning(filePath, key) {
387
+ return `${filePath}: Override key '${key}' did not match any commit in any scope (likely a stale reference)`;
388
+ }
389
+ function applyWorkspaceOverrides(newEntries, workspacePath, overrideContext) {
390
+ const { project, perWorkspace, overrideWarnings, globalMatchedRootKeys } = overrideContext;
391
+ const workspaceOverrides = perWorkspace.get(workspacePath);
392
+ const composed = composeOverrides(project, workspaceOverrides);
393
+ const applied = applyChangelogOverrides(newEntries, composed);
394
+ if (applied.errors.length > 0) {
395
+ throw new Error(`Changelog override application failed:
396
+ - ${applied.errors.join("\n - ")}`);
397
+ }
398
+ overrideWarnings.push(...applied.warnings);
399
+ const matchedSet = new Set(applied.matchedKeys);
400
+ if (workspaceOverrides !== void 0) {
401
+ for (const key of workspaceOverrides.keys()) {
402
+ if (!matchedSet.has(key)) {
403
+ overrideWarnings.push(formatStaleOverrideKeyWarning(key));
404
+ }
405
+ }
406
+ }
407
+ for (const key of applied.matchedKeys) {
408
+ if (workspaceOverrides?.has(key)) continue;
409
+ globalMatchedRootKeys.add(key);
410
+ }
411
+ return applied;
412
+ }
413
+ export {
414
+ applyChangelogOverrides,
415
+ applyWorkspaceOverrides,
416
+ composeOverrides,
417
+ createOverrideContext,
418
+ formatStaleOverrideKeyWarning,
419
+ loadChangelogOverrides,
420
+ loadOverridesForScopes,
421
+ resolveOverridePath,
422
+ validateAllChangelogOverrides,
423
+ validateChangelogOverrides
424
+ };
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { errorMessage, hasExpectedTopLevelShape } from "./workTypesUtils.js";
4
+ import { buildFetchInit, errorMessage, hasExpectedTopLevelShape } from "./workTypesUtils.js";
5
5
  const UPSTREAM_WORK_TYPES_URL = "https://raw.githubusercontent.com/williamthorsen/codeassembly/main/packages/agents/content/skills/_data/work-types.json";
6
6
  function resolveDefaultLocalPath() {
7
7
  const moduleDir = dirname(fileURLToPath(import.meta.url));
@@ -21,9 +21,10 @@ async function checkWorkTypesDrift(dependencies = {}) {
21
21
  message: `Local work-types.json is not valid JSON: ${errorMessage(error)}`
22
22
  };
23
23
  }
24
+ const fetchInit = buildFetchInit();
24
25
  let response;
25
26
  try {
26
- response = await fetcher(url);
27
+ response = fetchInit === void 0 ? await fetcher(url) : await fetcher(url, fetchInit);
27
28
  } catch (error) {
28
29
  return {
29
30
  exitCode: 2,
@@ -3,7 +3,7 @@ import { WORK_TYPES_DATA as WORK_TYPES_DATA2 } from "./workTypesData.js";
3
3
  function composeHeader(entry) {
4
4
  return `${entry.emoji} ${entry.label}`;
5
5
  }
6
- const DEV_ONLY_TIERS = /* @__PURE__ */ new Set(["Internal", "Process"]);
6
+ const DEV_ONLY_TIERS = /* @__PURE__ */ new Set(["internal", "process"]);
7
7
  function deriveDefaultWorkTypes() {
8
8
  const result = {};
9
9
  for (const entry of WORK_TYPES_DATA.types) {
@@ -1,8 +1,7 @@
1
- import type { ReleaseConfig } from './types.ts';
1
+ import type { WorkspaceConfig } from './types.ts';
2
2
  export declare function buildTagPattern(tagPrefixes: readonly string[]): string;
3
+ export declare function getAllTagPrefixes(workspace: WorkspaceConfig): string[];
3
4
  export interface GenerateChangelogOptions {
4
5
  tagPattern?: string;
5
6
  includePaths?: string[];
6
7
  }
7
- export declare function generateChangelog(config: Pick<ReleaseConfig, 'cliffConfigPath'>, changelogPath: string, tag: string, dryRun: boolean, options?: GenerateChangelogOptions): string[];
8
- export declare function generateChangelogs(config: ReleaseConfig, tag: string, dryRun: boolean): string[];
@@ -1,5 +1,3 @@
1
- import { resolveCliffConfigPath } from "./resolveCliffConfigPath.js";
2
- import { runGitCliff } from "./runGitCliff.js";
3
1
  function buildTagPattern(tagPrefixes) {
4
2
  if (tagPrefixes.length === 0) {
5
3
  throw new Error("buildTagPattern: tagPrefixes must contain at least one entry");
@@ -11,41 +9,13 @@ function buildTagPattern(tagPrefixes) {
11
9
  const escaped = tagPrefixes.map(escapeRegex);
12
10
  return `(${escaped.join("|")})[0-9].*`;
13
11
  }
12
+ function getAllTagPrefixes(workspace) {
13
+ return [workspace.tagPrefix, ...workspace.legacyIdentities?.map((identity) => identity.tagPrefix) ?? []];
14
+ }
14
15
  function escapeRegex(value) {
15
16
  return value.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
16
17
  }
17
- function generateChangelog(config, changelogPath, tag, dryRun, options) {
18
- const outputFile = `${changelogPath}/CHANGELOG.md`;
19
- if (dryRun) {
20
- return [outputFile];
21
- }
22
- const resolvedConfigPath = resolveCliffConfigPath(config.cliffConfigPath, import.meta.url);
23
- const cliffArgs = ["--output", outputFile, "--tag", tag];
24
- if (options?.tagPattern !== void 0) {
25
- cliffArgs.push("--tag-pattern", options.tagPattern);
26
- }
27
- for (const includePath of options?.includePaths ?? []) {
28
- cliffArgs.push("--include-path", includePath);
29
- }
30
- try {
31
- runGitCliff(resolvedConfigPath, cliffArgs, "inherit");
32
- } catch (error) {
33
- throw new Error(
34
- `Failed to generate changelog for ${outputFile}: ${error instanceof Error ? error.message : String(error)}`
35
- );
36
- }
37
- return [outputFile];
38
- }
39
- function generateChangelogs(config, tag, dryRun) {
40
- const tagPattern = buildTagPattern([config.tagPrefix]);
41
- const results = [];
42
- for (const changelogPath of config.changelogPaths) {
43
- results.push(...generateChangelog(config, changelogPath, tag, dryRun, { tagPattern }));
44
- }
45
- return results;
46
- }
47
18
  export {
48
19
  buildTagPattern,
49
- generateChangelog,
50
- generateChangelogs
20
+ getAllTagPrefixes
51
21
  };
@@ -1,2 +1,4 @@
1
+ export { type ChangelogOverrideScope, validateAllChangelogOverrides, type ValidateAllChangelogOverridesInputs, type ValidateAllChangelogOverridesResult, } from './changelogOverrides.ts';
1
2
  export type { SyncLabelsConfig } from './sync-labels/types.ts';
2
3
  export type { ReleaseKitConfig } from './types.ts';
4
+ export { formatValidateOverridesResult, type ValidateOverridesCommandResult } from './validateOverridesCommand.ts';
package/dist/esm/index.js CHANGED
@@ -0,0 +1,8 @@
1
+ import {
2
+ validateAllChangelogOverrides
3
+ } from "./changelogOverrides.js";
4
+ import { formatValidateOverridesResult } from "./validateOverridesCommand.js";
5
+ export {
6
+ formatValidateOverridesResult,
7
+ validateAllChangelogOverrides
8
+ };
@@ -12,4 +12,4 @@ export interface RootPackageInfo {
12
12
  }
13
13
  export declare function mergeMonorepoConfig(discoveredPaths: string[], userConfig: ReleaseKitConfig | undefined, rootPackage?: RootPackageInfo): MonorepoReleaseConfig;
14
14
  export declare function mergeSinglePackageConfig(userConfig: ReleaseKitConfig | undefined): ReleaseConfig;
15
- export declare function resolveWorkTypes(userWorkTypes?: Record<string, WorkTypeConfig>): Record<string, WorkTypeConfig>;
15
+ export declare function resolveWorkTypes(userWorkTypes?: ReleaseKitConfig['workTypes']): Record<string, WorkTypeConfig>;
@@ -1,23 +1,40 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { buildChangelogEntries } from "./buildChangelogEntries.js";
3
+ import { buildEmptyReleaseEntry } from "./buildEmptyReleaseEntry.js";
3
4
  import { bumpAllVersions, setAllVersions } from "./bumpAllVersions.js";
4
- import { resolveChangelogJsonPath, upsertChangelogJson } from "./changelogJsonFile.js";
5
+ import {
6
+ mergeChangelogEntriesWithDisk,
7
+ resolveChangelogJsonPath,
8
+ upsertChangelogJsonAndReturn
9
+ } from "./changelogJsonFile.js";
10
+ import {
11
+ applyChangelogOverrides,
12
+ formatStaleOverrideKeyWarning,
13
+ loadOverridesForScopes
14
+ } from "./changelogOverrides.js";
5
15
  import { createPolicyViolationCollector } from "./collectPolicyViolations.js";
6
16
  import { isForwardVersion } from "./compareVersions.js";
7
17
  import { DEFAULT_BREAKING_POLICIES, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
8
18
  import { determineBumpFromCommits } from "./determineBumpFromCommits.js";
9
- import { generateChangelogs } from "./generateChangelogs.js";
10
19
  import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
11
20
  import { hasPrettierConfig } from "./hasPrettierConfig.js";
12
21
  import { resolveWorkTypes } from "./loadConfig.js";
13
22
  import { readCurrentVersion } from "./readCurrentVersion.js";
23
+ import { writeChangelogMarkdown } from "./renderChangelogMarkdown.js";
14
24
  import { deriveSectionOrder } from "./resolveReleaseNotesConfig.js";
25
+ import { refreshGitCliffCache } from "./runGitCliff.js";
15
26
  import { writeReleaseNotesPreviews } from "./writeReleaseNotesPreviews.js";
16
27
  function releasePrepare(config, options) {
17
28
  const { dryRun, bumpOverride, setVersion, withReleaseNotes } = options;
18
29
  const workTypes = config.workTypes ?? { ...DEFAULT_WORK_TYPES };
19
30
  const versionPatterns = config.versionPatterns ?? { ...DEFAULT_VERSION_PATTERNS };
20
31
  const breakingPolicies = config.breakingPolicies ?? DEFAULT_BREAKING_POLICIES;
32
+ const overridesResult = loadOverridesForScopes({ project: "." });
33
+ if (overridesResult.errors.length > 0) {
34
+ throw new Error(`Failed to load changelog overrides:
35
+ - ${overridesResult.errors.join("\n - ")}`);
36
+ }
37
+ const overrides = overridesResult.project;
21
38
  const { tag, commits } = getCommitsSinceTarget([config.tagPrefix]);
22
39
  let releaseType;
23
40
  let parsedCommitCount;
@@ -36,6 +53,7 @@ function releasePrepare(config, options) {
36
53
  if (!isForwardVersion(currentVersion, setVersion)) {
37
54
  throw new Error(`--set-version ${setVersion} is not greater than current version ${currentVersion}`);
38
55
  }
56
+ refreshGitCliffCache();
39
57
  bump = setAllVersions(config.packageFiles, setVersion, dryRun);
40
58
  } else {
41
59
  if (bumpOverride === void 0) {
@@ -63,11 +81,20 @@ function releasePrepare(config, options) {
63
81
  dryRun
64
82
  };
65
83
  }
84
+ refreshGitCliffCache();
66
85
  bump = bumpAllVersions(config.packageFiles, releaseType, dryRun);
67
86
  }
68
87
  const newTag = `${config.tagPrefix}${bump.newVersion}`;
69
- const changelogFiles = generateChangelogs(config, newTag, dryRun);
70
- const changelogJsonFiles = config.changelogJson.enabled ? buildAndPersistChangelogJson(config, newTag, dryRun) : [];
88
+ const overrideWarnings = [];
89
+ const { changelogFiles, changelogJsonFiles } = writeSinglePackageChangelogs({
90
+ config,
91
+ commits,
92
+ newTag,
93
+ newVersion: bump.newVersion,
94
+ dryRun,
95
+ overrides,
96
+ overrideWarnings
97
+ });
71
98
  maybeWriteSinglePackagePreviews(withReleaseNotes === true, config, newTag, changelogJsonFiles[0], dryRun);
72
99
  const formatCommandStr = config.formatCommand ?? (hasPrettierConfig() ? "npx prettier --write" : void 0);
73
100
  let formatCommand;
@@ -102,12 +129,16 @@ function releasePrepare(config, options) {
102
129
  policyViolations: collector.violations.length > 0 ? collector.violations : void 0,
103
130
  setVersion
104
131
  });
105
- return {
132
+ const result = {
106
133
  workspaces: [released],
107
134
  tags: [newTag],
108
135
  formatCommand,
109
136
  dryRun
110
137
  };
138
+ if (overrideWarnings.length > 0) {
139
+ result.warnings = overrideWarnings;
140
+ }
141
+ return result;
111
142
  }
112
143
  function buildSkippedSinglePackage(args) {
113
144
  const skipped = {
@@ -172,17 +203,43 @@ function buildReleasedSinglePackage(args) {
172
203
  }
173
204
  return released;
174
205
  }
175
- function buildAndPersistChangelogJson(config, newTag, dryRun) {
206
+ function writeSinglePackageChangelogs(args) {
207
+ const { config, commits, newTag, newVersion, dryRun, overrides, overrideWarnings } = args;
208
+ const isEmptyRange = commits.length === 0;
209
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
210
+ const baseEntries = isEmptyRange ? [buildEmptyReleaseEntry(newVersion, today)] : buildChangelogEntries(config, newTag);
211
+ const applied = applyChangelogOverrides(baseEntries, overrides);
212
+ if (applied.errors.length > 0) {
213
+ throw new Error(`Changelog override application failed:
214
+ - ${applied.errors.join("\n - ")}`);
215
+ }
216
+ overrideWarnings.push(...applied.warnings);
217
+ const matchedSet = new Set(applied.matchedKeys);
218
+ for (const overrideKey of overrides.keys()) {
219
+ if (!matchedSet.has(overrideKey)) {
220
+ overrideWarnings.push(formatStaleOverrideKeyWarning(overrideKey));
221
+ }
222
+ }
223
+ const sectionOrder = deriveSectionOrder(resolveWorkTypes(config.workTypes));
224
+ const changelogFiles = [];
176
225
  const changelogJsonFiles = [];
177
- const entries = buildChangelogEntries(config, newTag);
178
226
  for (const changelogPath of config.changelogPaths) {
179
227
  const jsonPath = resolveChangelogJsonPath(config, changelogPath);
180
- if (!dryRun) {
181
- upsertChangelogJson(jsonPath, entries);
228
+ const shouldWriteJson = config.changelogJson.enabled && !dryRun;
229
+ const mergedEntries = shouldWriteJson ? upsertChangelogJsonAndReturn(jsonPath, applied.entries) : mergeChangelogEntriesWithDisk(jsonPath, applied.entries);
230
+ if (config.changelogJson.enabled) {
231
+ changelogJsonFiles.push(jsonPath);
182
232
  }
183
- changelogJsonFiles.push(jsonPath);
233
+ changelogFiles.push(
234
+ writeChangelogMarkdown({
235
+ changelogPath,
236
+ entries: mergedEntries,
237
+ sectionOrder,
238
+ dryRun
239
+ })
240
+ );
184
241
  }
185
- return changelogJsonFiles;
242
+ return { changelogFiles, changelogJsonFiles };
186
243
  }
187
244
  function maybeWriteSinglePackagePreviews(withReleaseNotes, config, newTag, changelogJsonPath, dryRun) {
188
245
  if (!withReleaseNotes) {