@williamthorsen/release-kit 5.0.0 → 5.2.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 (73) hide show
  1. package/CHANGELOG.md +149 -49
  2. package/README.md +275 -78
  3. package/cliff.toml.template +26 -17
  4. package/dist/esm/.cache +1 -1
  5. package/dist/esm/assertCleanWorkingTree.js +1 -1
  6. package/dist/esm/bin/release-kit.js +97 -4
  7. package/dist/esm/buildChangelogEntries.d.ts +4 -0
  8. package/dist/esm/buildChangelogEntries.js +173 -0
  9. package/dist/esm/buildDependencyGraph.d.ts +1 -0
  10. package/dist/esm/buildDependencyGraph.js +8 -1
  11. package/dist/esm/buildReleaseSummary.js +9 -1
  12. package/dist/esm/buildSyntheticChangelogEntry.d.ts +5 -0
  13. package/dist/esm/buildSyntheticChangelogEntry.js +13 -0
  14. package/dist/esm/changelogJsonFile.d.ts +4 -0
  15. package/dist/esm/changelogJsonFile.js +68 -0
  16. package/dist/esm/checkWorkTypesDrift.d.ts +11 -0
  17. package/dist/esm/checkWorkTypesDrift.js +110 -0
  18. package/dist/esm/collectPolicyViolations.d.ts +6 -0
  19. package/dist/esm/collectPolicyViolations.js +15 -0
  20. package/dist/esm/createGithubRelease.d.ts +12 -2
  21. package/dist/esm/createGithubRelease.js +12 -8
  22. package/dist/esm/createGithubReleaseCommand.js +10 -6
  23. package/dist/esm/decideRelease.d.ts +28 -0
  24. package/dist/esm/decideRelease.js +44 -0
  25. package/dist/esm/defaults.d.ts +8 -0
  26. package/dist/esm/defaults.js +43 -20
  27. package/dist/esm/deriveWorkspaceConfig.js +3 -0
  28. package/dist/esm/determineBumpFromCommits.d.ts +6 -1
  29. package/dist/esm/determineBumpFromCommits.js +9 -3
  30. package/dist/esm/generateChangelogs.js +14 -29
  31. package/dist/esm/index.d.ts +2 -43
  32. package/dist/esm/index.js +0 -82
  33. package/dist/esm/init/templates.js +2 -2
  34. package/dist/esm/loadConfig.d.ts +10 -1
  35. package/dist/esm/loadConfig.js +110 -24
  36. package/dist/esm/parseCommitMessage.d.ts +8 -2
  37. package/dist/esm/parseCommitMessage.js +32 -3
  38. package/dist/esm/prepareCommand.js +51 -9
  39. package/dist/esm/publish.d.ts +0 -1
  40. package/dist/esm/publish.js +3 -3
  41. package/dist/esm/publishCommand.js +31 -3
  42. package/dist/esm/releasePrepare.js +109 -41
  43. package/dist/esm/releasePrepareMono.js +156 -87
  44. package/dist/esm/releasePrepareProject.d.ts +9 -0
  45. package/dist/esm/releasePrepareProject.js +121 -0
  46. package/dist/esm/renderReleaseNotes.js +2 -1
  47. package/dist/esm/reportPrepare.js +88 -24
  48. package/dist/esm/resolveCommandTags.js +16 -6
  49. package/dist/esm/resolveReleaseTags.d.ts +8 -1
  50. package/dist/esm/resolveReleaseTags.js +11 -7
  51. package/dist/esm/runGitCliff.d.ts +2 -0
  52. package/dist/esm/runGitCliff.js +27 -0
  53. package/dist/esm/stripEmojiPrefix.d.ts +1 -0
  54. package/dist/esm/stripEmojiPrefix.js +7 -0
  55. package/dist/esm/syncWorkTypes.d.ts +10 -0
  56. package/dist/esm/syncWorkTypes.js +90 -0
  57. package/dist/esm/types.d.ts +72 -14
  58. package/dist/esm/validateConfig.js +26 -0
  59. package/dist/esm/validateOnlyExcludesStrandedDependents.d.ts +14 -0
  60. package/dist/esm/validateOnlyExcludesStrandedDependents.js +109 -0
  61. package/dist/esm/work-types.json +127 -0
  62. package/dist/esm/work-types.schema.json +73 -0
  63. package/dist/esm/workTypesData.d.ts +14 -0
  64. package/dist/esm/workTypesData.js +59 -0
  65. package/dist/esm/workTypesUtils.d.ts +5 -0
  66. package/dist/esm/workTypesUtils.js +16 -0
  67. package/package.json +9 -3
  68. package/presets/labels/common.yaml +9 -6
  69. package/schemas/label-map.json +24 -0
  70. package/dist/esm/generateChangelogJson.d.ts +0 -7
  71. package/dist/esm/generateChangelogJson.js +0 -232
  72. package/dist/esm/version.d.ts +0 -1
  73. package/dist/esm/version.js +0 -4
