@williamthorsen/release-kit 14.2.1 → 14.2.3

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 (91) hide show
  1. package/.meta/changelog.json +77 -0
  2. package/.readyup/kits/default.js +142 -152
  3. package/.readyup/kits/npm-auto-publish.js +183 -184
  4. package/.readyup/manifest.json +83 -9
  5. package/CHANGELOG.md +24 -0
  6. package/README.md +7 -1
  7. package/dist/esm/applyChangelogOverrides.d.ts +50 -0
  8. package/dist/esm/applyChangelogOverrides.js +162 -0
  9. package/dist/esm/attachChangelogDiagnostics.d.ts +1 -1
  10. package/dist/esm/bin/release-kit.js +6 -238
  11. package/dist/esm/buildChangelogEntries.d.ts +20 -31
  12. package/dist/esm/buildChangelogEntries.js +71 -188
  13. package/dist/esm/buildChangelogItem.d.ts +56 -0
  14. package/dist/esm/buildChangelogItem.js +119 -0
  15. package/dist/esm/buildReleasedResult.d.ts +20 -0
  16. package/dist/esm/buildReleasedResult.js +78 -0
  17. package/dist/esm/changelogJsonUtils.d.ts +1 -1
  18. package/dist/esm/changelogOverrides.d.ts +29 -143
  19. package/dist/esm/changelogOverrides.js +35 -467
  20. package/dist/esm/configSchema.d.ts +225 -0
  21. package/dist/esm/configSchema.js +103 -0
  22. package/dist/esm/createGithubReleaseCommand.js +4 -3
  23. package/dist/esm/decideDirectReleases.d.ts +30 -0
  24. package/dist/esm/decideDirectReleases.js +66 -0
  25. package/dist/esm/defaults.js +1 -4
  26. package/dist/esm/defineConfig.d.ts +2 -2
  27. package/dist/esm/deriveReleaseNotesConfig.d.ts +2 -1
  28. package/dist/esm/deriveReleaseNotesConfig.js +1 -1
  29. package/dist/esm/determineBumpType.js +4 -3
  30. package/dist/esm/enumerateReleaseWindows.js +4 -4
  31. package/dist/esm/formatPrepareFiles.d.ts +11 -0
  32. package/dist/esm/formatPrepareFiles.js +55 -0
  33. package/dist/esm/formatPrepareWarnings.d.ts +26 -0
  34. package/dist/esm/formatPrepareWarnings.js +72 -0
  35. package/dist/esm/help/initHelp.d.ts +4 -0
  36. package/dist/esm/help/initHelp.js +31 -0
  37. package/dist/esm/help/overridesHelp.d.ts +4 -0
  38. package/dist/esm/help/overridesHelp.js +32 -0
  39. package/dist/esm/help/releaseStepsHelp.d.ts +12 -0
  40. package/dist/esm/help/releaseStepsHelp.js +91 -0
  41. package/dist/esm/help/syncLabelsHelp.d.ts +8 -0
  42. package/dist/esm/help/syncLabelsHelp.js +55 -0
  43. package/dist/esm/help/usage.d.ts +2 -0
  44. package/dist/esm/help/usage.js +29 -0
  45. package/dist/esm/index.d.ts +1 -1
  46. package/dist/esm/index.js +1 -1
  47. package/dist/esm/init/initCommand.js +3 -7
  48. package/dist/esm/loadChangelogOverrides.d.ts +31 -0
  49. package/dist/esm/loadChangelogOverrides.js +138 -0
  50. package/dist/esm/loadConfig.d.ts +0 -40
  51. package/dist/esm/loadConfig.js +0 -242
  52. package/dist/esm/loadValidatedConfig.d.ts +1 -1
  53. package/dist/esm/mergeConfig.d.ts +41 -0
  54. package/dist/esm/mergeConfig.js +118 -0
  55. package/dist/esm/monorepoConfigAssertions.d.ts +49 -0
  56. package/dist/esm/monorepoConfigAssertions.js +126 -0
  57. package/dist/esm/planChangelogFiles.d.ts +16 -0
  58. package/dist/esm/planChangelogFiles.js +41 -0
  59. package/dist/esm/prepareArgs.d.ts +55 -0
  60. package/dist/esm/prepareArgs.js +69 -0
  61. package/dist/esm/prepareCommand.d.ts +0 -48
  62. package/dist/esm/prepareCommand.js +11 -77
  63. package/dist/esm/prepareComposite.d.ts +16 -0
  64. package/dist/esm/prepareComposite.js +39 -0
  65. package/dist/esm/prepareRunContext.d.ts +38 -0
  66. package/dist/esm/prepareRunContext.js +39 -0
  67. package/dist/esm/prepareTarget.d.ts +2 -7
  68. package/dist/esm/prepareTarget.js +2 -118
  69. package/dist/esm/previewTagPrefixes.d.ts +1 -1
  70. package/dist/esm/releasePrepareTargets.js +13 -207
  71. package/dist/esm/renderReleaseNotes.js +6 -5
  72. package/dist/esm/reportPrepare.js +3 -123
  73. package/dist/esm/resolveCommandTags.d.ts +1 -1
  74. package/dist/esm/resolveCommandTags.js +2 -1
  75. package/dist/esm/resolveReleaseDeclaration.d.ts +2 -1
  76. package/dist/esm/resolveReleaseTargets.d.ts +2 -1
  77. package/dist/esm/resolveReleaseTargets.js +2 -1
  78. package/dist/esm/stripScope.js +4 -3
  79. package/dist/esm/sync-labels/generateCommand.d.ts +1 -1
  80. package/dist/esm/sync-labels/resolveLabels.d.ts +1 -1
  81. package/dist/esm/topologicalSort.d.ts +12 -0
  82. package/dist/esm/topologicalSort.js +60 -0
  83. package/dist/esm/types.d.ts +1 -225
  84. package/dist/esm/types.js +1 -103
  85. package/dist/esm/validateAllChangelogOverrides.d.ts +56 -0
  86. package/dist/esm/validateAllChangelogOverrides.js +138 -0
  87. package/dist/esm/validateConfig.d.ts +1 -1
  88. package/dist/esm/validateConfig.js +7 -9
  89. package/dist/esm/validateOverridesCommand.d.ts +1 -1
  90. package/dist/esm/validateOverridesCommand.js +2 -1
  91. package/package.json +4 -4
