@williamthorsen/release-kit 5.3.0 → 5.3.2

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 5.3.2 — 2026-06-16
6
+
7
+ ### ♻️ Refactoring
8
+
9
+ - Migrate from js-yaml to yaml (#410)
10
+
11
+ Replaces the `js-yaml` dependency with the `yaml` package for all YAML reading and writing, with no change to behavior or generated output. The swap trims the dependency footprint by dropping a direct dependency and its companion type-definitions package, since `yaml` ships its own types.
12
+
13
+ ## 5.3.1 — 2026-05-19
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - Validate overrides against the full release history (#401)
18
+
19
+ Fixes an issue where `release-kit overrides validate` reported overrides as stale when they targeted commits in past releases, even though `release-kit prepare` correctly matched them. The two commands now agree on which overrides are stale.
20
+
21
+ ### ♻️ Refactoring
22
+
23
+ - Restructure tests and align core package directory with package name (#405)
24
+
25
+ Tests in every package are now typechecked alongside the code they cover, so type breakage in tests fails the build instead of slipping through. The `core` package's workspace directory is renamed to match its package name, so `nmr -F nmr-core ...` and `pnpm --filter nmr-core ...` now resolve where they previously failed.
26
+
5
27
  ## 5.3.0 — 2026-05-10
6
28
 
7
29
  ### 🎉 Features
@@ -435,6 +457,7 @@ All notable changes to this project will be documented in this file.
435
457
  - Cover multi-changelogPaths and error paths (#44)
436
458
 
437
459
  Add three tests for previously untested code paths:
460
+
438
461
  - `releasePrepareMono`: component with two `changelogPaths` entries, asserting `git-cliff` is invoked once per path with the correct `--output` target.
439
462
  - `getCommitsSinceTarget`: `git describe` failure with a non-128 exit status propagates as a wrapped error instead of being swallowed.
440
463
  - `getCommitsSinceTarget`: `git log` failure is wrapped and re-thrown with the commit range in the message.
@@ -451,6 +474,10 @@ All notable changes to this project will be documented in this file.
451
474
 
452
475
  ### 🎉 Features
453
476
 
477
+ - Migrate release-kit from toolbelt (#18)
478
+
479
+ Migrates the complete `@williamthorsen/release-kit` package (v1.0.1) from `williamthorsen/toolbelt` into `packages/release-kit/`, adds shebang preservation to the shared esbuild plugin for CLI binaries, and sets up dogfooding infrastructure so this monorepo uses release-kit for its own releases.
480
+
454
481
  - 🚨 **Breaking:** Slim down release workflow by removing unnecessary pnpm install (#21)
455
482
 
456
483
  Make release-kit self-contained by invoking git-cliff via `npx --yes` instead of requiring it on PATH, and by appending modified file paths to the format command so lightweight formatters like `npx prettier --write` work without a full `pnpm install`. Update init templates, README, and consuming repo config/workflow to reference workflow v3.
package/README.md CHANGED
@@ -4,43 +4,7 @@ Version-bumping and changelog-generation toolkit for release workflows.
4
4
 
5
5
  Provides a self-contained CLI that auto-discovers workspaces from `pnpm-workspace.yaml`, parses conventional commits, determines version bumps, updates `package.json` files, and generates changelogs from `git-cliff --context` output rendered in-process (with optional [editorial overrides](#editorial-overrides)).
6
6
 
7
- <!-- section:release-notes -->
8
- ## Release notes — v5.3.0 (2026-05-10)
9
-
10
- ### 🎉 Features
11
-
12
- - Enable editorial overrides for changelog entries (#387)
13
-
14
- Allows `release-kit` consumers to skip or correct historical changelog entries by means of an overrides file.
15
-
16
- - Decentralize changelog overrides to per-scope .meta/ files (#391)
17
-
18
- Adds support for workspace-scoped editorial-override files for `release-kit`-generated changelogs. A repo-root file applies overrides to every workspace's changelog; a workspace-tier file applies only to that workspace.
19
-
20
- - Add section markers and authenticated upstream fetch (#393)
21
-
22
- A new `markers` block in `work-types.json` describes the breaking-changes emoji and label, making them available for use by consumers.
23
-
24
- `work-types check` and `work-types sync` now authenticate when `GITHUB_TOKEN` is set, so they can reach private upstream repositories.
25
-
26
- - Validate changelog overrides from the command line (#395)
27
-
28
- Adds a `release-kit overrides validate` subcommand that audits every `.meta/changelog-overrides.json` file across the project root and per-workspace scopes in one pass. The command reports schema errors, ambiguous-prefix collisions, and stale-key warnings with tiered exit codes so CI can choose its own failure threshold. The same validation is also available via a library function exported by the package.
29
-
30
- ### 🐛 Bug fixes
31
-
32
- - Suppress git-cliff stale-version warnings on prepare (#373)
33
-
34
- Fixes an issue where `release-kit prepare` repeatedly printed git-cliff's "A new version of git-cliff is available" notice — twice per release unit, so 2 × N times for an N-package monorepo run — while never updating the locally cached git-cliff binary. Each `prepare` run now revalidates the npm cache once before any cliff work, so the binary stays current with upstream releases and the notice no longer surfaces on every per-workspace invocation.
35
-
36
- - Use synthetic changelogs for forced empty-range releases (#376)
37
-
38
- Fixes an issue where `release-kit prepare` with `--force`, `--bump=X`, or `--set-version` would invoke git-cliff against units that had no commits since their last tag, surfacing confusing `WARN git_cliff > There is already a tag (...)` lines (twice per affected unit) and silently leaving `CHANGELOG.md` and `.meta/changelog.json` stale. Empty-range bumps now write a synthetic `Notes / Forced version bump.` entry to both files instead of invoking git-cliff. Applies to all three release stages: single-package, per-workspace, and project. Prior changelog history is preserved on every path.
39
-
40
- - Accept `breakingPolicies` field in config files (#394)
41
-
42
- Fixes an issue where setting `breakingPolicies` in `release-kit.config.ts` was rejected as an unknown field, leaving per-work-type breaking-policy configuration unreachable from the config file. Each entry accepts `'forbidden'`, `'optional'`, or `'required'`; an empty object opts out of enforcement.
43
- <!-- /section:release-notes -->
7
+ <!-- section:release-notes --><!-- /section:release-notes -->
44
8
 
45
9
  ## Installation
46
10
 
@@ -495,7 +459,7 @@ The override file is validated when `release-kit prepare` loads it. Each error n
495
459
 
496
460
  #### Standalone validation: `release-kit overrides validate`
497
461
 
498
- For a fast overrides-only health check (locally or as a CI gate), run:
462
+ For a focused overrides-only health check (locally or as a CI gate), run:
499
463
 
500
464
  ```sh
501
465
  pnpm exec release-kit overrides validate
package/dist/esm/.cache CHANGED
@@ -1 +1 @@
1
- 18bd566d86a692ebdc23649a475e7d5e01562a58df99b6091e222e890cef8f80
1
+ a675735fcc8ee6abbe7201c05a9cec6517a6495d796f61322b141ed79e4e7bcf
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { glob } from "glob";
4
- import { load } from "js-yaml";
4
+ import { parse } from "yaml";
5
5
  import { isRecord } from "./typeGuards.js";
6
6
  async function discoverWorkspaces() {
7
7
  const workspaceFile = "pnpm-workspace.yaml";
@@ -15,7 +15,7 @@ async function discoverWorkspaces() {
15
15
  console.warn(`Warning: Failed to read ${workspaceFile}: ${error instanceof Error ? error.message : String(error)}`);
16
16
  return void 0;
17
17
  }
18
- const parsed = load(content);
18
+ const parsed = parse(content);
19
19
  if (!isRecord(parsed)) {
20
20
  return void 0;
21
21
  }
@@ -1,4 +1,6 @@
1
+ import type { WorkspaceConfig } from './types.ts';
1
2
  export declare function buildTagPattern(tagPrefixes: readonly string[]): string;
3
+ export declare function getAllTagPrefixes(workspace: WorkspaceConfig): string[];
2
4
  export interface GenerateChangelogOptions {
3
5
  tagPattern?: string;
4
6
  includePaths?: string[];
@@ -9,9 +9,13 @@ function buildTagPattern(tagPrefixes) {
9
9
  const escaped = tagPrefixes.map(escapeRegex);
10
10
  return `(${escaped.join("|")})[0-9].*`;
11
11
  }
12
+ function getAllTagPrefixes(workspace) {
13
+ return [workspace.tagPrefix, ...workspace.legacyIdentities?.map((identity) => identity.tagPrefix) ?? []];
14
+ }
12
15
  function escapeRegex(value) {
13
16
  return value.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
14
17
  }
15
18
  export {
16
- buildTagPattern
19
+ buildTagPattern,
20
+ getAllTagPrefixes
17
21
  };
@@ -19,7 +19,7 @@ import { isForwardVersion } from "./compareVersions.js";
19
19
  import { decideRelease } from "./decideRelease.js";
20
20
  import { DEFAULT_BREAKING_POLICIES, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
21
21
  import { detectUndeclaredTagPrefixes } from "./detectUndeclaredTagPrefixes.js";
22
- import { buildTagPattern } from "./generateChangelogs.js";
22
+ import { buildTagPattern, getAllTagPrefixes } from "./generateChangelogs.js";
23
23
  import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
24
24
  import { hasPrettierConfig } from "./hasPrettierConfig.js";
25
25
  import { resolveWorkTypes } from "./loadConfig.js";
@@ -550,9 +550,6 @@ function topologicalSort(releaseSet, graph) {
550
550
  }
551
551
  return { sorted, cyclicDirs };
552
552
  }
553
- function getAllTagPrefixes(workspace) {
554
- return [workspace.tagPrefix, ...workspace.legacyIdentities?.map((identity) => identity.tagPrefix) ?? []];
555
- }
556
553
  export {
557
554
  releasePrepareMono
558
555
  };
@@ -1,6 +1,6 @@
1
1
  import { mkdirSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
- import { dump } from "js-yaml";
3
+ import { stringify } from "yaml";
4
4
  import { loadSyncLabelsConfig, SYNC_LABELS_CONFIG_PATH } from "./loadSyncLabelsConfig.js";
5
5
  import { hashPresetFile } from "./presets.js";
6
6
  import { resolveLabels } from "./resolveLabels.js";
@@ -10,7 +10,7 @@ function formatLabelsYaml(labels, presetHashes) {
10
10
  for (const [name, hash] of [...presetHashes.entries()].sort(([a], [b]) => a.localeCompare(b))) {
11
11
  headerLines.push(`# ${name} preset hash: ${hash}`);
12
12
  }
13
- const yamlBody = dump(labels, { quotingType: "'", forceQuotes: false, lineWidth: -1 });
13
+ const yamlBody = stringify(labels, { singleQuote: true, lineWidth: 0 });
14
14
  return headerLines.join("\n") + "\n" + yamlBody;
15
15
  }
16
16
  async function generateCommand() {
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { findPackageRoot } from "@williamthorsen/nmr-core";
5
- import { load } from "js-yaml";
5
+ import { parse } from "yaml";
6
6
  import { isRecord } from "../typeGuards.js";
7
7
  function resolvePresetPath(presetName) {
8
8
  const root = findPackageRoot(import.meta.url);
@@ -28,7 +28,7 @@ function loadPreset(presetName) {
28
28
  const message = error instanceof Error ? error.message : String(error);
29
29
  throw new Error(`Failed to read preset "${presetName}": ${message}`);
30
30
  }
31
- const parsed = load(content);
31
+ const parsed = parse(content);
32
32
  if (!Array.isArray(parsed)) {
33
33
  throw new TypeError(`Preset "${presetName}" must be a YAML array of label definitions`);
34
34
  }
@@ -1,4 +1,5 @@
1
1
  import { type ValidateAllChangelogOverridesInputs, type ValidateAllChangelogOverridesResult } from './changelogOverrides.ts';
2
+ import type { ChangelogEntry, ReleaseConfig } from './types.ts';
2
3
  export interface ValidateOverridesCommandResult {
3
4
  exitCode: 0 | 1 | 2;
4
5
  message: string;
@@ -6,7 +7,7 @@ export interface ValidateOverridesCommandResult {
6
7
  export interface ValidateOverridesCommandDependencies {
7
8
  discoverWorkspaces?: () => Promise<string[] | undefined>;
8
9
  loadConfig?: () => Promise<unknown>;
9
- collectHashes?: (tagPrefixes: readonly string[], paths?: string[]) => readonly string[];
10
+ buildEntries?: (config: Pick<ReleaseConfig, 'cliffConfigPath' | 'changelogJson'>, tagPattern?: string, includePaths?: string[]) => ChangelogEntry[];
10
11
  validate?: (inputs: ValidateAllChangelogOverridesInputs) => ValidateAllChangelogOverridesResult;
11
12
  }
12
13
  export declare function validateOverridesCommand(dependencies?: ValidateOverridesCommandDependencies): Promise<ValidateOverridesCommandResult>;
@@ -1,15 +1,17 @@
1
+ import { buildChangelogEntries } from "./buildChangelogEntries.js";
1
2
  import {
2
3
  resolveOverridePath,
3
4
  validateAllChangelogOverrides
4
5
  } from "./changelogOverrides.js";
5
6
  import { discoverWorkspaces } from "./discoverWorkspaces.js";
6
- import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
7
+ import { buildTagPattern, getAllTagPrefixes } from "./generateChangelogs.js";
7
8
  import { loadConfig, mergeMonorepoConfig, mergeSinglePackageConfig, readRootPackageVersion } from "./loadConfig.js";
8
9
  import { validateConfig } from "./validateConfig.js";
10
+ const SYNTHETIC_VALIDATE_TAG = "validate-only";
9
11
  async function validateOverridesCommand(dependencies = {}) {
10
12
  const discover = dependencies.discoverWorkspaces ?? discoverWorkspaces;
11
13
  const load = dependencies.loadConfig ?? loadConfig;
12
- const collect = dependencies.collectHashes ?? defaultCollectHashes;
14
+ const buildEntries = dependencies.buildEntries ?? defaultBuildEntries;
13
15
  const validate = dependencies.validate ?? validateAllChangelogOverrides;
14
16
  let userConfig;
15
17
  try {
@@ -25,7 +27,7 @@ async function validateOverridesCommand(dependencies = {}) {
25
27
  }
26
28
  let inputs;
27
29
  try {
28
- inputs = discoveredPaths === void 0 ? buildSinglePackageInputs(userConfig, collect) : buildMonorepoInputs(discoveredPaths, userConfig, collect);
30
+ inputs = discoveredPaths === void 0 ? buildSinglePackageInputs(userConfig, buildEntries) : buildMonorepoInputs(discoveredPaths, userConfig, buildEntries);
29
31
  } catch (error) {
30
32
  return { exitCode: 2, message: `Error resolving overrides scope: ${errorMessage(error)}` };
31
33
  }
@@ -60,11 +62,28 @@ function pluralize(count, noun) {
60
62
  function errorMessage(error) {
61
63
  return error instanceof Error ? error.message : String(error);
62
64
  }
63
- function defaultCollectHashes(tagPrefixes, paths) {
64
- if (tagPrefixes.length === 0) {
65
- return [];
65
+ function defaultBuildEntries(config, tagPattern, includePaths) {
66
+ const options = {};
67
+ if (tagPattern !== void 0) {
68
+ options.tagPattern = tagPattern;
66
69
  }
67
- return getCommitsSinceTarget(tagPrefixes, paths).commits.map((commit) => commit.hash);
70
+ if (includePaths !== void 0) {
71
+ options.includePaths = includePaths;
72
+ }
73
+ return buildChangelogEntries(config, SYNTHETIC_VALIDATE_TAG, options);
74
+ }
75
+ function flattenEntriesToHashes(entries) {
76
+ const hashes = [];
77
+ for (const entry of entries) {
78
+ for (const section of entry.sections) {
79
+ for (const item of section.items) {
80
+ if (item.hash !== void 0) {
81
+ hashes.push(item.hash);
82
+ }
83
+ }
84
+ }
85
+ }
86
+ return hashes;
68
87
  }
69
88
  async function loadAndValidateConfig(load) {
70
89
  let rawConfig;
@@ -83,24 +102,21 @@ async function loadAndValidateConfig(load) {
83
102
  }
84
103
  return config;
85
104
  }
86
- function buildSinglePackageInputs(userConfig, collect) {
105
+ function buildSinglePackageInputs(userConfig, buildEntries) {
87
106
  const config = mergeSinglePackageConfig(userConfig);
88
- const hashes = [...collect([config.tagPrefix])];
107
+ const hashes = flattenEntriesToHashes(buildEntries(config));
89
108
  return {
90
109
  project: { filePath: resolveOverridePath("."), hashes }
91
110
  };
92
111
  }
93
- function buildMonorepoInputs(discoveredPaths, userConfig, collect) {
112
+ function buildMonorepoInputs(discoveredPaths, userConfig, buildEntries) {
94
113
  const rootPackage = readRootPackageVersion();
95
114
  const config = mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage);
96
115
  const workspaces = config.workspaces.map((workspace) => {
97
- const tagPrefixes = [
98
- workspace.tagPrefix,
99
- ...workspace.legacyIdentities?.map((identity) => identity.tagPrefix) ?? []
100
- ];
116
+ const tagPattern = buildTagPattern(getAllTagPrefixes(workspace));
101
117
  return {
102
118
  filePath: resolveOverridePath(workspace.workspacePath),
103
- hashes: [...collect(tagPrefixes, workspace.paths)]
119
+ hashes: flattenEntriesToHashes(buildEntries(config, tagPattern, workspace.paths))
104
120
  };
105
121
  });
106
122
  const project = config.project;
@@ -109,7 +125,8 @@ function buildMonorepoInputs(discoveredPaths, userConfig, collect) {
109
125
  };
110
126
  if (project !== void 0) {
111
127
  const contributingPaths = config.workspaces.flatMap((workspace) => workspace.paths);
112
- projectScope.hashes = [...collect([project.tagPrefix], contributingPaths)];
128
+ const projectTagPattern = buildTagPattern([project.tagPrefix]);
129
+ projectScope.hashes = flattenEntriesToHashes(buildEntries(config, projectTagPattern, contributingPaths));
113
130
  }
114
131
  return { project: projectScope, workspaces };
115
132
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/release-kit",
3
- "version": "5.3.0",
3
+ "version": "5.3.2",
4
4
  "description": "Version-bumping and changelog-generation toolkit for release workflows",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/williamthorsen/node-monorepo-tools/tree/main/packages/release-kit#readme",
@@ -34,14 +34,13 @@
34
34
  "dependencies": {
35
35
  "glob": "13.0.6",
36
36
  "jiti": "2.7.0",
37
- "js-yaml": "4.1.1",
38
37
  "json-stringify-pretty-compact": "4.0.0",
39
- "semver": "7.8.0",
38
+ "semver": "7.8.4",
39
+ "yaml": "2.9.0",
40
40
  "zod": "4.4.3",
41
- "@williamthorsen/nmr-core": "0.3.1"
41
+ "@williamthorsen/nmr-core": "0.3.2"
42
42
  },
43
43
  "devDependencies": {
44
- "@types/js-yaml": "4.0.9",
45
44
  "@types/semver": "7.7.1",
46
45
  "smol-toml": "1.6.1"
47
46
  },