@williamthorsen/release-kit 5.0.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +137 -43
  3. package/dist/esm/.cache +1 -1
  4. package/dist/esm/assertCleanWorkingTree.js +1 -1
  5. package/dist/esm/bin/release-kit.js +1 -1
  6. package/dist/esm/buildChangelogEntries.d.ts +3 -0
  7. package/dist/esm/{generateChangelogJson.js → buildChangelogEntries.js} +7 -80
  8. package/dist/esm/buildDependencyGraph.d.ts +1 -0
  9. package/dist/esm/buildDependencyGraph.js +8 -1
  10. package/dist/esm/buildReleaseSummary.js +9 -1
  11. package/dist/esm/buildSyntheticChangelogEntry.d.ts +5 -0
  12. package/dist/esm/buildSyntheticChangelogEntry.js +13 -0
  13. package/dist/esm/changelogJsonFile.d.ts +4 -0
  14. package/dist/esm/changelogJsonFile.js +68 -0
  15. package/dist/esm/decideRelease.d.ts +25 -0
  16. package/dist/esm/decideRelease.js +28 -0
  17. package/dist/esm/defaults.d.ts +1 -0
  18. package/dist/esm/defaults.js +2 -0
  19. package/dist/esm/index.d.ts +2 -43
  20. package/dist/esm/index.js +0 -82
  21. package/dist/esm/init/templates.js +2 -2
  22. package/dist/esm/loadConfig.d.ts +10 -1
  23. package/dist/esm/loadConfig.js +96 -2
  24. package/dist/esm/prepareCommand.js +51 -9
  25. package/dist/esm/publish.d.ts +0 -1
  26. package/dist/esm/publish.js +3 -3
  27. package/dist/esm/publishCommand.js +10 -1
  28. package/dist/esm/releasePrepare.js +83 -39
  29. package/dist/esm/releasePrepareMono.js +133 -87
  30. package/dist/esm/releasePrepareProject.d.ts +9 -0
  31. package/dist/esm/releasePrepareProject.js +109 -0
  32. package/dist/esm/reportPrepare.js +70 -24
  33. package/dist/esm/types.d.ts +57 -14
  34. package/dist/esm/validateConfig.js +26 -0
  35. package/dist/esm/validateOnlyExcludesStrandedDependents.d.ts +14 -0
  36. package/dist/esm/validateOnlyExcludesStrandedDependents.js +109 -0
  37. package/dist/esm/version.d.ts +1 -1
  38. package/dist/esm/version.js +1 -1
  39. package/package.json +4 -1
  40. package/presets/labels/common.yaml +9 -6
  41. package/schemas/label-map.json +24 -0
  42. package/dist/esm/generateChangelogJson.d.ts +0 -7
@@ -0,0 +1,25 @@
1
+ import type { Commit, ReleaseType, VersionPatterns, WorkTypeConfig } from './types.ts';
2
+ export interface DecideReleaseArgs {
3
+ commits: readonly Commit[];
4
+ force?: boolean | undefined;
5
+ bumpOverride: ReleaseType | undefined;
6
+ workTypes: Record<string, WorkTypeConfig>;
7
+ versionPatterns: VersionPatterns;
8
+ scopeAliases: Record<string, string> | undefined;
9
+ skipReasons: {
10
+ noCommits: string;
11
+ noBumpWorthy: string;
12
+ };
13
+ }
14
+ export type DecideReleaseResult = {
15
+ outcome: 'release';
16
+ releaseType: ReleaseType;
17
+ parsedCommitCount: number;
18
+ unparseableCommits: Commit[] | undefined;
19
+ } | {
20
+ outcome: 'skip';
21
+ skipReason: string;
22
+ parsedCommitCount: number;
23
+ unparseableCommits: Commit[] | undefined;
24
+ };
25
+ export declare function decideRelease(args: DecideReleaseArgs): DecideReleaseResult;
@@ -0,0 +1,28 @@
1
+ import { determineBumpType } from "./determineBumpType.js";
2
+ import { parseCommitMessage } from "./parseCommitMessage.js";
3
+ function decideRelease(args) {
4
+ const { commits, force = false, bumpOverride, workTypes, versionPatterns, scopeAliases, skipReasons } = args;
5
+ const parsedCommits = [];
6
+ const unparseable = [];
7
+ for (const commit of commits) {
8
+ const parsed = parseCommitMessage(commit.message, commit.hash, workTypes, scopeAliases);
9
+ if (parsed === void 0) {
10
+ unparseable.push(commit);
11
+ } else {
12
+ parsedCommits.push(parsed);
13
+ }
14
+ }
15
+ const parsedCommitCount = parsedCommits.length;
16
+ const unparseableCommits = unparseable.length > 0 ? unparseable : void 0;
17
+ const naturalBump = determineBumpType(parsedCommits, workTypes, versionPatterns);
18
+ const shouldRelease = naturalBump !== void 0 || force;
19
+ if (!shouldRelease) {
20
+ const skipReason = commits.length === 0 ? skipReasons.noCommits : skipReasons.noBumpWorthy;
21
+ return { outcome: "skip", skipReason, parsedCommitCount, unparseableCommits };
22
+ }
23
+ const releaseType = bumpOverride ?? naturalBump ?? "patch";
24
+ return { outcome: "release", releaseType, parsedCommitCount, unparseableCommits };
25
+ }
26
+ export {
27
+ decideRelease
28
+ };
@@ -3,3 +3,4 @@ export declare const DEFAULT_WORK_TYPES: Record<string, WorkTypeConfig>;
3
3
  export declare const DEFAULT_VERSION_PATTERNS: VersionPatterns;
