@williamthorsen/release-kit 4.5.0 → 4.6.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 (53) hide show
  1. package/CHANGELOG.md +118 -95
  2. package/LICENSE +4 -4
  3. package/cliff.toml.template +66 -54
  4. package/dist/esm/.cache +1 -1
  5. package/dist/esm/assertCleanWorkingTree.d.ts +1 -0
  6. package/dist/esm/assertCleanWorkingTree.js +15 -0
  7. package/dist/esm/bin/release-kit.js +29 -6
  8. package/dist/esm/changelogJsonUtils.d.ts +4 -0
  9. package/dist/esm/changelogJsonUtils.js +29 -0
  10. package/dist/esm/createGithubRelease.d.ts +11 -0
  11. package/dist/esm/createGithubRelease.js +54 -0
  12. package/dist/esm/defaults.d.ts +3 -1
  13. package/dist/esm/defaults.js +14 -0
  14. package/dist/esm/generateChangelogJson.d.ts +7 -0
  15. package/dist/esm/generateChangelogJson.js +196 -0
  16. package/dist/esm/generateChangelogs.js +25 -9
  17. package/dist/esm/githubReleaseCommand.d.ts +1 -0
  18. package/dist/esm/githubReleaseCommand.js +35 -0
  19. package/dist/esm/index.d.ts +11 -3
  20. package/dist/esm/index.js +27 -3
  21. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +2 -0
  22. package/dist/esm/injectReleaseNotesIntoReadme.js +42 -0
  23. package/dist/esm/injectSection.d.ts +1 -0
  24. package/dist/esm/injectSection.js +32 -0
  25. package/dist/esm/loadConfig.js +35 -3
  26. package/dist/esm/parseCommitMessage.js +1 -1
  27. package/dist/esm/prepareCommand.d.ts +1 -0
  28. package/dist/esm/prepareCommand.js +54 -20
  29. package/dist/esm/publish.d.ts +1 -1
  30. package/dist/esm/publish.js +7 -26
  31. package/dist/esm/publishCommand.js +49 -29
  32. package/dist/esm/releasePrepare.js +12 -1
  33. package/dist/esm/releasePrepareMono.js +23 -0
  34. package/dist/esm/renderReleaseNotes.d.ts +8 -0
  35. package/dist/esm/renderReleaseNotes.js +40 -0
  36. package/dist/esm/resolveCommandTags.d.ts +2 -0
  37. package/dist/esm/resolveCommandTags.js +36 -0
  38. package/dist/esm/resolveReleaseNotesConfig.d.ts +6 -0
  39. package/dist/esm/resolveReleaseNotesConfig.js +37 -0
  40. package/dist/esm/sync-labels/generateCommand.d.ts +1 -1
  41. package/dist/esm/sync-labels/generateCommand.js +13 -7
  42. package/dist/esm/sync-labels/presets.d.ts +1 -0
  43. package/dist/esm/sync-labels/presets.js +10 -0
  44. package/dist/esm/sync-labels/templates.js +5 -1
  45. package/dist/esm/typeGuards.d.ts +1 -0
  46. package/dist/esm/typeGuards.js +5 -1
  47. package/dist/esm/types.d.ts +29 -0
  48. package/dist/esm/validateConfig.d.ts +1 -0
  49. package/dist/esm/validateConfig.js +89 -6
  50. package/dist/esm/version.d.ts +1 -1
  51. package/dist/esm/version.js +1 -1
  52. package/package.json +4 -3
  53. package/presets/labels/common.yaml +43 -0
