@williamthorsen/release-kit 2.3.2 → 4.0.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 (43) hide show
  1. package/CHANGELOG.md +54 -38
  2. package/README.md +29 -12
  3. package/cliff.toml.template +13 -12
  4. package/dist/esm/.cache +1 -1
  5. package/dist/esm/bin/release-kit.js +28 -0
  6. package/dist/esm/buildDependencyGraph.d.ts +7 -0
  7. package/dist/esm/buildDependencyGraph.js +59 -0
  8. package/dist/esm/buildReleaseSummary.d.ts +2 -0
  9. package/dist/esm/buildReleaseSummary.js +22 -0
  10. package/dist/esm/commitCommand.d.ts +1 -0
  11. package/dist/esm/commitCommand.js +55 -0
  12. package/dist/esm/createTags.js +12 -1
  13. package/dist/esm/determineBumpFromCommits.d.ts +1 -1
  14. package/dist/esm/determineBumpFromCommits.js +2 -2
  15. package/dist/esm/generateChangelogs.d.ts +2 -0
  16. package/dist/esm/generateChangelogs.js +9 -1
  17. package/dist/esm/index.d.ts +4 -1
  18. package/dist/esm/index.js +8 -1
  19. package/dist/esm/init/initCommand.js +4 -2
  20. package/dist/esm/init/scaffold.js +3 -2
  21. package/dist/esm/init/templates.d.ts +1 -0
  22. package/dist/esm/init/templates.js +24 -2
  23. package/dist/esm/loadConfig.js +6 -6
  24. package/dist/esm/parseCommitMessage.d.ts +1 -1
  25. package/dist/esm/parseCommitMessage.js +9 -7
  26. package/dist/esm/prepareCommand.d.ts +1 -0
  27. package/dist/esm/prepareCommand.js +20 -0
  28. package/dist/esm/propagateBumps.d.ts +8 -0
  29. package/dist/esm/propagateBumps.js +54 -0
  30. package/dist/esm/publish.d.ts +1 -0
  31. package/dist/esm/publish.js +6 -3
  32. package/dist/esm/publishCommand.js +3 -2
  33. package/dist/esm/releasePrepare.js +2 -1
  34. package/dist/esm/releasePrepareMono.js +237 -48
  35. package/dist/esm/reportPrepare.js +29 -3
  36. package/dist/esm/stripScope.d.ts +1 -0
  37. package/dist/esm/stripScope.js +24 -0
  38. package/dist/esm/sync-labels/templates.js +1 -1
  39. package/dist/esm/types.d.ts +11 -4
  40. package/dist/esm/validateConfig.js +6 -6
  41. package/dist/esm/writeSyntheticChangelog.d.ts +9 -0
  42. package/dist/esm/writeSyntheticChangelog.js +27 -0
  43. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
1
  import type { RepoType } from './detectRepoType.ts';
2
2
  export declare function releaseConfigScript(repoType: RepoType): string;
3
+ export declare function publishWorkflow(repoType: RepoType): string;
3
4
  export declare function releaseWorkflow(repoType: RepoType): string;
