@williamthorsen/release-kit 15.0.3 → 16.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/.meta/changelog.json +93 -0
  2. package/.readyup/kits/default.js +14 -14
  3. package/.readyup/kits/npm-auto-publish.js +4 -4
  4. package/.readyup/manifest.json +13 -13
  5. package/CHANGELOG.md +27 -0
  6. package/README.md +12 -3
  7. package/dist/esm/applyChangelogOverrides.js +6 -6
  8. package/dist/esm/assertReleasesOnNpm.js +11 -11
  9. package/dist/esm/assertTaggedBaseline.js +5 -5
  10. package/dist/esm/assertTarballsComplete.d.ts +9 -0
  11. package/dist/esm/assertTarballsComplete.js +52 -0
  12. package/dist/esm/bin/release-kit.js +16 -16
  13. package/dist/esm/buildChangelogEntries.d.ts +19 -19
  14. package/dist/esm/buildChangelogEntries.js +40 -36
  15. package/dist/esm/buildChangelogItem.js +24 -24
  16. package/dist/esm/buildDependencyGraph.js +23 -23
  17. package/dist/esm/buildReleasedResult.js +21 -19
  18. package/dist/esm/buildSyntheticChangelogEntry.js +2 -2
  19. package/dist/esm/bumpVersion.js +8 -8
  20. package/dist/esm/changelogArchive.d.ts +1 -1
  21. package/dist/esm/changelogArchive.js +2 -2
  22. package/dist/esm/changelogJsonFile.js +5 -5
  23. package/dist/esm/changelogJsonUtils.js +3 -3
  24. package/dist/esm/changelogOverrides.d.ts +5 -5
  25. package/dist/esm/changelogOverrides.js +23 -23
  26. package/dist/esm/classifyChangelogCommit.js +4 -4
  27. package/dist/esm/commitCommand.js +3 -3
  28. package/dist/esm/compareVersions.js +13 -13
  29. package/dist/esm/configSchema.d.ts +3 -2
  30. package/dist/esm/configSchema.js +2 -1
  31. package/dist/esm/createGithubRelease.d.ts +4 -4
  32. package/dist/esm/createGithubRelease.js +10 -10
  33. package/dist/esm/createGithubReleaseCommand.js +9 -9
  34. package/dist/esm/createTags.d.ts +2 -2
  35. package/dist/esm/createTags.js +8 -8
  36. package/dist/esm/decideDirectReleases.d.ts +5 -5
  37. package/dist/esm/decideDirectReleases.js +25 -25
  38. package/dist/esm/decideRelease.d.ts +1 -1
  39. package/dist/esm/decideRelease.js +2 -2
  40. package/dist/esm/deriveDirectoryConfig.d.ts +1 -1
  41. package/dist/esm/deriveDirectoryConfig.js +18 -18
  42. package/dist/esm/deriveWorkspaceConfig.js +4 -4
  43. package/dist/esm/detectPackageManager.d.ts +2 -0
  44. package/dist/esm/detectPackageManager.js +8 -5
  45. package/dist/esm/detectUndeclaredTagPrefixes.js +6 -6
  46. package/dist/esm/enumerateReleaseWindows.d.ts +1 -1
  47. package/dist/esm/enumerateReleaseWindows.js +7 -7
  48. package/dist/esm/findMissingManifestPaths.d.ts +14 -0
  49. package/dist/esm/findMissingManifestPaths.js +90 -0
  50. package/dist/esm/findTransitiveDependents.js +5 -5
  51. package/dist/esm/findUnroutedEntryScopes.js +11 -11
  52. package/dist/esm/format.d.ts +1 -1
  53. package/dist/esm/format.js +1 -1
  54. package/dist/esm/formatPrepareFiles.d.ts +3 -3
  55. package/dist/esm/formatPrepareFiles.js +10 -10
  56. package/dist/esm/formatPrepareWarnings.js +6 -6
  57. package/dist/esm/hasPrettierConfig.js +2 -2
  58. package/dist/esm/init/checks.js +5 -3
  59. package/dist/esm/init/detectRepoType.js +3 -3
  60. package/dist/esm/init/initCommand.d.ts +4 -4
  61. package/dist/esm/init/initCommand.js +7 -7
  62. package/dist/esm/init/parseJsonRecord.d.ts +1 -1
  63. package/dist/esm/init/parseJsonRecord.js +4 -4
  64. package/dist/esm/init/scaffold.d.ts +4 -4
  65. package/dist/esm/init/scaffold.js +15 -15
  66. package/dist/esm/init/templates.d.ts +10 -10
  67. package/dist/esm/init/templates.js +5 -5
  68. package/dist/esm/init/updateTemplatesCommand.d.ts +2 -2
  69. package/dist/esm/init/updateTemplatesCommand.js +14 -12
  70. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +1 -1
  71. package/dist/esm/injectReleaseNotesIntoReadme.js +6 -6
  72. package/dist/esm/injectSection.js +8 -8
  73. package/dist/esm/loadChangelogOverrides.d.ts +1 -1
  74. package/dist/esm/loadChangelogOverrides.js +9 -9
  75. package/dist/esm/loadConfig.d.ts +3 -3
  76. package/dist/esm/loadConfig.js +10 -10
  77. package/dist/esm/loadValidatedConfig.js +4 -4
  78. package/dist/esm/mergeConfig.d.ts +1 -1
  79. package/dist/esm/mergeConfig.js +27 -27
  80. package/dist/esm/monorepoConfigAssertions.js +15 -15
  81. package/dist/esm/npmRegistry.js +4 -4
  82. package/dist/esm/packFromCleanCheckout.d.ts +11 -0
  83. package/dist/esm/packFromCleanCheckout.js +81 -0
  84. package/dist/esm/packsPath.js +3 -3
  85. package/dist/esm/parseChangeRecordBlock.js +33 -33
  86. package/dist/esm/parseCommitMessage.js +5 -5
  87. package/dist/esm/planChangelogFiles.js +8 -8
  88. package/dist/esm/planReleaseNotesPreviews.js +10 -10
  89. package/dist/esm/planVersionBump.js +24 -16
  90. package/dist/esm/prepareArgs.d.ts +9 -9
  91. package/dist/esm/prepareArgs.js +11 -11
  92. package/dist/esm/prepareCommand.js +27 -24
  93. package/dist/esm/prepareComposite.d.ts +4 -4
  94. package/dist/esm/prepareComposite.js +16 -16
  95. package/dist/esm/{releasePrepareTargets.d.ts → prepareReleaseTargets.d.ts} +9 -9
  96. package/dist/esm/{releasePrepareTargets.js → prepareReleaseTargets.js} +40 -40
  97. package/dist/esm/prepareRunContext.d.ts +1 -1
  98. package/dist/esm/prepareRunContext.js +5 -5
  99. package/dist/esm/prepareTarget.d.ts +3 -3
  100. package/dist/esm/prepareTarget.js +41 -39
  101. package/dist/esm/previewTagPrefixes.js +19 -19
  102. package/dist/esm/propagateBumps.js +8 -8
  103. package/dist/esm/publish.d.ts +3 -3
  104. package/dist/esm/publish.js +6 -11
  105. package/dist/esm/publishCommand.d.ts +3 -3
  106. package/dist/esm/publishCommand.js +28 -20
  107. package/dist/esm/pushCommand.js +8 -8
  108. package/dist/esm/pushRelease.d.ts +1 -1
  109. package/dist/esm/pushRelease.js +2 -2
  110. package/dist/esm/readChangelogSections.d.ts +1 -1
  111. package/dist/esm/readChangelogSections.js +16 -16
  112. package/dist/esm/releasePlan.js +25 -25
  113. package/dist/esm/renderChangelogMarkdown.d.ts +4 -4
  114. package/dist/esm/renderChangelogMarkdown.js +18 -15
  115. package/dist/esm/renderReleaseNotes.d.ts +1 -1
  116. package/dist/esm/renderReleaseNotes.js +16 -16
  117. package/dist/esm/reportPrepare.js +34 -34
  118. package/dist/esm/resolveCommandTags.js +9 -9
  119. package/dist/esm/resolveReleaseDeclaration.d.ts +2 -2
  120. package/dist/esm/resolveReleaseDeclaration.js +22 -22
  121. package/dist/esm/resolveReleaseTags.js +17 -15
  122. package/dist/esm/resolveReleaseTargets.js +2 -2
  123. package/dist/esm/showTagPrefixesCommand.js +4 -4
  124. package/dist/esm/sync-labels/generateCommand.d.ts +3 -3
  125. package/dist/esm/sync-labels/generateCommand.js +10 -10
  126. package/dist/esm/sync-labels/initCommand.d.ts +3 -3
  127. package/dist/esm/sync-labels/initCommand.js +15 -15
  128. package/dist/esm/sync-labels/presets.js +3 -3
  129. package/dist/esm/sync-labels/resolveLabels.js +13 -13
  130. package/dist/esm/sync-labels/templates.d.ts +2 -2
  131. package/dist/esm/sync-labels/templates.js +1 -1
  132. package/dist/esm/tagCommand.js +4 -4
  133. package/dist/esm/topologicalSort.d.ts +2 -2
  134. package/dist/esm/topologicalSort.js +7 -7
  135. package/dist/esm/types.d.ts +3 -3
  136. package/dist/esm/validateAllChangelogOverrides.js +9 -9
  137. package/dist/esm/validateConfig.d.ts +1 -1
  138. package/dist/esm/validateConfig.js +60 -50
  139. package/dist/esm/validateOverridesCommand.js +2 -2
  140. package/docs/changelogs.md +5 -5
  141. package/docs/configuration.md +16 -15
  142. package/docs/publishing.md +4 -0
  143. package/docs/releasing.md +6 -0
  144. package/docs/work-types.md +1 -1
  145. package/package.json +2 -2
  146. package/presets/labels/common.yaml +3 -0
