@williamthorsen/release-kit 5.1.0 → 5.2.1

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 (50) hide show
  1. package/CHANGELOG.md +113 -65
  2. package/README.md +147 -70
  3. package/cliff.toml.template +26 -17
  4. package/dist/esm/.cache +1 -1
  5. package/dist/esm/bin/release-kit.js +96 -3
  6. package/dist/esm/buildChangelogEntries.d.ts +1 -0
  7. package/dist/esm/buildChangelogEntries.js +39 -25
  8. package/dist/esm/checkWorkTypesDrift.d.ts +11 -0
  9. package/dist/esm/checkWorkTypesDrift.js +110 -0
  10. package/dist/esm/collectPolicyViolations.d.ts +6 -0
  11. package/dist/esm/collectPolicyViolations.js +15 -0
  12. package/dist/esm/createGithubRelease.d.ts +12 -2
  13. package/dist/esm/createGithubRelease.js +12 -8
  14. package/dist/esm/createGithubReleaseCommand.js +2 -7
  15. package/dist/esm/decideRelease.d.ts +3 -0
  16. package/dist/esm/decideRelease.js +19 -3
  17. package/dist/esm/defaults.d.ts +7 -0
  18. package/dist/esm/defaults.js +41 -20
  19. package/dist/esm/deriveWorkspaceConfig.js +3 -0
  20. package/dist/esm/determineBumpFromCommits.d.ts +6 -1
  21. package/dist/esm/determineBumpFromCommits.js +9 -3
  22. package/dist/esm/generateChangelogs.js +14 -29
  23. package/dist/esm/loadConfig.js +14 -22
  24. package/dist/esm/parseCommitMessage.d.ts +8 -2
  25. package/dist/esm/parseCommitMessage.js +32 -3
  26. package/dist/esm/publishCommand.js +21 -2
  27. package/dist/esm/releasePrepare.js +39 -15
  28. package/dist/esm/releasePrepareMono.js +26 -3
  29. package/dist/esm/releasePrepareProject.js +13 -1
  30. package/dist/esm/renderReleaseNotes.js +2 -1
  31. package/dist/esm/reportPrepare.js +18 -0
  32. package/dist/esm/resolveCommandTags.js +16 -6
  33. package/dist/esm/resolveReleaseTags.d.ts +8 -1
  34. package/dist/esm/resolveReleaseTags.js +11 -7
  35. package/dist/esm/runGitCliff.d.ts +2 -0
  36. package/dist/esm/runGitCliff.js +27 -0
  37. package/dist/esm/stripEmojiPrefix.d.ts +1 -0
  38. package/dist/esm/stripEmojiPrefix.js +7 -0
  39. package/dist/esm/syncWorkTypes.d.ts +10 -0
  40. package/dist/esm/syncWorkTypes.js +90 -0
  41. package/dist/esm/types.d.ts +15 -0
  42. package/dist/esm/work-types.json +127 -0
  43. package/dist/esm/work-types.schema.json +73 -0
  44. package/dist/esm/workTypesData.d.ts +14 -0
  45. package/dist/esm/workTypesData.js +59 -0
  46. package/dist/esm/workTypesUtils.d.ts +5 -0
  47. package/dist/esm/workTypesUtils.js +16 -0
  48. package/package.json +6 -3
  49. package/dist/esm/version.d.ts +0 -1
  50. package/dist/esm/version.js +0 -4
@@ -2,8 +2,9 @@ import { execSync } from "node:child_process";
2
2
  import { buildChangelogEntries } from "./buildChangelogEntries.js";
3
3
  import { bumpAllVersions, setAllVersions } from "./bumpAllVersions.js";
4
4
  import { resolveChangelogJsonPath, upsertChangelogJson } from "./changelogJsonFile.js";
5
+ import { createPolicyViolationCollector } from "./collectPolicyViolations.js";
5
6
  import { isForwardVersion } from "./compareVersions.js";