@@ -35,6 +35,27 @@ const config: ReleaseKitConfig = {
35
35
  export default config;
36
36
  `;
37
37
  }
38
+ function publishWorkflow(repoType) {
39
+ const tagPattern = repoType === "monorepo" ? "'*-v[0-9]*'" : "'v[0-9]*'";
40
+ return `# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
41
+ name: Publish
42
+
43
+ on:
44
+ push:
45
+ tags:
46
+ - ${tagPattern}
47
+
48
+ permissions:
49
+ id-token: write
50
+ contents: read
51
+
52
+ jobs:
53
+ publish:
54
+ uses: williamthorsen/node-monorepo-tools/.github/workflows/publish.reusable.yaml@publish-workflow-v1
55
+ with:
56
+ provenance: false # Set to true for public repos to generate npm provenance attestations
57
+ `;
58
+ }
38
59
  function releaseWorkflow(repoType) {
39
60
  if (repoType === "monorepo") {
40
61
  return `# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
@@ -68,7 +89,7 @@ permissions:
68
89
 
69
90
  jobs:
70
91
  release:
71
- uses: williamthorsen/node-monorepo-tools/.github/workflows/release-workflow.yaml@release-workflow-v1
92
+ uses: williamthorsen/node-monorepo-tools/.github/workflows/release.reusable.yaml@release-workflow-v1
72
93
  with:
73
94
  only: \${{ inputs.only }}
74
95
  bump: \${{ inputs.bump }}
@@ -102,13 +123,14 @@ permissions:
102
123
 
103
124
  jobs:
104
125
  release:
105
- uses: williamthorsen/node-monorepo-tools/.github/workflows/release-workflow.yaml@release-workflow-v1
126
+ uses: williamthorsen/node-monorepo-tools/.github/workflows/release.reusable.yaml@release-workflow-v1
106
127
  with:
107
128
  bump: \${{ inputs.bump }}
108
129
  force: \${{ inputs.force }}
109
130
  `;
110
131
  }
111
132
  export {
133
+ publishWorkflow,
112
134
  releaseConfigScript,
113
135
  releaseWorkflow
114
136
  };
@@ -47,9 +47,9 @@ function mergeMonorepoConfig(discoveredPaths, userConfig) {
47
47
  if (cliffConfigPath !== void 0) {
48
48
  result.cliffConfigPath = cliffConfigPath;
49
49
  }
50
- const workspaceAliases = userConfig?.workspaceAliases;
51
- if (workspaceAliases !== void 0) {
52
- result.workspaceAliases = workspaceAliases;
50
+ const scopeAliases = userConfig?.scopeAliases;
51
+ if (scopeAliases !== void 0) {
52
+ result.scopeAliases = scopeAliases;
53
53
  }
54
54
  return result;
55
55
  }
@@ -71,9 +71,9 @@ function mergeSinglePackageConfig(userConfig) {
71
71
  if (cliffConfigPath !== void 0) {
72
72
  result.cliffConfigPath = cliffConfigPath;
73
73
  }
74
- const workspaceAliases = userConfig?.workspaceAliases;
75
- if (workspaceAliases !== void 0) {
76
- result.workspaceAliases = workspaceAliases;
74
+ const scopeAliases = userConfig?.scopeAliases;
75
+ if (scopeAliases !== void 0) {
76
+ result.scopeAliases = scopeAliases;
77
77
  }
78
78
  return result;
79
79
  }
@@ -1,3 +1,3 @@
1
1
  import type { ParsedCommit, WorkTypeConfig } from './types.ts';
2
2
  export declare const COMMIT_PREPROCESSOR_PATTERNS: readonly RegExp[];
3
- export declare function parseCommitMessage(message: string, hash: string, workTypes: Record<string, WorkTypeConfig>, workspaceAliases?: Record<string, string>): ParsedCommit | undefined;
3
+ export declare function parseCommitMessage(message: string, hash: string, workTypes: Record<string, WorkTypeConfig>, scopeAliases?: Record<string, string>): ParsedCommit | undefined;
@@ -1,14 +1,15 @@
1
1
  const COMMIT_PREPROCESSOR_PATTERNS = [/^#\d+\s+/, /^[A-Z]+-\d+\s+/];
2
- function parseCommitMessage(message, hash, workTypes, workspaceAliases) {
2
+ function parseCommitMessage(message, hash, workTypes, scopeAliases) {
3
3
  const stripped = stripTicketPrefix(message);
4
- const match = stripped.match(/^(?:([^|]+)\|)?(\w+)(!)?:\s*(.*)$/);
4
+ const match = stripped.match(/^(?:([^|]+)\|)?(\w+)(?:\(([^)]+)\))?(!)?:\s*(.*)$/);
5
5
  if (!match) {
6
6
  return void 0;
7
7
  }
8
- const workspace = match[1];
8
+ const pipeScope = match[1];
9
9
  const rawType = match[2];
10
- const breakingMarker = match[3];
11
- const description = match[4];
10
+ const parenthesizedScope = match[3];
11
+ const breakingMarker = match[4];
12
+ const description = match[5];
12
13
  if (rawType === void 0 || description === void 0) {
13
14
  return void 0;
14
15
  }
@@ -17,14 +18,15 @@ function parseCommitMessage(message, hash, workTypes, workspaceAliases) {
17
18
  return void 0;
18
19
  }
19
20
  const breaking = breakingMarker === "!" || message.includes("BREAKING CHANGE:");
20
- const resolvedWorkspace = workspace !== void 0 && workspaceAliases !== void 0 ? workspaceAliases[workspace] ?? workspace : workspace;
21
+ const rawScope = pipeScope ?? parenthesizedScope;
22
+ const resolvedScope = rawScope !== void 0 && scopeAliases !== void 0 ? scopeAliases[rawScope] ?? rawScope : rawScope;
21
23
  return {
22
24
  message,
23
25
  hash,
24
26
  type: resolvedType,
25
27
  description,
26
28
  breaking,
27
- ...resolvedWorkspace !== void 0 && { workspace: resolvedWorkspace }
29
+ ...resolvedScope !== void 0 && { scope: resolvedScope }
28
30
  };
29
31
  }
30
32
  function resolveType(rawType, workTypes) {
@@ -1,6 +1,7 @@
1
1
  import type { WriteResult } from '@williamthorsen/node-monorepo-core';
2
2
  import type { ReleaseType } from './types.ts';
3
3
  export declare const RELEASE_TAGS_FILE = "tmp/.release-tags";
4
+ export declare const RELEASE_SUMMARY_FILE = "tmp/.release-summary";
4
5
  export declare function parseArgs(argv: string[]): {
5
6
  dryRun: boolean;
6
7
  force: boolean;
@@ -1,4 +1,5 @@
1
1
  import { writeFileWithCheck } from "@williamthorsen/node-monorepo-core";
2
+ import { buildReleaseSummary } from "./buildReleaseSummary.js";
2
3
  import { discoverWorkspaces } from "./discoverWorkspaces.js";
3
4
  import { dim } from "./format.js";
4
5
  import { loadConfig, mergeMonorepoConfig, mergeSinglePackageConfig } from "./loadConfig.js";
@@ -7,6 +8,7 @@ import { releasePrepareMono } from "./releasePrepareMono.js";
7
8
  import { reportPrepare } from "./reportPrepare.js";
8
9
  import { validateConfig } from "./validateConfig.js";
9
10
  const RELEASE_TAGS_FILE = "tmp/.release-tags";
11
+ const RELEASE_SUMMARY_FILE = "tmp/.release-summary";
10
12
  const VALID_BUMP_TYPES = ["major", "minor", "patch"];
11
13
  function isReleaseType(value) {
12
14
  return VALID_BUMP_TYPES.includes(value);
@@ -148,8 +150,26 @@ function runAndReport(execute, dryRun) {
148
150
  Release tags file: ${RELEASE_TAGS_FILE}`));
