@williamthorsen/release-kit 14.1.0 → 14.2.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 (92) hide show
  1. package/.meta/changelog.json +125 -0
  2. package/.readyup/kits/default.js +63 -6
  3. package/.readyup/kits/npm-auto-publish.js +47 -35
  4. package/.readyup/manifest.json +12 -7
  5. package/CHANGELOG.md +33 -0
  6. package/README.md +18 -8
  7. package/dist/esm/assertTaggedBaseline.d.ts +2 -2
  8. package/dist/esm/attachChangelogDiagnostics.d.ts +2 -2
  9. package/dist/esm/bin/release-kit.js +5 -4
  10. package/dist/esm/buildChangelogEntries.d.ts +3 -3
  11. package/dist/esm/buildChangelogEntries.js +3 -3
  12. package/dist/esm/buildDependencyGraph.d.ts +11 -11
  13. package/dist/esm/buildDependencyGraph.js +13 -13
  14. package/dist/esm/buildReleaseSummary.d.ts +3 -3
  15. package/dist/esm/buildReleaseSummary.js +4 -12
  16. package/dist/esm/changelogArchive.d.ts +14 -0
  17. package/dist/esm/changelogArchive.js +21 -0
  18. package/dist/esm/changelogJsonFile.d.ts +2 -2
  19. package/dist/esm/changelogOverrides.d.ts +9 -8
  20. package/dist/esm/changelogOverrides.js +3 -6
  21. package/dist/esm/createGithubReleaseCommand.d.ts +2 -1
  22. package/dist/esm/createGithubReleaseCommand.js +7 -4
  23. package/dist/esm/deriveDirectoryConfig.d.ts +13 -0
  24. package/dist/esm/deriveDirectoryConfig.js +59 -0
  25. package/dist/esm/deriveWorkspaceConfig.js +0 -1
  26. package/dist/esm/discoverWorkspaces.d.ts +3 -3
  27. package/dist/esm/findTransitiveDependents.js +3 -3
  28. package/dist/esm/findUnroutedEntryScopes.d.ts +2 -2
  29. package/dist/esm/formatDirectoryPublishSkip.d.ts +3 -0
  30. package/dist/esm/formatDirectoryPublishSkip.js +3 -0
  31. package/dist/esm/formatExcludedSkip.d.ts +5 -2
  32. package/dist/esm/formatExcludedSkip.js +2 -2
  33. package/dist/esm/formatProjectPublishSkip.d.ts +3 -0
  34. package/dist/esm/formatProjectPublishSkip.js +3 -0
  35. package/dist/esm/generateChangelogs.d.ts +7 -7
  36. package/dist/esm/generateChangelogs.js +2 -2
  37. package/dist/esm/help/prepareHelp.d.ts +1 -1
  38. package/dist/esm/help/prepareHelp.js +7 -5
  39. package/dist/esm/init/templates.js +1 -1
  40. package/dist/esm/loadConfig.d.ts +12 -12
  41. package/dist/esm/loadConfig.js +79 -30
  42. package/dist/esm/prepareCommand.d.ts +1 -1
  43. package/dist/esm/prepareCommand.js +47 -46
  44. package/dist/esm/prepareTarget.d.ts +103 -0
  45. package/dist/esm/prepareTarget.js +274 -0
  46. package/dist/esm/previewTagPrefixes.d.ts +5 -3
  47. package/dist/esm/previewTagPrefixes.js +58 -13
  48. package/dist/esm/propagateBumps.js +1 -1
  49. package/dist/esm/publishCommand.js +13 -1
  50. package/dist/esm/readChangelogSections.d.ts +6 -4
  51. package/dist/esm/readChangelogSections.js +18 -4
  52. package/dist/esm/releasePlan.d.ts +4 -2
  53. package/dist/esm/releasePlan.js +34 -12
  54. package/dist/esm/releasePrepareTargets.d.ts +47 -0
  55. package/dist/esm/releasePrepareTargets.js +335 -0
  56. package/dist/esm/renderChangelogMarkdown.d.ts +23 -0
  57. package/dist/esm/renderChangelogMarkdown.js +47 -11
  58. package/dist/esm/reportPrepare.d.ts +2 -3
  59. package/dist/esm/reportPrepare.js +24 -12
  60. package/dist/esm/resolveCommandTags.d.ts +3 -1
  61. package/dist/esm/resolveCommandTags.js +15 -22
  62. package/dist/esm/resolveReleaseDeclaration.d.ts +45 -0
  63. package/dist/esm/resolveReleaseDeclaration.js +153 -0
  64. package/dist/esm/resolveReleaseTags.d.ts +14 -3
  65. package/dist/esm/resolveReleaseTags.js +36 -15
  66. package/dist/esm/resolveReleaseTargets.d.ts +29 -0
  67. package/dist/esm/resolveReleaseTargets.js +131 -0
  68. package/dist/esm/sync-labels/generateCommand.js +2 -1
  69. package/dist/esm/sync-labels/resolveLabels.d.ts +4 -3
  70. package/dist/esm/sync-labels/resolveLabels.js +30 -1
  71. package/dist/esm/sync-labels/types.d.ts +2 -0
  72. package/dist/esm/types.d.ts +251 -167
  73. package/dist/esm/types.js +22 -0
  74. package/dist/esm/validateConfig.js +80 -21
  75. package/dist/esm/validateOverridesCommand.d.ts +1 -1
  76. package/dist/esm/validateOverridesCommand.js +17 -28
  77. package/docs/api.md +25 -58
  78. package/docs/changelogs.md +26 -6
  79. package/docs/configuration.md +79 -16
  80. package/docs/labels.md +25 -2
  81. package/docs/project-releases.md +42 -2
  82. package/docs/publishing.md +3 -3
  83. package/docs/releasing.md +4 -4
  84. package/docs/work-types.md +1 -1
  85. package/package.json +2 -2
  86. package/presets/labels/common.yaml +11 -0
  87. package/dist/esm/releasePrepare.d.ts +0 -36
  88. package/dist/esm/releasePrepare.js +0 -230
  89. package/dist/esm/releasePrepareMono.d.ts +0 -29
  90. package/dist/esm/releasePrepareMono.js +0 -503
  91. package/dist/esm/releasePrepareProject.d.ts +0 -47
  92. package/dist/esm/releasePrepareProject.js +0 -164