@@ -0,0 +1,121 @@
1
+ import { buildChangelogEntries } from "./buildChangelogEntries.js";
2
+ import { bumpAllVersions } from "./bumpAllVersions.js";
3
+ import { resolveChangelogJsonPath, writeChangelogJson } from "./changelogJsonFile.js";
4
+ import { createPolicyViolationCollector } from "./collectPolicyViolations.js";
5
+ import { decideRelease } from "./decideRelease.js";
6
+ import { DEFAULT_BREAKING_POLICIES, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
7
+ import { buildTagPattern, generateChangelog } from "./generateChangelogs.js";
8
+ import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
9
+ import { deriveSectionOrder } from "./resolveReleaseNotesConfig.js";
10
+ import { writeReleaseNotesPreviews } from "./writeReleaseNotesPreviews.js";
11
+ const ROOT_PACKAGE_FILE = "./package.json";
12
+ const ROOT_CHANGELOG_PATH = ".";
13
+ function releasePrepareProject(args) {
14
+ const { config, options, modifiedFiles, tags } = args;
15
+ const { dryRun, bumpOverride, withReleaseNotes, force } = options;
16
+ const project = config.project;
17
+ if (project === void 0) {
18
+ throw new Error("releasePrepareProject called without a configured project block");
19
+ }
20
+ const workTypes = config.workTypes ?? { ...DEFAULT_WORK_TYPES };
21
+ const versionPatterns = config.versionPatterns ?? { ...DEFAULT_VERSION_PATTERNS };
22
+ const breakingPolicies = config.breakingPolicies ?? DEFAULT_BREAKING_POLICIES;
23
+ const contributingPaths = config.workspaces.flatMap((workspace) => workspace.paths);
24
+ const { tag, commits } = getCommitsSinceTarget([project.tagPrefix], contributingPaths);
25
+ const since = tag === void 0 ? "(no previous release found)" : `since ${tag}`;
26
+ const collector = createPolicyViolationCollector();
27
+ const decision = decideRelease({
28
+ commits,
29
+ force,
30
+ bumpOverride,
31
+ workTypes,
32
+ versionPatterns,
33
+ scopeAliases: config.scopeAliases,
34
+ breakingPolicies,
35
+ onPolicyViolation: collector.onPolicyViolation,
36
+ skipReasons: {
37
+ noCommits: `No commits ${since}. Pass --force to release at patch. Skipping.`,
38
+ noBumpWorthy: `No bump-worthy commits ${since}. Pass --force to release at patch (or --force --bump=X for a different level). Skipping.`
39
+ }
40
+ });
41
+ const policyViolations = collector.violations.length > 0 ? collector.violations : void 0;
42
+ if (decision.outcome === "skip") {
43
+ const skipped = {
44
+ status: "skipped",
45
+ commitCount: commits.length,
46
+ parsedCommitCount: decision.parsedCommitCount,
47
+ skipReason: decision.skipReason
48
+ };
49
+ if (tag !== void 0) {
50
+ skipped.previousTag = tag;
51
+ }
52
+ if (decision.unparseableCommits !== void 0) {
53
+ skipped.unparseableCommits = decision.unparseableCommits;
54
+ }
55
+ if (policyViolations !== void 0) {
56
+ skipped.policyViolations = policyViolations;
57
+ }
58
+ return skipped;
59
+ }
60
+ const { releaseType, parsedCommitCount, unparseableCommits } = decision;
61
+ const bump = bumpAllVersions([ROOT_PACKAGE_FILE], releaseType, dryRun);
62
+ const newTag = `${project.tagPrefix}${bump.newVersion}`;
63
+ const tagPattern = buildTagPattern([project.tagPrefix]);
64
+ const changelogFiles = generateChangelog(config, ROOT_CHANGELOG_PATH, newTag, dryRun, {
65
+ tagPattern,
66
+ includePaths: contributingPaths
67
+ });
68
+ const changelogJsonFiles = [];
69
+ if (config.changelogJson.enabled) {
70
+ const changelogJsonPath = resolveChangelogJsonPath(config, ROOT_CHANGELOG_PATH);
71
+ const entries = buildChangelogEntries(config, newTag, {
72
+ tagPattern,
73
+ includePaths: contributingPaths
74
+ });
75
+ if (!dryRun) {
76
+ writeChangelogJson(changelogJsonPath, entries);
77
+ }
78
+ changelogJsonFiles.push(changelogJsonPath);
79
+ }
80
+ const firstChangelogJsonPath = changelogJsonFiles[0];
81
+ if (withReleaseNotes === true && config.changelogJson.enabled && firstChangelogJsonPath !== void 0) {
82
+ const sectionOrder = deriveSectionOrder(workTypes);
83
+ writeReleaseNotesPreviews({
84
+ workspacePath: ROOT_CHANGELOG_PATH,
85
+ tag: newTag,
86
+ changelogJsonPath: firstChangelogJsonPath,
87
+ sectionOrder,
88
+ dryRun
89
+ });
90
+ }
91
+ tags.push(newTag);
92
+ modifiedFiles.push(ROOT_PACKAGE_FILE, ...changelogFiles, ...changelogJsonFiles);
93
+ const result = {
94
+ status: "released",
95
+ commitCount: commits.length,
96
+ parsedCommitCount,
97
+ releaseType,
98
+ currentVersion: bump.currentVersion,
99
+ newVersion: bump.newVersion,
100
+ tag: newTag,
101
+ bumpedFiles: bump.files,
102
+ changelogFiles,
103
+ commits
104
+ };
105
+ if (tag !== void 0) {
106
+ result.previousTag = tag;
107
+ }
108
+ if (unparseableCommits !== void 0) {
109
+ result.unparseableCommits = unparseableCommits;
110
+ }
111
+ if (policyViolations !== void 0) {
112
+ result.policyViolations = policyViolations;
113
+ }
114
+ if (bumpOverride !== void 0) {
115
+ result.bumpOverride = bumpOverride;
116
+ }
117
+ return result;
118
+ }
119
+ export {
120
+ releasePrepareProject
121
+ };
@@ -26,7 +26,8 @@ function renderReleaseNotesSingle(entry, options) {
26
26
  }
27
27
  lines.push(`### ${section.title}`, "");
28
28
  for (const [index, item] of section.items.entries()) {
29
- lines.push(`- ${item.description}`);
29
+ const prefix = item.breaking === true ? "\u{1F6A8} **Breaking:** " : "";
30
+ lines.push(`- ${prefix}${item.description}`);
30
31
  if (item.body !== void 0 && item.body.length > 0) {
31
32
  lines.push("", ...indentBodyLines(item.body));
32
33
  if (index < section.items.length - 1) {
@@ -1,6 +1,6 @@
1
1
  import { bold, dim, sectionHeader } from "./format.js";
2
2
  function reportPrepare(result) {
3
- const isMultiWorkspace = result.workspaces.some((w) => w.name !== void 0);
3
+ const isMultiWorkspace = result.workspaces.some((w) => w.name !== void 0) || result.project !== void 0;
4
4
  if (isMultiWorkspace) {
5
5
  return formatMultiWorkspace(result);
6
6
  }
@@ -18,35 +18,31 @@ function formatSingleWorkspace(result) {
18
18
  lines.push(dim(` Parsed ${workspace.parsedCommitCount} typed commits`));
19
19
  }
20
20
  formatUnparseableWarning(lines, workspace);
21
+ formatPolicyViolations(lines, workspace.policyViolations);
21
22
  if (workspace.status === "skipped") {
22
- lines.push(`\u23ED\uFE0F ${workspace.skipReason ?? "Skipped"}`);
23
+ lines.push(`\u23ED\uFE0F ${workspace.skipReason}`);
23
24
  return lines.join("\n");
24
25
  }
25
26
  if (workspace.setVersion !== void 0) {
26
27
  lines.push(` Using version override: ${workspace.setVersion}`);
27
- } else if (workspace.parsedCommitCount === void 0 && workspace.releaseType !== void 0) {
28
- lines.push(` Using bump override: ${workspace.releaseType}`);
28
+ } else if (workspace.bumpOverride !== void 0) {
29
+ lines.push(` Using bump override: ${workspace.bumpOverride}`);
29
30
  }
30
31
  if (workspace.releaseType !== void 0) {
31
32
  lines.push(dim(`Bumping versions (${workspace.releaseType})...`));
32
33
  } else if (workspace.setVersion !== void 0) {
33
34
  lines.push(dim(`Bumping versions (version override)...`));
34
35
  }
35
- if (workspace.currentVersion !== void 0 && workspace.newVersion !== void 0) {
36
- if (workspace.setVersion !== void 0) {
37
- lines.push(`\u{1F4E6} ${workspace.currentVersion} \u2192 ${bold(workspace.newVersion)} (version override)`);
38
- } else if (workspace.releaseType !== void 0) {
39
- lines.push(`\u{1F4E6} ${workspace.currentVersion} \u2192 ${bold(workspace.newVersion)} (${workspace.releaseType})`);
40
- }
36
+ if (workspace.setVersion !== void 0) {
37
+ lines.push(`\u{1F4E6} ${workspace.currentVersion} \u2192 ${bold(workspace.newVersion)} (version override)`);
38
+ } else if (workspace.releaseType !== void 0) {
39
+ lines.push(`\u{1F4E6} ${workspace.currentVersion} \u2192 ${bold(workspace.newVersion)} (${workspace.releaseType})`);
41
40
  }
42
41
  formatBumpFiles(lines, workspace, result.dryRun);
43
42
  lines.push(dim("Generating changelogs..."));
44
43
  formatChangelogFiles(lines, workspace, result.dryRun);
45
44
  formatFormatCommand(lines, result);
46
- lines.push(`\u2705 Release preparation complete.`);
47
- if (workspace.tag !== void 0) {
48
- lines.push(` \u{1F3F7}\uFE0F ${bold(workspace.tag)}`);
49
- }
45
+ lines.push(`\u2705 Release preparation complete.`, ` \u{1F3F7}\uFE0F ${bold(workspace.tag)}`);
50
46
  return lines.join("\n");
51
47
  }
52
48
  function formatMultiWorkspace(result) {
@@ -54,6 +50,9 @@ function formatMultiWorkspace(result) {
54
50
  for (const workspace of result.workspaces) {
55
51
  formatWorkspaceSection(lines, workspace, result.dryRun);
56
52
  }
53
+ if (result.project !== void 0) {
54
+ formatProjectSection(lines, result.project, result.dryRun);
55
+ }
57
56
  formatFormatCommand(lines, result);
58
57
  formatWarnings(lines, result);
59
58
  if (result.tags.length > 0) {
@@ -68,6 +67,60 @@ function formatMultiWorkspace(result) {
68
67
  }
69
68
  return lines.join("\n");
70
69
  }
70
+ function formatProjectSection(lines, project, dryRun) {
71
+ lines.push(`
72
+ ${sectionHeader("project")}`);
73
+ const since = project.previousTag === void 0 ? "(no previous release found)" : `since ${project.previousTag}`;
74
+ lines.push(dim(` Found ${project.commitCount} commits ${since}`));
75
+ formatPolicyViolations(lines, project.policyViolations, " ");
76
+ if (project.status === "skipped") {
77
+ lines.push(` \u23ED\uFE0F ${project.skipReason}`);
78
+ return;
79
+ }
80
+ const { releaseType, currentVersion, newVersion, tag } = project;
81
+ if (project.parsedCommitCount > 0) {
82
+ lines.push(dim(` Parsed ${project.parsedCommitCount} typed commits`));
83
+ }
84
+ if (project.bumpOverride !== void 0) {
85
+ lines.push(` Using bump override: ${project.bumpOverride}`);
86
+ }
87
+ formatProjectUnparseable(lines, project);
88
+ lines.push(
89
+ dim(` Bumping versions (${releaseType})...`),
90
+ ` \u{1F4E6} ${currentVersion} \u2192 ${bold(newVersion)} (${releaseType})`
91
+ );
92
+ for (const file of project.bumpedFiles) {
93
+ if (dryRun) {
94
+ lines.push(dim(` [dry-run] Would bump ${file}`));
95
+ } else {
96
+ lines.push(dim(` Bumped ${file}`));
97
+ }
98
+ }
99
+ lines.push(dim(" Generating changelogs..."));
100
+ for (const file of project.changelogFiles) {
101
+ if (dryRun) {
102
+ lines.push(dim(` [dry-run] Would run: npx --yes git-cliff ... --output ${file}`));
103
+ } else {
104
+ lines.push(dim(` Generating changelog: ${file}`));
105
+ }
106
+ }
107
+ lines.push(` \u{1F3F7}\uFE0F ${bold(tag)}`);
108
+ }
109
+ function formatProjectUnparseable(lines, project) {
110
+ const unparseable = project.unparseableCommits;
111
+ if (unparseable === void 0 || unparseable.length === 0) {
112
+ return;
113
+ }
114
+ const count = unparseable.length;
115
+ const isPatchFloor = project.parsedCommitCount === 0;
116
+ const suffix = isPatchFloor ? " (defaulting to patch bump)" : "";
117
+ lines.push(` \u26A0\uFE0F ${count} commit${count === 1 ? "" : "s"} could not be parsed${suffix}`);
118
+ for (const commit of unparseable) {
119
+ const shortHash = commit.hash.slice(0, 7);
120
+ const truncatedMessage = commit.message.length > 72 ? `${commit.message.slice(0, 69)}...` : commit.message;
121
+ lines.push(` \xB7 ${shortHash} ${truncatedMessage}`);
122
+ }
123
+ }
71
124
  function formatWorkspaceSection(lines, workspace, dryRun) {
72
125
  if (workspace.name !== void 0) {
73
126
  lines.push(`
@@ -76,35 +129,34 @@ ${sectionHeader(workspace.name)}`);
76
129
  const since = workspace.previousTag === void 0 ? "(no previous release found)" : `since ${workspace.previousTag}`;
77
130
  lines.push(dim(` Found ${workspace.commitCount} commits ${since}`));
78
131
  if (workspace.status === "skipped") {
79
- lines.push(` \u23ED\uFE0F ${workspace.skipReason ?? "Skipped"}`);
132
+ lines.push(` \u23ED\uFE0F ${workspace.skipReason}`);
80
133
  return;
81
134
  }
82
135
  const { propagatedFrom } = workspace;
83
136
  const isPropagatedOnly = propagatedFrom !== void 0 && workspace.commitCount === 0;
84
137
  formatCommitSummary(lines, workspace, propagatedFrom, isPropagatedOnly);
85
138
  formatUnparseableWarning(lines, workspace, " ");
139
+ formatPolicyViolations(lines, workspace.policyViolations, " ");
86
140
  formatBumpLabels(lines, workspace, isPropagatedOnly);
87
141
  formatVersionLine(lines, workspace, propagatedFrom, isPropagatedOnly);
88
142
  formatBumpFiles(lines, workspace, dryRun, " ");
89
143
  lines.push(dim(" Generating changelogs..."));
90
144
  formatChangelogFiles(lines, workspace, dryRun, " ");
91
- if (workspace.tag !== void 0) {
92
- lines.push(` \u{1F3F7}\uFE0F ${bold(workspace.tag)}`);
93
- }
145
+ lines.push(` \u{1F3F7}\uFE0F ${bold(workspace.tag)}`);
94
146
  }
95
147
  function formatCommitSummary(lines, workspace, propagatedFrom, isPropagatedOnly) {
96
148
  if (isPropagatedOnly && propagatedFrom !== void 0) {
97
149
  const depNames = propagatedFrom.map((p) => p.packageName).join(", ");
98
150
  lines.push(dim(` 0 commits (bumped via dependency: ${depNames})`));
99
- } else if (workspace.parsedCommitCount !== void 0) {
151
+ } else if (workspace.parsedCommitCount !== void 0 && workspace.parsedCommitCount > 0) {
100
152
  lines.push(dim(` Parsed ${workspace.parsedCommitCount} typed commits`));
101
153
  }
102
154
  }
103
155
  function formatBumpLabels(lines, workspace, isPropagatedOnly) {
104
156
  if (workspace.setVersion !== void 0) {
105
157
  lines.push(` Using version override: ${workspace.setVersion}`);
106
- } else if (workspace.parsedCommitCount === void 0 && workspace.releaseType !== void 0 && !isPropagatedOnly) {
107
- lines.push(` Using bump override: ${workspace.releaseType}`);
158
+ } else if (workspace.bumpOverride !== void 0 && !isPropagatedOnly) {
159
+ lines.push(` Using bump override: ${workspace.bumpOverride}`);
108
160
  }
109
161
  if (workspace.releaseType !== void 0) {
110
162
  lines.push(dim(` Bumping versions (${workspace.releaseType})...`));
@@ -113,9 +165,6 @@ function formatBumpLabels(lines, workspace, isPropagatedOnly) {
113
165
  }
114
166
  }
115
167
  function formatVersionLine(lines, workspace, propagatedFrom, isPropagatedOnly) {
116
- if (workspace.currentVersion === void 0 || workspace.newVersion === void 0) {
117
- return;
118
- }
119
168
  if (workspace.setVersion !== void 0) {
120
169
  lines.push(` \u{1F4E6} ${workspace.currentVersion} \u2192 ${bold(workspace.newVersion)} (version override)`);
121
170
  } else if (workspace.releaseType !== void 0) {
@@ -156,6 +205,21 @@ function formatUnparseableWarning(lines, workspace, indent = "") {
156
205
  lines.push(`${indent} \xB7 ${shortHash} ${truncatedMessage}`);
157
206
  }
158
207
  }
208
+ function formatPolicyViolations(lines, violations, indent = "") {
209
+ if (violations === void 0 || violations.length === 0) {
210
+ return;
211
+ }
212
+ const count = violations.length;
213
+ lines.push(`${indent} \u26A0\uFE0F ${count} policy violation${count === 1 ? "" : "s"}:`);
214
+ for (const violation of violations) {
215
+ const shortHash = violation.commitHash.slice(0, 7);
216
+ const subject = violation.commitSubject;
217
+ const truncatedSubject = subject.length > 72 ? `${subject.slice(0, 69)}...` : subject;
218
+ lines.push(
219
+ `${indent} \xB7 ${shortHash} '${truncatedSubject}' \u2014 type '${violation.type}' at ${violation.surface} surface`
220
+ );
221
+ }
222
+ }
159
223
  function formatPropagationSuffix(propagatedFrom) {
160
224
  if (propagatedFrom === void 0 || propagatedFrom.length === 0) {
161
225
  return "";
@@ -10,15 +10,25 @@ async function resolveCommandTags(tags) {
10
10
  process.exit(1);
11
11
  }
12
12
  let workspaces;
13
- if (discoveredPaths !== void 0) {
14
- try {
13
+ let singleWorkspace;
14
+ try {
15
+ if (discoveredPaths === void 0) {
16
+ singleWorkspace = deriveWorkspaceConfig(".");
17
+ } else {
15
18
  workspaces = discoveredPaths.map((workspacePath) => deriveWorkspaceConfig(workspacePath));
16
- } catch (error) {
17
- console.error(`Error resolving workspaces: ${error instanceof Error ? error.message : String(error)}`);
18
- process.exit(1);
19
19
  }
20
+ } catch (error) {
21
+ console.error(`Error resolving workspaces: ${error instanceof Error ? error.message : String(error)}`);
22
+ process.exit(1);
23
+ }
24
+ let resolvedTags;
25
+ if (workspaces !== void 0) {
26
+ resolvedTags = resolveReleaseTags({ workspaces });
27
+ } else if (singleWorkspace !== void 0) {
28
+ resolvedTags = resolveReleaseTags({ singleWorkspace });
29
+ } else {
30
+ throw new Error("resolveCommandTags: invariant violated \u2014 neither workspaces nor singleWorkspace was derived");
20
31
  }
21
- let resolvedTags = resolveReleaseTags(workspaces);
22
32
  if (resolvedTags.length === 0) {
23
33
  console.error("Error: No release tags found on HEAD. Create tags with `release-kit tag` first.");
24
34
  process.exit(1);
@@ -3,5 +3,12 @@ export interface ResolvedTag {
3
3
  tag: string;
4
4
  dir: string;
5
5
  workspacePath: string;
6
+ isPublishable: boolean;
6
7
  }
7
- export declare function resolveReleaseTags(workspaces?: readonly WorkspaceConfig[]): ResolvedTag[];
8
+ type ResolveReleaseTagsArgs = {
9
+ workspaces: readonly WorkspaceConfig[];
10
+ } | {
11
+ singleWorkspace: WorkspaceConfig;
12
+ };
13
+ export declare function resolveReleaseTags(args?: ResolveReleaseTagsArgs): ResolvedTag[];
14
+ export {};
@@ -1,23 +1,27 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  const VERSION_PATTERN = /^v\d+\.\d+\.\d+/;
3
3
  const SEMVER_SUFFIX_PATTERN = /^\d+\.\d+\.\d+/;
4
- function resolveReleaseTags(workspaces) {
4
+ function resolveReleaseTags(args) {
5
5
  const output = execFileSync("git", ["tag", "--points-at", "HEAD"], { encoding: "utf8" });
6
6
  const tags = output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
7
- if (workspaces === void 0) {
7
+ if (args === void 0) {
8
8
  return resolveSinglePackageTags(tags);
9
9
  }
10
- return resolveMonorepoTags(tags, workspaces);
10
+ if ("workspaces" in args) {
11
+ return resolveMonorepoTags(tags, args.workspaces);
12
+ }
13
+ return resolveSinglePackageTags(tags, args.singleWorkspace);
11
14
  }
12
- function resolveSinglePackageTags(tags) {
15
+ function resolveSinglePackageTags(tags, singleWorkspace) {
13
16
  const matched = tags.filter((tag) => VERSION_PATTERN.test(tag));
17
+ const isPublishable = singleWorkspace?.isPublishable ?? true;
14
18
  if (matched.length > 1) {
15
19
  console.warn(
16
20
  `Warning: Multiple version tags found on HEAD: ${matched.join(", ")}. Publishing the same package multiple times is almost certainly unintended. Using only the first tag.`
17
21
  );
18
- return matched.slice(0, 1).map((tag) => ({ tag, dir: ".", workspacePath: "." }));
22
+ return matched.slice(0, 1).map((tag) => ({ tag, dir: ".", workspacePath: ".", isPublishable }));
19
23
  }
20
- return matched.map((tag) => ({ tag, dir: ".", workspacePath: "." }));
24
+ return matched.map((tag) => ({ tag, dir: ".", workspacePath: ".", isPublishable }));
21
25
  }
22
26
  function resolveMonorepoTags(tags, workspaces) {
23
27
  const sortedWorkspaces = [...workspaces].sort((a, b) => b.tagPrefix.length - a.tagPrefix.length);
@@ -25,7 +29,7 @@ function resolveMonorepoTags(tags, workspaces) {
25
29
  for (const tag of tags) {
26
30
  const match = findMatchingWorkspace(tag, sortedWorkspaces);
27
31
  if (match !== void 0) {
28
- resolved.push({ tag, dir: match.dir, workspacePath: match.workspacePath });
32
+ resolved.push({ tag, dir: match.dir, workspacePath: match.workspacePath, isPublishable: match.isPublishable });
29
33
  }
30
34
  }
31
35
  return resolved;
@@ -0,0 +1,2 @@
1
+ import { type StdioOptions } from 'node:child_process';
2
+ export declare function runGitCliff(cliffConfigPath: string, cliffArgs: readonly string[], stdio: StdioOptions): string;
@@ -0,0 +1,27 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { copyFileSync, mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ function runGitCliff(cliffConfigPath, cliffArgs, stdio) {
6
+ let configPath = cliffConfigPath;
7
+ let tempDir;
8
+ try {
9
+ if (cliffConfigPath.endsWith(".template")) {
10
+ tempDir = mkdtempSync(join(tmpdir(), "cliff-"));
11
+ configPath = join(tempDir, "cliff.toml");
12
+ copyFileSync(cliffConfigPath, configPath);
13
+ }
14
+ return execFileSync("npx", ["--prefer-offline", "--yes", "git-cliff", "--config", configPath, ...cliffArgs], {
15
+ encoding: "utf8",
16
+ stdio,
17
+ env: { ...process.env, npm_config_progress: "false" }
18
+ });
19
+ } finally {
20
+ if (tempDir !== void 0) {
21
+ rmSync(tempDir, { recursive: true, force: true });
22
+ }
23
+ }
24
+ }
25
+ export {
26
+ runGitCliff
27
+ };
@@ -0,0 +1 @@
1
+ export declare function stripEmojiPrefix(value: string): string;
@@ -0,0 +1,7 @@
1
+ const LEADING_EMOJI = /^\p{Extended_Pictographic}️? /u;
2
+ function stripEmojiPrefix(value) {
3
+ return value.replace(LEADING_EMOJI, "");
4
+ }
5
+ export {
6
+ stripEmojiPrefix
7
+ };
@@ -0,0 +1,10 @@
1
+ export interface SyncResult {
2
+ exitCode: 0 | 2 | 3;
3
+ message: string;
4
+ }
5
+ export interface SyncWorkTypesDependencies {
6
+ localPath?: string;
7
+ fetch?: typeof globalThis.fetch;
8
+ upstreamUrl?: string;
9
+ }
10
+ export declare function syncWorkTypes(dependencies?: SyncWorkTypesDependencies): Promise<SyncResult>;
@@ -0,0 +1,90 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { UPSTREAM_WORK_TYPES_URL } from "./checkWorkTypesDrift.js";
5
+ import { isRecord } from "./typeGuards.js";
6
+ import { errorMessage, hasExpectedTopLevelShape } from "./workTypesUtils.js";
7
+ function resolveDefaultLocalPath() {
8
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
9
+ return resolve(moduleDir, "work-types.json");
10
+ }
11
+ function extractLocalSchemaUrl(content) {
12
+ let parsed;
13
+ try {
14
+ parsed = JSON.parse(content);
15
+ } catch {
16
+ return void 0;
17
+ }
18
+ if (!isRecord(parsed)) {
19
+ return void 0;
20
+ }
21
+ const schema = parsed.$schema;
22
+ return typeof schema === "string" ? schema : void 0;
23
+ }
24
+ async function syncWorkTypes(dependencies = {}) {
25
+ const localPath = dependencies.localPath ?? resolveDefaultLocalPath();
26
+ const fetcher = dependencies.fetch ?? globalThis.fetch;
27
+ const url = dependencies.upstreamUrl ?? UPSTREAM_WORK_TYPES_URL;
28
+ let response;
29
+ try {
30
+ response = await fetcher(url);
31
+ } catch (error) {
32
+ return {
33
+ exitCode: 2,
34
+ message: `Network error fetching upstream work-types.json: ${errorMessage(error)}`
35
+ };
36
+ }
37
+ if (!response.ok) {
38
+ return {
39
+ exitCode: 2,
40
+ message: `Failed to fetch upstream work-types.json: HTTP ${response.status} ${response.statusText}`
41
+ };
42
+ }
43
+ const upstreamText = await response.text();
44
+ let upstreamJson;
45
+ try {
46
+ upstreamJson = JSON.parse(upstreamText);
47
+ } catch (error) {
48
+ return {
49
+ exitCode: 3,
50
+ message: `Upstream work-types.json is not valid JSON: ${errorMessage(error)}`
51
+ };
52
+ }
53
+ if (!hasExpectedTopLevelShape(upstreamJson)) {
54
+ return {
55
+ exitCode: 3,
56
+ message: "Upstream work-types.json does not match the expected schema shape (missing `tiers` or `types`)."
57
+ };
58
+ }
59
+ let priorContent;
60
+ try {
61
+ priorContent = readFileSync(localPath, "utf8");
62
+ } catch {
63
+ priorContent = void 0;
64
+ }
65
+ const localSchemaUrl = priorContent !== void 0 ? extractLocalSchemaUrl(priorContent) : void 0;
66
+ const outputJson = localSchemaUrl !== void 0 ? { $schema: localSchemaUrl, ...upstreamJson } : upstreamJson;
67
+ const formatted = `${JSON.stringify(outputJson, null, 2)}
68
+ `;
69
+ if (priorContent === formatted) {
70
+ return {
71
+ exitCode: 0,
72
+ message: `Local work-types.json already matches upstream (${localPath}).`
73
+ };
74
+ }
75
+ try {
76
+ writeFileSync(localPath, formatted, "utf8");
77
+ } catch (error) {
78
+ return {
79
+ exitCode: 2,
80
+ message: `Failed to write ${localPath}: ${errorMessage(error)}`
81
+ };
82
+ }
83
+ return {
84
+ exitCode: 0,
85
+ message: `Synced work-types.json from ${url} \u2192 ${localPath}.`
86
+ };
87
+ }
88
+ export {
89
+ syncWorkTypes
90
+ };