4
4
  export declare const DEFAULT_CHANGELOG_JSON_CONFIG: ChangelogJsonConfig;
5
5
  export declare const DEFAULT_RELEASE_NOTES_CONFIG: ReleaseNotesConfig;
6
+ export declare const DEFAULT_PROJECT_TAG_PREFIX = "v";
@@ -28,8 +28,10 @@ const DEFAULT_CHANGELOG_JSON_CONFIG = {
28
28
  const DEFAULT_RELEASE_NOTES_CONFIG = {
29
29
  shouldInjectIntoReadme: false
30
30
  };
31
+ const DEFAULT_PROJECT_TAG_PREFIX = "v";
31
32
  export {
32
33
  DEFAULT_CHANGELOG_JSON_CONFIG,
34
+ DEFAULT_PROJECT_TAG_PREFIX,
33
35
  DEFAULT_RELEASE_NOTES_CONFIG,
34
36
  DEFAULT_VERSION_PATTERNS,
35
37
  DEFAULT_WORK_TYPES
@@ -1,43 +1,2 @@
1
- export type { CreateTagsOptions } from './createTags.ts';
2
- export type { PackageManager } from './detectPackageManager.ts';
3
- export type { GenerateChangelogOptions } from './generateChangelogs.ts';
4
- export type { RetiredPackagePreviewEntry } from './previewTagPrefixes.ts';
5
- export type { PublishOptions } from './publish.ts';
6
- export type { ReleasePrepareOptions } from './releasePrepare.ts';
7
- export type { ResolvedTag } from './resolveReleaseTags.ts';
8
- export type { LabelDefinition, SyncLabelsConfig } from './sync-labels/types.ts';
9
- export type { BumpResult, ChangelogAudience, ChangelogEntry, ChangelogItem, ChangelogJsonConfig, ChangelogSection, Commit, LegacyIdentity, MonorepoReleaseConfig, ParsedCommit, PrepareResult, ReleaseConfig, ReleaseKitConfig, ReleaseNotesConfig, ReleaseType, RetiredPackage, VersionPatterns, WorkspaceConfig, WorkspaceOverride, WorkspacePrepareResult, WorkTypeConfig, } from './types.ts';
10
- export { DEFAULT_CHANGELOG_JSON_CONFIG, DEFAULT_RELEASE_NOTES_CONFIG, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES, } from './defaults.ts';
11
- export { buildReleaseSummary } from './buildReleaseSummary.ts';
12
- export { bumpAllVersions } from './bumpAllVersions.ts';
13
- export { bumpVersion } from './bumpVersion.ts';
14
- export { commitCommand } from './commitCommand.ts';
15
- export type { CreateGithubReleaseOptions } from './createGithubRelease.ts';
16
- export { createGithubRelease, createGithubReleases } from './createGithubRelease.ts';
17
- export { createTags } from './createTags.ts';
18
- export { deleteFileIfExists } from './deleteFileIfExists.ts';
19
- export { deriveWorkspaceConfig } from './deriveWorkspaceConfig.ts';
20
- export { detectPackageManager } from './detectPackageManager.ts';
21
- export { determineBumpType } from './determineBumpType.ts';
22
- export { discoverWorkspaces } from './discoverWorkspaces.ts';
23
- export { generateChangelogJson, generateSyntheticChangelogJson } from './generateChangelogJson.ts';
24
- export { generateChangelog, generateChangelogs } from './generateChangelogs.ts';
25
- export { getCommitsSinceTarget } from './getCommitsSinceTarget.ts';
26
- export type { RenderedInjectedReadme } from './injectReleaseNotesIntoReadme.ts';
27
- export { injectReleaseNotesIntoReadme, renderInjectedReadme, resolveReadmePath, } from './injectReleaseNotesIntoReadme.ts';
28
- export { injectSection } from './injectSection.ts';
29
- export { COMMIT_PREPROCESSOR_PATTERNS, parseCommitMessage } from './parseCommitMessage.ts';
30
- export { RELEASE_SUMMARY_FILE, RELEASE_TAGS_FILE, writeReleaseTags } from './prepareCommand.ts';
31
- export { publishPackage } from './publish.ts';
32
- export type { PushReleaseOptions } from './pushRelease.ts';
33
- export { pushRelease } from './pushRelease.ts';
34
- export { releasePrepare } from './releasePrepare.ts';
35
- export { releasePrepareMono } from './releasePrepareMono.ts';
36
- export type { RenderOptions } from './renderReleaseNotes.ts';
37
- export { matchesAudience, renderReleaseNotesMulti, renderReleaseNotesSingle } from './renderReleaseNotes.ts';
38
- export { reportPrepare } from './reportPrepare.ts';
39
- export { resolveCommandTags } from './resolveCommandTags.ts';
40
- export { resolveReleaseTags } from './resolveReleaseTags.ts';
41
- export { stripScope } from './stripScope.ts';
42
- export type { PreviewFileResult, WriteReleaseNotesPreviewsOptions, WriteReleaseNotesPreviewsResult, } from './writeReleaseNotesPreviews.ts';
43
- export { writeReleaseNotesPreviews } from './writeReleaseNotesPreviews.ts';
1
+ export type { SyncLabelsConfig } from './sync-labels/types.ts';
2
+ export type { ReleaseKitConfig } from './types.ts';
package/dist/esm/index.js CHANGED
@@ -1,82 +0,0 @@
1
- import {
2
- DEFAULT_CHANGELOG_JSON_CONFIG,
3
- DEFAULT_RELEASE_NOTES_CONFIG,
4
- DEFAULT_VERSION_PATTERNS,
5
- DEFAULT_WORK_TYPES
6
- } from "./defaults.js";
7
- import { buildReleaseSummary } from "./buildReleaseSummary.js";
8
- import { bumpAllVersions } from "./bumpAllVersions.js";
9
- import { bumpVersion } from "./bumpVersion.js";
10
- import { commitCommand } from "./commitCommand.js";
11
- import { createGithubRelease, createGithubReleases } from "./createGithubRelease.js";
12
- import { createTags } from "./createTags.js";
13
- import { deleteFileIfExists } from "./deleteFileIfExists.js";
14
- import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
15
- import { detectPackageManager } from "./detectPackageManager.js";
16
- import { determineBumpType } from "./determineBumpType.js";
17
- import { discoverWorkspaces } from "./discoverWorkspaces.js";
18
- import { generateChangelogJson, generateSyntheticChangelogJson } from "./generateChangelogJson.js";
19
- import { generateChangelog, generateChangelogs } from "./generateChangelogs.js";
20
- import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
21
- import {
22
- injectReleaseNotesIntoReadme,
23
- renderInjectedReadme,
24
- resolveReadmePath
25
- } from "./injectReleaseNotesIntoReadme.js";
26
- import { injectSection } from "./injectSection.js";
27
- import { COMMIT_PREPROCESSOR_PATTERNS, parseCommitMessage } from "./parseCommitMessage.js";
28
- import { RELEASE_SUMMARY_FILE, RELEASE_TAGS_FILE, writeReleaseTags } from "./prepareCommand.js";
29
- import { publishPackage } from "./publish.js";
30
- import { pushRelease } from "./pushRelease.js";
31
- import { releasePrepare } from "./releasePrepare.js";
32
- import { releasePrepareMono } from "./releasePrepareMono.js";
33
- import { matchesAudience, renderReleaseNotesMulti, renderReleaseNotesSingle } from "./renderReleaseNotes.js";
34
- import { reportPrepare } from "./reportPrepare.js";
35
- import { resolveCommandTags } from "./resolveCommandTags.js";
36
- import { resolveReleaseTags } from "./resolveReleaseTags.js";
37
- import { stripScope } from "./stripScope.js";
38
- import { writeReleaseNotesPreviews } from "./writeReleaseNotesPreviews.js";
39
- export {
40
- COMMIT_PREPROCESSOR_PATTERNS,
41
- DEFAULT_CHANGELOG_JSON_CONFIG,
42
- DEFAULT_RELEASE_NOTES_CONFIG,
43
- DEFAULT_VERSION_PATTERNS,
44
- DEFAULT_WORK_TYPES,
45
- RELEASE_SUMMARY_FILE,
46
- RELEASE_TAGS_FILE,
47
- buildReleaseSummary,
48
- bumpAllVersions,
49
- bumpVersion,
50
- commitCommand,
51
- createGithubRelease,
52
- createGithubReleases,
53
- createTags,
54
- deleteFileIfExists,
55
- deriveWorkspaceConfig,
56
- detectPackageManager,
57
- determineBumpType,
58
- discoverWorkspaces,
59
- generateChangelog,
60
- generateChangelogJson,
61
- generateChangelogs,
62
- generateSyntheticChangelogJson,
63
- getCommitsSinceTarget,
64
- injectReleaseNotesIntoReadme,
65
- injectSection,
66
- matchesAudience,
67
- parseCommitMessage,
68
- publishPackage,
69
- pushRelease,
70
- releasePrepare,
71
- releasePrepareMono,
72
- renderInjectedReadme,
73
- renderReleaseNotesMulti,
74
- renderReleaseNotesSingle,
75
- reportPrepare,
76
- resolveCommandTags,
77
- resolveReadmePath,
78
- resolveReleaseTags,
79
- stripScope,
80
- writeReleaseNotesPreviews,
81
- writeReleaseTags
82
- };
@@ -107,7 +107,7 @@ on:
107
107
  - minor
108
108
  - major
109
109
  force:
110
- description: 'Force a release even when there are no commits since the last tag (requires --bump)'
110
+ description: 'Force a release even when no commits or no bump-worthy commits exist (defaults to patch; combine with --bump for a different level)'
111
111
  required: false
112
112
  type: boolean
113
113
  default: false
@@ -141,7 +141,7 @@ on:
141
141
  - minor
142
142
  - major
143
143
  force:
144
- description: 'Force a release even when there are no commits since the last tag (requires --bump)'
144
+ description: 'Force a release even when no commits or no bump-worthy commits exist (defaults to patch; combine with --bump for a different level)'
145
145
  required: false
146
146
  type: boolean
147
147
  default: false
@@ -1,6 +1,15 @@
1
1
  import type { MonorepoReleaseConfig, ReleaseConfig, ReleaseKitConfig, WorkTypeConfig } from './types.ts';
2
+ export declare const ROOT_PACKAGE_JSON_PATH = "package.json";
3
+ export declare function readRootPackageVersion(): {
4
+ exists: boolean;
5
+ version: string | undefined;
6
+ };
2
7
  export declare const CONFIG_FILE_PATH = ".config/release-kit.config.ts";
3
8
  export declare function loadConfig(): Promise<unknown>;
4
- export declare function mergeMonorepoConfig(discoveredPaths: string[], userConfig: ReleaseKitConfig | undefined): MonorepoReleaseConfig;
9
+ export interface RootPackageInfo {
10
+ exists: boolean;
11
+ version: string | undefined;
12
+ }
13
+ export declare function mergeMonorepoConfig(discoveredPaths: string[], userConfig: ReleaseKitConfig | undefined, rootPackage?: RootPackageInfo): MonorepoReleaseConfig;
5
14
  export declare function mergeSinglePackageConfig(userConfig: ReleaseKitConfig | undefined): ReleaseConfig;
6
15
  export declare function resolveWorkTypes(userWorkTypes?: Record<string, WorkTypeConfig>): Record<string, WorkTypeConfig>;
@@ -1,13 +1,41 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import {
4
4
  DEFAULT_CHANGELOG_JSON_CONFIG,
5
+ DEFAULT_PROJECT_TAG_PREFIX,
5
6
  DEFAULT_RELEASE_NOTES_CONFIG,
6
7
  DEFAULT_VERSION_PATTERNS,
7
8
  DEFAULT_WORK_TYPES
8
9
  } from "./defaults.js";
9
10
  import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
10
11
  import { isRecord } from "./typeGuards.js";
12
+ const ROOT_PACKAGE_JSON_PATH = "package.json";
13
+ function readRootPackageVersion() {
14
+ const absolutePath = path.resolve(process.cwd(), ROOT_PACKAGE_JSON_PATH);
15
+ if (!existsSync(absolutePath)) {
16
+ return { exists: false, version: void 0 };
17
+ }
18
+ let contents;
19
+ try {
20
+ contents = readFileSync(absolutePath, "utf8");
21
+ } catch (error) {
22
+ throw new Error(
23
+ `Failed to read root ${ROOT_PACKAGE_JSON_PATH}: ${error instanceof Error ? error.message : String(error)}`
24
+ );
25
+ }
26
+ let parsed;
27
+ try {
28
+ parsed = JSON.parse(contents);
29
+ } catch (error) {
30
+ throw new Error(
31
+ `Failed to parse root ${ROOT_PACKAGE_JSON_PATH}: ${error instanceof Error ? error.message : String(error)}`
32
+ );
33
+ }
34
+ if (!isRecord(parsed)) {
35
+ return { exists: true, version: void 0 };
36
+ }
37
+ return { exists: true, version: typeof parsed.version === "string" ? parsed.version : void 0 };
38
+ }
11
39
  const CONFIG_FILE_PATH = ".config/release-kit.config.ts";
12
40
  async function loadConfig() {
13
41
  const absoluteConfigPath = path.resolve(process.cwd(), CONFIG_FILE_PATH);
@@ -28,7 +56,7 @@ async function loadConfig() {
28
56
  }
29
57
  return resolved;
30
58
  }
31
- function mergeMonorepoConfig(discoveredPaths, userConfig) {
59
+ function mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage) {
32
60
  let workspaces = discoveredPaths.map((workspacePath) => deriveWorkspaceConfig(workspacePath));
33
61
  assertUniqueTagPrefixes(workspaces);
34
62
  if (userConfig?.workspaces !== void 0) {
@@ -48,10 +76,12 @@ function mergeMonorepoConfig(discoveredPaths, userConfig) {
48
76
  if (userConfig?.retiredPackages !== void 0) {
49
77
  assertRetiredPackagesDoNotCollideWithActive(workspaces, userConfig.retiredPackages);
50
78
  }
79
+ const project = resolveProjectConfig(userConfig?.project, rootPackage);
51
80
  const workTypes = resolveWorkTypes(userConfig?.workTypes);
52
81
  const versionPatterns = userConfig?.versionPatterns === void 0 ? { ...DEFAULT_VERSION_PATTERNS } : { ...userConfig.versionPatterns };
53
82
  const changelogJson = mergeChangelogJsonConfig(userConfig?.changelogJson);
54
83
  const releaseNotes = mergeReleaseNotesConfig(userConfig?.releaseNotes);
84
+ assertNoTagPrefixCollisions(workspaces, userConfig?.retiredPackages, project);
55
85
  const result = {
56
86
  workspaces,
57
87
  workTypes,
@@ -59,6 +89,9 @@ function mergeMonorepoConfig(discoveredPaths, userConfig) {
59
89
  changelogJson,
60
90
  releaseNotes
61
91
  };
92
+ if (project !== void 0) {
93
+ result.project = project;
94
+ }
62
95
  const formatCommand = userConfig?.formatCommand;
63
96
  if (formatCommand !== void 0) {
64
97
  result.formatCommand = formatCommand;
@@ -74,6 +107,9 @@ function mergeMonorepoConfig(discoveredPaths, userConfig) {
74
107
  return result;
75
108
  }
76
109
  function mergeSinglePackageConfig(userConfig) {
110
+ if (userConfig?.project !== void 0) {
111
+ throw new Error("project block is not supported in single-package mode");
112
+ }
77
113
  const workTypes = resolveWorkTypes(userConfig?.workTypes);
78
114
  const versionPatterns = userConfig?.versionPatterns === void 0 ? { ...DEFAULT_VERSION_PATTERNS } : { ...userConfig.versionPatterns };
79
115
  const changelogJson = mergeChangelogJsonConfig(userConfig?.changelogJson);
@@ -137,6 +173,62 @@ function assertRetiredPackagesDoNotCollideWithActive(workspaces, retiredPackages
137
173
  }
138
174
  }
139
175
  }
176
+ function resolveProjectConfig(userProject, rootPackage) {
177
+ if (userProject === void 0) {
178
+ return void 0;
179
+ }
180
+ if (rootPackage === void 0 || !rootPackage.exists) {
181
+ throw new Error(
182
+ `project block requires a root ${ROOT_PACKAGE_JSON_PATH}; create one with a 'version' field at the repo root`
183
+ );
184
+ }
185
+ if (rootPackage.version === void 0) {
186
+ throw new Error(
187
+ `project block requires root ${ROOT_PACKAGE_JSON_PATH} to have a 'version' field; add a 'version' field to your root package.json`
188
+ );
189
+ }
190
+ return { tagPrefix: userProject.tagPrefix ?? DEFAULT_PROJECT_TAG_PREFIX };
191
+ }
192
+ function assertNoTagPrefixCollisions(workspaces, retiredPackages, project) {
193
+ const sources = [];
194
+ for (const workspace of workspaces) {
195
+ const owner = `ws:${workspace.dir}`;
196
+ sources.push({ prefix: workspace.tagPrefix, label: `workspace '${workspace.dir}'`, owner });
197
+ for (const identity of workspace.legacyIdentities ?? []) {
198
+ sources.push({
199
+ prefix: identity.tagPrefix,
200
+ label: `workspace '${workspace.dir}' legacyIdentities entry (name='${identity.name}')`,
201
+ owner
202
+ });
203
+ }
204
+ }
205
+ for (const [index, retired] of (retiredPackages ?? []).entries()) {
206
+ sources.push({
207
+ prefix: retired.tagPrefix,
208
+ label: `retiredPackages entry (name='${retired.name}')`,
209
+ owner: `retired:${index}`
210
+ });
211
+ }
212
+ if (project !== void 0) {
213
+ sources.push({ prefix: project.tagPrefix, label: "project", owner: "project" });
214
+ }
215
+ for (let i = 0; i < sources.length; i++) {
216
+ for (let j = i + 1; j < sources.length; j++) {
217
+ const a = sources[i];
218
+ const b = sources[j];
219
+ if (a === void 0 || b === void 0) continue;
220
+ if (a.owner === b.owner) continue;
221
+ if (isPrefixCollision(a.prefix, b.prefix)) {
222
+ throw new Error(
223
+ `Tag prefix collision: '${a.prefix}' (${a.label}) and '${b.prefix}' (${b.label}). One prefix is identical to or a strict prefix of the other; this would cause \`git describe --match=<prefix>*\` to return cross-matches.`
224
+ );
225
+ }
226
+ }
227
+ }
228
+ }
229
+ function isPrefixCollision(a, b) {
230
+ return a === b || a.startsWith(b) || b.startsWith(a);
231
+ }
140
232
  function assertUniqueTagPrefixes(workspaces) {
141
233
  const pathsByPrefix = /* @__PURE__ */ new Map();
142
234
  for (const workspace of workspaces) {
@@ -163,8 +255,10 @@ function mergeReleaseNotesConfig(partial) {
163
255
  }
164
256
  export {
165
257
  CONFIG_FILE_PATH,
258
+ ROOT_PACKAGE_JSON_PATH,
166
259
  loadConfig,
167
260
  mergeMonorepoConfig,
168
261
  mergeSinglePackageConfig,
262
+ readRootPackageVersion,
169
263
  resolveWorkTypes
170
264
  };
@@ -1,13 +1,16 @@
1
1
  import { parseArgs as coreParseArgs, translateParseError, writeFileWithCheck } from "@williamthorsen/nmr-core";
2
2
  import { assertCleanWorkingTree } from "./assertCleanWorkingTree.js";
3
+ import { buildDependencyGraph } from "./buildDependencyGraph.js";
3
4
  import { buildReleaseSummary } from "./buildReleaseSummary.js";
4
5
  import { discoverWorkspaces } from "./discoverWorkspaces.js";
5
6
  import { dim } from "./format.js";
6
- import { loadConfig, mergeMonorepoConfig, mergeSinglePackageConfig } from "./loadConfig.js";
7
+ import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
8
+ import { loadConfig, mergeMonorepoConfig, mergeSinglePackageConfig, readRootPackageVersion } from "./loadConfig.js";
7
9
  import { releasePrepare } from "./releasePrepare.js";
8
10
  import { releasePrepareMono } from "./releasePrepareMono.js";
9
11
  import { reportPrepare } from "./reportPrepare.js";
10
12
  import { validateConfig } from "./validateConfig.js";
13
+ import { validateOnlyExcludesStrandedDependents } from "./validateOnlyExcludesStrandedDependents.js";
11
14
  const RELEASE_TAGS_FILE = "tmp/.release-tags";
12
15
  const RELEASE_SUMMARY_FILE = "tmp/.release-summary";
13
16
  const VALID_BUMP_TYPES = ["major", "minor", "patch"];
@@ -22,10 +25,14 @@ Usage: npx @williamthorsen/release-kit prepare [options]
22
25
  Options:
23
26
  --dry-run Run without modifying any files
24
27
  --bump=major|minor|patch Override the bump type for all workspaces
25
- --set-version=X.Y.Z Set an explicit version; bypasses commit-derived bumps. Requires --only in monorepo mode.
26
- --force Force a release even when there are no commits since the last tag (requires --bump)
28
+ --set-version=X.Y.Z Set an explicit version; bypasses commit-derived bumps.
29
+ Requires --only in monorepo mode (rejected when a 'project' block is configured).
30
+ --force Release even when no commits or no bump-worthy commits exist
31
+ since the last tag. Defaults to patch when --bump is not given;
32
+ use --bump=X to release at a different level.
27
33
  --no-git-checks, -n Skip the clean-working-tree check
28
- --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only)
34
+ --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only;
35
+ rejected when a 'project' block is configured)
29
36
  --with-release-notes Also write per-workspace release-notes previews under {workspacePath}/docs/
30
37
  (docs/README.v{version}.md and docs/RELEASE_NOTES.v{version}.md).
31
38
  Recommended .gitignore entry: packages/*/docs/*.v*.md (or docs/*.v*.md).
@@ -84,9 +91,6 @@ function parseArgs(argv) {
84
91
  if (setVersion !== void 0 && flags.force) {
85
92
  throw new Error("--set-version cannot be combined with --force");
86
93
  }
87
- if (flags.force && bumpOverride === void 0) {
88
- throw new Error("--force requires --bump to specify the version bump type");
89
- }
90
94
  return {
91
95
  dryRun: flags.dryRun,
92
96
  force: flags.force,
@@ -157,17 +161,36 @@ function runSinglePackageMode(userConfig, options, only, dryRun) {
157
161
  console.error("Error: --only is only supported for monorepo configurations");
158
162
  process.exit(1);
159
163
  }
164
+ if (options.force && options.bumpOverride === void 0) {
165
+ console.error(
166
+ "Error: --force without --bump is only supported for monorepo configurations. Use --bump=major|minor|patch to set the level for a single-package release."
167
+ );
168
+ process.exit(1);
169
+ }
160
170
  const config = mergeSinglePackageConfig(userConfig);
161
171
  runAndReport(() => releasePrepare(config, options), dryRun);
162
172
  }
163
173
  function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion, dryRun) {
164
174
  let config;
165
175
  try {
166
- config = mergeMonorepoConfig(discoveredPaths, userConfig);
176
+ const rootPackage = readRootPackageVersion();
177
+ config = mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage);
167
178
  } catch (error) {
168
179
  console.error(`Error resolving workspaces: ${error instanceof Error ? error.message : String(error)}`);
169
180
  process.exit(1);
170
181
  }
182
+ if (setVersion !== void 0 && config.project !== void 0) {
183
+ console.error(
184
+ "Error: --set-version cannot be combined with a project release. --set-version operates on a single workspace; a project release rolls up every contributing workspace. To use --set-version, run on a config without a `project` block."
185
+ );
186
+ process.exit(1);
187
+ }
188
+ if (only !== void 0 && config.project !== void 0) {
189
+ console.error(
190
+ "Error: --only cannot be combined with a project release. To release a single workspace, use a config without a `project` block, or run a full `prepare` (no --only) to include the project release."
191
+ );
192
+ process.exit(1);
193
+ }
171
194
  if (only !== void 0) {
172
195
  const knownNames = config.workspaces.map((w) => w.dir);
173
196
  for (const name of only) {
@@ -176,6 +199,25 @@ function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion,
176
199
  process.exit(1);
177
200
  }
178
201
  }
202
+ const graph = buildDependencyGraph(config.workspaces);
203
+ const violations = validateOnlyExcludesStrandedDependents(config.workspaces, only, graph, (workspace) => {
204
+ const tagPrefixes = [
205
+ workspace.tagPrefix,
206
+ ...workspace.legacyIdentities?.map((identity) => identity.tagPrefix) ?? []
207
+ ];
208
+ const result = getCommitsSinceTarget(tagPrefixes, workspace.paths);
209
+ return { has: result.commits.length > 0, tag: result.tag };
210
+ });
211
+ if (violations !== void 0) {
212
+ console.error("Error: --only excludes packages with changes that would be stranded by the release.");
213
+ console.error("The following packages must be added to --only or have their dependencies removed:");
214
+ for (const violation of violations) {
215
+ const since = violation.tag ?? "the beginning";
216
+ console.error(` - ${violation.dir} (downstream of ${violation.downstreamOf}; has commits since ${since})`);
217
+ }
218
+ console.error("Alternatively, run `release-kit prepare` without --only to release everything.");
219
+ process.exit(1);
220
+ }
179
221
  config.workspaces = config.workspaces.filter((w) => only.includes(w.dir));
180
222
  }
181
223
  if (setVersion !== void 0) {
@@ -221,7 +263,7 @@ function runAndReport(execute, dryRun) {
221
263
  try {
222
264
  result = execute();
223
265
  } catch (error) {
224
- console.error("Error preparing release:", error instanceof Error ? error.message : String(error));
266
+ console.error(error instanceof Error ? error.message : String(error));
225
267
  process.exit(1);
226
268
  }
227
269
  process.stdout.write(reportPrepare(result) + "\n");
@@ -2,7 +2,6 @@ import type { PackageManager } from './detectPackageManager.ts';
2
2
  import type { ResolvedTag } from './resolveReleaseTags.ts';
3
3
  export interface PublishOptions {
4
4
  dryRun: boolean;
5
- noGitChecks: boolean;
6
5
  provenance: boolean;
7
6
  }
8
7
  export declare function publishPackage(resolvedTag: ResolvedTag, packageManager: PackageManager, options: PublishOptions): void;
@@ -1,8 +1,8 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  function publishPackage(resolvedTag, packageManager, options) {
3
- const { dryRun, noGitChecks, provenance } = options;
3
+ const { dryRun, provenance } = options;
4
4
  const executable = resolveExecutable(packageManager);
5
- const args = buildPublishArgs(packageManager, { dryRun, noGitChecks, provenance });
5
+ const args = buildPublishArgs(packageManager, { dryRun, provenance });
6
6
  console.info(
7
7
  `
8
8
  ${dryRun ? "[dry-run] " : ""}Running: ${executable} ${args.join(" ")} (cwd: ${resolvedTag.workspacePath})`
@@ -20,7 +20,7 @@ function buildPublishArgs(packageManager, options) {
20
20
  if (options.dryRun) {
21
21
  args.push("--dry-run");
22
22
  }
23
- if (options.noGitChecks && packageManager === "pnpm") {
23
+ if (packageManager === "pnpm") {
24
24
  args.push("--no-git-checks");
25
25
  }
26
26
  if (options.provenance && packageManager !== "yarn") {
@@ -1,6 +1,7 @@
1
1
  import { writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { parseArgs, translateParseError } from "@williamthorsen/nmr-core";
4
+ import { assertCleanWorkingTree } from "./assertCleanWorkingTree.js";
4
5
  import { detectPackageManager } from "./detectPackageManager.js";
5
6
  import { injectReleaseNotesIntoReadme, resolveReadmePath } from "./injectReleaseNotesIntoReadme.js";
6
7
  import { parseRequestedTags } from "./parseRequestedTags.js";
@@ -22,6 +23,14 @@ async function publishCommand(argv) {
22
23
  process.exit(1);
23
24
  }
24
25
  const { dryRun, noGitChecks, provenance } = parsed.flags;
26
+ if (!dryRun && !noGitChecks) {
27
+ try {
28
+ assertCleanWorkingTree();
29
+ } catch (error) {
30
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
31
+ process.exit(1);
32
+ }
33
+ }
25
34
  const requestedTags = parseRequestedTags(parsed.flags.tags);
26
35
  const resolvedTags = await resolveCommandTags(requestedTags);
27
36
  if (resolvedTags.length === 0) {
@@ -51,7 +60,7 @@ async function publishCommand(argv) {
51
60
  }
52
61
  }
53
62
  try {
54
- publishPackage(resolvedTag, packageManager, { dryRun, noGitChecks, provenance });
63
+ publishPackage(resolvedTag, packageManager, { dryRun, provenance });
55
64
  published.push(resolvedTag.tag);
56
65
  } finally {
57
66
  if (readmePath !== void 0 && originalReadme !== void 0) {