@@ -1,18 +1,19 @@
1
- import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
1
+ import { formatStaleOverrideKeyWarning } from "./applyChangelogOverrides.js";
2
2
  import { assertTaggedBaseline } from "./assertTaggedBaseline.js";
3
3
  import { buildDependencyGraph } from "./buildDependencyGraph.js";
4
4
  import { buildReleaseSummary } from "./buildReleaseSummary.js";
5
- import { createOverrideContext, formatStaleOverrideKeyWarning } from "./changelogOverrides.js";
5
+ import { createOverrideContext } from "./changelogOverrides.js";
6
+ import { decideDirectReleases, reportUnroutedEntryScopes } from "./decideDirectReleases.js";
6
7
  import { deriveDependenciesHeader, deriveSectionOrder } from "./deriveReleaseNotesConfig.js";
7
- import { detectUndeclaredTagPrefixes } from "./detectUndeclaredTagPrefixes.js";
8
8
  import { findTransitiveDependents } from "./findTransitiveDependents.js";
9
- import { findUnroutedEntryScopes } from "./findUnroutedEntryScopes.js";
10
- import { getAllTagPrefixes } from "./generateChangelogs.js";
11
9
  import { hasPrettierConfig } from "./hasPrettierConfig.js";
12
- import { resolveWorkTypes } from "./loadConfig.js";
13
- import { buildSkippedTargetResult, decideTargetRelease, isRepoRoot, planTargetRelease, readTargetHistory, } from "./prepareTarget.js";
10
+ import { resolveWorkTypes } from "./mergeConfig.js";
11
+ import { describeCompositeSkip, prepareComposite } from "./prepareComposite.js";
12
+ import { collect, describeStage, isRepoRootTarget, readHistory, runStage, } from "./prepareRunContext.js";
13
+ import { buildSkippedTargetResult, planTargetRelease } from "./prepareTarget.js";
14
14
  import { propagateBumps } from "./propagateBumps.js";