@@ -6,23 +6,23 @@ export function resolveLabels(config) {
6
6
  const groupList = caseVariantGroups.map((group) => group.map((name) => `'${name}'`).join(' and ')).join('; ');
7
7
  throw new Error(`Keys in 'labels' differ only in case, which GitHub treats as the same label: ${groupList}. Keep one entry per label.`);
8
8
  }
9
- const resolved = new Map();
9
+ const resolvedLabels = new Map();
10
10
  const presetNames = config.extends ?? [];
11
11
  for (const presetName of presetNames) {
12
12
  for (const label of loadPreset(presetName)) {
13
- resolved.set(toLabelKey(label.name), label);
13
+ resolvedLabels.set(getLabelKey(label.name), label);
14
14
  }
15
15
  }
16
16
  for (const [name, spec] of labelEntries) {
17
- const key = toLabelKey(name);
17
+ const key = getLabelKey(name);
18
18
  if (spec === null) {
19
- if (!resolved.has(key)) {
19
+ if (!resolvedLabels.has(key)) {
20
20
  throw new Error(`Label '${name}' is set to null (remove), but the presets in 'extends' don't define it. Fix the name or delete the entry.`);
21
21
  }
22
- resolved.delete(key);
22
+ resolvedLabels.delete(key);
23
23
  }
24
24
  else {
25
- resolved.set(key, {
25
+ resolvedLabels.set(key, {
26
26
  name,
27
27
  color: spec.color,
28
28
  ...(spec.description !== undefined && { description: spec.description }),
@@ -31,7 +31,7 @@ export function resolveLabels(config) {
31
31
  });
32
32
  }
33
33
  }
34
- const labels = sortLabels(resolved.values().toArray());
34
+ const labels = sortLabels(resolvedLabels.values().toArray());
35
35
  const aliasCollisions = findAliasCollisions(labels);
36
36
  if (aliasCollisions.length > 0) {
37
37
  throw new Error(`Aliases must name labels that are not declared and must not repeat, or the sync merges one label into another and deletes it: ${aliasCollisions.join('; ')}.`);
@@ -39,13 +39,13 @@ export function resolveLabels(config) {
39
39
  return labels;
40
40
  }
41
41
  function findAliasCollisions(labels) {
42
- const labelNamesByKey = new Map(labels.map((label) => [toLabelKey(label.name), label.name]));
42
+ const labelNamesByKey = new Map(labels.map((label) => [getLabelKey(label.name), label.name]));
43
43
  const aliasOwnersByKey = new Map();
44
44
  const collisions = [];
45
45
  for (const label of labels) {
46
46
  const aliases = label.aliases ?? [];
47
47
  for (const alias of aliases) {
48
- const key = toLabelKey(alias);
48
+ const key = getLabelKey(alias);
49
49
  const namedLabel = labelNamesByKey.get(key);
50
50
  const earlierOwner = aliasOwnersByKey.get(key);
51
51
  if (namedLabel !== undefined) {
@@ -62,14 +62,14 @@ function findAliasCollisions(labels) {
62
62
  return collisions;
63
63
  }
64
64
  function findCaseVariantGroups(names) {
65
- return Map.groupBy(names, toLabelKey)
65
+ return Map.groupBy(names, getLabelKey)
66
66
  .values()
67
67
  .filter((group) => group.length > 1)
68
68
  .toArray();
69
69
  }
70
+ function getLabelKey(name) {
71
+ return name.toLowerCase();
72
+ }
70
73
  function sortLabels(labels) {
71
74
  return labels.toSorted((a, b) => a.name.localeCompare(b.name));
72
75
  }
73
- function toLabelKey(name) {
74
- return name.toLowerCase();
75
- }
@@ -15,5 +15,5 @@ export declare function buildScopeLabels(workspacePaths: string[], retiredNames?
15
15
  * `.config/release-kit.config.ts`.
16
16
  */
17
17
  export declare function renderRepoLabelsBlock(scopeLabels: LabelDefinition[]): string;
18
- /** Generates a new `.config/release-kit.config.ts` containing the `repoLabels` block. */
19
- export declare function repoLabelsConfigScript(scopeLabels: LabelDefinition[]): string;
18
+ /** Renders a new `.config/release-kit.config.ts` containing the `repoLabels` block. */
19
+ export declare function renderRepoLabelsConfigScript(scopeLabels: LabelDefinition[]): string;
@@ -33,7 +33,7 @@ export function renderRepoLabelsBlock(scopeLabels) {
33
33
  ${labelsBlock}
34
34
  },`;
35
35
  }
36
- export function repoLabelsConfigScript(scopeLabels) {
36
+ export function renderRepoLabelsConfigScript(scopeLabels) {
37
37
  return `import { defineConfig } from '@williamthorsen/release-kit/config';
38
38
 
39
39
  export default defineConfig({
@@ -2,13 +2,13 @@ import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
2
2
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
3
  import { createTags } from "./createTags.js";
4
4
  const tagFlagSchema = {
5
- dryRun: { long: '--dry-run', type: 'boolean' },
6
- noGitChecks: { long: '--no-git-checks', type: 'boolean' },
5
+ isDryRun: { long: '--dry-run', type: 'boolean' },
6
+ shouldSkipGitChecks: { long: '--no-git-checks', type: 'boolean' },
7
7
  };
8
8
  export function tagCommand(argv, styles) {
9
- const { dryRun, noGitChecks } = parseArgsOrExit(argv, tagFlagSchema).flags;
9
+ const { isDryRun, shouldSkipGitChecks } = parseArgsOrExit(argv, tagFlagSchema).flags;
10
10
  try {
11
- createTags({ dryRun, noGitChecks, style: styles.stdout });
11
+ createTags({ isDryRun, shouldSkipGitChecks, style: styles.stdout });
12
12
  }
13
13
  catch (error) {
14
14
  reportError(describeError(error));
@@ -6,7 +6,7 @@ import type { ReleaseEntry } from './propagateBumps.js';
6
6
  * Uses Kahn's algorithm. Targets not in the release set are excluded. If the graph has
7
7
  * cycles, the remaining nodes are appended in arbitrary order and reported via `cyclicIds`.
8
8
  */
9
- export declare function topologicalSort(releaseSet: Map<string, ReleaseEntry>, graph: DependencyGraph): {
10
- sorted: string[];
9
+ export declare function sortTopologically(releaseSet: Map<string, ReleaseEntry>, graph: DependencyGraph): {
10
+ sortedIds: string[];
11
11
  cyclicIds: string[];
12
12
  };
@@ -1,7 +1,7 @@
1
- export function topologicalSort(releaseSet, graph) {
1
+ export function sortTopologically(releaseSet, graph) {
2
2
  const releaseIds = new Set(releaseSet.keys());
3
3
  if (releaseIds.size === 0) {
4
- return { sorted: [], cyclicIds: [] };
4
+ return { sortedIds: [], cyclicIds: [] };
5
5
  }
6
6
  const inDegree = new Map();
7
7
  const forwardEdges = new Map();
@@ -31,13 +31,13 @@ export function topologicalSort(releaseSet, graph) {
31
31
  queue.push(id);
32
32
  }
33
33
  }
34
- const sorted = [];
34
+ const sortedIds = [];
35
35
  while (queue.length > 0) {
36
36
  const id = queue.shift();
37
37
  if (id === undefined) {
38
38
  break;
39
39
  }
40
- sorted.push(id);
40
+ sortedIds.push(id);
41
41
  const dependents = forwardEdges.get(id) ?? [];
42
42
  for (const dependent of dependents) {
43
43
  const newDegree = (inDegree.get(dependent) ?? 1) - 1;
@@ -47,14 +47,14 @@ export function topologicalSort(releaseSet, graph) {
47
47
  }
48
48
  }
49
49
  }
50
- const sortedSet = new Set(sorted);
50
+ const sortedSet = new Set(sortedIds);
51
51
  const cyclicIds = [];
52
52
  for (const id of releaseIds) {
53
53
  if (sortedSet.has(id)) {
54
54
  continue;
55
55
  }
56
- sorted.push(id);
56
+ sortedIds.push(id);
57
57
  cyclicIds.push(id);
58
58
  }
59
- return { sorted, cyclicIds };
59
+ return { sortedIds, cyclicIds };
60
60
  }
@@ -70,7 +70,7 @@ export interface ChangelogMarkdownConfig {
70
70
  * How many of the newest versioned sections `CHANGELOG.md` keeps; older sections go to per-major archive files
71
71
  * under `changelogs/`. When omitted, `CHANGELOG.md` keeps every section.
72
72
  */
73
- keepVersions?: number;
73
+ keptVersionCount?: number;
74
74
  }
75
75
  /**
76
76
  * Editorial override for changelog items, keyed in the override file by `<hash>` (every item of the commit) or
@@ -245,7 +245,7 @@ export interface ReleasedPackageResult extends PackageResultFields, ReleaseField
245
245
  /** Present when propagation alone set the bump, the package's own commits having called for none. */
246
246
  propagatedOnly?: true;
247
247
  /** Present when the package was written via `--set-version`; the explicit version that was applied. */
248
- setVersion?: string;
248
+ versionOverride?: string;
249
249
  }
250
250
  /** Result of a package target that skips: the diagnostic fields and a human-readable `skipReason`. */
251
251
  export interface SkippedPackageResult extends PackageResultFields {
@@ -280,7 +280,7 @@ export interface ReleasedDirectoryResult extends DirectoryResultFields, ReleaseF
280
280
  /** Raw commits of the directory's unreleased window. */
281
281
  commits: Commit[];
282
282
  /** Present when the directory was written via `--set-version`; the explicit version that was applied. */
283
- setVersion?: string;
283
+ versionOverride?: string;
284
284
  }
285
285
  /** Result of a directory target that skips: the diagnostic fields and a human-readable `skipReason`. */
286
286
  export interface SkippedDirectoryResult extends DirectoryResultFields {
@@ -89,8 +89,8 @@ function prefixWithFilePath(filePath, message) {
89
89
  }
90
90
  function processProjectScope(args) {
91
91
  const { projectFilePath, projectMap, projectItems, errors, globalMatchedRootKeys } = args;
92
- const applied = applyChangelogOverrides(makeValidationEntries(projectItems), projectMap);
93
- for (const message of applied.errors) {
92
+ const appliedResult = applyChangelogOverrides(makeValidationEntries(projectItems), projectMap);
93
+ for (const message of appliedResult.errors) {
94
94
  errors.push(prefixWithFilePath(projectFilePath, message));
95
95
  }
96
96
  const positionsByHash = indexItemPositions(projectItems);
@@ -105,19 +105,19 @@ function processWorkspaceScope(args) {
105
105
  const { filePath, items, map } = workspace;
106
106
  const validationEntries = makeValidationEntries(items);
107
107
  const positionsByHash = indexItemPositions(items);
108
- const workspaceApplied = applyChangelogOverrides(validationEntries, map);
109
- for (const message of workspaceApplied.errors) {
108
+ const workspaceAppliedResult = applyChangelogOverrides(validationEntries, map);
109
+ for (const message of workspaceAppliedResult.errors) {
110
110
  errors.push(prefixWithFilePath(filePath, message));
111
111
  }
112
112
  if (projectFilePath !== undefined && projectMap.size > 0) {
113
113
  const projectMinusShadowed = filterShadowedKeys(projectMap, map);
114
- const projectApplied = applyChangelogOverrides(validationEntries, projectMinusShadowed);
115
- for (const message of projectApplied.errors) {
114
+ const projectAppliedResult = applyChangelogOverrides(validationEntries, projectMinusShadowed);
115
+ for (const message of projectAppliedResult.errors) {
116
116
  errors.push(prefixWithFilePath(projectFilePath, message));
117
117
  }
118
- const perFileErrors = new Set([...workspaceApplied.errors, ...projectApplied.errors]);
119
- const composedApplied = applyChangelogOverrides(validationEntries, composeOverrides(projectMap, map));
120
- for (const message of composedApplied.errors) {
118
+ const perFileErrors = new Set([...workspaceAppliedResult.errors, ...projectAppliedResult.errors]);
119
+ const composedAppliedResult = applyChangelogOverrides(validationEntries, composeOverrides(projectMap, map));
120
+ for (const message of composedAppliedResult.errors) {
121
121
  if (!perFileErrors.has(message)) {
122
122
  errors.push(prefixWithFilePath(filePath, message));
123
123
  }
@@ -3,7 +3,7 @@ import { type ReleaseKitConfig } from './configSchema.js';
3
3
  * Validates a raw config object loaded from `.config/release-kit.config.ts`. Returns `{}` with the errors for a config
4
4
  * that fails the schema parse, and runs the cross-field checks on one that passes.
5
5
  */
6
- export declare function validateConfig(raw: unknown): {
6
+ export declare function validateConfig(rawConfig: unknown): {
7
7
  config: ReleaseKitConfig;
8
8
  errors: string[];
9
9
  warnings: string[];
@@ -1,15 +1,18 @@
1
1
  import { releaseKitConfigSchema } from "./configSchema.js";
2
2
  import { isRecord } from "./typeGuards.js";
3
- export function validateConfig(raw) {
4
- if (!isRecord(raw)) {
3
+ export function validateConfig(rawConfig) {
4
+ if (!isRecord(rawConfig)) {
5
5
  return { config: {}, errors: ['Config must be an object'], warnings: [] };
6
6
  }
7
- const { cleaned, deprecationErrors } = preprocessDeprecatedKeys(raw);
8
- const parseResult = releaseKitConfigSchema.safeParse(cleaned);
7
+ const { cleanedInput, deprecationErrors } = preprocessDeprecatedKeys(rawConfig);
8
+ const parseResult = releaseKitConfigSchema.safeParse(cleanedInput);
9
9
  if (!parseResult.success) {
10
10
  return {
11
11
  config: {},
12
- errors: [...deprecationErrors, ...parseResult.error.issues.flatMap((issue) => formatZodIssue(issue, cleaned))],
12
+ errors: [
13
+ ...deprecationErrors,
14
+ ...parseResult.error.issues.flatMap((issue) => formatZodIssue(issue, cleanedInput)),
15
+ ],
13
16
  warnings: [],
14
17
  };
15
18
  }
@@ -26,40 +29,47 @@ export function validateConfig(raw) {
26
29
  }
27
30
  return { config, errors, warnings };
28
31
  }
29
- function preprocessDeprecatedKeys(raw) {
30
- if (!isRecord(raw))
31
- return { cleaned: raw, deprecationErrors: [] };
32
+ function preprocessDeprecatedKeys(rawConfig) {
33
+ if (!isRecord(rawConfig))
34
+ return { cleanedInput: rawConfig, deprecationErrors: [] };
32
35
  const errors = [];
33
- const cleaned = { ...raw };
34
- if (Object.hasOwn(cleaned, 'cliffConfigPath')) {
36
+ const cleanedInput = { ...rawConfig };
37
+ if (Object.hasOwn(cleanedInput, 'cliffConfigPath')) {
35
38
  errors.push('cliffConfigPath is no longer supported. release-kit reads changelog history from git directly and does not use any external config file. Remove this field from the config, and delete the file that it names.');
36
- delete cleaned['cliffConfigPath'];
39
+ delete cleanedInput['cliffConfigPath'];
37
40
  }
38
- if (isRecord(cleaned['releaseNotes']) && Object.hasOwn(cleaned['releaseNotes'], 'shouldCreateGithubRelease')) {
41
+ if (isRecord(cleanedInput['releaseNotes']) &&
42
+ Object.hasOwn(cleanedInput['releaseNotes'], 'shouldCreateGithubRelease')) {
39
43
  errors.push('releaseNotes.shouldCreateGithubRelease is no longer supported. Adoption is now signaled by installing the create-github-release workflow, which `release-kit init` scaffolds. Remove this field from the config.');
40
- const releaseNotesCopy = { ...cleaned['releaseNotes'] };
44
+ const releaseNotesCopy = { ...cleanedInput['releaseNotes'] };
41
45
  delete releaseNotesCopy['shouldCreateGithubRelease'];
42
- cleaned['releaseNotes'] = releaseNotesCopy;
43
- }
44
- if (Array.isArray(cleaned['workspaces'])) {
45
- cleaned['workspaces'] = cleaned['workspaces'].map((ws, i) => {
46
- if (!isRecord(ws))
47
- return ws;
48
- const wsCopy = { ...ws };
49
- if (Object.hasOwn(wsCopy, 'tagPrefix')) {
50
- const dir = typeof wsCopy['dir'] === 'string' && wsCopy['dir'] !== '' ? wsCopy['dir'] : '<dir>';
51
- errors.push(`workspaces[${i}]: 'tagPrefix' is no longer supported; remove it to use the default '${dir}-v'`);
52
- delete wsCopy['tagPrefix'];
46
+ cleanedInput['releaseNotes'] = releaseNotesCopy;
47
+ }
48
+ if (isRecord(cleanedInput['changelogMarkdown']) && Object.hasOwn(cleanedInput['changelogMarkdown'], 'keepVersions')) {
49
+ errors.push('changelogMarkdown.keepVersions has been renamed. Rename it to changelogMarkdown.keptVersionCount in the config.');
50
+ const changelogMarkdownCopy = { ...cleanedInput['changelogMarkdown'] };
51
+ delete changelogMarkdownCopy['keepVersions'];
52
+ cleanedInput['changelogMarkdown'] = changelogMarkdownCopy;
53
+ }
54
+ if (Array.isArray(cleanedInput['workspaces'])) {
55
+ cleanedInput['workspaces'] = cleanedInput['workspaces'].map((workspace, index) => {
56
+ if (!isRecord(workspace))
57
+ return workspace;
58
+ const workspaceCopy = { ...workspace };
59
+ if (Object.hasOwn(workspaceCopy, 'tagPrefix')) {
60
+ const dir = typeof workspaceCopy['dir'] === 'string' && workspaceCopy['dir'] !== '' ? workspaceCopy['dir'] : '<dir>';
61
+ errors.push(`workspaces[${index}]: 'tagPrefix' is no longer supported; remove it to use the default '${dir}-v'`);
62
+ delete workspaceCopy['tagPrefix'];
53
63
  }
54
- if (Object.hasOwn(wsCopy, 'legacyTagPrefixes')) {
55
- errors.push(`workspaces[${i}]: 'legacyTagPrefixes' is no longer supported; use 'legacyIdentities: [{ name, tagPrefix }, ...]' instead`);
56
- delete wsCopy['legacyTagPrefixes'];
64
+ if (Object.hasOwn(workspaceCopy, 'legacyTagPrefixes')) {
65
+ errors.push(`workspaces[${index}]: 'legacyTagPrefixes' is no longer supported; use 'legacyIdentities: [{ name, tagPrefix }, ...]' instead`);
66
+ delete workspaceCopy['legacyTagPrefixes'];
57
67
  }
58
- return wsCopy;
68
+ return workspaceCopy;
59
69
  });
60
70
  }
61
- if (isRecord(cleaned['releases'])) {
62
- const releasesCopy = { ...cleaned['releases'] };
71
+ if (isRecord(cleanedInput['releases'])) {
72
+ const releasesCopy = { ...cleanedInput['releases'] };
63
73
  for (const [name, entry] of Object.entries(releasesCopy)) {
64
74
  if (!isRecord(entry) || !Object.hasOwn(entry, 'paths'))
65
75
  continue;
@@ -68,9 +78,9 @@ function preprocessDeprecatedKeys(raw) {
68
78
  delete entryCopy['paths'];
69
79
  releasesCopy[name] = entryCopy;
70
80
  }
71
- cleaned['releases'] = releasesCopy;
81
+ cleanedInput['releases'] = releasesCopy;
72
82
  }
73
- return { cleaned, deprecationErrors: errors };
83
+ return { cleanedInput, deprecationErrors: errors };
74
84
  }
75
85
  function formatZodIssue(issue, input) {
76
86
  if (issue.code === 'invalid_union' && issue.path.length === 2 && issue.path[0] === 'releases') {
@@ -92,42 +102,42 @@ function customizeMessage(issue) {
92
102
  }
93
103
  const RELEASE_ENTRY_SHAPES = 'must be false, { path, tagPrefix?, legacyIdentities? }, or { includes, tagPrefix? }';
94
104
  function renderPath(path) {
95
- let rendered = '';
105
+ let renderedPath = '';
96
106
  for (const segment of path) {
97
107
  if (typeof segment === 'number') {
98
- rendered += `[${segment}]`;
108
+ renderedPath += `[${segment}]`;
99
109
  }
100
- else if (rendered === '') {
101
- rendered += String(segment);
110
+ else if (renderedPath === '') {
111
+ renderedPath += String(segment);
102
112
  }
103
113
  else {
104
- rendered += `.${String(segment)}`;
114
+ renderedPath += `.${String(segment)}`;
105
115
  }
106
116
  }
107
- return rendered;
117
+ return renderedPath;
108
118
  }
109
119
  function detectLegacyIdentityDuplicates(config, errors) {
110
120
  for (const { label, legacyIdentities } of listLegacyIdentityDeclarations(config)) {
111
- const seen = new Set();
121
+ const seenKeys = new Set();
112
122
  for (const [entryIndex, identity] of legacyIdentities.entries()) {
113
123
  const key = `${identity.name}\0${identity.tagPrefix}`;
114
- if (seen.has(key)) {
124
+ if (seenKeys.has(key)) {
115
125
  errors.push(`${label}.legacyIdentities[${entryIndex}]: duplicate identity (name='${identity.name}', tagPrefix='${identity.tagPrefix}')`);
116
126
  }
117
- seen.add(key);
127
+ seenKeys.add(key);
118
128
  }
119
129
  }
120
130
  }
121
131
  function detectRetiredPackageDuplicates(config, errors) {
122
132
  if (config.retiredPackages === undefined)
123
133
  return;
124
- const seen = new Set();
125
- for (const [index, retired] of config.retiredPackages.entries()) {
126
- const key = `${retired.name}\0${retired.tagPrefix}`;
127
- if (seen.has(key)) {
128
- errors.push(`retiredPackages[${index}]: duplicate package (name='${retired.name}', tagPrefix='${retired.tagPrefix}')`);
134
+ const seenKeys = new Set();
135
+ for (const [index, retiredPackage] of config.retiredPackages.entries()) {
136
+ const key = `${retiredPackage.name}\0${retiredPackage.tagPrefix}`;
137
+ if (seenKeys.has(key)) {
138
+ errors.push(`retiredPackages[${index}]: duplicate package (name='${retiredPackage.name}', tagPrefix='${retiredPackage.tagPrefix}')`);
129
139
  }
130
- seen.add(key);
140
+ seenKeys.add(key);
131
141
  }
132
142
  }
133
143
  function detectRetiredVsLegacyCollisions(config, errors) {
@@ -141,10 +151,10 @@ function detectRetiredVsLegacyCollisions(config, errors) {
141
151
  }
142
152
  }
143
153
  }
144
- for (const [index, retired] of config.retiredPackages.entries()) {
145
- const collidingOwner = legacyPrefixToOwner.get(retired.tagPrefix);
154
+ for (const [index, retiredPackage] of config.retiredPackages.entries()) {
155
+ const collidingOwner = legacyPrefixToOwner.get(retiredPackage.tagPrefix);
146
156
  if (collidingOwner !== undefined) {
147
- errors.push(`retiredPackages[${index}]: tagPrefix '${retired.tagPrefix}' collides with a declared legacyIdentities[].tagPrefix on ${collidingOwner}`);
157
+ errors.push(`retiredPackages[${index}]: tagPrefix '${retiredPackage.tagPrefix}' collides with a declared legacyIdentities[].tagPrefix on ${collidingOwner}`);
148
158
  }
149
159
  }
150
160
  }
@@ -11,7 +11,7 @@ const SYNTHETIC_VALIDATE_TAG = 'validate-only';
11
11
  export async function validateOverridesCommand(styles, configPath, dependencies = {}) {
12
12
  const discover = dependencies.discoverWorkspaces ?? discoverWorkspaces;
13
13
  const load = dependencies.loadValidatedConfig ?? (() => loadValidatedConfig(configPath));
14
- const buildEntries = dependencies.buildEntries ?? defaultBuildEntries;
14
+ const buildEntries = dependencies.buildEntries ?? buildEntriesByDefault;
15
15
  const validate = dependencies.validate ?? validateAllChangelogOverrides;
16
16
  const configResult = await load();
17
17
  if (configResult.status === 'invalid') {
@@ -66,7 +66,7 @@ function formatSummaryLine(errorCount, warningCount) {
66
66
  function pluralize(count, noun) {
67
67
  return count === 1 ? `${count} ${noun}` : `${count} ${noun}s`;
68
68
  }
69
- function defaultBuildEntries(config, options) {
69
+ function buildEntriesByDefault(config, options) {
70
70
  return buildChangelogEntries(config, SYNTHETIC_VALIDATE_TAG, options).entries;
71
71
  }
72
72
  function flattenEntriesToItems(entries) {
@@ -2,7 +2,7 @@
2
2
 
3
3
  What a published tarball includes, what each `.meta/changelog.json` item contains, how `CHANGELOG.md` archives older versions, which commits appear in a changelog, how a merge commit's change-record block becomes items, and how release notes are written into a README.
4
4
 
5
- npm doesn't include either `CHANGELOG.md` or `.meta/changelog.json` in a published tarball on its own. npm stopped including `CHANGELOG` automatically in npm 7, so a package that declares a `files` field publishes a changelog only when that field names `CHANGELOG.md` and `.meta/changelog.json`. release-kit's readyup kit reports a publishable workspace whose `files` field omits either, and, when [`changelogMarkdown.keepVersions`](#archived-versions) is set, one whose `files` field omits `changelogs/`.
5
+ npm doesn't include either `CHANGELOG.md` or `.meta/changelog.json` in a published tarball on its own. npm stopped including `CHANGELOG` automatically in npm 7, so a package that declares a `files` field publishes a changelog only when that field names `CHANGELOG.md` and `.meta/changelog.json`. release-kit's readyup kit reports a publishable workspace whose `files` field omits either, and, when [`changelogMarkdown.keptVersionCount`](#archived-versions) is set, one whose `files` field omits `changelogs/`.
6
6
 
7
7
  ## `changelog.json` item schema
8
8
 
@@ -43,19 +43,19 @@ The label match is exact: `migration:` and `**Migration:**` are not recognized.
43
43
 
44
44
  ## Archived versions
45
45
 
46
- `changelogMarkdown.keepVersions` bounds `CHANGELOG.md`, which otherwise grows by one section per release. `.meta/changelog.json` is not affected: It contains every version whatever the option says.
46
+ `changelogMarkdown.keptVersionCount` bounds `CHANGELOG.md`, which otherwise grows by one section per release. `.meta/changelog.json` is not affected: It contains every version whatever the option says.
47
47
 
48
48
  ```typescript
49
49
  export default defineConfig({
50
- changelogMarkdown: { keepVersions: 20 },
50
+ changelogMarkdown: { keptVersionCount: 20 },
51
51
  });
52
52
  ```
53
53
 
54
54
  With the option set, `prepare` writes each changelog as follows:
55
55
 
56
- - **`CHANGELOG.md`** contains the newest `keepVersions` sections, counting [hand-written sections](#existing-changelogmd-sections), and a paragraph below its header that links to each archive file.
56
+ - **`CHANGELOG.md`** contains as many of the newest sections as `keptVersionCount` names, counting [hand-written sections](#existing-changelogmd-sections), and a paragraph below its header that links to each archive file.
57
57
  - **`changelogs/CHANGELOG-v{major}.md`**, beside `CHANGELOG.md`, contains every older section of that major version, newest first, under the same header and footer. `0.x` versions share `CHANGELOG-v0.md`, and a prerelease belongs to its major.
58
- - **An archive file that the render no longer produces**, after `keepVersions` is raised or the option is removed: `prepare` deletes it and renders its sections in `CHANGELOG.md` again. Every section is therefore in exactly one file.
58
+ - **An archive file that the render no longer produces**, after `keptVersionCount` is raised or the option is removed: `prepare` deletes it and renders its sections in `CHANGELOG.md` again. Every section is therefore in exactly one file.
59
59
 
60
60
  `prepare` regenerates the archive files on every release of the target, so an override applies to an archived version as it does to any other. A target that does not release keeps its unbounded `CHANGELOG.md` until its next release. The prepare report lists each archive file that it writes and each that it removes.
61
61
 
@@ -16,21 +16,22 @@ The two paths differ in what an absent file means. An absent default path means
16
16
 
17
17
  ## `ReleaseKitConfig` reference
18
18
 
19
- | Field | Type | Description |
20
- | ------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
21
- | `releases` | `Record<string, ReleaseEntry>` | Names, excludes, or composes release targets; see [`releases`](#releases). Can't be combined with `workspaces` or `project` |
22
- | `workspaces` | `WorkspaceOverride[]` | Override or exclude discovered workspaces (matched by `dir`) |
23
- | `formatCommand` | `string` | Shell command to run after changelog generation; modified file paths are appended as arguments |
24
- | `versionPatterns` | `VersionPatterns` | Rules for which commit types trigger major/minor bumps |
25
- | `scopeAliases` | `Record<string, string>` | Maps shorthand scope names to canonical names in commit titles and in change-record entry `scopes`, which [route items to workspaces](changelogs.md#routing-to-workspaces). A renamed workspace directory or [release target](#releases) needs an entry from its old name to its new one |
26
- | `workTypes` | `Record<string, WorkTypeConfig>` | Work type definitions, merged with defaults by key |
27
- | `breakingPolicies` | `Record<string, 'forbidden' \| 'optional'>` | Per-type `!`-policy lookup. Defaults to `DEFAULT_BREAKING_POLICIES`. Replaces the default entirely when provided. Set to `{}` to disable enforcement |
28
- | `retiredPackages` | `RetiredPackage[]` | Packages that once lived in this repo but have been extracted or removed; suppresses undeclared-tag-prefix warnings |
29
- | `project` | `ProjectConfig` | Opt-in project-level release block. Declaring `project: {}` (even empty) enables a project-release stage in `prepare` |
30
- | `repoLabels` | `RepoLabelsConfig` | Declares the repository's label registry for `sync-labels`; see [Label configuration](labels.md#label-configuration) |
31
- | `changelogJson` | `ChangelogJsonConfig` | Controls `.meta/changelog.json`: `enabled` (default `true`), `outputPath` (default `'.meta/changelog.json'`), and `devOnlySections`; see [Custom work types](work-types.md#custom-work-types) |
32
- | `changelogMarkdown` | `ChangelogMarkdownConfig` | `keepVersions` (an integer, at least 1) bounds `CHANGELOG.md` to that many of the newest versions and moves the rest into per-major archive files; omitted, `CHANGELOG.md` renders every version. See [Archived versions](changelogs.md#archived-versions) |
33
- | `releaseNotes` | `ReleaseNotesConfig` | `shouldInjectIntoReadme` (default `false`) injects release notes into each package's README while it publishes; see [Release-notes injection](changelogs.md#release-notes-injection) |
19
+ | Field | Type | Description |
20
+ | --------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
21
+ | `releases` | `Record<string, ReleaseEntry>` | Names, excludes, or composes release targets; see [`releases`](#releases). Can't be combined with `workspaces` or `project` |
22
+ | `workspaces` | `WorkspaceOverride[]` | Override or exclude discovered workspaces (matched by `dir`) |
23
+ | `devExportConditions` | `string[]` | Export conditions that resolve only inside the repo, such as one that points at unpublished source; the [tarball check](releasing.md#packages-whose-tarball-would-lack-a-manifest-file) does not require their targets in the tarball. `source` is exempt without being listed |
24
+ | `formatCommand` | `string` | Shell command to run after changelog generation; modified file paths are appended as arguments |
25
+ | `versionPatterns` | `VersionPatterns` | Rules for which commit types trigger major/minor bumps |
26
+ | `scopeAliases` | `Record<string, string>` | Maps shorthand scope names to canonical names in commit titles and in change-record entry `scopes`, which [route items to workspaces](changelogs.md#routing-to-workspaces). A renamed workspace directory or [release target](#releases) needs an entry from its old name to its new one |
27
+ | `workTypes` | `Record<string, WorkTypeConfig>` | Work type definitions, merged with defaults by key |
28
+ | `breakingPolicies` | `Record<string, 'forbidden' \| 'optional'>` | Per-type `!`-policy lookup. Defaults to `DEFAULT_BREAKING_POLICIES`. Replaces the default entirely when provided. Set to `{}` to disable enforcement |
29
+ | `retiredPackages` | `RetiredPackage[]` | Packages that once lived in this repo but have been extracted or removed; suppresses undeclared-tag-prefix warnings |
30
+ | `project` | `ProjectConfig` | Opt-in project-level release block. Declaring `project: {}` (even empty) enables a project-release stage in `prepare` |
31
+ | `repoLabels` | `RepoLabelsConfig` | Declares the repository's label registry for `sync-labels`; see [Label configuration](labels.md#label-configuration) |
32
+ | `changelogJson` | `ChangelogJsonConfig` | Controls `.meta/changelog.json`: `enabled` (default `true`), `outputPath` (default `'.meta/changelog.json'`), and `devOnlySections`; see [Custom work types](work-types.md#custom-work-types) |
33
+ | `changelogMarkdown` | `ChangelogMarkdownConfig` | `keptVersionCount` (an integer, at least 1) bounds `CHANGELOG.md` to that many of the newest versions and moves the rest into per-major archive files; omitted, `CHANGELOG.md` renders every version. See [Archived versions](changelogs.md#archived-versions) |
34
+ | `releaseNotes` | `ReleaseNotesConfig` | `shouldInjectIntoReadme` (default `false`) injects release notes into each package's README while it publishes; see [Release-notes injection](changelogs.md#release-notes-injection) |
34
35
 
35
36
  All fields are optional.
36
37
 
@@ -23,6 +23,10 @@ Example warning when an explicit tag is unpublishable:
23
23
  Skipping basic-v1.0.0 (packages/basic): package.json#private is true.
24
24
  ```
25
25
 
26
+ ### Tarball check
27
+
28
+ Before it publishes any package, and in a dry run as well, `publish` packs every publishable package from a clean checkout of HEAD and refuses when a tarball lacks a file that its manifest names. It is the same check that `prepare` runs; see [Packages whose tarball would lack a manifest file](releasing.md#packages-whose-tarball-would-lack-a-manifest-file).
29
+
26
30
  ## `release-kit create-github-release`
27
31
 
28
32
  Creates GitHub Releases from `changelog.json` for tags on HEAD. Independent of `npm publish`: Invoking this command creates Releases for publishable packages whether or not they were published to a registry. Private workspaces are skipped, and a project tag gets a Release (see [Publishability filter](#publishability-filter)).
package/docs/releasing.md CHANGED
@@ -63,6 +63,12 @@ release-kit prepare --bump=minor
63
63
 
64
64
  Before it writes any file, and in a dry run as well, `prepare` asks npm about each publishable package that the release includes, querying the registry that the package's `publishConfig.registry` names when it sets one. OIDC trusted publishing can't publish a package's first version, so `prepare` refuses to release a package that npm doesn't list, and names each one with its fix: Run `npm publish --access public` from the package's directory once, then register the package as a trusted publisher. The `npm-auto-publish` [readiness kit](readiness-checks.md) prints the `npm trust` command. `prepare` also refuses when it can't look a package up, such as when the registry is unreachable, and reports npm's error for that package.
65
65
 
66
+ ### Packages whose tarball would lack a manifest file
67
+
68
+ Before it writes any file, and in a dry run as well, `prepare` packs each publishable package that the release includes as the publish workflow would: It clones HEAD into a temporary directory, runs the package manager's frozen install there, runs the package's `prepublishOnly` script when it declares one, and packs the package. Build output in the working tree therefore does not count, and the check adds the time of one install and one build to every release. `prepare` refuses the release when a tarball lacks a file named by the package's `main`, `types`, `bin`, or `exports`, and names each package with each missing path and the manifest field that names it. The usual fix is to build the files in the package's publish lifecycle, such as in a `prepack` script. A failed install or build in the clean checkout also refuses the release, with the failing step's output.
69
+
70
+ A target under the `source` export condition is exempt, and so is a target under a condition listed in [`devExportConditions`](configuration.md#releasekitconfig-reference), which declares the conditions that resolve only inside the repo. [`publish`](publishing.md#tarball-check) runs the same check before it publishes any package.
71
+
66
72
  ### Previewing release notes with `--with-release-notes`
67
73
 
68
74
  `--with-release-notes` writes two versioned files per workspace after each workspace's `changelog.json` is produced:
@@ -51,7 +51,7 @@ Each work-type has a `breakingPolicy` value:
51
51
 
52
52
  A `BREAKING CHANGE:` body footer on a `forbidden`-policy type triggers the same warning path as the prefix `!` does: A type that cannot be breaking is parsed as non-breaking whichever surface has the signal. The warning is all that a footer triggers; it doesn't raise a bump (see [`🚨 **Breaking:**` bullet marker](#-breaking-bullet-marker)).
53
53
 
54
- The release-prepare orchestrator (`releasePrepareTargets`) applies `DEFAULT_BREAKING_POLICIES` automatically. Violations in the titles and change-record entries of each workspace's or project's unreleased window are collected onto the corresponding result's `policyViolations` field, on a skipped result as on a released one, and rendered under the section in the prepare report:
54
+ The release-prepare orchestrator (`prepareReleaseTargets`) applies `DEFAULT_BREAKING_POLICIES` automatically. Violations in the titles and change-record entries of each workspace's or project's unreleased window are collected onto the corresponding result's `policyViolations` field, on a skipped result as on a released one, and rendered under the section in the prepare report:
55
55
 
56
56
  ```
57
57
  arrays
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/release-kit",
3
- "version": "15.0.3",
3
+ "version": "16.1.0",
4
4
  "description": "Version-bumping and changelog-generation toolkit for release workflows",
5
5
  "keywords": [
6
6
  "changelog",
@@ -63,7 +63,7 @@
63
63
  "@types/semver": "7.8.0",
64
64
  "@williamthorsen/toolbelt.filesystem": "0.10.0",
65
65
  "@williamthorsen/toolbelt.testing": "0.8.0",
66
- "@williamthorsen/toolbelt.vitest": "0.13.0",
66
+ "@williamthorsen/toolbelt.vitest": "0.13.1",
67
67
  "actionlint": "2.0.6",
68
68
  "esbuild": "0.28.2",
69
69
  "readyup": "0.44.0",
@@ -9,6 +9,9 @@
9
9
  - name: bug
10
10
  color: d73a4a
11
11
  description: "Something isn't working"
12
+ - name: content
13
+ color: 0e8a16
14
+ description: Consumer-facing material, such as copy, images, or displayed data, rather than functionality
12
15
  - name: operational
13
16
  color: '000000'
14
17
  description: Actions outside the codebase