@@ -0,0 +1,274 @@
1
+ import { join as joinPath } from 'node:path';
2
+ import { findUntaggedBaseline } from "./assertTaggedBaseline.js";
3
+ import { attachChangelogDiagnostics } from "./attachChangelogDiagnostics.js";
4
+ import { readReleaseHistory, toReleaseEntries } from "./buildChangelogEntries.js";
5
+ import { buildSyntheticChangelogEntry } from "./buildSyntheticChangelogEntry.js";
6
+ import { describeArchivePath, listArchiveFiles } from "./changelogArchive.js";
7
+ import { mergeChangelogEntriesWithDisk, renderChangelogJson, resolveChangelogJsonPath } from "./changelogJsonFile.js";
8
+ import { applyWorkspaceOverrides } from "./changelogOverrides.js";
9
+ import { decideRelease } from "./decideRelease.js";
10
+ import { getAllTagPrefixes } from "./generateChangelogs.js";
11
+ import { planReleaseNotesPreviews } from "./planReleaseNotesPreviews.js";
12
+ import { planVersionBump, planVersionSet } from "./planVersionBump.js";
13
+ import { planPreservedSections } from "./readChangelogSections.js";
14
+ import { renderChangelogFiles } from "./renderChangelogMarkdown.js";
15
+ import { PROJECT_TARGET_ID } from "./resolveReleaseTargets.js";
16
+ export function readTargetHistory(target, settings) {
17
+ const options = describeHistoryWindow(target);
18
+ const history = readReleaseHistory(settings, options);
19
+ const untagged = findUntaggedBaseline({
20
+ label: describeBaselineLabel(target),
21
+ packageFiles: target.versionFiles,
22
+ changelogPaths: [target.changelogDir],
23
+ tagPrefixes: options.tagPrefixes,
24
+ previousTag: history.previousTag,
25
+ }, settings);
26
+ return { history, untagged };
27
+ }
28
+ export function describeHistoryWindow(target) {
29
+ return {
30
+ tagPrefixes: getAllTagPrefixes(target),
31
+ ...(target.paths !== undefined && { paths: target.paths }),
32
+ ...(isScopeRouted(target) && { scope: target.id }),
33
+ };
34
+ }
35
+ export function decideTargetRelease(target, history, options) {
36
+ const { force, bumpOverride, setVersion } = options;
37
+ if (setVersion !== undefined) {
38
+ return { outcome: 'release', releaseType: undefined, bumpOverride: undefined, setVersion };
39
+ }
40
+ const tag = history.previousTag;
41
+ const since = tag === undefined ? '(no previous release found)' : `since ${tag}`;
42
+ const subject = isScopeRouted(target) || target.kind === 'directory' ? ` for ${target.id}` : '';
43
+ const decision = decideRelease({
44
+ naturalBump: history.unreleased.bump,
45
+ commitCount: history.unreleased.commits.length,
46
+ force,
47
+ bumpOverride,
48
+ skipReasons: {
49
+ noCommits: `No commits${subject} ${since}. Pass --force to release at patch. Skipping.`,
50
+ noBumpWorthy: `No bump-worthy commits${subject} ${since}. Pass --force to release at patch (or --force --bump=X for a different level). Skipping.`,
51
+ },
52
+ });
53
+ if (decision.outcome === 'skip') {
54
+ return decision;
55
+ }
56
+ return { outcome: 'release', releaseType: decision.releaseType, bumpOverride, setVersion: undefined };
57
+ }
58
+ export function planTargetRelease(args) {
59
+ const { target, settings, release, decision, history, overrideContext, sectionOrder, shouldPlanPreviews, today } = args;
60
+ const setVersion = decision?.setVersion;
61
+ const bump = setVersion === undefined
62
+ ? planVersionBump(target.versionFiles, release.releaseType)
63
+ : planVersionSet(target.versionFiles, setVersion);
64
+ const tag = `${target.tagPrefix}${bump.newVersion}`;
65
+ const writes = [...bump.writes];
66
+ const entries = decision === undefined
67
+ ? buildPropagationEntries(target, release, bump.newVersion, today, args.dependenciesHeader)
68
+ : toReleaseEntries(history, tag, today);
69
+ const overrideScope = describeOverrideScope(target);
70
+ const applied = applyWorkspaceOverrides(entries, overrideScope, overrideContext);
71
+ const changelogs = planChangelogFiles(target.changelogDir, applied.entries, settings, sectionOrder);
72
+ writes.push(...changelogs.writes);
73
+ const warnings = [];
74
+ const previewFiles = [];
75
+ if (shouldPlanPreviews) {
76
+ const previews = planReleaseNotesPreviews({
77
+ workspacePath: target.changelogDir,
78
+ tag,
79
+ entries: changelogs.entries,
80
+ sectionOrder,
81
+ });
82
+ writes.push(...previews.writes);
83
+ warnings.push(...previews.warnings);
84
+ previewFiles.push(...previews.writes.map((write) => write.path));
85
+ }
86
+ const releaseFields = {
87
+ targetId: target.id,
88
+ commitCount: history.unreleased.commits.length,
89
+ currentVersion: bump.currentVersion,
90
+ newVersion: bump.newVersion,
91
+ tag,
92
+ bumpedFiles: bump.writes.map((write) => write.path),
93
+ changelogFiles: changelogs.changelogFiles,
94
+ ...(changelogs.removals.length > 0 && { removedChangelogFiles: changelogs.removals }),
95
+ ...(previewFiles.length > 0 && { previewFiles }),
96
+ ...(changelogs.preservation !== undefined && { changelogPreservation: [changelogs.preservation] }),
97
+ ...(history.previousTag !== undefined && { previousTag: history.previousTag }),
98
+ };
99
+ const result = buildReleasedResult(target, releaseFields, release, decision, history);
100
+ return {
101
+ result,
102
+ tag,
103
+ writes,
104
+ removals: changelogs.removals,
105
+ modifiedFiles: [...target.versionFiles, ...changelogs.changelogFiles, ...changelogs.changelogJsonFiles],
106
+ warnings,
107
+ };
108
+ }
109
+ export function buildSkippedTargetResult(target, history, skipReason) {
110
+ const { unreleased } = history;
111
+ const fields = {
112
+ targetId: target.id,
113
+ status: 'skipped',
114
+ commitCount: unreleased.commits.length,
115
+ parsedCommitCount: unreleased.parsedCommitCount,
116
+ skipReason,
117
+ ...(history.previousTag !== undefined && { previousTag: history.previousTag }),
118
+ ...(unreleased.unparseableCommits !== undefined && { unparseableCommits: unreleased.unparseableCommits }),
119
+ };
120
+ const skipped = labelSkippedResult(target, fields);
121
+ attachChangelogDiagnostics(skipped, unreleased.diagnostics);
122
+ return skipped;
123
+ }
124
+ export function isRepoRoot(target) {
125
+ return target.directoryPath === '.';
126
+ }
127
+ function buildReleasedDirectory(target, fields, release, decision, history) {
128
+ const { unreleased } = history;
129
+ const setVersion = decision?.setVersion;
130
+ const result = {
131
+ ...fields,
132
+ status: 'released',
133
+ kind: 'directory',
134
+ name: target.id,
135
+ commits: unreleased.commits,
136
+ ...(setVersion === undefined && {
137
+ releaseType: release.releaseType,
138
+ parsedCommitCount: unreleased.parsedCommitCount,
139
+ }),
140
+ ...(decision?.bumpOverride !== undefined && { bumpOverride: decision.bumpOverride }),
141
+ ...(setVersion !== undefined && { setVersion }),
142
+ };
143
+ if (setVersion === undefined && unreleased.unparseableCommits !== undefined) {
144
+ result.unparseableCommits = unreleased.unparseableCommits;
145
+ }
146
+ attachChangelogDiagnostics(result, unreleased.diagnostics);
147
+ return result;
148
+ }
149
+ function buildReleasedComposite(fields, release, decision, history) {
150
+ const { unreleased } = history;
151
+ const result = {
152
+ ...fields,
153
+ status: 'released',
154
+ kind: 'composite',
155
+ parsedCommitCount: unreleased.parsedCommitCount,
156
+ releaseType: release.releaseType,
157
+ commits: unreleased.commits,
158
+ ...(unreleased.unparseableCommits !== undefined && { unparseableCommits: unreleased.unparseableCommits }),
159
+ ...(decision?.bumpOverride !== undefined && { bumpOverride: decision.bumpOverride }),
160
+ };
161
+ attachChangelogDiagnostics(result, unreleased.diagnostics);
162
+ return result;
163
+ }
164
+ function buildReleasedPackage(target, fields, release, decision, history) {
165
+ const { unreleased } = history;
166
+ const setVersion = decision?.setVersion;
167
+ const hasParseCounts = decision !== undefined && setVersion === undefined;
168
+ const result = {
169
+ ...fields,
170
+ status: 'released',
171
+ kind: 'package',
172
+ commits: unreleased.commits,
173
+ ...(target.isWorkspace && { name: target.id }),
174
+ ...(setVersion === undefined && { releaseType: release.releaseType }),
175
+ ...(hasParseCounts && { parsedCommitCount: unreleased.parsedCommitCount }),
176
+ ...(decision === undefined && { propagatedOnly: true }),
177
+ ...(release.propagatedFrom !== undefined && { propagatedFrom: release.propagatedFrom }),
178
+ ...(decision?.bumpOverride !== undefined && { bumpOverride: decision.bumpOverride }),
179
+ ...(setVersion !== undefined && { setVersion }),
180
+ };
181
+ if (setVersion === undefined && unreleased.unparseableCommits !== undefined) {
182
+ result.unparseableCommits = unreleased.unparseableCommits;
183
+ }
184
+ attachChangelogDiagnostics(result, unreleased.diagnostics);
185
+ return result;
186
+ }
187
+ function buildReleasedResult(target, fields, release, decision, history) {
188
+ switch (target.kind) {
189
+ case 'package':
190
+ return buildReleasedPackage(target, fields, release, decision, history);
191
+ case 'directory':
192
+ return buildReleasedDirectory(target, fields, release, decision, history);
193
+ case 'composite':
194
+ return buildReleasedComposite(fields, release, decision, history);
195
+ }
196
+ }
197
+ function buildPropagationEntries(target, release, newVersion, today, dependenciesHeader) {
198
+ if (release.propagatedFrom === undefined) {
199
+ throw new Error(`Workspace '${target.id}' has neither a direct release nor a propagation source`);
200
+ }
201
+ return [buildSyntheticChangelogEntry(release.propagatedFrom, newVersion, today, dependenciesHeader)];
202
+ }
203
+ function describeBaselineLabel(target) {
204
+ switch (target.kind) {
205
+ case 'composite':
206
+ return target.id === PROJECT_TARGET_ID ? 'project' : `composite '${target.id}'`;
207
+ case 'directory':
208
+ return `directory '${target.id}'`;
209
+ case 'package':
210
+ return target.isWorkspace ? `workspace '${target.id}'` : 'package';
211
+ }
212
+ }
213
+ function labelSkippedResult(target, fields) {
214
+ switch (target.kind) {
215
+ case 'package':
216
+ return { ...fields, kind: 'package', ...(target.isWorkspace && { name: target.id }) };
217
+ case 'directory':
218
+ return { ...fields, kind: 'directory', name: target.id };
219
+ case 'composite':
220
+ return { ...fields, kind: 'composite' };
221
+ }
222
+ }
223
+ function describeOverrideScope(target) {
224
+ switch (target.kind) {
225
+ case 'package':
226
+ return target.packagePath;
227
+ case 'directory':
228
+ return isRepoRoot(target) ? undefined : target.directoryPath;
229
+ case 'composite':
230
+ return undefined;
231
+ }
232
+ }
233
+ function isScopeRouted(target) {
234
+ if (target.kind === 'directory') {
235
+ return !isRepoRoot(target);
236
+ }
237
+ return target.kind === 'package' && target.isWorkspace;
238
+ }
239
+ function planChangelogFiles(changelogDir, newEntries, settings, sectionOrder) {
240
+ const jsonPath = resolveChangelogJsonPath(settings, changelogDir);
241
+ const entries = mergeChangelogEntriesWithDisk(jsonPath, newEntries);
242
+ const writes = [];
243
+ const changelogJsonFiles = [];
244
+ if (settings.changelogJson.enabled) {
245
+ writes.push({ path: jsonPath, content: renderChangelogJson(entries) });
246
+ changelogJsonFiles.push(jsonPath);
247
+ }
248
+ const changelogFile = joinPath(changelogDir, 'CHANGELOG.md');
249
+ const preserved = planPreservedSections(changelogDir, entries);
250
+ const keepVersions = settings.changelogMarkdown?.keepVersions;
251
+ const rendered = renderChangelogFiles(entries, {
252
+ sectionOrder,
253
+ preservedSections: preserved.sections,
254
+ ...(keepVersions !== undefined && { keepVersions }),
255
+ });
256
+ writes.push({ path: changelogFile, content: rendered.main });
257
+ const archiveFiles = [];
258
+ for (const { major, content } of rendered.archives) {
259
+ const path = joinPath(changelogDir, describeArchivePath(major));
260
+ writes.push({ path, content });
261
+ archiveFiles.push(path);
262
+ }
263
+ const removals = listArchiveFiles(changelogDir)
264
+ .map((archive) => archive.path)
265
+ .filter((path) => !archiveFiles.includes(path));
266
+ return {
267
+ changelogFiles: [changelogFile, ...archiveFiles],
268
+ changelogJsonFiles,
269
+ entries,
270
+ preservation: preserved.preservation,
271
+ removals,
272
+ writes,
273
+ };
274
+ }
@@ -1,6 +1,6 @@
1
1
  import { type UndeclaredTagPrefix } from './detectUndeclaredTagPrefixes.js';
