@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.
- package/.meta/changelog.json +125 -0
- package/.readyup/kits/default.js +63 -6
- package/.readyup/kits/npm-auto-publish.js +47 -35
- package/.readyup/manifest.json +12 -7
- package/CHANGELOG.md +33 -0
- package/README.md +18 -8
- package/dist/esm/assertTaggedBaseline.d.ts +2 -2
- package/dist/esm/attachChangelogDiagnostics.d.ts +2 -2
- package/dist/esm/bin/release-kit.js +5 -4
- package/dist/esm/buildChangelogEntries.d.ts +3 -3
- package/dist/esm/buildChangelogEntries.js +3 -3
- package/dist/esm/buildDependencyGraph.d.ts +11 -11
- package/dist/esm/buildDependencyGraph.js +13 -13
- package/dist/esm/buildReleaseSummary.d.ts +3 -3
- package/dist/esm/buildReleaseSummary.js +4 -12
- package/dist/esm/changelogArchive.d.ts +14 -0
- package/dist/esm/changelogArchive.js +21 -0
- package/dist/esm/changelogJsonFile.d.ts +2 -2
- package/dist/esm/changelogOverrides.d.ts +9 -8
- package/dist/esm/changelogOverrides.js +3 -6
- package/dist/esm/createGithubReleaseCommand.d.ts +2 -1
- package/dist/esm/createGithubReleaseCommand.js +7 -4
- package/dist/esm/deriveDirectoryConfig.d.ts +13 -0
- package/dist/esm/deriveDirectoryConfig.js +59 -0
- package/dist/esm/deriveWorkspaceConfig.js +0 -1
- package/dist/esm/discoverWorkspaces.d.ts +3 -3
- package/dist/esm/findTransitiveDependents.js +3 -3
- package/dist/esm/findUnroutedEntryScopes.d.ts +2 -2
- package/dist/esm/formatDirectoryPublishSkip.d.ts +3 -0
- package/dist/esm/formatDirectoryPublishSkip.js +3 -0
- package/dist/esm/formatExcludedSkip.d.ts +5 -2
- package/dist/esm/formatExcludedSkip.js +2 -2
- package/dist/esm/formatProjectPublishSkip.d.ts +3 -0
- package/dist/esm/formatProjectPublishSkip.js +3 -0
- package/dist/esm/generateChangelogs.d.ts +7 -7
- package/dist/esm/generateChangelogs.js +2 -2
- package/dist/esm/help/prepareHelp.d.ts +1 -1
- package/dist/esm/help/prepareHelp.js +7 -5
- package/dist/esm/init/templates.js +1 -1
- package/dist/esm/loadConfig.d.ts +12 -12
- package/dist/esm/loadConfig.js +79 -30
- package/dist/esm/prepareCommand.d.ts +1 -1
- package/dist/esm/prepareCommand.js +47 -46
- package/dist/esm/prepareTarget.d.ts +103 -0
- package/dist/esm/prepareTarget.js +274 -0
- package/dist/esm/previewTagPrefixes.d.ts +5 -3
- package/dist/esm/previewTagPrefixes.js +58 -13
- package/dist/esm/propagateBumps.js +1 -1
- package/dist/esm/publishCommand.js +13 -1
- package/dist/esm/readChangelogSections.d.ts +6 -4
- package/dist/esm/readChangelogSections.js +18 -4
- package/dist/esm/releasePlan.d.ts +4 -2
- package/dist/esm/releasePlan.js +34 -12
- package/dist/esm/releasePrepareTargets.d.ts +47 -0
- package/dist/esm/releasePrepareTargets.js +335 -0
- package/dist/esm/renderChangelogMarkdown.d.ts +23 -0
- package/dist/esm/renderChangelogMarkdown.js +47 -11
- package/dist/esm/reportPrepare.d.ts +2 -3
- package/dist/esm/reportPrepare.js +24 -12
- package/dist/esm/resolveCommandTags.d.ts +3 -1
- package/dist/esm/resolveCommandTags.js +15 -22
- package/dist/esm/resolveReleaseDeclaration.d.ts +45 -0
- package/dist/esm/resolveReleaseDeclaration.js +153 -0
- package/dist/esm/resolveReleaseTags.d.ts +14 -3
- package/dist/esm/resolveReleaseTags.js +36 -15
- package/dist/esm/resolveReleaseTargets.d.ts +29 -0
- package/dist/esm/resolveReleaseTargets.js +131 -0
- package/dist/esm/sync-labels/generateCommand.js +2 -1
- package/dist/esm/sync-labels/resolveLabels.d.ts +4 -3
- package/dist/esm/sync-labels/resolveLabels.js +30 -1
- package/dist/esm/sync-labels/types.d.ts +2 -0
- package/dist/esm/types.d.ts +251 -167
- package/dist/esm/types.js +22 -0
- package/dist/esm/validateConfig.js +80 -21
- package/dist/esm/validateOverridesCommand.d.ts +1 -1
- package/dist/esm/validateOverridesCommand.js +17 -28
- package/docs/api.md +25 -58
- package/docs/changelogs.md +26 -6
- package/docs/configuration.md +79 -16
- package/docs/labels.md +25 -2
- package/docs/project-releases.md +42 -2
- package/docs/publishing.md +3 -3
- package/docs/releasing.md +4 -4
- package/docs/work-types.md +1 -1
- package/package.json +2 -2
- package/presets/labels/common.yaml +11 -0
- package/dist/esm/releasePrepare.d.ts +0 -36
- package/dist/esm/releasePrepare.js +0 -230
- package/dist/esm/releasePrepareMono.d.ts +0 -29
- package/dist/esm/releasePrepareMono.js +0 -503
- package/dist/esm/releasePrepareProject.d.ts +0 -47
- 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
|
|
16
|
-
const workspaces =
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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.
|
|
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(
|
|
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`
|
|
16
|
-
* `entries`: every versioned section whose version matches none of the entries.
|
|
17
|
-
* file
|
|
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(
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const
|
|
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
|
-
*
|
|
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
|
|
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[];
|
package/dist/esm/releasePlan.js
CHANGED
|
@@ -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 =
|
|
7
|
+
const ordered = orderPlannedSteps(plan);
|
|
6
8
|
const applied = [];
|
|
7
|
-
for (const [index,
|
|
8
|
-
const
|
|
9
|
-
if (
|
|
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({
|
|
13
|
+
throw new Error(describeApplyFailure({ failedStep: step, error: outcome.error, applied, unwritten }));
|
|
12
14
|
}
|
|
13
|
-
applied.push({ path:
|
|
15
|
+
applied.push({ path: step.path, created: outcome.outcome === 'created' });
|
|
14
16
|
}
|
|
15
17
|
return applied.map((write) => write.path);
|
|
16
18
|
}
|
|
17
|
-
function
|
|
18
|
-
|
|
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 {
|
|
49
|
+
const { failedStep, error, applied, unwritten } = args;
|
|
29
50
|
const total = applied.length + unwritten.length;
|
|
30
|
-
const
|
|
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;
|