6
- import { DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
7
+ import { DEFAULT_BREAKING_POLICIES, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
7
8
  import { determineBumpFromCommits } from "./determineBumpFromCommits.js";
8
9
  import { generateChangelogs } from "./generateChangelogs.js";
9
10
  import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
@@ -16,10 +17,12 @@ function releasePrepare(config, options) {
16
17
  const { dryRun, bumpOverride, setVersion, withReleaseNotes } = options;
17
18
  const workTypes = config.workTypes ?? { ...DEFAULT_WORK_TYPES };
18
19
  const versionPatterns = config.versionPatterns ?? { ...DEFAULT_VERSION_PATTERNS };
20
+ const breakingPolicies = config.breakingPolicies ?? DEFAULT_BREAKING_POLICIES;
19
21
  const { tag, commits } = getCommitsSinceTarget([config.tagPrefix]);
20
22
  let releaseType;
21
23
  let parsedCommitCount;
22
24
  let unparseableCommits;
25
+ const collector = createPolicyViolationCollector();
23
26
  let bump;
24
27
  if (setVersion !== void 0) {
25
28
  const primaryPackageFile = config.packageFiles[0];
@@ -36,7 +39,10 @@ function releasePrepare(config, options) {
36
39
  bump = setAllVersions(config.packageFiles, setVersion, dryRun);
37
40
  } else {
38
41
  if (bumpOverride === void 0) {
39
- const determination = determineBumpFromCommits(commits, workTypes, versionPatterns, config.scopeAliases);
42
+ const determination = determineBumpFromCommits(commits, workTypes, versionPatterns, config.scopeAliases, {
43
+ breakingPolicies,
44
+ onPolicyViolation: collector.onPolicyViolation
45
+ });
40
46
  parsedCommitCount = determination.parsedCommitCount;
41
47
  unparseableCommits = determination.unparseableCommits;
42
48
  releaseType = determination.releaseType;
@@ -44,20 +50,13 @@ function releasePrepare(config, options) {
44
50
  releaseType = bumpOverride;
45
51
  }
46
52
  if (releaseType === void 0) {
47
- const skipped = {
48
- status: "skipped",
53
+ const skipped = buildSkippedSinglePackage({
49
54
  commitCount: commits.length,
50
- skipReason: "No release-worthy changes found. Skipping."
51
- };
52
- if (tag !== void 0) {
53
- skipped.previousTag = tag;
54
- }
55
- if (parsedCommitCount !== void 0) {
56
- skipped.parsedCommitCount = parsedCommitCount;
57
- }
58
- if (unparseableCommits !== void 0) {
59
- skipped.unparseableCommits = unparseableCommits;
60
- }
55
+ previousTag: tag,
56
+ parsedCommitCount,
57
+ unparseableCommits,
58
+ policyViolations: collector.violations.length > 0 ? collector.violations : void 0
59
+ });
61
60
  return {
62
61
  workspaces: [skipped],
63
62
  tags: [],
@@ -100,6 +99,7 @@ function releasePrepare(config, options) {
100
99
  parsedCommitCount,
101
100
  releaseType,
102
101
  unparseableCommits,
102
+ policyViolations: collector.violations.length > 0 ? collector.violations : void 0,
103
103
  setVersion
104
104
  });
105
105
  return {
@@ -109,6 +109,26 @@ function releasePrepare(config, options) {
109
109
  dryRun
110
110
  };
111
111
  }
112
+ function buildSkippedSinglePackage(args) {
113
+ const skipped = {
114
+ status: "skipped",
115
+ commitCount: args.commitCount,
116
+ skipReason: "No release-worthy changes found. Skipping."
117
+ };
118
+ if (args.previousTag !== void 0) {
119
+ skipped.previousTag = args.previousTag;
120
+ }
121
+ if (args.parsedCommitCount !== void 0) {
122
+ skipped.parsedCommitCount = args.parsedCommitCount;
123
+ }
124
+ if (args.unparseableCommits !== void 0) {
125
+ skipped.unparseableCommits = args.unparseableCommits;
126
+ }
127
+ if (args.policyViolations !== void 0) {
128
+ skipped.policyViolations = args.policyViolations;
129
+ }
130
+ return skipped;
131
+ }
112
132
  function buildReleasedSinglePackage(args) {
113
133
  const {
114
134
  commits,
@@ -119,6 +139,7 @@ function buildReleasedSinglePackage(args) {
119
139
  parsedCommitCount,
120
140
  releaseType,
121
141
  unparseableCommits,
142
+ policyViolations,
122
143
  setVersion
123
144
  } = args;
124
145
  const released = {
@@ -143,6 +164,9 @@ function buildReleasedSinglePackage(args) {
143
164
  if (unparseableCommits !== void 0) {
144
165
  released.unparseableCommits = unparseableCommits;
145
166
  }
167
+ if (policyViolations !== void 0) {
168
+ released.policyViolations = policyViolations;
169
+ }
146
170
  if (setVersion !== void 0) {
147
171
  released.setVersion = setVersion;
148
172
  }
@@ -4,9 +4,10 @@ import { buildDependencyGraph } from "./buildDependencyGraph.js";
4
4
  import { buildSyntheticChangelogEntry } from "./buildSyntheticChangelogEntry.js";
5
5
  import { bumpAllVersions, setAllVersions } from "./bumpAllVersions.js";
6
6
  import { resolveChangelogJsonPath, upsertChangelogJson } from "./changelogJsonFile.js";
7
+ import { createPolicyViolationCollector } from "./collectPolicyViolations.js";
7
8
  import { isForwardVersion } from "./compareVersions.js";
8
9
  import { decideRelease } from "./decideRelease.js";
9
- import { DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
10
+ import { DEFAULT_BREAKING_POLICIES, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
10
11
  import { detectUndeclaredTagPrefixes } from "./detectUndeclaredTagPrefixes.js";
11
12
  import { buildTagPattern, generateChangelog } from "./generateChangelogs.js";
12
13
  import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
@@ -83,6 +84,7 @@ function determineDirectBumps(config, options) {
83
84
  }
84
85
  const workTypes = config.workTypes ?? { ...DEFAULT_WORK_TYPES };
85
86
  const versionPatterns = config.versionPatterns ?? { ...DEFAULT_VERSION_PATTERNS };
87
+ const breakingPolicies = config.breakingPolicies ?? DEFAULT_BREAKING_POLICIES;
86
88
  const directBumps = /* @__PURE__ */ new Map();
87
89
  const directResults = /* @__PURE__ */ new Map();
88
90
  const skippedResults = [];
@@ -125,11 +127,13 @@ function determineDirectBumps(config, options) {
125
127
  releaseType: void 0,
126
128
  parsedCommitCount: void 0,
127
129
  unparseableCommits: void 0,
130
+ policyViolations: void 0,
128
131
  bumpOverride: void 0,
129
132
  setVersion
130
133
  });
131
134
  continue;
132
135
  }
136
+ const collector = createPolicyViolationCollector();
133
137
  const decision = tryStage(
134
138
  stageLabel,
135
139
  () => decideRelease({
@@ -139,12 +143,15 @@ function determineDirectBumps(config, options) {
139
143
  workTypes,
140
144
  versionPatterns,
141
145
  scopeAliases: config.scopeAliases,
146
+ breakingPolicies,
147
+ onPolicyViolation: collector.onPolicyViolation,
142
148
  skipReasons: {
143
149
  noCommits: `No commits for ${name} ${since}. Pass --force to release at patch. Skipping.`,
144
150
  noBumpWorthy: `No bump-worthy commits for ${name} ${since}. Pass --force to release at patch (or --force --bump=X for a different level). Skipping.`
145
151
  }
146
152
  })
147
153
  );
154
+ const policyViolations = collector.violations.length > 0 ? collector.violations : void 0;
148
155
  if (decision.outcome === "skip") {
149
156
  skippedResults.push({
150
157
  workspace,
@@ -152,6 +159,7 @@ function determineDirectBumps(config, options) {
152
159
  commitCount: commits.length,
153
160
  parsedCommitCount: decision.parsedCommitCount,
154
161
  unparseableCommits: decision.unparseableCommits,
162
+ policyViolations,
155
163
  skipReason: decision.skipReason
156
164
  });
157
165
  continue;
@@ -164,6 +172,7 @@ function determineDirectBumps(config, options) {
164
172
  releaseType: decision.releaseType,
165
173
  parsedCommitCount: decision.parsedCommitCount,
166
174
  unparseableCommits: decision.unparseableCommits,
175
+ policyViolations,
167
176
  bumpOverride
168
177
  });
169
178
  }
@@ -190,6 +199,9 @@ function collectSkippedWorkspaces(skippedResults, fullReleaseSet) {
190
199
  if (skipped.unparseableCommits !== void 0) {
191
200
  result.unparseableCommits = skipped.unparseableCommits;
192
201
  }
202
+ if (skipped.policyViolations !== void 0) {
203
+ result.policyViolations = skipped.policyViolations;
204
+ }
193
205
  workspaces.push(result);
194
206
  }
195
207
  return workspaces;
@@ -270,7 +282,16 @@ function executeWorkspaceRelease(args) {
270
282
  bumpedFiles: bump.files,
271
283
  changelogFiles
272
284
  };
273
- const previousTag = directResult?.tag ?? previousTags.get(dir);
285
+ attachReleasedWorkspaceOptionals(released, {
286
+ previousTag: directResult?.tag ?? previousTags.get(dir),
287
+ directResult,
288
+ releaseEntry,
289
+ setVersionTarget
290
+ });
291
+ workspaces.push(released);
292
+ }
293
+ function attachReleasedWorkspaceOptionals(released, args) {
294
+ const { previousTag, directResult, releaseEntry, setVersionTarget } = args;
274
295
  if (previousTag !== void 0) {
275
296
  released.previousTag = previousTag;
276
297
  }
@@ -286,6 +307,9 @@ function executeWorkspaceRelease(args) {
286
307
  if (directResult?.unparseableCommits !== void 0) {
287
308
  released.unparseableCommits = directResult.unparseableCommits;
288
309
  }
310
+ if (directResult?.policyViolations !== void 0) {
311
+ released.policyViolations = directResult.policyViolations;
312
+ }
289
313
  if (releaseEntry.propagatedFrom !== void 0) {
290
314
  released.propagatedFrom = releaseEntry.propagatedFrom;
291
315
  }
@@ -295,7 +319,6 @@ function executeWorkspaceRelease(args) {
295
319
  if (setVersionTarget !== void 0) {
296
320
  released.setVersion = setVersionTarget;
297
321
  }
298
- workspaces.push(released);
299
322
  }
300
323
  function generateWorkspaceChangelogs(args) {
301
324
  const {
@@ -1,8 +1,9 @@
1
1
  import { buildChangelogEntries } from "./buildChangelogEntries.js";
2
2
  import { bumpAllVersions } from "./bumpAllVersions.js";
3
3
  import { resolveChangelogJsonPath, writeChangelogJson } from "./changelogJsonFile.js";
4
+ import { createPolicyViolationCollector } from "./collectPolicyViolations.js";
4
5
  import { decideRelease } from "./decideRelease.js";
5
- import { DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
6
+ import { DEFAULT_BREAKING_POLICIES, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
6
7
  import { buildTagPattern, generateChangelog } from "./generateChangelogs.js";
7
8
  import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
8
9
  import { deriveSectionOrder } from "./resolveReleaseNotesConfig.js";
@@ -18,9 +19,11 @@ function releasePrepareProject(args) {
18
19
  }
19
20
  const workTypes = config.workTypes ?? { ...DEFAULT_WORK_TYPES };
20
21
  const versionPatterns = config.versionPatterns ?? { ...DEFAULT_VERSION_PATTERNS };
22
+ const breakingPolicies = config.breakingPolicies ?? DEFAULT_BREAKING_POLICIES;
21
23
  const contributingPaths = config.workspaces.flatMap((workspace) => workspace.paths);
22
24
  const { tag, commits } = getCommitsSinceTarget([project.tagPrefix], contributingPaths);
23
25
  const since = tag === void 0 ? "(no previous release found)" : `since ${tag}`;
26
+ const collector = createPolicyViolationCollector();
24
27
  const decision = decideRelease({
25
28
  commits,
26
29
  force,
@@ -28,11 +31,14 @@ function releasePrepareProject(args) {
28
31
  workTypes,
29
32
  versionPatterns,
30
33
  scopeAliases: config.scopeAliases,
34
+ breakingPolicies,
35
+ onPolicyViolation: collector.onPolicyViolation,
31
36
  skipReasons: {
32
37
  noCommits: `No commits ${since}. Pass --force to release at patch. Skipping.`,
33
38
  noBumpWorthy: `No bump-worthy commits ${since}. Pass --force to release at patch (or --force --bump=X for a different level). Skipping.`
34
39
  }
35
40
  });
41
+ const policyViolations = collector.violations.length > 0 ? collector.violations : void 0;
36
42
  if (decision.outcome === "skip") {
37
43
  const skipped = {
38
44
  status: "skipped",
@@ -46,6 +52,9 @@ function releasePrepareProject(args) {
46
52
  if (decision.unparseableCommits !== void 0) {
47
53
  skipped.unparseableCommits = decision.unparseableCommits;
48
54
  }
55
+ if (policyViolations !== void 0) {
56
+ skipped.policyViolations = policyViolations;
57
+ }
49
58
  return skipped;
50
59
  }
51
60
  const { releaseType, parsedCommitCount, unparseableCommits } = decision;
@@ -99,6 +108,9 @@ function releasePrepareProject(args) {
99
108
  if (unparseableCommits !== void 0) {
100
109
  result.unparseableCommits = unparseableCommits;
101
110
  }
111
+ if (policyViolations !== void 0) {
112
+ result.policyViolations = policyViolations;
113
+ }
102
114
  if (bumpOverride !== void 0) {
103
115
  result.bumpOverride = bumpOverride;
104
116
  }
@@ -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) {
@@ -18,6 +18,7 @@ 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
23
  lines.push(`\u23ED\uFE0F ${workspace.skipReason}`);
23
24
  return lines.join("\n");
@@ -71,6 +72,7 @@ function formatProjectSection(lines, project, dryRun) {
71
72
  ${sectionHeader("project")}`);
72
73
  const since = project.previousTag === void 0 ? "(no previous release found)" : `since ${project.previousTag}`;
73
74
  lines.push(dim(` Found ${project.commitCount} commits ${since}`));
75
+ formatPolicyViolations(lines, project.policyViolations, " ");
74
76
  if (project.status === "skipped") {
75
77
  lines.push(` \u23ED\uFE0F ${project.skipReason}`);
76
78
  return;
@@ -134,6 +136,7 @@ ${sectionHeader(workspace.name)}`);
134
136
  const isPropagatedOnly = propagatedFrom !== void 0 && workspace.commitCount === 0;
135
137
  formatCommitSummary(lines, workspace, propagatedFrom, isPropagatedOnly);
136
138
  formatUnparseableWarning(lines, workspace, " ");
139
+ formatPolicyViolations(lines, workspace.policyViolations, " ");
137
140
  formatBumpLabels(lines, workspace, isPropagatedOnly);
138
141
  formatVersionLine(lines, workspace, propagatedFrom, isPropagatedOnly);
139
142
  formatBumpFiles(lines, workspace, dryRun, " ");
@@ -202,6 +205,21 @@ function formatUnparseableWarning(lines, workspace, indent = "") {
202
205
  lines.push(`${indent} \xB7 ${shortHash} ${truncatedMessage}`);
203
206
  }
204
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
+ }
205
223
  function formatPropagationSuffix(propagatedFrom) {
206
224
  if (propagatedFrom === void 0 || propagatedFrom.length === 0) {
207
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
+ };
@@ -3,6 +3,7 @@ export type ChangelogAudience = 'all' | 'dev';
3
3
  export interface ChangelogItem {
4
4
  description: string;
5
5
  body?: string;
6
+ breaking?: boolean;
6
7
  }
7
8
  export interface ChangelogSection {
8
9
  title: string;
@@ -37,6 +38,12 @@ export interface BumpResult {
37
38
  newVersion: string;
38
39
  files: string[];
39
40
  }
41
+ export interface PolicyViolation {
42
+ commitHash: string;
43
+ commitSubject: string;
44
+ type: string;
45
+ surface: 'prefix' | 'body';
46
+ }
40
47
  export interface ReleasedWorkspaceResult {
41
48
  status: 'released';
42
49
  name?: string;
@@ -44,6 +51,7 @@ export interface ReleasedWorkspaceResult {
44
51
  commitCount: number;
45
52
  parsedCommitCount?: number;
46
53
  unparseableCommits?: Commit[];
54
+ policyViolations?: PolicyViolation[];
47
55
  releaseType?: ReleaseType;
48
56
  currentVersion: string;
49
57
  newVersion: string;
@@ -62,6 +70,7 @@ export interface SkippedWorkspaceResult {
62
70
  commitCount: number;
63
71
  parsedCommitCount?: number;
64
72
  unparseableCommits?: Commit[];
73
+ policyViolations?: PolicyViolation[];
65
74
  skipReason: string;
66
75
  }
67
76
  export type WorkspacePrepareResult = ReleasedWorkspaceResult | SkippedWorkspaceResult;
@@ -71,6 +80,7 @@ export interface ReleasedProjectResult {
71
80
  commitCount: number;
72
81
  parsedCommitCount: number;
73
82
  unparseableCommits?: Commit[];
83
+ policyViolations?: PolicyViolation[];
74
84
  releaseType: ReleaseType;
75
85
  currentVersion: string;
76
86
  newVersion: string;
@@ -86,6 +96,7 @@ export interface SkippedProjectResult {
86
96
  commitCount: number;
87
97
  parsedCommitCount: number;
88
98
  unparseableCommits?: Commit[];
99
+ policyViolations?: PolicyViolation[];
89
100
  skipReason: string;
90
101
  }
91
102
  export type ProjectPrepareResult = ReleasedProjectResult | SkippedProjectResult;
@@ -113,6 +124,7 @@ export interface ReleaseKitConfig {
113
124
  workspaces?: WorkspaceOverride[];
114
125
  versionPatterns?: VersionPatterns;
115
126
  workTypes?: Record<string, WorkTypeConfig>;
127
+ breakingPolicies?: Record<string, 'forbidden' | 'optional' | 'required'>;
116
128
  formatCommand?: string;
117
129
  cliffConfigPath?: string;
118
130
  scopeAliases?: Record<string, string>;
@@ -152,6 +164,7 @@ export interface WorkspaceConfig {
152
164
  name: string;
153
165
  tagPrefix: string;
154
166
  workspacePath: string;
167
+ isPublishable: boolean;
155
168
  packageFiles: string[];
156
169
  changelogPaths: string[];
157
170
  paths: string[];
@@ -161,6 +174,7 @@ export interface MonorepoReleaseConfig {
161
174
  workspaces: WorkspaceConfig[];
162
175
  workTypes?: Record<string, WorkTypeConfig>;
163
176
  versionPatterns?: VersionPatterns;
177
+ breakingPolicies?: Record<string, 'forbidden' | 'optional' | 'required'>;
164
178
  formatCommand?: string;
165
179
  cliffConfigPath?: string;
166
180
  scopeAliases?: Record<string, string>;
@@ -174,6 +188,7 @@ export interface ReleaseConfig {
174
188
  changelogPaths: string[];
175
189
  workTypes?: Record<string, WorkTypeConfig>;
176
190
  versionPatterns?: VersionPatterns;
191
+ breakingPolicies?: Record<string, 'forbidden' | 'optional' | 'required'>;
177
192
  formatCommand?: string;
178
193
  cliffConfigPath?: string;
179
194
  scopeAliases?: Record<string, string>;