2
2
  import type { ReleaseKitConfig } from './types.js';
3
- /** One workspace's preview row in the tag-prefix preview. */
3
+ /** One workspace's or directory target's preview row in the tag-prefix preview. */
4
4
  export interface TagPrefixPreviewRow {
5
5
  /** Directory path relative to the repo root (e.g., `packages/core`). */
6
6
  workspacePath: string;
@@ -40,12 +40,14 @@ export interface TagPrefixPreview {
40
40
  retiredPackages: RetiredPackagePreviewEntry[];
41
41
  }
42
42
  /**
43
- * Builds a structured preview of tag-prefix state for every discovered workspace.
43
+ * Builds a structured preview of tag-prefix state for every discovered workspace and every directory that the
44
+ * `releases` block declares.
44
45
  *
45
46
  * Records a workspace's derivation error rather than aborting. For a repo releasing as a single package, the
46
47
  * preview doesn't contain any rows; for one declaring patterns that don't resolve to any package, the function
47
48
  * throws, because the manifest then needs repairing and a clean table would hide that. Also reports the legacy
48
49
  * prefixes that the validated `config` declares per workspace, undeclared candidate prefixes among the local git
49
- * tags, and collisions across the successfully derived prefixes.
50
+ * tags, and collisions across the successfully derived prefixes. A declared composite's tag prefix counts as known, so
51
+ * that its tags are not reported as undeclared.
50
52
  */
51
53
  export declare function previewTagPrefixes(config?: ReleaseKitConfig): TagPrefixPreview;
@@ -2,47 +2,78 @@ import { execFileSync } from 'node:child_process';
2
2
  import { basename } from 'node:path';
3
3
  import { GIT_OUTPUT_LIMIT } from '@williamthorsen/nmr-core';
4
4
  import { describeError } from '@williamthorsen/toolbelt.errors';
5
+ import { DEFAULT_PROJECT_TAG_PREFIX } from "./defaults.js";
6
+ import { deriveDirectoryConfig } from "./deriveDirectoryConfig.js";
5
7
  import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
6
8
  import { detectUndeclaredTagPrefixes } from "./detectUndeclaredTagPrefixes.js";
7
9
  import { describeEmptyWorkspace, discoverWorkspaces } from "./discoverWorkspaces.js";
8
10
  import { matchesTagPrefix } from "./matchesTagPrefix.js";
11
+ import { normalizeWorkspacePath } from "./resolveReleaseDeclaration.js";
9
12
  export function previewTagPrefixes(config) {
10
13
  const workspace = discoverWorkspaces();
11
14
  if (workspace.kind === 'empty') {
12
15
  throw new Error(`No workspace package to preview. ${describeEmptyWorkspace(workspace)}`);
13
16
  }
14
17
  const workspacePaths = workspace.kind === 'packages' ? workspace.packageDirs : [];
15
- const overridesByDir = buildOverrideMap(config);
16
- const workspaces = Array.from(workspacePaths, (workspacePath) => buildPreviewRow(workspacePath, overridesByDir));
18
+ const overridesByPath = buildOverrideMap(config, workspacePaths);
19
+ const workspaces = [
20
+ ...Array.from(workspacePaths, (workspacePath) => buildPreviewRow(workspacePath, () => deriveWorkspaceConfig(workspacePath).tagPrefix, overridesByPath)),
21
+ ...buildDirectoryRows(config, workspacePaths, overridesByPath),
22
+ ];
17
23
  const retiredPackages = buildRetiredPreviewEntries(config?.retiredPackages ?? []);
18
24
  const collisions = detectCollisions(workspaces);
19
- const knownPrefixes = collectKnownPrefixes(workspaces, retiredPackages);
25
+ const knownPrefixes = collectKnownPrefixes(workspaces, retiredPackages, findCompositeTagPrefix(config));
20
26
  const undeclaredCandidates = detectUndeclaredTagPrefixes(knownPrefixes);
21
27
  return { workspaces, collisions, undeclaredCandidates, retiredPackages };
22
28
  }
23
- function buildOverrideMap(config) {
29
+ function buildOverrideMap(config, workspacePaths) {
24
30
  const map = new Map();
25
- if (config?.workspaces === undefined)
26
- return map;
27
- for (const entry of config.workspaces) {
28
- if (entry.legacyIdentities !== undefined) {
29
- map.set(entry.dir, entry.legacyIdentities);
31
+ const workspaceEntries = config?.workspaces ?? [];
32
+ for (const { dir, legacyIdentities } of workspaceEntries) {
33
+ if (legacyIdentities === undefined)
34
+ continue;
35
+ for (const workspacePath of workspacePaths) {
36
+ if (basename(workspacePath) === dir) {
37
+ map.set(workspacePath, legacyIdentities);
38
+ }
39
+ }
40
+ }
41
+ const releaseEntries = Object.values(config?.releases ?? {});
42
+ for (const entry of releaseEntries) {
43
+ if (entry !== false && 'path' in entry && entry.legacyIdentities !== undefined) {
44
+ map.set(normalizeWorkspacePath(entry.path), entry.legacyIdentities);
30
45
  }
31
46
  }
32
47
  return map;
33
48
  }
34
- function buildPreviewRow(workspacePath, overridesByDir) {
49
+ function buildDirectoryRows(config, workspacePaths, overridesByPath) {
50
+ const discovered = new Set(workspacePaths);
51
+ return Object.entries(config?.releases ?? {}).flatMap(([name, entry]) => {
52
+ if (entry === false || !('path' in entry))
53
+ return [];
54
+ const directoryPath = normalizeWorkspacePath(entry.path);
55
+ if (discovered.has(directoryPath))
56
+ return [];
57
+ const declared = {
58
+ name,
59
+ path: directoryPath,
60
+ ...(entry.tagPrefix !== undefined && { tagPrefix: entry.tagPrefix }),
61
+ };
62
+ return [buildPreviewRow(directoryPath, () => deriveDirectoryConfig(declared).tagPrefix, overridesByPath)];
63
+ });
64
+ }
65
+ function buildPreviewRow(workspacePath, derivePrefix, overridesByPath) {
35
66
  const dir = basename(workspacePath);
36
67
  let derivedPrefix = null;
37
68
  let derivationError = null;
38
69
  try {
39
- derivedPrefix = deriveWorkspaceConfig(workspacePath).tagPrefix;
70
+ derivedPrefix = derivePrefix();
40
71
  }
41
72
  catch (error) {
42
73
  derivationError = describeError(error);
43
74
  }
44
75
  const derivedTagCount = derivedPrefix === null ? 0 : countTagsMatching(derivedPrefix);
45
- const declaredIdentities = overridesByDir.get(dir) ?? [];
76
+ const declaredIdentities = overridesByPath.get(workspacePath) ?? [];
46
77
  const legacyEntries = declaredIdentities.map((identity) => ({
47
78
  prefix: identity.tagPrefix,
48
79
  tagCount: countTagsMatching(identity.tagPrefix),
@@ -90,7 +121,7 @@ function detectCollisions(rows) {
90
121
  }
91
122
  return collisions;
92
123
  }
93
- function collectKnownPrefixes(rows, retiredPackages) {
124
+ function collectKnownPrefixes(rows, retiredPackages, compositeTagPrefix) {
94
125
  const known = new Set();
95
126
  for (const row of rows) {
96
127
  if (row.derivedPrefix !== null)
@@ -102,6 +133,8 @@ function collectKnownPrefixes(rows, retiredPackages) {
102
133
  for (const retired of retiredPackages) {
103
134
  known.add(retired.tagPrefix);
104
135
  }
136
+ if (compositeTagPrefix !== undefined)
137
+ known.add(compositeTagPrefix);
105
138
  return [...known];
106
139
  }
107
140
  function buildRetiredPreviewEntries(retiredPackages) {
@@ -112,3 +145,15 @@ function buildRetiredPreviewEntries(retiredPackages) {
112
145
  ...(retired.successor !== undefined && { successor: retired.successor }),
113
146
  }));
114
147
  }
148
+ function findCompositeTagPrefix(config) {
149
+ if (config?.project !== undefined) {
150
+ return config.project.tagPrefix ?? DEFAULT_PROJECT_TAG_PREFIX;
151
+ }
152
+ const releaseEntries = Object.values(config?.releases ?? {});
153
+ for (const entry of releaseEntries) {
154
+ if (entry !== false && 'includes' in entry) {
155
+ return entry.tagPrefix ?? DEFAULT_PROJECT_TAG_PREFIX;
156
+ }
157
+ }
158
+ return undefined;
159
+ }
@@ -30,7 +30,7 @@ export function propagateBumps(directBumps, graph) {
30
30
  continue;
31
31
  }
32
32
  for (const dependent of dependents) {
33
- const dependentDir = dependent.dir;
33
+ const dependentDir = dependent.id;
34
34
  const existing = result.get(dependentDir);
35
35
  const propagationInfo = { packageName, newVersion };
36
36
  if (existing === undefined) {
@@ -6,7 +6,9 @@ import { assertCleanWorkingTree } from "./assertCleanWorkingTree.js";
6
6
  import { configFlagSchema } from "./configFlagSchema.js";
7
7
  import { deriveReleaseNotesConfig } from "./deriveReleaseNotesConfig.js";
8
8
  import { detectPackageManager } from "./detectPackageManager.js";
9
+ import { formatDirectoryPublishSkip } from "./formatDirectoryPublishSkip.js";
9
10
  import { formatPrivateSkip } from "./formatPrivateSkip.js";
11
+ import { formatProjectPublishSkip } from "./formatProjectPublishSkip.js";
10
12
  import { injectReleaseNotesIntoReadme, resolveReadmePath } from "./injectReleaseNotesIntoReadme.js";
11
13
  import { loadUsableConfig } from "./loadValidatedConfig.js";
12
14
  import { parseRequestedTags } from "./parseRequestedTags.js";
@@ -92,8 +94,18 @@ function filterPublishableTags(resolvedTags, isExplicit) {
92
94
  }
93
95
  if (isExplicit) {
94
96
  for (const resolvedTag of unpublishable) {
95
- console.warn(formatPrivateSkip(resolvedTag));
97
+ console.warn(formatPublishSkip(resolvedTag));
96
98
  }
97
99
  }
98
100
  return publishable;
99
101
  }
102
+ function formatPublishSkip(resolvedTag) {
103
+ switch (resolvedTag.kind) {
104
+ case 'directory':
105
+ return formatDirectoryPublishSkip(resolvedTag);
106
+ case 'project':
107
+ return formatProjectPublishSkip(resolvedTag);
108
+ case 'package':
109
+ return formatPrivateSkip(resolvedTag);
110
+ }
111
+ }
@@ -12,11 +12,13 @@ export interface ChangelogMarkdownSections {
12
12
  unversionedHeadings: string[];
13
13
  }
14
14
  /**
15
- * Plans which sections of the existing `CHANGELOG.md` at `changelogFile` to keep when the file is regenerated from
16
- * `entries`: every versioned section whose version matches none of the entries. Also returns what to report about the
17
- * file, or undefined when nothing was kept and nothing was dropped.
15
+ * Plans which sections of the existing `CHANGELOG.md` in `changelogDir` and its archive files to keep when they are
16
+ * regenerated from `entries`: every versioned section whose version matches none of the entries. A version found in
17
+ * more than one file is taken from the first file read, `CHANGELOG.md` first, then archives newest major first. Also
18
+ * returns what to report about the changelog, under the `CHANGELOG.md` path, or undefined when nothing was kept and
19
+ * nothing was dropped.
18
20
  */
19
- export declare function planPreservedSections(changelogFile: string, entries: readonly ChangelogEntry[]): {
21
+ export declare function planPreservedSections(changelogDir: string, entries: readonly ChangelogEntry[]): {
20
22
  sections: VersionedMarkdownSection[];
21
23
  preservation: ChangelogPreservation | undefined;
22
24
  };
@@ -1,9 +1,23 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
2
3
  import semver from 'semver';
3
- export function planPreservedSections(changelogFile, entries) {
4
- const { versioned, unversionedHeadings } = readChangelogSections(changelogFile);
5
- const renderedVersions = new Set(entries.map((entry) => entry.version));
6
- const sections = versioned.filter((section) => !renderedVersions.has(section.version));
4
+ import { listArchiveFiles } from "./changelogArchive.js";
5
+ export function planPreservedSections(changelogDir, entries) {
6
+ const changelogFile = join(changelogDir, 'CHANGELOG.md');
7
+ const files = [changelogFile, ...listArchiveFiles(changelogDir).map((archive) => archive.path)];
8
+ const seenVersions = new Set(entries.map((entry) => entry.version));
9
+ const sections = [];
10
+ const unversionedHeadings = [];
11
+ for (const file of files) {
12
+ const read = readChangelogSections(file);
13
+ for (const section of read.versioned) {
14
+ if (seenVersions.has(section.version))
15
+ continue;
16
+ seenVersions.add(section.version);
17
+ sections.push(section);
18
+ }
19
+ unversionedHeadings.push(...read.unversionedHeadings);
20
+ }
7
21
  if (sections.length === 0 && unversionedHeadings.length === 0) {
8
22
  return { sections, preservation: undefined };
9
23
  }
@@ -15,17 +15,19 @@ export interface PlannedWrite {
15
15
  */
16
16
  export interface ReleasePlan extends PrepareResult {
17
17
  writes: readonly PlannedWrite[];
18
+ /** Files that the release removes, such as changelog archive files that the render no longer produces. */
19
+ removals: readonly string[];
18
20
  /** Body of the release commit message, written alongside the tags file. */
19
21
  summary: string;
20
22
  }
21
23
  /**
22
- * Writes every file that a plan describes: content files first, then the summary, then the tags file.
24
+ * Applies every step that a plan describes: content files first, then removals, then the summary, then the tags file.
23
25
  *
24
26
  * The tags file goes last so that its presence means that the whole plan was written to disk.
25
27
  * Both `release-kit commit` and the reusable release workflow read it as the signal that a
26
28
  * release is ready to commit, so writing it earlier would let a failure produce a tree that
27
29
  * looks releasable.
28
30
  *
29
- * Returns the paths written, in order. Throws if any write fails, naming what was written and what was not.
31
+ * Returns the paths written or removed, in order. Throws if any step fails, naming what was applied and what was not.
30
32
  */
31
33
  export declare function applyReleasePlan(plan: ReleasePlan): string[];
@@ -1,33 +1,55 @@
1
1
  import { writeFileWithCheck } from '@williamthorsen/nmr-core';
2
+ import { describeError } from '@williamthorsen/toolbelt.errors';
3
+ import { deleteFileIfExists } from "./deleteFileIfExists.js";
2
4
  import { RELEASE_SUMMARY_FILE, RELEASE_TAGS_FILE } from "./releaseFiles.js";
3
5
  const HANDOFF_FILES = new Set([RELEASE_SUMMARY_FILE, RELEASE_TAGS_FILE]);
4
6
  export function applyReleasePlan(plan) {
5
- const ordered = orderPlannedWrites(plan);
7
+ const ordered = orderPlannedSteps(plan);
6
8
  const applied = [];
7
- for (const [index, write] of ordered.entries()) {
8
- const result = writeFileWithCheck(write.path, write.content, { dryRun: false, overwrite: true });
9
- if (result.outcome === 'failed') {
9
+ for (const [index, step] of ordered.entries()) {
10
+ const outcome = applyStep(step);
11
+ if (outcome.outcome === 'failed') {
10
12
  const unwritten = ordered.slice(index).map((pending) => pending.path);
11
- throw new Error(describeApplyFailure({ failedPath: write.path, error: result.error, applied, unwritten }));
13
+ throw new Error(describeApplyFailure({ failedStep: step, error: outcome.error, applied, unwritten }));
12
14
  }
13
- applied.push({ path: write.path, created: result.outcome === 'created' });
15
+ applied.push({ path: step.path, created: outcome.outcome === 'created' });
14
16
  }
15
17
  return applied.map((write) => write.path);
16
18
  }
17
- function orderPlannedWrites(plan) {
18
- const ordered = [...plan.writes];
19
+ function applyStep(step) {
20
+ if (step.kind === 'remove') {
21
+ try {
22
+ deleteFileIfExists(step.path);
23
+ return { outcome: 'applied' };
24
+ }
25
+ catch (error) {
26
+ return { outcome: 'failed', error: describeError(error) };
27
+ }
28
+ }
29
+ const result = writeFileWithCheck(step.path, step.content, { dryRun: false, overwrite: true });
30
+ if (result.outcome === 'failed') {
31
+ return { outcome: 'failed', ...(result.error !== undefined && { error: result.error }) };
32
+ }
33
+ return { outcome: result.outcome === 'created' ? 'created' : 'applied' };
34
+ }
35
+ function orderPlannedSteps(plan) {
36
+ const ordered = [
37
+ ...plan.writes.map((write) => ({ kind: 'write', ...write })),
38
+ ...plan.removals.map((path) => ({ kind: 'remove', path })),
39
+ ];
19
40
  if (plan.summary.length > 0) {
20
- ordered.push({ path: RELEASE_SUMMARY_FILE, content: plan.summary });
41
+ ordered.push({ kind: 'write', path: RELEASE_SUMMARY_FILE, content: plan.summary });
21
42
  }
22
43
  if (plan.tags.length > 0) {
23
- ordered.push({ path: RELEASE_TAGS_FILE, content: plan.tags.join('\n') });
44
+ ordered.push({ kind: 'write', path: RELEASE_TAGS_FILE, content: plan.tags.join('\n') });
24
45
  }
25
46
  return ordered;
26
47
  }
27
48
  function describeApplyFailure(args) {
28
- const { failedPath, error, applied, unwritten } = args;
49
+ const { failedStep, error, applied, unwritten } = args;
29
50
  const total = applied.length + unwritten.length;
30
- const lines = [`Failed to write ${failedPath}: ${error ?? 'unknown error'}`, ''];
51
+ const verb = failedStep.kind === 'remove' ? 'remove' : 'write';
52
+ const lines = [`Failed to ${verb} ${failedStep.path}: ${error ?? 'unknown error'}`, ''];
31
53
  if (applied.length === 0) {
32
54
  lines.push(`The release was not applied. None of its ${total} files were written.`);
33
55
  return lines.join('\n');
@@ -0,0 +1,47 @@
1
+ import { type TargetPrepareSettings } from './prepareTarget.js';
2
+ import type { ReleasePlan } from './releasePlan.js';
3
+ import type { ReleaseTarget, ReleaseType } from './types.js';
4
+ /** The targets of a prepare run and the settings that they share. */
5
+ export interface TargetPrepareConfig {
6
+ targets: ReleaseTarget[];
7
+ settings: TargetPrepareSettings;
8
+ }
9
+ /** Options for the release preparation workflow. */
10
+ export interface TargetPrepareOptions {
11
+ /**
12
+ * Release even when the range since the last tag doesn't contain any commits or any bump-worthy commits.
13
+ * Orthogonal to `bumpOverride`: When `bumpOverride` is not given, the release falls back to `patch`.
14
+ */
15
+ force?: boolean;
16
+ /** Level of the release, in place of the level that the changelog items call for; it doesn't trigger a release. */
17
+ bumpOverride?: ReleaseType;
18
+ /**
19
+ * If true, plan release-notes previews under each released target's `docs/` (`README.v{version}.md` and
20
+ * `RELEASE_NOTES.v{version}.md`) from its changelog entries. Requires `changelogJson.enabled`; when disabled, the
21
+ * plan records a warning and doesn't plan any previews.
22
+ */
23
+ withReleaseNotes?: boolean;
24
+ /**
25
+ * Ids of the targets from which the release starts; the run plans the named package targets and their transitive
26
+ * dependents, and each named composite, which reads its own window. Present only for a narrowed run, which skips
27
+ * every composite that it doesn't name.
28
+ */
29
+ only?: readonly string[];
30
+ /**
31
+ * Explicit versions (canonical `N.N.N`), keyed by package target id. Each named target starts the release and
32
+ * releases at its version, which takes precedence over `bumpOverride` and `force`.
33
+ */
34
+ setVersions?: ReadonlyMap<string, string>;
35
+ }
36
+ /**
37
+ * Plans a release over a list of targets, without writing any file.
38
+ *
39
+ * 1. Selects the planned package and directory targets: every one, or, under `only`, those named and their transitive
40
+ * dependents.
41
+ * 2. Reads each planned target's history and decides its direct release.
42
+ * 3. Propagates bumps to dependents, asserts the baselines that matter, and sorts the release set topologically.
43
+ * 4. Plans each release in dependency order.
44
+ * 5. Prepares each composite after every package target, unless `only` narrowed the run without naming it.
45
+ * 6. Renders the format command over the modified files.
46
+ */
47
+ export declare function releasePrepareTargets(config: TargetPrepareConfig, options: TargetPrepareOptions): ReleasePlan;