15
- import { isSinglePackageTargets, PROJECT_TARGET_ID } from "./resolveReleaseTargets.js";
15
+ import { isSinglePackageTargets } from "./resolveReleaseTargets.js";
16
+ import { topologicalSort } from "./topologicalSort.js";
16
17
  const PREVIEWS_REQUIRE_JSON_WARNING = '--with-release-notes requires changelogJson.enabled; skipping preview generation';
17
18
  export function releasePrepareTargets(config, options) {
18
19
  const { settings } = config;
@@ -107,12 +108,6 @@ export function releasePrepareTargets(config, options) {
107
108
  ...(allWarnings.length > 0 && { warnings: allWarnings }),
108
109
  };
109
110
  }
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
111
  function composeSinglePackageWarnings(collected, overrideContext, arePreviewsBlocked, previewWarnings) {
117
112
  if (collected.tags.length === 0) {
118
113
  return [];
@@ -123,75 +118,11 @@ function composeSinglePackageWarnings(collected, overrideContext, arePreviewsBlo
123
118
  ...previewWarnings,
124
119
  ];
125
120
  }
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;
121
+ function listOverrideDirectory(target) {
122
+ if (target.kind === 'directory') {
123
+ return isRepoRootTarget(target) ? [] : [target.directoryPath];
179
124
  }
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;
125
+ return target.isWorkspace ? [target.packagePath] : [];
195
126
  }
196
127
  function planFormatCommand(configuredCommand, collected) {
197
128
  const formatCommandStr = configuredCommand ?? (hasPrettierConfig() ? 'npx prettier --write' : undefined);
@@ -201,71 +132,6 @@ function planFormatCommand(configuredCommand, collected) {
201
132
  const files = collected.modifiedFiles;
202
133
  return { command: `${formatCommandStr} ${files.join(' ')}`, files };
203
134
  }
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
135
  function selectStartIds(memberTargets, options) {
270
136
  const { only, setVersions } = options;
271
137
  if (only === undefined) {
@@ -273,63 +139,3 @@ function selectStartIds(memberTargets, options) {
273
139
  }
274
140
  return new Set([...only, ...(setVersions?.keys() ?? [])]);
275
141
  }
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
- }
@@ -33,11 +33,12 @@ export function renderReleaseNotesSingle(entry, options) {
33
33
  for (const [index, item] of section.items.entries()) {
34
34
  const prefix = item.breaking === true ? getBreakingPrefix() : '';
35
35
  lines.push(`- ${prefix}${item.description}`);
36
- if (item.body !== undefined && item.body.length > 0) {
37
- lines.push('', ...indentBodyLines(item.body));
38
- if (index < section.items.length - 1) {
39
- lines.push('');
40
- }
36
+ if (item.body === undefined || item.body.length === 0) {
37
+ continue;
38
+ }
39
+ lines.push('', ...indentBodyLines(item.body));
40
+ if (index < section.items.length - 1) {
41
+ lines.push('');
41
42
  }
42
43
  }
43
44
  }
@@ -1,8 +1,9 @@
1
- import { formatGlyphLine, formatStatusLine, measureGlyphColumn, STATUS_GLYPHS, truncateToWidth, } from '@williamthorsen/nmr-core';
1
+ import { formatGlyphLine, formatStatusLine, measureGlyphColumn, STATUS_GLYPHS, } from '@williamthorsen/nmr-core';
2
2
  import { bold, dim, sectionHeader } from "./format.js";
3
+ import { formatBumpFiles, formatChangelogFiles, formatPreviewFiles } from "./formatPrepareFiles.js";
4
+ import { formatChangeRecordWarnings, formatPolicyViolations, formatUnparseableWarning, formatWarnings, } from "./formatPrepareWarnings.js";
3
5
  import { RELEASE_GLYPHS } from "./glyphs.js";
4
6
  import { PROJECT_TARGET_ID } from "./resolveReleaseTargets.js";
5
- const SUBJECT_COLUMN_BUDGET = 72;
6
7
  export function reportPrepare(plan, options) {
7
8
  const isMultiWorkspace = plan.targets.some((target) => target.kind === 'composite' || target.name !== undefined);
8
9
  if (isMultiWorkspace) {
@@ -178,114 +179,6 @@ function formatVersionLine(lines, workspace, propagatedFrom, isPropagatedOnly, s
178
179
  lines.push(` ${formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${versions} (${workspace.releaseType}${suffix})`)}`);
179
180
  }
180
181
  }
181
- function formatBumpFiles(lines, workspace, dryRun, indent = '') {
182
- for (const file of workspace.bumpedFiles) {
183
- if (dryRun) {
184
- lines.push(dim(`${indent} [dry-run] Would bump ${file}`));
185
- }
186
- else {
187
- lines.push(dim(`${indent} Bumped ${file}`));
188
- }
189
- }
190
- }
191
- function formatChangelogFiles(lines, release, dryRun, style, indent = '') {
192
- for (const file of release.changelogFiles) {
193
- if (dryRun) {
194
- lines.push(dim(`${indent} [dry-run] Would generate changelog: ${file}`));
195
- }
196
- else {
197
- lines.push(dim(`${indent} Generating changelog: ${file}`));
198
- }
199
- const preservation = release.changelogPreservation?.find((candidate) => candidate.file === file);
200
- if (preservation === undefined) {
201
- continue;
202
- }
203
- if (preservation.preservedVersions.length > 0) {
204
- lines.push(dim(`${indent} Kept from the existing changelog: ${preservation.preservedVersions.join(', ')}`));
205
- }
206
- const dropped = preservation.droppedUnversionedHeadings;
207
- if (dropped.length > 0) {
208
- const message = `Dropped ${dropped.length} section${dropped.length === 1 ? '' : 's'} without a version: ${dropped.join(', ')}`;
209
- lines.push(`${indent} ${formatStatusLine(style, 'warning', message)}`);
210
- }
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
- }
221
- }
222
- function formatPreviewFiles(lines, release, dryRun, indent = '') {
223
- const previewFiles = release.previewFiles ?? [];
224
- for (const file of previewFiles) {
225
- if (dryRun) {
226
- lines.push(dim(`${indent} [dry-run] Would write ${file}`));
227
- }
228
- else {
229
- lines.push(dim(`${indent} Wrote ${file}`));
230
- }
231
- }
232
- }
233
- function formatUnparseableWarning(lines, release, style, indent = '') {
234
- const unparseable = release.unparseableCommits;
235
- if (unparseable === undefined || unparseable.length === 0) {
236
- return;
237
- }
238
- const count = unparseable.length;
239
- lines.push(`${indent} ${formatStatusLine(style, 'warning', `${count} commit${count === 1 ? '' : 's'} could not be parsed`)}`);
240
- for (const commit of unparseable) {
241
- const shortHash = commit.hash.slice(0, 7);
242
- const truncatedSubject = truncateSubject(commit.subject);
243
- lines.push(`${indent} · ${shortHash} ${truncatedSubject}`);
244
- }
245
- }
246
- function formatPolicyViolations(lines, violations, style, indent = '') {
247
- if (violations === undefined || violations.length === 0) {
248
- return;
249
- }
250
- const count = violations.length;
251
- lines.push(`${indent} ${formatStatusLine(style, 'warning', `${count} policy violation${count === 1 ? '' : 's'}:`)}`);
252
- for (const violation of violations) {
253
- const shortHash = violation.commitHash.slice(0, 7);
254
- const subject = violation.commitSubject;
255
- const truncatedSubject = truncateSubject(subject);
256
- const location = violation.surface === 'entry' ? `entry ${violation.entryPosition ?? '?'}` : `${violation.surface} surface`;
257
- lines.push(`${indent} · ${shortHash} '${truncatedSubject}': type '${violation.type}' at ${location}`);
258
- }
259
- }
260
- function formatChangeRecordWarnings(lines, release, style, indent = '') {
261
- const { malformedBlocks = [], undeclaredEntryTypes = [], unroutedEntryScopes = [] } = release;
262
- if (malformedBlocks.length > 0) {
263
- const count = malformedBlocks.length;
264
- const header = `${count} change-record block${count === 1 ? '' : 's'} could not be read (item taken from the title)`;
265
- lines.push(`${indent} ${formatStatusLine(style, 'warning', `${header}:`)}`);
266
- for (const block of malformedBlocks) {
267
- lines.push(`${indent} · ${block.commitHash.slice(0, 7)} '${truncateSubject(block.commitSubject)}': ${block.reason}`);
268
- }
269
- }
270
- if (undeclaredEntryTypes.length > 0) {
271
- const count = undeclaredEntryTypes.length;
272
- const header = `${count} change-record entr${count === 1 ? 'y has an' : 'ies have'} undeclared type${count === 1 ? '' : 's'} (no item)`;
273
- lines.push(`${indent} ${formatStatusLine(style, 'warning', `${header}:`)}`);
274
- for (const entry of undeclaredEntryTypes) {
275
- const shortHash = entry.commitHash.slice(0, 7);
276
- lines.push(`${indent} · ${shortHash} '${truncateSubject(entry.commitSubject)}': type '${entry.type}' at entry ${entry.entryPosition}`);
277
- }
278
- }
279
- if (unroutedEntryScopes.length > 0) {
280
- const count = unroutedEntryScopes.length;
281
- const header = `${count} change-record entry scope${count === 1 ? ' does not match' : 's do not match'} any workspace in the commit's window (no item there)`;
282
- lines.push(`${indent} ${formatStatusLine(style, 'warning', `${header}:`)}`);
283
- for (const finding of unroutedEntryScopes) {
284
- const shortHash = finding.commitHash.slice(0, 7);
285
- lines.push(`${indent} · ${shortHash} '${truncateSubject(finding.commitSubject)}': scope '${finding.scope}' at entry ${finding.entryPosition}`);
286
- }
287
- }
288
- }
289
182
  function formatPropagationSuffix(propagatedFrom) {
290
183
  if (propagatedFrom === undefined || propagatedFrom.length === 0) {
291
184
  return '';
@@ -293,16 +186,6 @@ function formatPropagationSuffix(propagatedFrom) {
293
186
  const names = propagatedFrom.map((p) => p.packageName).join(', ');
294
187
  return `, dependency: ${names}`;
295
188
  }
296
- function formatWarnings(lines, result, style) {
297
- const { warnings } = result;
298
- if (warnings === undefined || warnings.length === 0) {
299
- return;
300
- }
301
- lines.push('');
302
- for (const warning of warnings) {
303
- lines.push(formatStatusLine(style, 'warning', warning));
304
- }
305
- }
306
189
  function formatFormatCommand(lines, result, options) {
307
190
  if (result.formatCommand === undefined) {
308
191
  return;
@@ -318,6 +201,3 @@ function formatFormatCommand(lines, result, options) {
318
201
  const messageIndent = ' '.repeat(2 + measureGlyphColumn(STATUS_GLYPHS[options.style]) + 1);
319
202
  lines.push(`\n ${formatStatusLine(options.style, 'warning', `Format command failed: ${result.formatCommand.command}`)}`, `${messageIndent}${options.formatError}`);
320
203
  }
321
- function truncateSubject(subject) {
322
- return truncateToWidth(subject, { width: SUBJECT_COLUMN_BUDGET });
323
- }
@@ -1,5 +1,5 @@
1
+ import type { ReleaseKitConfig } from './configSchema.js';
1
2
  import { type ResolvedTag } from './resolveReleaseTags.js';
2
- import type { ReleaseKitConfig } from './types.js';
3
3
  /**
4
4
  * Discovers workspaces, resolves release tags from HEAD, validates `--tags` names against the full resolved tag
5
5
  * names (e.g., `nmr-core-v1.3.0`), and returns the filtered tag list. Exits with an error message on any validation
@@ -3,7 +3,8 @@ import { describeError } from '@williamthorsen/toolbelt.errors';
3
3
  import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
4
4
  import { describeEmptyWorkspace, discoverWorkspaces } from "./discoverWorkspaces.js";
5
5
  import { formatExcludedSkip } from "./formatExcludedSkip.js";
6
- import { mergeMonorepoConfig, readRootPackageVersion } from "./loadConfig.js";
6
+ import { readRootPackageVersion } from "./loadConfig.js";
7
+ import { mergeMonorepoConfig } from "./mergeConfig.js";
7
8
  import { resolveReleaseTags } from "./resolveReleaseTags.js";
8
9
  export function resolveCommandTags(tags, userConfig) {
9
10
  let workspace;
@@ -1,4 +1,5 @@
1
- import type { ReleaseKitConfig, WorkspaceConfig } from './types.js';
1
+ import type { ReleaseKitConfig } from './configSchema.js';
2
+ import type { WorkspaceConfig } from './types.js';
2
3
  /** The composite that a config declares, before the defaults and the root `package.json` check of `loadConfig`. */
3
4
  export interface DeclaredComposite {
4
5
  /** Name that selects the composite; `undefined` for a `project` block. */
@@ -1,5 +1,6 @@
1
+ import type { ReleaseKitConfig } from './configSchema.js';
1
2
  import type { WorkspaceDiscovery } from './discoverWorkspaces.js';
2
- import type { MonorepoReleaseConfig, ReleaseKitConfig, ReleaseSettings, ReleaseTarget } from './types.js';
3
+ import type { MonorepoReleaseConfig, ReleaseSettings, ReleaseTarget } from './types.js';
3
4
  /** Id of the composite target that a `project` block declares. A target name is never empty, so ids cannot clash. */
4
5
  export declare const PROJECT_TARGET_ID = "";
5
6
  /** Id of the package target at the root of a single-package repo. */
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { isPublishableManifest } from "./isPublishableManifest.js";
3
- import { mergeMonorepoConfig, mergeSinglePackageConfig, readRootPackageVersion } from "./loadConfig.js";
3
+ import { readRootPackageVersion } from "./loadConfig.js";
4
+ import { mergeMonorepoConfig, mergeSinglePackageConfig } from "./mergeConfig.js";
4
5
  import { getTargetName } from "./resolveReleaseDeclaration.js";
5
6
  export const PROJECT_TARGET_ID = '';
6
7
  export const SINGLE_PACKAGE_TARGET_ID = '.';
@@ -5,10 +5,11 @@ export function stripScope(message) {
5
5
  let remainder = message;
6
6
  for (const pattern of TICKET_PREFIX_PATTERNS) {
7
7
  const match = remainder.match(pattern);
8
- if (match) {
9
- ticketPrefix += match[0];
10
- remainder = remainder.slice(match[0].length);
8
+ if (!match) {
9
+ continue;
11
10
  }
11
+ ticketPrefix += match[0];
12
+ remainder = remainder.slice(match[0].length);
12
13
  }
13
14
  const pipeMatch = remainder.match(PIPE_SCOPE_PATTERN);
14
15
  if (pipeMatch) {
@@ -1,5 +1,5 @@
1
1
  import { type OutputStyle, type StreamStyles } from '@williamthorsen/nmr-core';
2
- import type { RepoLabelsConfig } from '../types.js';
2
+ import type { RepoLabelsConfig } from '../configSchema.js';
3
3
  import type { LabelDefinition } from './types.js';
4
4
  /** Output path for the generated labels file. */
5
5
  export declare const LABELS_OUTPUT_PATH = ".github/labels.yaml";
@@ -1,4 +1,4 @@
1
- import type { RepoLabelsConfig } from '../types.js';
1
+ import type { RepoLabelsConfig } from '../configSchema.js';
2
2
  import type { LabelDefinition } from './types.js';
3
3
  /**
4
4
  * Resolves the repository's label set from a `repoLabels` config block.
@@ -0,0 +1,12 @@
1
+ import type { DependencyGraph } from './buildDependencyGraph.js';
2
+ import type { ReleaseEntry } from './propagateBumps.js';
3
+ /**
4
+ * Sorts target ids topologically so that dependencies are processed before their dependents.
5
+ *
6
+ * Uses Kahn's algorithm. Targets not in the release set are excluded. If the graph has
7
+ * cycles, the remaining nodes are appended in arbitrary order and reported via `cyclicIds`.
8
+ */
9
+ export declare function topologicalSort(releaseSet: Map<string, ReleaseEntry>, graph: DependencyGraph): {
10
+ sorted: string[];
11
+ cyclicIds: string[];
12
+ };
@@ -0,0 +1,60 @@
1
+ export function topologicalSort(releaseSet, graph) {
2
+ const releaseIds = new Set(releaseSet.keys());
3
+ if (releaseIds.size === 0) {
4
+ return { sorted: [], cyclicIds: [] };
5
+ }
6
+ const inDegree = new Map();
7
+ const forwardEdges = new Map();
8
+ for (const id of releaseIds) {
9
+ inDegree.set(id, 0);
10
+ forwardEdges.set(id, []);
11
+ }
12
+ for (const [packageName, dependents] of graph.dependentsOf) {
13
+ const dependencyId = graph.packageNameToDir.get(packageName);
14
+ if (dependencyId === undefined || !releaseIds.has(dependencyId)) {
15
+ continue;
16
+ }
17
+ for (const dependent of dependents) {
18
+ if (!releaseIds.has(dependent.id)) {
19
+ continue;
20
+ }
21
+ const edges = forwardEdges.get(dependencyId);
22
+ if (edges !== undefined) {
23
+ edges.push(dependent.id);
24
+ }
25
+ inDegree.set(dependent.id, (inDegree.get(dependent.id) ?? 0) + 1);
26
+ }
27
+ }
28
+ const queue = [];
29
+ for (const [id, degree] of inDegree) {
30
+ if (degree === 0) {
31
+ queue.push(id);
32
+ }
33
+ }
34
+ const sorted = [];
35
+ while (queue.length > 0) {
36
+ const id = queue.shift();
37
+ if (id === undefined) {
38
+ break;
39
+ }
40
+ sorted.push(id);
41
+ const dependents = forwardEdges.get(id) ?? [];
42
+ for (const dependent of dependents) {
43
+ const newDegree = (inDegree.get(dependent) ?? 1) - 1;
44
+ inDegree.set(dependent, newDegree);
45
+ if (newDegree === 0) {
46
+ queue.push(dependent);
47
+ }
48
+ }
49
+ }
50
+ const sortedSet = new Set(sorted);
51
+ const cyclicIds = [];
52
+ for (const id of releaseIds) {
53
+ if (sortedSet.has(id)) {
54
+ continue;
55
+ }
56
+ sorted.push(id);
57
+ cyclicIds.push(id);
58
+ }
59
+ return { sorted, cyclicIds };
60
+ }