@@ -0,0 +1,32 @@
1
+ function openMarker(key) {
2
+ return `<!-- section:${key} -->`;
3
+ }
4
+ function closeMarker(key) {
5
+ return `<!-- /section:${key} -->`;
6
+ }
7
+ function injectSection(content, key, injection) {
8
+ const open = openMarker(key);
9
+ const close = closeMarker(key);
10
+ const openIndex = content.indexOf(open);
11
+ const closeIndex = content.indexOf(close);
12
+ if (openIndex !== -1 && closeIndex !== -1 && closeIndex > openIndex) {
13
+ const before = content.slice(0, openIndex + open.length);
14
+ const after = content.slice(closeIndex);
15
+ return `${before}
16
+ ${injection}
17
+ ${after}`;
18
+ }
19
+ const section = `${open}
20
+ ${injection}
21
+ ${close}`;
22
+ if (content.length === 0) {
23
+ return `${section}
24
+ `;
25
+ }
26
+ return `${section}
27
+
28
+ ${content}`;
29
+ }
30
+ export {
31
+ injectSection
32
+ };
@@ -1,7 +1,12 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { component } from "./component.js";
4
- import { DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
4
+ import {
5
+ DEFAULT_CHANGELOG_JSON_CONFIG,
6
+ DEFAULT_RELEASE_NOTES_CONFIG,
7
+ DEFAULT_VERSION_PATTERNS,
8
+ DEFAULT_WORK_TYPES
9
+ } from "./defaults.js";
5
10
  import { isRecord } from "./typeGuards.js";
6
11
  const CONFIG_FILE_PATH = ".config/release-kit.config.ts";
7
12
  async function loadConfig() {
@@ -34,10 +39,14 @@ function mergeMonorepoConfig(discoveredPaths, userConfig) {
34
39
  }
35
40
  const workTypes = userConfig?.workTypes === void 0 ? { ...DEFAULT_WORK_TYPES } : { ...DEFAULT_WORK_TYPES, ...userConfig.workTypes };
36
41
  const versionPatterns = userConfig?.versionPatterns === void 0 ? { ...DEFAULT_VERSION_PATTERNS } : { ...userConfig.versionPatterns };
42
+ const changelogJson = mergeChangelogJsonConfig(userConfig?.changelogJson);
43
+ const releaseNotes = mergeReleaseNotesConfig(userConfig?.releaseNotes);
37
44
  const result = {
38
45
  components,
39
46
  workTypes,
40
- versionPatterns
47
+ versionPatterns,
48
+ changelogJson,
49
+ releaseNotes
41
50
  };
42
51
  const formatCommand = userConfig?.formatCommand;
43
52
  if (formatCommand !== void 0) {
@@ -56,12 +65,16 @@ function mergeMonorepoConfig(discoveredPaths, userConfig) {
56
65
  function mergeSinglePackageConfig(userConfig) {
57
66
  const workTypes = userConfig?.workTypes === void 0 ? { ...DEFAULT_WORK_TYPES } : { ...DEFAULT_WORK_TYPES, ...userConfig.workTypes };
58
67
  const versionPatterns = userConfig?.versionPatterns === void 0 ? { ...DEFAULT_VERSION_PATTERNS } : { ...userConfig.versionPatterns };
68
+ const changelogJson = mergeChangelogJsonConfig(userConfig?.changelogJson);
69
+ const releaseNotes = mergeReleaseNotesConfig(userConfig?.releaseNotes);
59
70
  const result = {
60
71
  tagPrefix: "v",
61
72
  packageFiles: ["package.json"],
62
73
  changelogPaths: ["."],
63
74
  workTypes,
64
- versionPatterns
75
+ versionPatterns,
76
+ changelogJson,
77
+ releaseNotes
65
78
  };
66
79
  const formatCommand = userConfig?.formatCommand;
67
80
  if (formatCommand !== void 0) {
@@ -77,6 +90,25 @@ function mergeSinglePackageConfig(userConfig) {
77
90
  }
78
91
  return result;
79
92
  }
93
+ function mergeChangelogJsonConfig(partial) {
94
+ if (partial === void 0) {
95
+ return { ...DEFAULT_CHANGELOG_JSON_CONFIG };
96
+ }
97
+ return {
98
+ enabled: partial.enabled ?? DEFAULT_CHANGELOG_JSON_CONFIG.enabled,
99
+ outputPath: partial.outputPath ?? DEFAULT_CHANGELOG_JSON_CONFIG.outputPath,
100
+ devOnlySections: partial.devOnlySections ?? [...DEFAULT_CHANGELOG_JSON_CONFIG.devOnlySections]
101
+ };
102
+ }
103
+ function mergeReleaseNotesConfig(partial) {
104
+ if (partial === void 0) {
105
+ return { ...DEFAULT_RELEASE_NOTES_CONFIG };
106
+ }
107
+ return {
108
+ shouldInjectIntoReadme: partial.shouldInjectIntoReadme ?? DEFAULT_RELEASE_NOTES_CONFIG.shouldInjectIntoReadme,
109
+ shouldCreateGithubRelease: partial.shouldCreateGithubRelease ?? DEFAULT_RELEASE_NOTES_CONFIG.shouldCreateGithubRelease
110
+ };
111
+ }
80
112
  export {
81
113
  CONFIG_FILE_PATH,
82
114
  loadConfig,
@@ -1,4 +1,4 @@
1
- const COMMIT_PREPROCESSOR_PATTERNS = [/^#\d+\s+/, /^[A-Z]+-\d+\s+/];
1
+ const COMMIT_PREPROCESSOR_PATTERNS = [/^#\d+([.-]\d+)?\s+/, /^[A-Z]+-\d+\s+/];
2
2
  function parseCommitMessage(message, hash, workTypes, scopeAliases) {
3
3
  const stripped = stripTicketPrefix(message);
4
4
  const match = stripped.match(/^(?:([^|]+)\|)?(\w+)(?:\(([^)]+)\))?(!)?:\s*(.*)$/);
@@ -5,6 +5,7 @@ export declare const RELEASE_SUMMARY_FILE = "tmp/.release-summary";
5
5
  export declare function parseArgs(argv: string[]): {
6
6
  dryRun: boolean;
7
7
  force: boolean;
8
+ noGitChecks: boolean;
8
9
  bumpOverride: ReleaseType | undefined;
9
10
  only: string[] | undefined;
10
11
  };
@@ -3,6 +3,7 @@ import {
3
3
  translateParseError,
4
4
  writeFileWithCheck
5
5
  } from "@williamthorsen/node-monorepo-core";
6
+ import { assertCleanWorkingTree } from "./assertCleanWorkingTree.js";
6
7
  import { buildReleaseSummary } from "./buildReleaseSummary.js";
7
8
  import { discoverWorkspaces } from "./discoverWorkspaces.js";
8
9
  import { dim } from "./format.js";
@@ -25,6 +26,7 @@ Options:
25
26
  --dry-run Run without modifying any files
26
27
  --bump=major|minor|patch Override the bump type for all components
27
28
  --force Bypass the "no commits since last tag" check (monorepo only, requires --bump)
29
+ --no-git-checks, -n Skip the clean-working-tree check
28
30
  --only=name1,name2 Only process the named components (comma-separated, monorepo only)
29
31
  --help Show this help message
30
32
  `);
@@ -32,6 +34,11 @@ Options:
32
34
  const prepareFlagSchema = {
33
35
  dryRun: { long: "--dry-run", type: "boolean" },
34
36
  force: { long: "--force", type: "boolean" },
37
+ noGitChecks: {
38
+ long: "--no-git-checks",
39
+ type: "boolean",
40
+ short: "-n"
41
+ },
35
42
  bump: { long: "--bump", type: "string" },
36
43
  only: { long: "--only", type: "string" },
37
44
  help: { long: "--help", type: "boolean", short: "-h" }
@@ -62,21 +69,31 @@ function parseArgs(argv) {
62
69
  if (flags.force && bumpOverride === void 0) {
63
70
  throw new Error("--force requires --bump to specify the version bump type");
64
71
  }
65
- return { dryRun: flags.dryRun, force: flags.force, bumpOverride, only };
72
+ return {
73
+ dryRun: flags.dryRun,
74
+ force: flags.force,
75
+ noGitChecks: flags.noGitChecks,
76
+ bumpOverride,
77
+ only
78
+ };
66
79
  }
67
80
  function writeReleaseTags(tags, dryRun) {
68
81
  if (tags.length === 0) {
69
82
  return void 0;
70
83
  }
71
- return writeFileWithCheck(RELEASE_TAGS_FILE, tags.join("\n"), { dryRun, overwrite: true });
84
+ return writeFileWithCheck(RELEASE_TAGS_FILE, tags.join("\n"), {
85
+ dryRun,
86
+ overwrite: true
87
+ });
72
88
  }
73
89
  async function prepareCommand(argv) {
74
90
  let dryRun;
75
91
  let force;
92
+ let noGitChecks;
76
93
  let bumpOverride;
77
94
  let only;
78
95
  try {
79
- ({ dryRun, force, bumpOverride, only } = parseArgs(argv));
96
+ ({ dryRun, force, noGitChecks, bumpOverride, only } = parseArgs(argv));
80
97
  } catch (error) {
81
98
  console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
82
99
  process.exit(1);
@@ -89,25 +106,15 @@ async function prepareCommand(argv) {
89
106
  if (dryRun) {
90
107
  console.info("\n\u{1F50D} DRY RUN \u2014 no files will be modified\n");
91
108
  }
92
- let rawConfig;
93
- try {
94
- rawConfig = await loadConfig();
95
- } catch (error) {
96
- console.error(`Error loading config: ${error instanceof Error ? error.message : String(error)}`);
97
- process.exit(1);
98
- }
99
- let userConfig;
100
- if (rawConfig !== void 0) {
101
- const { config, errors } = validateConfig(rawConfig);
102
- if (errors.length > 0) {
103
- console.error("Invalid config:");
104
- for (const err of errors) {
105
- console.error(` \u274C ${err}`);
106
- }
109
+ if (!dryRun && !noGitChecks) {
110
+ try {
111
+ assertCleanWorkingTree();
112
+ } catch (error) {
113
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
107
114
  process.exit(1);
108
115
  }
109
- userConfig = config;
110
116
  }
117
+ const userConfig = await loadAndValidateConfig();
111
118
  let discoveredPaths;
112
119
  try {
113
120
  discoveredPaths = await discoverWorkspaces();
@@ -137,6 +144,30 @@ async function prepareCommand(argv) {
137
144
  runAndReport(() => releasePrepareMono(config, options), dryRun);
138
145
  }
139
146
  }
147
+ async function loadAndValidateConfig() {
148
+ let rawConfig;
149
+ try {
150
+ rawConfig = await loadConfig();
151
+ } catch (error) {
152
+ console.error(`Error loading config: ${error instanceof Error ? error.message : String(error)}`);
153
+ process.exit(1);
154
+ }
155
+ if (rawConfig === void 0) {
156
+ return void 0;
157
+ }
158
+ const { config, errors, warnings } = validateConfig(rawConfig);
159
+ if (errors.length > 0) {
160
+ console.error("Invalid config:");
161
+ for (const err of errors) {
162
+ console.error(` \u274C ${err}`);
163
+ }
164
+ process.exit(1);
165
+ }
166
+ for (const warning of warnings) {
167
+ console.warn(` \u26A0\uFE0F ${warning}`);
168
+ }
169
+ return config;
170
+ }
140
171
  function runAndReport(execute, dryRun) {
141
172
  let result;
142
173
  try {
@@ -162,7 +193,10 @@ function runAndReport(execute, dryRun) {
162
193
  }
163
194
  const summary = buildReleaseSummary(result);
164
195
  if (summary.length > 0) {
165
- const summaryResult = writeFileWithCheck(RELEASE_SUMMARY_FILE, summary, { dryRun, overwrite: true });
196
+ const summaryResult = writeFileWithCheck(RELEASE_SUMMARY_FILE, summary, {
197
+ dryRun,
198
+ overwrite: true
199
+ });
166
200
  if (summaryResult.outcome === "failed") {
167
201
  console.error(`Error writing release summary: ${summaryResult.error ?? "unknown error"}`);
168
202
  process.exit(1);
@@ -5,4 +5,4 @@ export interface PublishOptions {
5
5
  noGitChecks: boolean;
6
6
  provenance: boolean;
7
7
  }
8
- export declare function publish(resolvedTags: ResolvedTag[], packageManager: PackageManager, options: PublishOptions): void;
8
+ export declare function publishPackage(resolvedTag: ResolvedTag, packageManager: PackageManager, options: PublishOptions): void;
@@ -1,32 +1,13 @@
1
1
  import { execFileSync } from "node:child_process";
2
- function publish(resolvedTags, packageManager, options) {
2
+ function publishPackage(resolvedTag, packageManager, options) {
3
3
  const { dryRun, noGitChecks, provenance } = options;
4
- if (resolvedTags.length === 0) {
5
- return;
6
- }
7
- console.info(dryRun ? "[dry-run] Would publish:" : "Publishing:");
8
- for (const { tag, workspacePath } of resolvedTags) {
9
- console.info(` ${tag} (${workspacePath})`);
10
- }
11
- const published = [];
12
4
  const executable = resolveExecutable(packageManager);
13
5
  const args = buildPublishArgs(packageManager, { dryRun, noGitChecks, provenance });
14
- for (const { tag, workspacePath } of resolvedTags) {
15
- try {
16
- console.info(`
17
- ${dryRun ? "[dry-run] " : ""}Running: ${executable} ${args.join(" ")} (cwd: ${workspacePath})`);
18
- execFileSync(executable, args, { cwd: workspacePath, stdio: "inherit" });
19
- published.push(tag);
20
- } catch (error) {
21
- if (published.length > 0) {
22
- console.warn("Packages published before failure:");
23
- for (const t of published) {
24
- console.warn(` ${t}`);
25
- }
26
- }
27
- throw error;
28
- }
29
- }
6
+ console.info(
7
+ `
8
+ ${dryRun ? "[dry-run] " : ""}Running: ${executable} ${args.join(" ")} (cwd: ${resolvedTag.workspacePath})`
9
+ );
10
+ execFileSync(executable, args, { cwd: resolvedTag.workspacePath, stdio: "inherit" });
30
11
  }
31
12
  function resolveExecutable(packageManager) {
32
13
  if (packageManager === "yarn-berry") {
@@ -48,5 +29,5 @@ function buildPublishArgs(packageManager, options) {
48
29
  return args;
49
30
  }
50
31
  export {
51
- publish
32
+ publishPackage
52
33
  };
@@ -1,9 +1,12 @@
1
- import { basename } from "node:path";
1
+ import { writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
2
3
  import { parseArgs, translateParseError } from "@williamthorsen/node-monorepo-core";
4
+ import { createGithubReleases } from "./createGithubRelease.js";
3
5
  import { detectPackageManager } from "./detectPackageManager.js";
4
- import { discoverWorkspaces } from "./discoverWorkspaces.js";
5
- import { publish } from "./publish.js";
6
- import { resolveReleaseTags } from "./resolveReleaseTags.js";
6
+ import { injectReleaseNotesIntoReadme, resolveReadmePath } from "./injectReleaseNotesIntoReadme.js";
7
+ import { publishPackage } from "./publish.js";
8
+ import { resolveCommandTags } from "./resolveCommandTags.js";
9
+ import { resolveReleaseNotesConfig } from "./resolveReleaseNotesConfig.js";
7
10
  const publishFlagSchema = {
8
11
  dryRun: { long: "--dry-run", type: "boolean" },
9
12
  noGitChecks: { long: "--no-git-checks", type: "boolean" },
@@ -20,38 +23,55 @@ async function publishCommand(argv) {
20
23
  }
21
24
  const { dryRun, noGitChecks, provenance } = parsed.flags;
22
25
  const only = parsed.flags.only?.split(",");
23
- let discoveredPaths;
24
- try {
25
- discoveredPaths = await discoverWorkspaces();
26
- } catch (error) {
27
- console.error(`Error discovering workspaces: ${error instanceof Error ? error.message : String(error)}`);
28
- process.exit(1);
29
- }
30
- if (only !== void 0 && discoveredPaths === void 0) {
31
- console.error("Error: --only is only supported for monorepo configurations");
32
- process.exit(1);
33
- }
34
- const workspaceMap = discoveredPaths === void 0 ? void 0 : new Map(discoveredPaths.map((p) => [basename(p), p]));
35
- let resolvedTags = resolveReleaseTags(workspaceMap);
26
+ const resolvedTags = await resolveCommandTags(only);
36
27
  if (resolvedTags.length === 0) {
37
- console.error("Error: No release tags found on HEAD. Create tags with `release-kit tag` first.");
38
- process.exit(1);
28
+ return;
29
+ }
30
+ const packageManager = detectPackageManager();
31
+ const { releaseNotes, changelogJsonOutputPath } = await resolveReleaseNotesConfig();
32
+ const shouldInject = releaseNotes.shouldInjectIntoReadme;
33
+ console.info(dryRun ? "[dry-run] Would publish:" : "Publishing:");
34
+ for (const { tag, workspacePath } of resolvedTags) {
35
+ console.info(` ${tag} (${workspacePath})`);
39
36
  }
40
- if (only !== void 0) {
41
- const availableNames = resolvedTags.map((t) => t.dir);
42
- for (const name of only) {
43
- if (!availableNames.includes(name)) {
44
- console.error(`Error: Unknown package "${name}" in --only. Available: ${availableNames.join(", ")}`);
45
- process.exit(1);
37
+ const published = [];
38
+ try {
39
+ for (const resolvedTag of resolvedTags) {
40
+ let readmePath;
41
+ let originalReadme;
42
+ if (shouldInject) {
43
+ readmePath = resolveReadmePath(resolvedTag.workspacePath);
44
+ if (readmePath !== void 0) {
45
+ originalReadme = injectReleaseNotesIntoReadme(
46
+ readmePath,
47
+ join(resolvedTag.workspacePath, changelogJsonOutputPath),
48
+ resolvedTag.tag
49
+ );
50
+ }
51
+ }
52
+ try {
53
+ publishPackage(resolvedTag, packageManager, { dryRun, noGitChecks, provenance });
54
+ published.push(resolvedTag.tag);
55
+ } finally {
56
+ if (readmePath !== void 0 && originalReadme !== void 0) {
57
+ writeFileSync(readmePath, originalReadme, "utf8");
58
+ }
59
+ }
60
+ }
61
+ } catch (error) {
62
+ if (published.length > 0) {
63
+ console.warn("Packages published before failure:");
64
+ for (const t of published) {
65
+ console.warn(` ${t}`);
46
66
  }
47
67
  }
48
- resolvedTags = resolvedTags.filter((t) => only.includes(t.dir));
68
+ console.error(error instanceof Error ? error.message : String(error));
69
+ process.exit(1);
49
70
  }
50
- const packageManager = detectPackageManager();
51
71
  try {
52
- publish(resolvedTags, packageManager, { dryRun, noGitChecks, provenance });
72
+ createGithubReleases(resolvedTags, releaseNotes, changelogJsonOutputPath, dryRun);
53
73
  } catch (error) {
54
- console.error(error instanceof Error ? error.message : String(error));
74
+ console.error(`Error creating GitHub Releases: ${error instanceof Error ? error.message : String(error)}`);
55
75
  process.exit(1);
56
76
  }
57
77
  }
@@ -2,6 +2,7 @@ import { execSync } from "node:child_process";
2
2
  import { bumpAllVersions } from "./bumpAllVersions.js";
3
3
  import { DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
4
4
  import { determineBumpFromCommits } from "./determineBumpFromCommits.js";
5
+ import { generateChangelogJson } from "./generateChangelogJson.js";
5
6
  import { generateChangelogs } from "./generateChangelogs.js";
6
7
  import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
7
8
  import { hasPrettierConfig } from "./hasPrettierConfig.js";
@@ -42,10 +43,20 @@ function releasePrepare(config, options) {
42
43
  const bump = bumpAllVersions(config.packageFiles, releaseType, dryRun);
43
44
  const newTag = `${config.tagPrefix}${bump.newVersion}`;
44
45
  const changelogFiles = generateChangelogs(config, newTag, dryRun);
46
+ const changelogJsonFiles = [];
47
+ if (config.changelogJson.enabled) {
48
+ for (const changelogPath of config.changelogPaths) {
49
+ changelogJsonFiles.push(...generateChangelogJson(config, changelogPath, newTag, dryRun));
50
+ }
51
+ }
45
52
  const formatCommandStr = config.formatCommand ?? (hasPrettierConfig() ? "npx prettier --write" : void 0);
46
53
  let formatCommand;
47
54
  if (formatCommandStr !== void 0) {
48
- const modifiedFiles = [...config.packageFiles, ...config.changelogPaths.map((p) => `${p}/CHANGELOG.md`)];
55
+ const modifiedFiles = [
56
+ ...config.packageFiles,
57
+ ...config.changelogPaths.map((p) => `${p}/CHANGELOG.md`),
58
+ ...changelogJsonFiles
59
+ ];
49
60
  const fullCommand = `${formatCommandStr} ${modifiedFiles.join(" ")}`;
50
61
  if (dryRun) {
51
62
  formatCommand = { command: fullCommand, executed: false, files: modifiedFiles };
@@ -4,6 +4,7 @@ import { buildDependencyGraph } from "./buildDependencyGraph.js";
4
4
  import { bumpAllVersions } from "./bumpAllVersions.js";
5
5
  import { DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
6
6
  import { determineBumpFromCommits } from "./determineBumpFromCommits.js";
7
+ import { generateChangelogJson, generateSyntheticChangelogJson } from "./generateChangelogJson.js";
7
8
  import { buildTagPattern, generateChangelog } from "./generateChangelogs.js";
8
9
  import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
9
10
  import { hasPrettierConfig } from "./hasPrettierConfig.js";
@@ -169,6 +170,19 @@ function executeReleaseSet(sortedDirs, fullReleaseSet, config, directResults, pr
169
170
  })
170
171
  );
171
172
  }
173
+ if (config.changelogJson.enabled) {
174
+ for (const changelogPath of component.changelogPaths) {
175
+ const jsonFiles = generateSyntheticChangelogJson(
176
+ config,
177
+ changelogPath,
178
+ bump.newVersion,
179
+ today,
180
+ releaseEntry.propagatedFrom,
181
+ dryRun
182
+ );
183
+ modifiedFiles.push(...jsonFiles);
184
+ }
185
+ }
172
186
  } else {
173
187
  for (const changelogPath of component.changelogPaths) {
174
188
  changelogFiles.push(
@@ -178,6 +192,15 @@ function executeReleaseSet(sortedDirs, fullReleaseSet, config, directResults, pr
178
192
  })
179
193
  );
180
194
  }
195
+ if (config.changelogJson.enabled) {
196
+ for (const changelogPath of component.changelogPaths) {
197
+ const jsonFiles = generateChangelogJson(config, changelogPath, newTag, dryRun, {
198
+ tagPattern: buildTagPattern(component.tagPrefix),
199
+ includePaths: component.paths
200
+ });
201
+ modifiedFiles.push(...jsonFiles);
202
+ }
203
+ }
181
204
  }
182
205
  components.push({
183
206
  name: dir,
@@ -0,0 +1,8 @@
1
+ import type { ChangelogAudience, ChangelogEntry, ChangelogSection } from './types.ts';
2
+ export interface RenderOptions {
3
+ filter?: (section: ChangelogSection) => boolean;
4
+ includeHeading?: boolean;
5
+ }
6
+ export declare function matchesAudience(audience: ChangelogAudience): (section: ChangelogSection) => boolean;
7
+ export declare function renderReleaseNotesSingle(entry: ChangelogEntry, options?: RenderOptions): string;
8
+ export declare function renderReleaseNotesMulti(entries: ChangelogEntry[], options?: RenderOptions): string;
@@ -0,0 +1,40 @@
1
+ function allSections() {
2
+ return true;
3
+ }
4
+ function publicSections(section) {
5
+ return section.audience === "all";
6
+ }
7
+ function matchesAudience(audience) {
8
+ return audience === "dev" ? allSections : publicSections;
9
+ }
10
+ function renderReleaseNotesSingle(entry, options) {
11
+ const filter = options?.filter;
12
+ const includeHeading = options?.includeHeading ?? true;
13
+ const sections = filter !== void 0 ? entry.sections.filter(filter) : entry.sections;
14
+ if (sections.length === 0) {
15
+ return "";
16
+ }
17
+ const lines = [];
18
+ if (includeHeading) {
19
+ lines.push(`## ${entry.version} \u2014 ${entry.date}`);
20
+ }
21
+ for (const section of sections) {
22
+ if (lines.length > 0) {
23
+ lines.push("");
24
+ }
25
+ lines.push(`### ${section.title}`, "");
26
+ for (const item of section.items) {
27
+ lines.push(`- ${item.description}`);
28
+ }
29
+ }
30
+ return lines.join("\n") + "\n";
31
+ }
32
+ function renderReleaseNotesMulti(entries, options) {
33
+ const parts = entries.map((entry) => renderReleaseNotesSingle(entry, options)).filter((part) => part.length > 0);
34
+ return parts.join("\n");
35
+ }
36
+ export {
37
+ matchesAudience,
38
+ renderReleaseNotesMulti,
39
+ renderReleaseNotesSingle
40
+ };
@@ -0,0 +1,2 @@
1
+ import type { ResolvedTag } from './resolveReleaseTags.ts';
2
+ export declare function resolveCommandTags(only: string[] | undefined): Promise<ResolvedTag[]>;
@@ -0,0 +1,36 @@
1
+ import { basename } from "node:path";
2
+ import { discoverWorkspaces } from "./discoverWorkspaces.js";
3
+ import { resolveReleaseTags } from "./resolveReleaseTags.js";
4
+ async function resolveCommandTags(only) {
5
+ let discoveredPaths;
6
+ try {
7
+ discoveredPaths = await discoverWorkspaces();
8
+ } catch (error) {
9
+ console.error(`Error discovering workspaces: ${error instanceof Error ? error.message : String(error)}`);
10
+ process.exit(1);
11
+ }
12
+ if (only !== void 0 && discoveredPaths === void 0) {
13
+ console.error("Error: --only is only supported for monorepo configurations");
14
+ process.exit(1);
15
+ }
16
+ const workspaceMap = discoveredPaths === void 0 ? void 0 : new Map(discoveredPaths.map((p) => [basename(p), p]));
17
+ let resolvedTags = resolveReleaseTags(workspaceMap);
18
+ if (resolvedTags.length === 0) {
19
+ console.error("Error: No release tags found on HEAD. Create tags with `release-kit tag` first.");
20
+ process.exit(1);
21
+ }
22
+ if (only !== void 0) {
23
+ const availableNames = resolvedTags.map((t) => t.dir);
24
+ for (const name of only) {
25
+ if (!availableNames.includes(name)) {
26
+ console.error(`Error: Unknown package "${name}" in --only. Available: ${availableNames.join(", ")}`);
27
+ process.exit(1);
28
+ }
29
+ }
30
+ resolvedTags = resolvedTags.filter((t) => only.includes(t.dir));
31
+ }
32
+ return resolvedTags;
33
+ }
34
+ export {
35
+ resolveCommandTags
36
+ };
@@ -0,0 +1,6 @@
1
+ import type { ReleaseNotesConfig } from './types.ts';
2
+ export interface ResolvedReleaseNotesConfig {
3
+ releaseNotes: ReleaseNotesConfig;
4
+ changelogJsonOutputPath: string;
5
+ }
6
+ export declare function resolveReleaseNotesConfig(): Promise<ResolvedReleaseNotesConfig>;
@@ -0,0 +1,37 @@
1
+ import { DEFAULT_CHANGELOG_JSON_CONFIG, DEFAULT_RELEASE_NOTES_CONFIG } from "./defaults.js";
2
+ import { loadConfig } from "./loadConfig.js";
3
+ import { validateConfig } from "./validateConfig.js";
4
+ async function resolveReleaseNotesConfig() {
5
+ let rawConfig;
6
+ try {
7
+ rawConfig = await loadConfig();
8
+ } catch (error) {
9
+ console.warn(
10
+ `Warning: failed to load config; using defaults: ${error instanceof Error ? error.message : String(error)}`
11
+ );
12
+ }
13
+ if (rawConfig === void 0) {
14
+ return {
15
+ releaseNotes: { ...DEFAULT_RELEASE_NOTES_CONFIG },
16
+ changelogJsonOutputPath: DEFAULT_CHANGELOG_JSON_CONFIG.outputPath
17
+ };
18
+ }
19
+ const { config, errors, warnings } = validateConfig(rawConfig);
20
+ if (errors.length > 0) {
21
+ console.error("Invalid config:");
22
+ for (const err of errors) {
23
+ console.error(` \u274C ${err}`);
24
+ }
25
+ process.exit(1);
26
+ }
27
+ for (const warning of warnings) {
28
+ console.warn(` \u26A0\uFE0F ${warning}`);
29
+ }
30
+ return {
31
+ releaseNotes: { ...DEFAULT_RELEASE_NOTES_CONFIG, ...config.releaseNotes },
32
+ changelogJsonOutputPath: config.changelogJson?.outputPath ?? DEFAULT_CHANGELOG_JSON_CONFIG.outputPath
33
+ };
34
+ }
35
+ export {
36
+ resolveReleaseNotesConfig
37
+ };
@@ -1,4 +1,4 @@
1
1
  import type { LabelDefinition } from './types.ts';
2
2
  export declare const LABELS_OUTPUT_PATH = ".github/labels.yaml";
3
- export declare function formatLabelsYaml(labels: LabelDefinition[]): string;
3
+ export declare function formatLabelsYaml(labels: LabelDefinition[], presetHashes: Map<string, string>): string;
4
4
  export declare function generateCommand(): Promise<number>;
@@ -2,14 +2,16 @@ import { mkdirSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { dump } from "js-yaml";
4
4
  import { loadSyncLabelsConfig, SYNC_LABELS_CONFIG_PATH } from "./loadSyncLabelsConfig.js";
5
+ import { hashPresetFile } from "./presets.js";
5
6
  import { resolveLabels } from "./resolveLabels.js";
6
7
  const LABELS_OUTPUT_PATH = ".github/labels.yaml";
7
- const GENERATED_HEADER = `# Generated by release-kit sync-labels \u2014 do not edit.
8
- # Source: ${SYNC_LABELS_CONFIG_PATH}
9
- `;
10
- function formatLabelsYaml(labels) {
11
- const yamlBody = dump(labels, { quotingType: '"', forceQuotes: false, lineWidth: -1 });
12
- return GENERATED_HEADER + yamlBody;
8
+ function formatLabelsYaml(labels, presetHashes) {
9
+ const headerLines = ["# Generated by release-kit sync-labels \u2014 do not edit.", `# Source: ${SYNC_LABELS_CONFIG_PATH}`];
10
+ for (const [name, hash] of [...presetHashes.entries()].sort(([a], [b]) => a.localeCompare(b))) {
11
+ headerLines.push(`# ${name} preset hash: ${hash}`);
12
+ }
13
+ const yamlBody = dump(labels, { quotingType: "'", forceQuotes: false, lineWidth: -1 });
14
+ return headerLines.join("\n") + "\n" + yamlBody;
13
15
  }
14
16
  async function generateCommand() {
15
17
  let config;
@@ -32,7 +34,11 @@ async function generateCommand() {
32
34
  console.error(`Error resolving labels: ${message}`);
33
35
  return 1;
34
36
  }
35
- const content = formatLabelsYaml(labels);
37
+ const presetHashes = /* @__PURE__ */ new Map();
38
+ for (const presetName of config.presets ?? []) {
39
+ presetHashes.set(presetName, hashPresetFile(presetName));
40
+ }
41
+ const content = formatLabelsYaml(labels, presetHashes);
36
42
  try {
37
43
  mkdirSync(dirname(LABELS_OUTPUT_PATH), { recursive: true });
38
44
  writeFileSync(LABELS_OUTPUT_PATH, content, "utf8");
@@ -1,2 +1,3 @@
1
1
  import type { LabelDefinition } from './types.ts';
2
+ export declare function hashPresetFile(presetName: string): string;
2
3
  export declare function loadPreset(presetName: string): LabelDefinition[];