149
151
  }
150
152
  }
153
+ const summary = buildReleaseSummary(result);
154
+ if (summary.length > 0) {
155
+ const summaryResult = writeFileWithCheck(RELEASE_SUMMARY_FILE, summary, { dryRun, overwrite: true });
156
+ if (summaryResult.outcome === "failed") {
157
+ console.error(`Error writing release summary: ${summaryResult.error ?? "unknown error"}`);
158
+ process.exit(1);
159
+ }
160
+ if (dryRun) {
161
+ console.info(dim(` [dry-run] Would write ${RELEASE_SUMMARY_FILE}`));
162
+ } else {
163
+ console.info(dim(` Wrote ${RELEASE_SUMMARY_FILE}`));
164
+ }
165
+ }
166
+ if (writeResult && !dryRun) {
167
+ console.error(`
168
+ Run 'release-kit commit' to create the release commit.`);
169
+ }
151
170
  }
152
171
  export {
172
+ RELEASE_SUMMARY_FILE,
153
173
  RELEASE_TAGS_FILE,
154
174
  parseArgs,
155
175
  prepareCommand,
@@ -0,0 +1,8 @@
1
+ import type { DependencyGraph } from './buildDependencyGraph.ts';
2
+ import type { PropagationSource, ReleaseType } from './types.ts';
3
+ export interface ReleaseEntry {
4
+ releaseType: ReleaseType;
5
+ propagatedFrom?: PropagationSource[];
6
+ }
7
+ export type CurrentVersions = Map<string, string>;
8
+ export declare function propagateBumps(directBumps: Map<string, ReleaseEntry>, graph: DependencyGraph, currentVersions: CurrentVersions): Map<string, ReleaseEntry>;
@@ -0,0 +1,54 @@
1
+ import { bumpVersion } from "./bumpVersion.js";
2
+ function propagateBumps(directBumps, graph, currentVersions) {
3
+ const result = /* @__PURE__ */ new Map();
4
+ for (const [dir, entry] of directBumps) {
5
+ result.set(dir, { ...entry });
6
+ }
7
+ const queue = [...directBumps.keys()];
8
+ const visited = /* @__PURE__ */ new Set();
9
+ while (queue.length > 0) {
10
+ const dir = queue.shift();
11
+ if (dir === void 0) {
12
+ break;
13
+ }
14
+ if (visited.has(dir)) {
15
+ continue;
16
+ }
17
+ visited.add(dir);
18
+ const packageName = graph.dirToPackageName.get(dir);
19
+ if (packageName === void 0) {
20
+ continue;
21
+ }
22
+ const currentVersion = currentVersions.get(dir);
23
+ const entry = result.get(dir);
24
+ if (currentVersion === void 0 || entry === void 0) {
25
+ continue;
26
+ }
27
+ const newVersion = bumpVersion(currentVersion, entry.releaseType);
28
+ const dependents = graph.dependentsOf.get(packageName);
29
+ if (dependents === void 0) {
30
+ continue;
31
+ }
32
+ for (const dependent of dependents) {
33
+ const dependentDir = dependent.dir;
34
+ const existing = result.get(dependentDir);
35
+ const propagationInfo = { packageName, newVersion };
36
+ if (existing === void 0) {
37
+ result.set(dependentDir, {
38
+ releaseType: "patch",
39
+ propagatedFrom: [propagationInfo]
40
+ });
41
+ } else {
42
+ const existingPropagated = existing.propagatedFrom ?? [];
43
+ existing.propagatedFrom = [...existingPropagated, propagationInfo];
44
+ }
45
+ if (!visited.has(dependentDir)) {
46
+ queue.push(dependentDir);
47
+ }
48
+ }
49
+ }
50
+ return result;
51
+ }
52
+ export {
53
+ propagateBumps
54
+ };
@@ -3,5 +3,6 @@ import type { ResolvedTag } from './resolveReleaseTags.ts';
3
3
  export interface PublishOptions {
4
4
  dryRun: boolean;
5
5
  noGitChecks: boolean;
6
+ provenance: boolean;
6
7
  }
7
8
  export declare function publish(resolvedTags: ResolvedTag[], packageManager: PackageManager, options: PublishOptions): void;
@@ -1,6 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  function publish(resolvedTags, packageManager, options) {
3
- const { dryRun, noGitChecks } = options;
3
+ const { dryRun, noGitChecks, provenance } = options;
4
4
  if (resolvedTags.length === 0) {
5
5
  return;
6
6
  }
@@ -9,9 +9,9 @@ function publish(resolvedTags, packageManager, options) {
9
9
  console.info(` ${tag} (${workspacePath})`);
10
10
  }
11
11
  const published = [];
12
+ const executable = resolveExecutable(packageManager);
13
+ const args = buildPublishArgs(packageManager, { dryRun, noGitChecks, provenance });
12
14
  for (const { tag, workspacePath } of resolvedTags) {
13
- const executable = resolveExecutable(packageManager);
14
- const args = buildPublishArgs(packageManager, { dryRun, noGitChecks });
15
15
  try {
16
16
  console.info(`
17
17
  ${dryRun ? "[dry-run] " : ""}Running: ${executable} ${args.join(" ")} (cwd: ${workspacePath})`);
@@ -42,6 +42,9 @@ function buildPublishArgs(packageManager, options) {
42
42
  if (options.noGitChecks && packageManager === "pnpm") {
43
43
  args.push("--no-git-checks");
44
44
  }
45
+ if (options.provenance && packageManager !== "yarn") {
46
+ args.push("--provenance");
47
+ }
45
48
  return args;
46
49
  }
47
50
  export {
@@ -4,7 +4,7 @@ import { discoverWorkspaces } from "./discoverWorkspaces.js";
4
4
  import { publish } from "./publish.js";
5
5
  import { resolveReleaseTags } from "./resolveReleaseTags.js";
6
6
  async function publishCommand(argv) {
7
- const knownFlags = /* @__PURE__ */ new Set(["--dry-run", "--no-git-checks"]);
7
+ const knownFlags = /* @__PURE__ */ new Set(["--dry-run", "--no-git-checks", "--provenance"]);
8
8
  const unknownFlags = argv.filter((f) => !f.startsWith("--only=") && !knownFlags.has(f));
9
9
  if (unknownFlags.length > 0) {
10
10
  console.error(`Error: Unknown option: ${unknownFlags[0]}`);
@@ -12,6 +12,7 @@ async function publishCommand(argv) {
12
12
  }
13
13
  const dryRun = argv.includes("--dry-run");
14
14
  const noGitChecks = argv.includes("--no-git-checks");
15
+ const provenance = argv.includes("--provenance");
15
16
  const onlyArg = argv.find((f) => f.startsWith("--only="));
16
17
  const only = onlyArg?.slice("--only=".length).split(",");
17
18
  let discoveredPaths;
@@ -43,7 +44,7 @@ async function publishCommand(argv) {
43
44
  }
44
45
  const packageManager = detectPackageManager();
45
46
  try {
46
- publish(resolvedTags, packageManager, { dryRun, noGitChecks });
47
+ publish(resolvedTags, packageManager, { dryRun, noGitChecks, provenance });
47
48
  } catch (error) {
48
49
  console.error(error instanceof Error ? error.message : String(error));
49
50
  process.exit(1);
@@ -14,7 +14,7 @@ function releasePrepare(config, options) {
14
14
  let parsedCommitCount;
15
15
  let unparseableCommits;
16
16
  if (bumpOverride === void 0) {
17
- const determination = determineBumpFromCommits(commits, workTypes, versionPatterns, config.workspaceAliases);
17
+ const determination = determineBumpFromCommits(commits, workTypes, versionPatterns, config.scopeAliases);
18
18
  parsedCommitCount = determination.parsedCommitCount;
19
19
  unparseableCommits = determination.unparseableCommits;
20
20
  releaseType = determination.releaseType;
@@ -73,6 +73,7 @@ function releasePrepare(config, options) {
73
73
  tag: newTag,
74
74
  bumpedFiles: bump.files,
75
75
  changelogFiles,
76
+ commits,
76
77
  unparseableCommits
77
78
  }
78
79
  ],
@@ -1,29 +1,84 @@
1
1
  import { execSync } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import { buildDependencyGraph } from "./buildDependencyGraph.js";
2
4
  import { bumpAllVersions } from "./bumpAllVersions.js";
3
5
  import { DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
4
6
  import { determineBumpFromCommits } from "./determineBumpFromCommits.js";
5
- import { generateChangelog } from "./generateChangelogs.js";
7
+ import { buildTagPattern, generateChangelog } from "./generateChangelogs.js";
6
8
  import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
7
9
  import { hasPrettierConfig } from "./hasPrettierConfig.js";
10
+ import { propagateBumps } from "./propagateBumps.js";
11
+ import { writeSyntheticChangelog } from "./writeSyntheticChangelog.js";
8
12
  function releasePrepareMono(config, options) {
9
- const { dryRun, force, bumpOverride } = options;
13
+ const { dryRun } = options;
14
+ const { directBumps, directResults, skippedResults, currentVersions } = determineDirectBumps(config, options);
15
+ const previousTags = /* @__PURE__ */ new Map();
16
+ for (const result of directResults.values()) {
17
+ previousTags.set(result.component.dir, result.tag);
18
+ }
19
+ for (const skipped of skippedResults) {
20
+ previousTags.set(skipped.component.dir, skipped.tag);
21
+ }
22
+ const graph = buildDependencyGraph(config.components);
23
+ const fullReleaseSet = propagateBumps(directBumps, graph, currentVersions);
24
+ const { sorted: sortedDirs, cyclicDirs } = topologicalSort(fullReleaseSet, graph);
25
+ const warnings = [];
26
+ if (cyclicDirs.length > 0) {
27
+ warnings.push(
28
+ `Circular workspace dependencies detected among: ${cyclicDirs.join(", ")}. Propagation metadata may be incomplete for these components.`
29
+ );
30
+ }
31
+ const components = collectSkippedComponents(skippedResults, fullReleaseSet);
32
+ const { tags, modifiedFiles } = executeReleaseSet(
33
+ sortedDirs,
34
+ fullReleaseSet,
35
+ config,
36
+ directResults,
37
+ previousTags,
38
+ dryRun,
39
+ components
40
+ );
41
+ const configOrder = new Map(config.components.map((c, i) => [c.dir, i]));
42
+ components.sort((a, b) => {
43
+ const orderA = configOrder.get(a.name ?? "") ?? 0;
44
+ const orderB = configOrder.get(b.name ?? "") ?? 0;
45
+ return orderA - orderB;
46
+ });
47
+ const formatCommand = runFormatCommand(config, tags, modifiedFiles, dryRun);
48
+ return {
49
+ components,
50
+ tags,
51
+ formatCommand,
52
+ dryRun,
53
+ ...warnings.length > 0 ? { warnings } : {}
54
+ };
55
+ }
56
+ function determineDirectBumps(config, options) {
57
+ const { force, bumpOverride } = options;
10
58
  const workTypes = config.workTypes ?? { ...DEFAULT_WORK_TYPES };
11
59
  const versionPatterns = config.versionPatterns ?? { ...DEFAULT_VERSION_PATTERNS };
12
- const tags = [];
13
- const modifiedFiles = [];
14
- const components = [];
60
+ const directBumps = /* @__PURE__ */ new Map();
61
+ const directResults = /* @__PURE__ */ new Map();
62
+ const skippedResults = [];
63
+ const currentVersions = /* @__PURE__ */ new Map();
15
64
  for (const component of config.components) {
16
65
  const name = component.dir;
17
66
  const { tag, commits } = getCommitsSinceTarget(component.tagPrefix, component.paths);
18
67
  const since = tag === void 0 ? "(no previous release found)" : `since ${tag}`;
68
+ const primaryPackageFile = component.packageFiles[0];
69
+ if (primaryPackageFile !== void 0) {
70
+ const currentVersion = readCurrentVersion(primaryPackageFile);
71
+ if (currentVersion !== void 0) {
72
+ currentVersions.set(component.dir, currentVersion);
73
+ }
74
+ }
19
75
  if (commits.length === 0 && !force) {
20
- components.push({
21
- name,
22
- status: "skipped",
23
- previousTag: tag,
76
+ skippedResults.push({
77
+ component,
78
+ tag,
24
79
  commitCount: 0,
25
- bumpedFiles: [],
26
- changelogFiles: [],
80
+ parsedCommitCount: void 0,
81
+ unparseableCommits: void 0,
27
82
  skipReason: `No changes for ${name} ${since}. Skipping.`
28
83
  });
29
84
  continue;
@@ -32,7 +87,7 @@ function releasePrepareMono(config, options) {
32
87
  let parsedCommitCount;
33
88
  let unparseableCommits;
34
89
  if (bumpOverride === void 0) {
35
- const determination = determineBumpFromCommits(commits, workTypes, versionPatterns, config.workspaceAliases);
90
+ const determination = determineBumpFromCommits(commits, workTypes, versionPatterns, config.scopeAliases);
36
91
  parsedCommitCount = determination.parsedCommitCount;
37
92
  unparseableCommits = determination.unparseableCommits;
38
93
  releaseType = determination.releaseType;
@@ -40,67 +95,201 @@ function releasePrepareMono(config, options) {
40
95
  releaseType = bumpOverride;
41
96
  }
42
97
  if (releaseType === void 0) {
43
- components.push({
44
- name,
45
- status: "skipped",
46
- previousTag: tag,
98
+ skippedResults.push({
99
+ component,
100
+ tag,
47
101
  commitCount: commits.length,
48
102
  parsedCommitCount,
49
103
  unparseableCommits,
50
- bumpedFiles: [],
51
- changelogFiles: [],
52
104
  skipReason: `No release-worthy changes for ${name} ${since}. Skipping.`
53
105
  });
54
106
  continue;
55
107
  }
56
- const bump = bumpAllVersions(component.packageFiles, releaseType, dryRun);
108
+ directBumps.set(component.dir, { releaseType });
109
+ directResults.set(component.dir, {
110
+ component,
111
+ tag,
112
+ commits,
113
+ releaseType,
114
+ parsedCommitCount,
115
+ unparseableCommits
116
+ });
117
+ }
118
+ return { directBumps, directResults, skippedResults, currentVersions };
119
+ }
120
+ function collectSkippedComponents(skippedResults, fullReleaseSet) {
121
+ const components = [];
122
+ for (const skipped of skippedResults) {
123
+ if (fullReleaseSet.has(skipped.component.dir)) {
124
+ continue;
125
+ }
126
+ components.push({
127
+ name: skipped.component.dir,
128
+ status: "skipped",
129
+ previousTag: skipped.tag,
130
+ commitCount: skipped.commitCount,
131
+ parsedCommitCount: skipped.parsedCommitCount,
132
+ unparseableCommits: skipped.unparseableCommits,
133
+ bumpedFiles: [],
134
+ changelogFiles: [],
135
+ skipReason: skipped.skipReason
136
+ });
137
+ }
138
+ return components;
139
+ }
140
+ function executeReleaseSet(sortedDirs, fullReleaseSet, config, directResults, previousTags, dryRun, components) {
141
+ const tags = [];
142
+ const modifiedFiles = [];
143
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
144
+ for (const dir of sortedDirs) {
145
+ const releaseEntry = fullReleaseSet.get(dir);
146
+ if (releaseEntry === void 0) {
147
+ continue;
148
+ }
149
+ const component = findComponent(config.components, dir);
150
+ if (component === void 0) {
151
+ continue;
152
+ }
153
+ const bump = bumpAllVersions(component.packageFiles, releaseEntry.releaseType, dryRun);
57
154
  const newTag = `${component.tagPrefix}${bump.newVersion}`;
58
155
  tags.push(newTag);
59
156
  modifiedFiles.push(...component.packageFiles, ...component.changelogPaths.map((p) => `${p}/CHANGELOG.md`));
60
157
  const changelogFiles = [];
61
- for (const changelogPath of component.changelogPaths) {
62
- changelogFiles.push(
63
- ...generateChangelog(config, changelogPath, newTag, dryRun, { includePaths: component.paths })
64
- );
158
+ const directResult = directResults.get(dir);
159
+ const isPropagationOnly = directResult === void 0;
160
+ if (isPropagationOnly && releaseEntry.propagatedFrom !== void 0) {
161
+ for (const changelogPath of component.changelogPaths) {
162
+ changelogFiles.push(
163
+ writeSyntheticChangelog({
164
+ changelogPath,
165
+ newVersion: bump.newVersion,
166
+ date: today,
167
+ propagatedFrom: releaseEntry.propagatedFrom,
168
+ dryRun
169
+ })
170
+ );
171
+ }
172
+ } else {
173
+ for (const changelogPath of component.changelogPaths) {
174
+ changelogFiles.push(
175
+ ...generateChangelog(config, changelogPath, newTag, dryRun, {
176
+ tagPattern: buildTagPattern(component.tagPrefix),
177
+ includePaths: component.paths
178
+ })
179
+ );
180
+ }
65
181
  }
66
182
  components.push({
67
- name,
183
+ name: dir,
68
184
  status: "released",
69
- previousTag: tag,
70
- commitCount: commits.length,
71
- parsedCommitCount,
72
- releaseType,
185
+ previousTag: directResult?.tag ?? previousTags.get(dir),
186
+ commitCount: directResult?.commits.length ?? 0,
187
+ parsedCommitCount: directResult?.parsedCommitCount,
188
+ releaseType: releaseEntry.releaseType,
73
189
  currentVersion: bump.currentVersion,
74
190
  newVersion: bump.newVersion,
75
191
  tag: newTag,
76
192
  bumpedFiles: bump.files,
77
193
  changelogFiles,
78
- unparseableCommits
194
+ commits: directResult?.commits,
195
+ unparseableCommits: directResult?.unparseableCommits,
196
+ propagatedFrom: releaseEntry.propagatedFrom
79
197
  });
80
198
  }
199
+ return { tags, modifiedFiles };
200
+ }
201
+ function runFormatCommand(config, tags, modifiedFiles, dryRun) {
81
202
  const formatCommandStr = config.formatCommand ?? (hasPrettierConfig() ? "npx prettier --write" : void 0);
82
- let formatCommand;
83
- if (tags.length > 0 && formatCommandStr !== void 0) {
84
- const fullCommand = `${formatCommandStr} ${modifiedFiles.join(" ")}`;
85
- if (dryRun) {
86
- formatCommand = { command: fullCommand, executed: false, files: modifiedFiles };
87
- } else {
88
- try {
89
- execSync(fullCommand, { stdio: "inherit" });
90
- } catch (error) {
91
- throw new Error(
92
- `Format command failed ('${fullCommand}'): ${error instanceof Error ? error.message : String(error)}`
93
- );
203
+ if (tags.length === 0 || formatCommandStr === void 0) {
204
+ return void 0;
205
+ }
206
+ const fullCommand = `${formatCommandStr} ${modifiedFiles.join(" ")}`;
207
+ if (dryRun) {
208
+ return { command: fullCommand, executed: false, files: modifiedFiles };
209
+ }
210
+ try {
211
+ execSync(fullCommand, { stdio: "inherit" });
212
+ } catch (error) {
213
+ throw new Error(
214
+ `Format command failed ('${fullCommand}'): ${error instanceof Error ? error.message : String(error)}`
215
+ );
216
+ }
217
+ return { command: fullCommand, executed: true, files: modifiedFiles };
218
+ }
219
+ function findComponent(components, dir) {
220
+ return components.find((c) => c.dir === dir);
221
+ }
222
+ function hasVersionField(value) {
223
+ return typeof value === "object" && value !== null && "version" in value && typeof value.version === "string";
224
+ }
225
+ function readCurrentVersion(filePath) {
226
+ try {
227
+ const content = readFileSync(filePath, "utf8");
228
+ const parsed = JSON.parse(content);
229
+ if (hasVersionField(parsed)) {
230
+ return parsed.version;
231
+ }
232
+ } catch {
233
+ }
234
+ return void 0;
235
+ }
236
+ function topologicalSort(releaseSet, graph) {
237
+ const releaseDirs = new Set(releaseSet.keys());
238
+ if (releaseDirs.size === 0) {
239
+ return { sorted: [], cyclicDirs: [] };
240
+ }
241
+ const inDegree = /* @__PURE__ */ new Map();
242
+ const forwardEdges = /* @__PURE__ */ new Map();
243
+ for (const dir of releaseDirs) {
244
+ inDegree.set(dir, 0);
245
+ forwardEdges.set(dir, []);
246
+ }
247
+ for (const [packageName, dependents] of graph.dependentsOf) {
248
+ const depDir = graph.packageNameToDir.get(packageName);
249
+ if (depDir === void 0 || !releaseDirs.has(depDir)) {
250
+ continue;
251
+ }
252
+ for (const dependent of dependents) {
253
+ if (!releaseDirs.has(dependent.dir)) {
254
+ continue;
94
255
  }
95
- formatCommand = { command: fullCommand, executed: true, files: modifiedFiles };
256
+ const edges = forwardEdges.get(depDir);
257
+ if (edges !== void 0) {
258
+ edges.push(dependent.dir);
259
+ }
260
+ inDegree.set(dependent.dir, (inDegree.get(dependent.dir) ?? 0) + 1);
96
261
  }
97
262
  }
98
- return {
99
- components,
100
- tags,
101
- formatCommand,
102
- dryRun
103
- };
263
+ const queue = [];
264
+ for (const [dir, degree] of inDegree) {
265
+ if (degree === 0) {
266
+ queue.push(dir);
267
+ }
268
+ }
269
+ const sorted = [];
270
+ while (queue.length > 0) {
271
+ const dir = queue.shift();
272
+ if (dir === void 0) {
273
+ break;
274
+ }
275
+ sorted.push(dir);
276
+ for (const dependent of forwardEdges.get(dir) ?? []) {
277
+ const newDegree = (inDegree.get(dependent) ?? 1) - 1;
278
+ inDegree.set(dependent, newDegree);
279
+ if (newDegree === 0) {
280
+ queue.push(dependent);
281
+ }
282
+ }
283
+ }
284
+ const sortedSet = new Set(sorted);
285
+ const cyclicDirs = [];
286
+ for (const dir of releaseDirs) {
287
+ if (!sortedSet.has(dir)) {
288
+ sorted.push(dir);
289
+ cyclicDirs.push(dir);
290
+ }
291
+ }
292
+ return { sorted, cyclicDirs };
104
293
  }
105
294
  export {
106
295
  releasePrepareMono