@williamthorsen/release-kit 14.1.0 → 14.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.meta/changelog.json +136 -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 +39 -0
- package/README.md +2 -16
- 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 +3 -3
- 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,335 @@
|
|
|
1
|
+
import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
|
|
2
|
+
import { assertTaggedBaseline } from "./assertTaggedBaseline.js";
|
|
3
|
+
import { buildDependencyGraph } from "./buildDependencyGraph.js";
|
|
4
|
+
import { buildReleaseSummary } from "./buildReleaseSummary.js";
|
|
5
|
+
import { createOverrideContext, formatStaleOverrideKeyWarning } from "./changelogOverrides.js";
|
|
6
|
+
import { deriveDependenciesHeader, deriveSectionOrder } from "./deriveReleaseNotesConfig.js";
|
|
7
|
+
import { detectUndeclaredTagPrefixes } from "./detectUndeclaredTagPrefixes.js";
|
|
8
|
+
import { findTransitiveDependents } from "./findTransitiveDependents.js";
|
|
9
|
+
import { findUnroutedEntryScopes } from "./findUnroutedEntryScopes.js";
|
|
10
|
+
import { getAllTagPrefixes } from "./generateChangelogs.js";
|
|
11
|
+
import { hasPrettierConfig } from "./hasPrettierConfig.js";
|
|
12
|
+
import { resolveWorkTypes } from "./loadConfig.js";
|
|
13
|
+
import { buildSkippedTargetResult, decideTargetRelease, isRepoRoot, planTargetRelease, readTargetHistory, } from "./prepareTarget.js";
|
|
14
|
+
import { propagateBumps } from "./propagateBumps.js";
|
|
15
|
+
import { isSinglePackageTargets, PROJECT_TARGET_ID } from "./resolveReleaseTargets.js";
|
|
16
|
+
const PREVIEWS_REQUIRE_JSON_WARNING = '--with-release-notes requires changelogJson.enabled; skipping preview generation';
|
|
17
|
+
export function releasePrepareTargets(config, options) {
|
|
18
|
+
const { settings } = config;
|
|
19
|
+
const { only, withReleaseNotes } = options;
|
|
20
|
+
const isSinglePackage = isSinglePackageTargets(config.targets);
|
|
21
|
+
const memberTargets = config.targets.filter((target) => target.kind !== 'composite');
|
|
22
|
+
const compositeTargets = config.targets.filter((target) => target.kind === 'composite');
|
|
23
|
+
const warnings = [];
|
|
24
|
+
const graph = buildDependencyGraph(memberTargets.filter((target) => target.kind === 'package').filter((target) => target.isWorkspace));
|
|
25
|
+
const startIds = selectStartIds(memberTargets, options);
|
|
26
|
+
const plannedIds = findTransitiveDependents(graph, startIds);
|
|
27
|
+
const planned = memberTargets.filter((target) => plannedIds.has(target.id));
|
|
28
|
+
const arePreviewsBlocked = withReleaseNotes === true && !settings.changelogJson.enabled;
|
|
29
|
+
if (arePreviewsBlocked && !isSinglePackage) {
|
|
30
|
+
warnings.push(PREVIEWS_REQUIRE_JSON_WARNING);
|
|
31
|
+
}
|
|
32
|
+
const workTypes = resolveWorkTypes(settings.workTypes);
|
|
33
|
+
const context = {
|
|
34
|
+
settings,
|
|
35
|
+
overrideContext: createOverrideContext(planned.flatMap(listOverrideDirectory)),
|
|
36
|
+
sectionOrder: deriveSectionOrder(workTypes),
|
|
37
|
+
dependenciesHeader: deriveDependenciesHeader(workTypes),
|
|
38
|
+
shouldPlanPreviews: withReleaseNotes === true && settings.changelogJson.enabled,
|
|
39
|
+
today: new Date().toISOString().slice(0, 10),
|
|
40
|
+
};
|
|
41
|
+
const direct = decideDirectReleases(planned, settings, options, startIds);
|
|
42
|
+
if (!isSinglePackage) {
|
|
43
|
+
reportUnroutedEntryScopes(planned, direct, settings, memberTargets);
|
|
44
|
+
}
|
|
45
|
+
const releaseSet = propagateBumps(direct.bumps, graph);
|
|
46
|
+
assertTaggedBaseline([...direct.untaggedBaselines]
|
|
47
|
+
.filter(([id]) => direct.bumps.has(id) || !releaseSet.has(id))
|
|
48
|
+
.map(([, untagged]) => untagged));
|
|
49
|
+
const { sorted: sortedIds, cyclicIds } = topologicalSort(releaseSet, graph);
|
|
50
|
+
if (cyclicIds.length > 0) {
|
|
51
|
+
warnings.push(`Circular workspace dependencies detected among: ${cyclicIds.join(', ')}. ` +
|
|
52
|
+
'Propagation metadata may be incomplete for these workspaces.');
|
|
53
|
+
}
|
|
54
|
+
const resultsById = new Map();
|
|
55
|
+
for (const target of planned) {
|
|
56
|
+
const decision = direct.decisions.get(target.id);
|
|
57
|
+
if (decision?.outcome === 'skip' && !releaseSet.has(target.id)) {
|
|
58
|
+
resultsById.set(target.id, buildSkippedTargetResult(target, readHistory(direct.readings, target), decision.skipReason));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const collected = { tags: [], writes: [], removals: [], modifiedFiles: [] };
|
|
62
|
+
for (const id of sortedIds) {
|
|
63
|
+
const target = planned.find((candidate) => candidate.id === id);
|
|
64
|
+
const release = releaseSet.get(id);
|
|
65
|
+
if (target === undefined || release === undefined) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const decision = direct.decisions.get(id);
|
|
69
|
+
const plannedRelease = runStage(describeStage(target), () => planTargetRelease({
|
|
70
|
+
...context,
|
|
71
|
+
target,
|
|
72
|
+
release,
|
|
73
|
+
decision: decision?.outcome === 'release' ? decision : undefined,
|
|
74
|
+
history: readHistory(direct.readings, target),
|
|
75
|
+
}));
|
|
76
|
+
collect(collected, plannedRelease);
|
|
77
|
+
warnings.push(...plannedRelease.warnings);
|
|
78
|
+
resultsById.set(id, plannedRelease.result);
|
|
79
|
+
}
|
|
80
|
+
const results = planned.flatMap((target) => resultsById.get(target.id) ?? []);
|
|
81
|
+
for (const composite of compositeTargets) {
|
|
82
|
+
const skipWarning = describeCompositeSkip(composite, only);
|
|
83
|
+
if (skipWarning !== undefined) {
|
|
84
|
+
warnings.push(skipWarning);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const result = runStage(describeStage(composite), () => prepareComposite(composite, context, options, collected, warnings));
|
|
88
|
+
results.push(result);
|
|
89
|
+
}
|
|
90
|
+
const formatCommand = planFormatCommand(settings.formatCommand, collected);
|
|
91
|
+
const { overrideContext } = context;
|
|
92
|
+
for (const overrideKey of overrideContext.project.keys()) {
|
|
93
|
+
if (!overrideContext.globalMatchedRootKeys.has(overrideKey)) {
|
|
94
|
+
overrideContext.overrideWarnings.push(formatStaleOverrideKeyWarning(overrideKey));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const allWarnings = isSinglePackage
|
|
98
|
+
? composeSinglePackageWarnings(collected, overrideContext, arePreviewsBlocked, warnings)
|
|
99
|
+
: [...warnings, ...overrideContext.overrideWarnings];
|
|
100
|
+
return {
|
|
101
|
+
targets: results,
|
|
102
|
+
tags: collected.tags,
|
|
103
|
+
writes: collected.writes,
|
|
104
|
+
removals: collected.removals,
|
|
105
|
+
summary: buildReleaseSummary({ targets: results }),
|
|
106
|
+
formatCommand,
|
|
107
|
+
...(allWarnings.length > 0 && { warnings: allWarnings }),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function collect(collected, planned) {
|
|
111
|
+
collected.tags.push(planned.tag);
|
|
112
|
+
collected.writes.push(...planned.writes);
|
|
113
|
+
collected.removals.push(...planned.removals);
|
|
114
|
+
collected.modifiedFiles.push(...planned.modifiedFiles);
|
|
115
|
+
}
|
|
116
|
+
function composeSinglePackageWarnings(collected, overrideContext, arePreviewsBlocked, previewWarnings) {
|
|
117
|
+
if (collected.tags.length === 0) {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
return [
|
|
121
|
+
...overrideContext.overrideWarnings,
|
|
122
|
+
...(arePreviewsBlocked ? [PREVIEWS_REQUIRE_JSON_WARNING] : []),
|
|
123
|
+
...previewWarnings,
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
function decideDirectReleases(planned, settings, options, startIds) {
|
|
127
|
+
const { force, bumpOverride, setVersions } = options;
|
|
128
|
+
const direct = {
|
|
129
|
+
readings: new Map(),
|
|
130
|
+
decisions: new Map(),
|
|
131
|
+
bumps: new Map(),
|
|
132
|
+
untaggedBaselines: new Map(),
|
|
133
|
+
};
|
|
134
|
+
const hintState = { emitted: false };
|
|
135
|
+
const knownPrefixes = planned.flatMap(getAllTagPrefixes);
|
|
136
|
+
for (const target of planned) {
|
|
137
|
+
const { history, untagged } = runStage(describeStage(target), () => readTargetHistory(target, settings));
|
|
138
|
+
direct.readings.set(target.id, history);
|
|
139
|
+
if (untagged !== undefined) {
|
|
140
|
+
direct.untaggedBaselines.set(target.id, untagged);
|
|
141
|
+
}
|
|
142
|
+
if (history.previousTag === undefined && target.kind === 'package' && target.isWorkspace) {
|
|
143
|
+
maybeEmitBaselineHint(target, knownPrefixes, hintState);
|
|
144
|
+
}
|
|
145
|
+
const setVersion = setVersions?.get(target.id);
|
|
146
|
+
const decision = decideTargetRelease(target, history, {
|
|
147
|
+
force: force === true && startIds.has(target.id),
|
|
148
|
+
bumpOverride,
|
|
149
|
+
setVersion,
|
|
150
|
+
});
|
|
151
|
+
direct.decisions.set(target.id, decision);
|
|
152
|
+
if (decision.outcome === 'skip') {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
direct.bumps.set(target.id, decision.setVersion === undefined
|
|
156
|
+
? { releaseType: decision.releaseType }
|
|
157
|
+
: { releaseType: 'patch', newVersionOverride: decision.setVersion });
|
|
158
|
+
}
|
|
159
|
+
return direct;
|
|
160
|
+
}
|
|
161
|
+
function describeCompositeSkip(composite, only) {
|
|
162
|
+
if (only === undefined || only.includes(composite.id)) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
if (composite.id === PROJECT_TARGET_ID) {
|
|
166
|
+
return (`Project release skipped: --only narrows this run to ${only.join(', ')}. ` +
|
|
167
|
+
'Run `release-kit prepare` without --only to include the project release.');
|
|
168
|
+
}
|
|
169
|
+
return `Release '${composite.id}' skipped: --only does not name it. Add ${composite.id} to --only to include it.`;
|
|
170
|
+
}
|
|
171
|
+
function describeStage(target) {
|
|
172
|
+
switch (target.kind) {
|
|
173
|
+
case 'composite':
|
|
174
|
+
return 'project release stage';
|
|
175
|
+
case 'directory':
|
|
176
|
+
return `directory '${target.id}' release stage`;
|
|
177
|
+
case 'package':
|
|
178
|
+
return target.isWorkspace ? `workspace '${target.id}' release stage` : undefined;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function maybeEmitBaselineHint(target, knownPrefixes, state) {
|
|
182
|
+
if (state.emitted)
|
|
183
|
+
return;
|
|
184
|
+
if ((target.legacyIdentities?.length ?? 0) > 0)
|
|
185
|
+
return;
|
|
186
|
+
const candidates = detectUndeclaredTagPrefixes(knownPrefixes);
|
|
187
|
+
if (candidates.length === 0)
|
|
188
|
+
return;
|
|
189
|
+
const totalTags = candidates.reduce((sum, candidate) => sum + candidate.tagCount, 0);
|
|
190
|
+
const example = candidates[0]?.exampleTags[0] ?? `${candidates[0]?.prefix ?? ''}?`;
|
|
191
|
+
process.stderr.write(`Hint: no baseline tag found for ${target.id} under '${target.tagPrefix}', but ` +
|
|
192
|
+
`${totalTags} candidate-shaped tags exist (e.g., ${example}). ` +
|
|
193
|
+
"Run 'release-kit show-tag-prefixes' to check for undeclared legacy prefixes.\n");
|
|
194
|
+
state.emitted = true;
|
|
195
|
+
}
|
|
196
|
+
function planFormatCommand(configuredCommand, collected) {
|
|
197
|
+
const formatCommandStr = configuredCommand ?? (hasPrettierConfig() ? 'npx prettier --write' : undefined);
|
|
198
|
+
if (collected.tags.length === 0 || formatCommandStr === undefined) {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
const files = collected.modifiedFiles;
|
|
202
|
+
return { command: `${formatCommandStr} ${files.join(' ')}`, files };
|
|
203
|
+
}
|
|
204
|
+
function prepareComposite(composite, context, options, collected, warnings) {
|
|
205
|
+
const { history, untagged } = readTargetHistory(composite, context.settings);
|
|
206
|
+
assertTaggedBaseline([untagged]);
|
|
207
|
+
const decision = decideTargetRelease(composite, history, {
|
|
208
|
+
force: options.force === true,
|
|
209
|
+
bumpOverride: options.bumpOverride,
|
|
210
|
+
setVersion: undefined,
|
|
211
|
+
});
|
|
212
|
+
if (decision.outcome === 'skip') {
|
|
213
|
+
return buildSkippedTargetResult(composite, history, decision.skipReason);
|
|
214
|
+
}
|
|
215
|
+
if (decision.setVersion !== undefined) {
|
|
216
|
+
throw new Error('A composite target cannot take --set-version');
|
|
217
|
+
}
|
|
218
|
+
const planned = planTargetRelease({
|
|
219
|
+
...context,
|
|
220
|
+
target: composite,
|
|
221
|
+
release: { releaseType: decision.releaseType },
|
|
222
|
+
decision,
|
|
223
|
+
history,
|
|
224
|
+
});
|
|
225
|
+
collect(collected, planned);
|
|
226
|
+
warnings.push(...planned.warnings);
|
|
227
|
+
return planned.result;
|
|
228
|
+
}
|
|
229
|
+
function isRepoRootTarget(target) {
|
|
230
|
+
return target.kind === 'directory' && isRepoRoot(target);
|
|
231
|
+
}
|
|
232
|
+
function listOverrideDirectory(target) {
|
|
233
|
+
if (target.kind === 'directory') {
|
|
234
|
+
return isRepoRootTarget(target) ? [] : [target.directoryPath];
|
|
235
|
+
}
|
|
236
|
+
return target.isWorkspace ? [target.packagePath] : [];
|
|
237
|
+
}
|
|
238
|
+
function readHistory(readings, target) {
|
|
239
|
+
const history = readings.get(target.id);
|
|
240
|
+
if (history === undefined) {
|
|
241
|
+
throw new Error(`Target '${target.id}' was planned without a history`);
|
|
242
|
+
}
|
|
243
|
+
return history;
|
|
244
|
+
}
|
|
245
|
+
function reportUnroutedEntryScopes(planned, direct, settings, memberTargets) {
|
|
246
|
+
const releasing = planned.filter((target) => direct.decisions.get(target.id)?.outcome === 'release');
|
|
247
|
+
const windows = [...releasing, ...planned.filter((target) => !releasing.includes(target))].map((target) => ({
|
|
248
|
+
target,
|
|
249
|
+
history: readHistory(direct.readings, target),
|
|
250
|
+
}));
|
|
251
|
+
const findings = findUnroutedEntryScopes(windows.map(({ target, history }) => ({ dir: target.id, commits: history.unreleased.commits })), memberTargets.map((target) => target.id), settings.scopeAliases ?? {});
|
|
252
|
+
for (const { target, history } of windows) {
|
|
253
|
+
history.unreleased.diagnostics.unroutedEntryScopes = isRepoRootTarget(target)
|
|
254
|
+
? []
|
|
255
|
+
: (findings.get(target.id) ?? []);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function runStage(stage, fn) {
|
|
259
|
+
if (stage === undefined) {
|
|
260
|
+
return fn();
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
return fn();
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
throw chainError(stage, error);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function selectStartIds(memberTargets, options) {
|
|
270
|
+
const { only, setVersions } = options;
|
|
271
|
+
if (only === undefined) {
|
|
272
|
+
return new Set(memberTargets.map((target) => target.id));
|
|
273
|
+
}
|
|
274
|
+
return new Set([...only, ...(setVersions?.keys() ?? [])]);
|
|
275
|
+
}
|
|
276
|
+
function topologicalSort(releaseSet, graph) {
|
|
277
|
+
const releaseIds = new Set(releaseSet.keys());
|
|
278
|
+
if (releaseIds.size === 0) {
|
|
279
|
+
return { sorted: [], cyclicIds: [] };
|
|
280
|
+
}
|
|
281
|
+
const inDegree = new Map();
|
|
282
|
+
const forwardEdges = new Map();
|
|
283
|
+
for (const id of releaseIds) {
|
|
284
|
+
inDegree.set(id, 0);
|
|
285
|
+
forwardEdges.set(id, []);
|
|
286
|
+
}
|
|
287
|
+
for (const [packageName, dependents] of graph.dependentsOf) {
|
|
288
|
+
const dependencyId = graph.packageNameToDir.get(packageName);
|
|
289
|
+
if (dependencyId === undefined || !releaseIds.has(dependencyId)) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
for (const dependent of dependents) {
|
|
293
|
+
if (!releaseIds.has(dependent.id)) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const edges = forwardEdges.get(dependencyId);
|
|
297
|
+
if (edges !== undefined) {
|
|
298
|
+
edges.push(dependent.id);
|
|
299
|
+
}
|
|
300
|
+
inDegree.set(dependent.id, (inDegree.get(dependent.id) ?? 0) + 1);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const queue = [];
|
|
304
|
+
for (const [id, degree] of inDegree) {
|
|
305
|
+
if (degree === 0) {
|
|
306
|
+
queue.push(id);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const sorted = [];
|
|
310
|
+
while (queue.length > 0) {
|
|
311
|
+
const id = queue.shift();
|
|
312
|
+
if (id === undefined) {
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
sorted.push(id);
|
|
316
|
+
const dependents = forwardEdges.get(id) ?? [];
|
|
317
|
+
for (const dependent of dependents) {
|
|
318
|
+
const newDegree = (inDegree.get(dependent) ?? 1) - 1;
|
|
319
|
+
inDegree.set(dependent, newDegree);
|
|
320
|
+
if (newDegree === 0) {
|
|
321
|
+
queue.push(dependent);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const sortedSet = new Set(sorted);
|
|
326
|
+
const cyclicIds = [];
|
|
327
|
+
for (const id of releaseIds) {
|
|
328
|
+
if (sortedSet.has(id)) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
sorted.push(id);
|
|
332
|
+
cyclicIds.push(id);
|
|
333
|
+
}
|
|
334
|
+
return { sorted, cyclicIds };
|
|
335
|
+
}
|
|
@@ -10,6 +10,29 @@ export interface RenderChangelogMarkdownOptions {
|
|
|
10
10
|
*/
|
|
11
11
|
preservedSections?: readonly VersionedMarkdownSection[];
|
|
12
12
|
}
|
|
13
|
+
/** Options for `renderChangelogFiles`. */
|
|
14
|
+
export interface RenderChangelogFilesOptions extends RenderChangelogMarkdownOptions {
|
|
15
|
+
/** How many of the newest sections the main file keeps; every section when omitted. Must be at least 1. */
|
|
16
|
+
keepVersions?: number;
|
|
17
|
+
}
|
|
18
|
+
/** A rendered archive file: the sections of one major version that the main file does not keep. */
|
|
19
|
+
export interface RenderedChangelogArchive {
|
|
20
|
+
major: number;
|
|
21
|
+
content: string;
|
|
22
|
+
}
|
|
23
|
+
/** The rendered `CHANGELOG.md` and its archive files, newest major first. */
|
|
24
|
+
export interface RenderedChangelogFiles {
|
|
25
|
+
main: string;
|
|
26
|
+
archives: RenderedChangelogArchive[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Renders entries and preserved sections into `CHANGELOG.md` and its per-major archive files.
|
|
30
|
+
*
|
|
31
|
+
* The main file keeps the newest `keepVersions` sections, counting preserved sections, and links to each archive file
|
|
32
|
+
* from a paragraph below its header. Each older section goes to the archive file of its major version. Every file
|
|
33
|
+
* has the same header and footer.
|
|
34
|
+
*/
|
|
35
|
+
export declare function renderChangelogFiles(entries: ChangelogEntry[], options?: RenderChangelogFilesOptions): RenderedChangelogFiles;
|
|
13
36
|
/**
|
|
14
37
|
* Renders `ChangelogEntry[]` into a complete `CHANGELOG.md`-shaped string.
|
|
15
38
|
*
|
|
@@ -1,22 +1,43 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
import { describeArchivePath } from "./changelogArchive.js";
|
|
1
3
|
import { compareVersionsDescending } from "./compareVersions.js";
|
|
2
4
|
import { renderReleaseNotesSingle } from "./renderReleaseNotes.js";
|
|
3
5
|
const CHANGELOG_HEADER = `# Changelog
|
|
4
6
|
|
|
5
7
|
All notable changes to this project will be documented in this file.`;
|
|
6
8
|
const CHANGELOG_FOOTER = `<!-- Generated by release-kit. Do not edit this file. Use .meta/changelog-overrides.json to override entries. -->`;
|
|
9
|
+
export function renderChangelogFiles(entries, options) {
|
|
10
|
+
const sections = interleavePreservedSections(entries.map((entry) => ({ version: entry.version, text: renderReleaseNotesSingle(entry, options) })), options?.preservedSections ?? []).filter((section) => section.text.length > 0);
|
|
11
|
+
const keepVersions = options?.keepVersions;
|
|
12
|
+
if (keepVersions === undefined || sections.length <= keepVersions) {
|
|
13
|
+
return { main: renderFile(sections), archives: [] };
|
|
14
|
+
}
|
|
15
|
+
const archived = groupByMajor(sections.slice(keepVersions));
|
|
16
|
+
return {
|
|
17
|
+
main: renderFile(sections.slice(0, keepVersions), describeArchivePointer(archived.map(({ major }) => major))),
|
|
18
|
+
archives: archived.map(({ major, sections: majorSections }) => ({ major, content: renderFile(majorSections) })),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
7
21
|
export function renderChangelogMarkdown(entries, options) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
return renderChangelogFiles(entries, options).main;
|
|
23
|
+
}
|
|
24
|
+
function describeArchivePointer(majors) {
|
|
25
|
+
const links = majors.map((major) => `[v${major}](${describeArchivePath(major)})`);
|
|
26
|
+
return `Earlier versions: ${links.join(', ')}.`;
|
|
27
|
+
}
|
|
28
|
+
function groupByMajor(sections) {
|
|
29
|
+
const groups = new Map();
|
|
30
|
+
for (const section of sections) {
|
|
31
|
+
const major = semver.major(section.version);
|
|
32
|
+
const group = groups.get(major);
|
|
33
|
+
if (group === undefined) {
|
|
34
|
+
groups.set(major, [section]);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
group.push(section);
|
|
38
|
+
}
|
|
17
39
|
}
|
|
18
|
-
|
|
19
|
-
return parts.join('\n\n') + '\n';
|
|
40
|
+
return Array.from(groups, ([major, majorSections]) => ({ major, sections: majorSections })).toSorted((a, b) => b.major - a.major);
|
|
20
41
|
}
|
|
21
42
|
function interleavePreservedSections(rendered, preserved) {
|
|
22
43
|
const sorted = preserved.toSorted((a, b) => compareVersionsDescending(a.version, b.version));
|
|
@@ -35,6 +56,21 @@ function interleavePreservedSections(rendered, preserved) {
|
|
|
35
56
|
result.push(...pending.slice(index));
|
|
36
57
|
return result;
|
|
37
58
|
}
|
|
59
|
+
function renderFile(sections, paragraph) {
|
|
60
|
+
const body = sections
|
|
61
|
+
.map((section) => section.text)
|
|
62
|
+
.join('\n')
|
|
63
|
+
.trimEnd();
|
|
64
|
+
const parts = [CHANGELOG_HEADER];
|
|
65
|
+
if (paragraph !== undefined) {
|
|
66
|
+
parts.push(paragraph);
|
|
67
|
+
}
|
|
68
|
+
if (body.length > 0) {
|
|
69
|
+
parts.push(body);
|
|
70
|
+
}
|
|
71
|
+
parts.push(CHANGELOG_FOOTER);
|
|
72
|
+
return parts.join('\n\n') + '\n';
|
|
73
|
+
}
|
|
38
74
|
function withTrailingNewline(section) {
|
|
39
75
|
return { version: section.version, text: `${section.text}\n` };
|
|
40
76
|
}
|
|
@@ -11,9 +11,8 @@ export interface ReportPrepareOptions {
|
|
|
11
11
|
/**
|
|
12
12
|
* Formats a release plan into styled terminal output, without writing it.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* at the end.
|
|
14
|
+
* A plan whose package results lack a `name` and that lacks a composite result renders flat output, as a
|
|
15
|
+
* single-package release does; any other plan renders a section per target and a tag summary at the end.
|
|
17
16
|
*
|
|
18
17
|
* Whether the plan was applied is a property of the render, not of the plan: The same plan
|
|
19
18
|
* produces the dry-run report and the applied report.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { formatGlyphLine, formatStatusLine, measureGlyphColumn, STATUS_GLYPHS, truncateToWidth, } from '@williamthorsen/nmr-core';
|
|
2
2
|
import { bold, dim, sectionHeader } from "./format.js";
|
|
3
3
|
import { RELEASE_GLYPHS } from "./glyphs.js";
|
|
4
|
+
import { PROJECT_TARGET_ID } from "./resolveReleaseTargets.js";
|
|
4
5
|
const SUBJECT_COLUMN_BUDGET = 72;
|
|
5
6
|
export function reportPrepare(plan, options) {
|
|
6
|
-
const isMultiWorkspace = plan.
|
|
7
|
+
const isMultiWorkspace = plan.targets.some((target) => target.kind === 'composite' || target.name !== undefined);
|
|
7
8
|
if (isMultiWorkspace) {
|
|
8
9
|
return formatMultiWorkspace(plan, options);
|
|
9
10
|
}
|
|
@@ -13,8 +14,8 @@ function formatSingleWorkspace(result, options) {
|
|
|
13
14
|
const dryRun = !options.applied;
|
|
14
15
|
const { style } = options;
|
|
15
16
|
const lines = [];
|
|
16
|
-
const workspace = result.
|
|
17
|
-
if (workspace === undefined) {
|
|
17
|
+
const workspace = result.targets[0];
|
|
18
|
+
if (workspace === undefined || workspace.kind === 'composite') {
|
|
18
19
|
return '';
|
|
19
20
|
}
|
|
20
21
|
const since = workspace.previousTag === undefined ? 'the beginning' : workspace.previousTag;
|
|
@@ -62,11 +63,13 @@ function formatMultiWorkspace(result, options) {
|
|
|
62
63
|
const dryRun = !options.applied;
|
|
63
64
|
const { style } = options;
|
|
64
65
|
const lines = [];
|
|
65
|
-
for (const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
for (const target of result.targets) {
|
|
67
|
+
if (target.kind === 'composite') {
|
|
68
|
+
formatProjectSection(lines, target, dryRun, style);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
formatWorkspaceSection(lines, target, dryRun, style);
|
|
72
|
+
}
|
|
70
73
|
}
|
|
71
74
|
formatFormatCommand(lines, result, options);
|
|
72
75
|
formatWarnings(lines, result, style);
|
|
@@ -82,7 +85,7 @@ function formatMultiWorkspace(result, options) {
|
|
|
82
85
|
return lines.join('\n');
|
|
83
86
|
}
|
|
84
87
|
function formatProjectSection(lines, project, dryRun, style) {
|
|
85
|
-
lines.push(`\n${sectionHeader('project')}`);
|
|
88
|
+
lines.push(`\n${sectionHeader(project.targetId === PROJECT_TARGET_ID ? 'project' : project.targetId)}`);
|
|
86
89
|
const since = project.previousTag === undefined ? '(no previous release found)' : `since ${project.previousTag}`;
|
|
87
90
|
lines.push(dim(` Found ${project.commitCount} commits ${since}`));
|
|
88
91
|
formatPolicyViolations(lines, project.policyViolations, style, ' ');
|
|
@@ -128,8 +131,8 @@ function formatWorkspaceSection(lines, workspace, dryRun, style) {
|
|
|
128
131
|
lines.push(` ${formatStatusLine(style, 'skipped', workspace.skipReason)}`);
|
|
129
132
|
return;
|
|
130
133
|
}
|
|
131
|
-
const
|
|
132
|
-
const isPropagatedOnly = workspace.propagatedOnly === true;
|
|
134
|
+
const propagatedFrom = workspace.kind === 'package' ? workspace.propagatedFrom : undefined;
|
|
135
|
+
const isPropagatedOnly = workspace.kind === 'package' && workspace.propagatedOnly === true;
|
|
133
136
|
formatCommitSummary(lines, workspace, propagatedFrom, isPropagatedOnly);
|
|
134
137
|
formatUnparseableWarning(lines, workspace, style, ' ');
|
|
135
138
|
formatPolicyViolations(lines, workspace.policyViolations, style, ' ');
|
|
@@ -198,7 +201,7 @@ function formatChangelogFiles(lines, release, dryRun, style, indent = '') {
|
|
|
198
201
|
continue;
|
|
199
202
|
}
|
|
200
203
|
if (preservation.preservedVersions.length > 0) {
|
|
201
|
-
lines.push(dim(`${indent} Kept from the existing
|
|
204
|
+
lines.push(dim(`${indent} Kept from the existing changelog: ${preservation.preservedVersions.join(', ')}`));
|
|
202
205
|
}
|
|
203
206
|
const dropped = preservation.droppedUnversionedHeadings;
|
|
204
207
|
if (dropped.length > 0) {
|
|
@@ -206,6 +209,15 @@ function formatChangelogFiles(lines, release, dryRun, style, indent = '') {
|
|
|
206
209
|
lines.push(`${indent} ${formatStatusLine(style, 'warning', message)}`);
|
|
207
210
|
}
|
|
208
211
|
}
|
|
212
|
+
const removedFiles = release.removedChangelogFiles ?? [];
|
|
213
|
+
for (const file of removedFiles) {
|
|
214
|
+
if (dryRun) {
|
|
215
|
+
lines.push(dim(`${indent} [dry-run] Would remove changelog: ${file}`));
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
lines.push(dim(`${indent} Removing changelog: ${file}`));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
209
221
|
}
|
|
210
222
|
function formatPreviewFiles(lines, release, dryRun, indent = '') {
|
|
211
223
|
const previewFiles = release.previewFiles ?? [];
|
|
@@ -7,7 +7,9 @@ import type { ReleaseKitConfig } from './types.js';
|
|
|
7
7
|
* `mergeMonorepoConfig` rejects, and a workspace that does not resolve to any package, which is not single-package
|
|
8
8
|
* mode.
|
|
9
9
|
*
|
|
10
|
-
* In monorepo mode the workspace list is `userConfig` merged over the discovered workspaces, as `prepare` builds it
|
|
10
|
+
* In monorepo mode the workspace list is `userConfig` merged over the discovered workspaces, as `prepare` builds it,
|
|
11
|
+
* and each directory target and a declared composite (a `project` block or a `releases` composite) contribute their
|
|
12
|
+
* `tagPrefix`, so that their tags resolve alongside them.
|
|
11
13
|
* A tag on HEAD whose workspace the config excludes still counts as a tag on HEAD, so `--tags` may name it and a
|
|
12
14
|
* HEAD whose tags are all excluded returns an empty list rather than failing. Each excluded tag that the run
|
|
13
15
|
* selects, every one without `--tags` or the named ones with it, is dropped with a warning.
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { basename } from 'node:path';
|
|
2
1
|
import { reportError } from '@williamthorsen/nmr-core';
|
|
3
2
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
4
3
|
import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
|
|
@@ -20,14 +19,21 @@ export function resolveCommandTags(tags, userConfig) {
|
|
|
20
19
|
process.exit(1);
|
|
21
20
|
}
|
|
22
21
|
let workspaces;
|
|
22
|
+
let directories = [];
|
|
23
|
+
let project;
|
|
23
24
|
let singleWorkspace;
|
|
24
|
-
|
|
25
|
+
let excludedPaths = new Set();
|
|
25
26
|
try {
|
|
26
27
|
if (workspace.kind === 'single-package') {
|
|
27
28
|
singleWorkspace = deriveWorkspaceConfig('.');
|
|
28
29
|
}
|
|
29
30
|
else {
|
|
30
|
-
|
|
31
|
+
const merged = mergeMonorepoConfig(workspace.packageDirs, userConfig, readRootPackageVersion());
|
|
32
|
+
const excluded = merged.excludedWorkspaces ?? [];
|
|
33
|
+
workspaces = [...merged.workspaces, ...excluded];
|
|
34
|
+
directories = merged.directories ?? [];
|
|
35
|
+
project = merged.project;
|
|
36
|
+
excludedPaths = new Set(excluded.map((excludedWorkspace) => excludedWorkspace.workspacePath));
|
|
31
37
|
}
|
|
32
38
|
}
|
|
33
39
|
catch (error) {
|
|
@@ -36,7 +42,7 @@ export function resolveCommandTags(tags, userConfig) {
|
|
|
36
42
|
}
|
|
37
43
|
let resolvedTags;
|
|
38
44
|
if (workspaces !== undefined) {
|
|
39
|
-
resolvedTags = resolveReleaseTags({ workspaces });
|
|
45
|
+
resolvedTags = resolveReleaseTags({ workspaces, directories, ...(project !== undefined && { project }) });
|
|
40
46
|
}
|
|
41
47
|
else if (singleWorkspace !== undefined) {
|
|
42
48
|
resolvedTags = resolveReleaseTags({ singleWorkspace });
|
|
@@ -59,27 +65,14 @@ export function resolveCommandTags(tags, userConfig) {
|
|
|
59
65
|
}
|
|
60
66
|
resolvedTags = resolvedTags.filter((t) => tags.includes(t.tag));
|
|
61
67
|
}
|
|
62
|
-
return dropExcludedTags(resolvedTags,
|
|
68
|
+
return dropExcludedTags(resolvedTags, excludedPaths, userConfig?.releases === undefined ? 'legacy' : 'releases');
|
|
63
69
|
}
|
|
64
|
-
function
|
|
65
|
-
const { workspaces } = mergeMonorepoConfig(packageDirs, userConfig, readRootPackageVersion());
|
|
66
|
-
if (excludedDirs.size === 0) {
|
|
67
|
-
return workspaces;
|
|
68
|
-
}
|
|
69
|
-
const excluded = packageDirs
|
|
70
|
-
.filter((packageDir) => excludedDirs.has(basename(packageDir)))
|
|
71
|
-
.map((packageDir) => deriveWorkspaceConfig(packageDir));
|
|
72
|
-
return [...workspaces, ...excluded];
|
|
73
|
-
}
|
|
74
|
-
function collectExcludedDirs(userConfig) {
|
|
75
|
-
const overrides = userConfig?.workspaces ?? [];
|
|
76
|
-
return new Set(overrides.filter((override) => override.shouldExclude === true).map((override) => override.dir));
|
|
77
|
-
}
|
|
78
|
-
function dropExcludedTags(resolvedTags, excludedDirs) {
|
|
70
|
+
function dropExcludedTags(resolvedTags, excludedPaths, surface) {
|
|
79
71
|
const retained = [];
|
|
80
72
|
for (const resolvedTag of resolvedTags) {
|
|
81
|
-
if (
|
|
82
|
-
|
|
73
|
+
if (resolvedTag.kind === 'package' && excludedPaths.has(resolvedTag.workspacePath)) {
|
|
74
|
+
const setting = surface === 'releases' ? `releases.${resolvedTag.dir}: false` : undefined;
|
|
75
|
+
console.warn(formatExcludedSkip(resolvedTag, setting));
|
|
83
76
|
}
|
|
84
77
|
else {
|
|
85
78
|
retained.push(resolvedTag);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ReleaseKitConfig, WorkspaceConfig } from './types.js';
|
|
2
|
+
/** The composite that a config declares, before the defaults and the root `package.json` check of `loadConfig`. */
|
|
3
|
+
export interface DeclaredComposite {
|
|
4
|
+
/** Name that selects the composite; `undefined` for a `project` block. */
|
|
5
|
+
name?: string;
|
|
6
|
+
/** Target names of the members; `undefined` means every retained workspace. */
|
|
7
|
+
members?: string[];
|
|
8
|
+
/** Commit window; `undefined` means the union of the members' windows. */
|
|
9
|
+
paths?: string[];
|
|
10
|
+
tagPrefix?: string;
|
|
11
|
+
}
|
|
12
|
+
/** A directory that a `releases` entry declares as a target, before `loadConfig` checks it on disk. */
|
|
13
|
+
export interface DeclaredDirectory {
|
|
14
|
+
/** Name of the target, its entry key. */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Repo-relative POSIX path of the directory; `.` for the repo root. */
|
|
17
|
+
path: string;
|
|
18
|
+
/** Commit window; `undefined` for the repo root, whose window is the whole repo. */
|
|
19
|
+
paths?: string[];
|
|
20
|
+
tagPrefix?: string;
|
|
21
|
+
legacyIdentities?: WorkspaceConfig['legacyIdentities'];
|
|
22
|
+
}
|
|
23
|
+
/** The release targets that a config declares over the discovered workspaces. */
|
|
24
|
+
export interface ReleaseDeclaration {
|
|
25
|
+
/** Retained workspaces in discovery order, each with its target name and legacy identities applied. */
|
|
26
|
+
retained: WorkspaceConfig[];
|
|
27
|
+
/** Discovered workspaces that the config excludes, in discovery order. */
|
|
28
|
+
excluded: WorkspaceConfig[];
|
|
29
|
+
/** Directories that `releases` entries declare as targets, in config order. */
|
|
30
|
+
directories: DeclaredDirectory[];
|
|
31
|
+
composite?: DeclaredComposite;
|
|
32
|
+
}
|
|
33
|
+
/** Returns the name of a workspace's release target. */
|
|
34
|
+
export declare function getTargetName(workspace: WorkspaceConfig): string;
|
|
35
|
+
/** Normalizes a configured path to the repo-relative POSIX form of a discovered `workspacePath`. */
|
|
36
|
+
export declare function normalizeWorkspacePath(configuredPath: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Projects the config's `releases` block, or its `workspaces` and `project` blocks, onto the discovered workspaces.
|
|
39
|
+
*
|
|
40
|
+
* Every discovered workspace is retained under its `dir` unless an entry excludes or renames it, and a `releases`
|
|
41
|
+
* entry whose path isn't a discovered workspace declares a directory. Throws when a `releases` entry names a path
|
|
42
|
+
* outside the repo, sets `tagPrefix` on a workspace, excludes a name that isn't a workspace's `dir`, repeats a path,
|
|
43
|
+
* declares a second composite, or includes a target that is neither a retained workspace nor a directory.
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveReleaseDeclaration(workspaces: readonly WorkspaceConfig[], userConfig: ReleaseKitConfig | undefined): ReleaseDeclaration;
|