@williamthorsen/release-kit 10.3.2 → 10.4.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.
@@ -1,6 +1,6 @@
1
1
  /** @noformat — @generated. Do not edit. Compiled by rdy. */
2
2
  /* eslint-disable */
3
- export const __readyupVersion = "0.27.0";
3
+ export const __readyupVersion = "0.28.0";
4
4
 
5
5
 
6
6
  // .readyup/kits/default.ts
@@ -52,7 +52,7 @@ function detectRepoType() {
52
52
 
53
53
  // .readyup/kits/default.ts
54
54
  function getMinVersion() {
55
- const picked = { "version": "10.3.2" };
55
+ const picked = { "version": "10.4.0" };
56
56
  if (typeof picked["version"] !== "string") {
57
57
  throw new TypeError("release-kit/package.json: 'version' must be a string");
58
58
  }
@@ -1,6 +1,6 @@
1
1
  /** @noformat — @generated. Do not edit. Compiled by rdy. */
2
2
  /* eslint-disable */
3
- export const __readyupVersion = "0.27.0";
3
+ export const __readyupVersion = "0.28.0";
4
4
 
5
5
 
6
6
  // .readyup/kits/npm-auto-publish.ts
@@ -5,24 +5,63 @@
5
5
  "checklists": [
6
6
  "release-kit"
7
7
  ],
8
+ "esbuildVersion": "0.28.1",
9
+ "inputs": [
10
+ {
11
+ "hash": "fe239d01",
12
+ "kind": "module",
13
+ "path": "kits/default.ts"
14
+ },
15
+ {
16
+ "hash": "07509c48",
17
+ "kind": "inline",
18
+ "path": "../package.json",
19
+ "paths": [
20
+ "version"
21
+ ]
22
+ },
23
+ {
24
+ "hash": "850b703f",
25
+ "kind": "module",
26
+ "path": "../src/init/detectRepoType.ts"
27
+ },
28
+ {
29
+ "hash": "b6873aba",
30
+ "kind": "module",
31
+ "path": "../src/init/parseJsonRecord.ts"
32
+ },
33
+ {
34
+ "hash": "646fe51b",
35
+ "kind": "module",
36
+ "path": "../src/typeGuards.ts"
37
+ }
38
+ ],
8
39
  "name": "default",
9
40
  "path": "kits/default.js",
10
- "readyupVersion": "0.27.0",
41
+ "readyupVersion": "0.28.0",
11
42
  "source": "kits/default.ts",
12
43
  "sourceHash": "fe239d01",
13
- "targetHash": "6b363809"
44
+ "targetHash": "5413984b"
14
45
  },
15
46
  {
16
47
  "checklists": [
17
48
  "repo",
18
49
  "packages"
19
50
  ],
51
+ "esbuildVersion": "0.28.1",
52
+ "inputs": [
53
+ {
54
+ "hash": "ac484613",
55
+ "kind": "module",
56
+ "path": "kits/npm-auto-publish.ts"
57
+ }
58
+ ],
20
59
  "name": "npm-auto-publish",
21
60
  "path": "kits/npm-auto-publish.js",
22
- "readyupVersion": "0.27.0",
61
+ "readyupVersion": "0.28.0",
23
62
  "source": "kits/npm-auto-publish.ts",
24
63
  "sourceHash": "ac484613",
25
- "targetHash": "2e324215"
64
+ "targetHash": "0636c995"
26
65
  }
27
66
  ]
28
67
  }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,42 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 10.4.0 — 2026-08-17
6
+
7
+ ### 🎉 Features
8
+
9
+ - Let a project release cover repo-root paths (#685)
10
+
11
+ Extends `release-kit`'s project release to a repo whose content lives at the root. Such a repo declares `project: { paths: ['**'] }` and gets the root `CHANGELOG.md`, the root `.meta/changelog.json`, the `--with-release-notes` previews under root `docs/`, the root `package.json` bump, and the project tag that the stage produces.
12
+
13
+ `paths` scopes any project release's window, not only the whole-tree case, and the project tier of `release-kit validate-overrides` with it. A declared value replaces the workspace union rather than extending it; omitting `paths` resolves to that union, so existing consumers see no change.
14
+
15
+ ### ♻️ Refactoring
16
+
17
+ - Adopt silenceConsole across the test suites (#677)
18
+
19
+ Adopts `silenceConsole` from `@williamthorsen/toolbelt.vitest` to replace all hand-rolled `vi.spyOn(console, …)` spies and reduce boilerplate across the repo's test suites. The function returns a disposable, allowing the caller to use `using` to enable restoration of the console method at the end of the test scope.
20
+
21
+ - Adopt `isError` and consolidate errno narrowing on a shared helper (#697)
22
+
23
+ Replaces every hand-rolled `instanceof Error` narrowing with `hasErrnoCode`, a new predicate exported from `@williamthorsen/nmr-core`, or with `isError` from `@williamthorsen/toolbelt.errors`. Previously the errno question was answered by inline copies alongside two private helpers of differing shape; both helpers are removed.
24
+
25
+ Separately, `eslint.config.ts` now bans `instanceof Error` in every position rather than in a ternary alone.
26
+
27
+ ### 🧪 Tests
28
+
29
+ - Replace the stdio spies with captureStdio (#678)
30
+
31
+ Replaces every `process.stdout` and `process.stderr` spy in the test suites with `captureStdio` from `@williamthorsen/toolbelt.testing` and deletes the obviated local capture helpers. `captureStdio` buffers writes rather than recording calls, so assertions that previously used `toHaveBeenCalledWith` instead check `stderr`/`stdout` strings and their chunk arrays.
32
+
33
+ Separately, `unicorn/no-nonstandard-builtin-properties` is off for test files. Its hand-maintained symbol table omits `Symbol.dispose`, which a suite-scoped capture calls to restore the streams, and `schema: []` offers no way to extend the table.
34
+
35
+ - Adopt throwOnProcessExit and captureError across the test suites (#690)
36
+
37
+ Adopts `throwOnProcessExit` from `@williamthorsen/toolbelt.vitest`, replacing every hand-rolled `process.exit` mock in the `release-kit` and `nmr-core` test suites, and `captureError` from `@williamthorsen/toolbelt.testing`, replacing inline try/catch/`instanceof` blocks.
38
+
39
+ Separately, `@williamthorsen/toolbelt.vitest` is now listed in the ReadyUp config, so that its kit is run by `rdy run --packages`.
40
+
5
41
  ## 10.3.2 — 2026-08-13
6
42
 
7
43
  ### ♻️ Refactoring
package/README.md CHANGED
@@ -4,7 +4,17 @@ 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 --><!-- /section:release-notes -->
7
+ <!-- section:release-notes -->
8
+ ## Release notes — v10.4.0 (2026-08-17)
9
+
10
+ ### 🎉 Features
11
+
12
+ - Let a project release cover repo-root paths (#685)
13
+
14
+ Extends `release-kit`'s project release to a repo whose content lives at the root. Such a repo declares `project: { paths: ['**'] }` and gets the root `CHANGELOG.md`, the root `.meta/changelog.json`, the `--with-release-notes` previews under root `docs/`, the root `package.json` bump, and the project tag that the stage produces.
15
+
16
+ `paths` scopes any project release's window, not only the whole-tree case, and the project tier of `release-kit validate-overrides` with it. A declared value replaces the workspace union rather than extending it; omitting `paths` resolves to that union, so existing consumers see no change.
17
+ <!-- /section:release-notes -->
8
18
 
9
19
  ## Installation
10
20
 
@@ -201,33 +211,50 @@ export default config;
201
211
 
202
212
  When configured, each `release-kit prepare` run additionally:
203
213
 
204
- - Computes commits since the last project tag (`<tagPrefix><version>`), filtered to the union of every contributing workspace's paths.
214
+ - Computes commits since the last project tag (`<tagPrefix><version>`), filtered to `paths` (by default, the union of every contributing workspace's paths).
205
215
  - Bumps the root `package.json`'s `version` field using the same bump-derivation rules as workspaces (or the `--bump=...` override).
206
- - Regenerates the root `./CHANGELOG.md` from the structured `ChangelogEntry[]` produced by `git-cliff --context` (scoped to the project's `tagPrefix` and contributing paths) and any matching editorial overrides.
216
+ - Regenerates the root `./CHANGELOG.md` from the structured `ChangelogEntry[]` produced by `git-cliff --context` (scoped to the project's `tagPrefix` and the same `paths`) and any matching editorial overrides.
207
217
  - Emits `./.meta/changelog.json` (when `changelogJson.enabled`).
208
218
  - With `--with-release-notes`, additionally emits `./docs/RELEASE_NOTES.v<version>.md`.
209
219
  - Appends the project tag to `tmp/.release-tags` so `release-kit commit` and `release-kit tag` pick it up alongside per-workspace tags.
210
220
 
211
- If no contributing workspace has commits since the last project tag, the project release is silently skipped — same behavior as a per-workspace skip.
221
+ If no commit under `paths` has landed since the last project tag, the project release is silently skipped — same behavior as a per-workspace skip.
212
222
 
213
223
  #### `ProjectConfig`
214
224
 
215
225
  ```typescript
216
226
  interface ProjectConfig {
227
+ paths?: string[]; // Defaults to the union of every contributing workspace's paths
217
228
  tagPrefix?: string; // Defaults to 'v'
218
229
  }
219
230
  ```
220
231
 
221
- | Field | Default | Description |
222
- | ----------- | ------- | -------------------------------------------------------------------- |
223
- | `tagPrefix` | `'v'` | Prefix for project tags. The full tag is `${tagPrefix}${newVersion}` |
232
+ | Field | Default | Description |
233
+ | ----------- | ----------------------- | -------------------------------------------------------------------- |
234
+ | `paths` | Contributing-path union | Patterns selecting the commits the project release considers |
235
+ | `tagPrefix` | `'v'` | Prefix for project tags. The full tag is `${tagPrefix}${newVersion}` |
236
+
237
+ By default the contributing workspaces are implicit: every non-excluded discovered workspace contributes its `<dir>/**` glob, and the project release considers commits under their union.
238
+
239
+ Declare `paths` to choose the window yourself. Each entry reaches two matchers: `git log` as a pathspec, and `git-cliff --include-path` as a glob. A declared value **replaces** the workspace union rather than extending it: `paths: ['docs/**']` drops every workspace commit from the project release.
240
+
241
+ A repo whose content lives at the root — where no commit matches any `<dir>/**` glob, so the project release would skip every run — declares the whole tree:
242
+
243
+ ```typescript
244
+ const config: ReleaseKitConfig = {
245
+ project: { paths: ['**'] },
246
+ };
247
+ ```
248
+
249
+ `'**'` is the whole-tree pattern, matching root-level files, dotfiles, and nested paths alike.
224
250
 
225
- Contributing workspaces are implicit: every non-excluded discovered workspace contributes. There is no field to override the contributing set in this initial release; if a future consumer needs to release a workspace as a component but exclude it from the project release, that override can be added then.
251
+ Terminate a directory scope with `/**`. The two matchers agree on `'aws/**'` but diverge on a bare `'aws'`, which git reads as the whole subtree and git-cliff matches against nothing; `'.'` is the same trap for the repo root. A release under such an entry finds commits, bumps the version, and writes the tag, while its changelog gains no entry for them — and nothing reports an error.
226
252
 
227
253
  Validation rules:
228
254
 
229
255
  - The root `package.json` must exist and declare a `version` field. release-kit reports an error at config-load time if either is missing.
230
- - The `project` block is rejected in single-package mode (the implicit "all non-excluded workspaces contribute" rule is meaningless in a single-package repo).
256
+ - The `project` block is rejected in single-package mode (the package's own release already covers the whole repo, so a project tier would only duplicate it).
257
+ - `paths`, when declared, must hold at least one non-empty string. An empty array is rejected: it reaches git and git-cliff as no filter at all, which is the inverse of how it reads.
231
258
  - Unknown fields inside `project` are rejected.
232
259
 
233
260
  CLI flag interactions:
@@ -1,6 +1,6 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { readFileSync } from 'node:fs';
3
- import { GIT_OUTPUT_LIMIT, parseArgsOrExit } from '@williamthorsen/nmr-core';
3
+ import { GIT_OUTPUT_LIMIT, hasErrnoCode, parseArgsOrExit } from '@williamthorsen/nmr-core';
4
4
  import { readReleaseTags, RELEASE_SUMMARY_FILE, resolveReleaseTagsPath } from "./releaseFiles.js";
5
5
  const commitFlagSchema = {
6
6
  dryRun: { long: '--dry-run', type: 'boolean' },
@@ -16,7 +16,7 @@ export function commitCommand(argv) {
16
16
  summary = readFileSync(RELEASE_SUMMARY_FILE, 'utf8').trim();
17
17
  }
18
18
  catch (error) {
19
- if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
19
+ if (hasErrnoCode(error, 'ENOENT')) {
20
20
  }
21
21
  else {
22
22
  throw error;
@@ -1,5 +1,5 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { GIT_OUTPUT_LIMIT } from '@williamthorsen/nmr-core';
2
+ import { GIT_OUTPUT_LIMIT, hasErrnoCode } from '@williamthorsen/nmr-core';
3
3
  import { deleteFileIfExists } from "./deleteFileIfExists.js";
4
4
  import { readReleaseTags, RELEASE_SUMMARY_FILE, RELEASE_TAGS_FILE } from "./releaseFiles.js";
5
5
  export function createTags(options) {
@@ -48,7 +48,7 @@ function assertCleanWorkingTree() {
48
48
  execFileSync('git', ['diff', '--quiet', '--cached'], { maxBuffer: GIT_OUTPUT_LIMIT });
49
49
  }
50
50
  catch (error) {
51
- if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
51
+ if (hasErrnoCode(error, 'ENOENT')) {
52
52
  throw error;
53
53
  }
54
54
  throw new Error('Working tree is dirty. Commit or stash changes before tagging, or use `--no-git-checks` to skip this check.', { cause: error });
@@ -1,10 +1,11 @@
1
1
  import { unlinkSync } from 'node:fs';
2
+ import { hasErrnoCode } from '@williamthorsen/nmr-core';
2
3
  export function deleteFileIfExists(filePath) {
3
4
  try {
4
5
  unlinkSync(filePath);
5
6
  }
6
7
  catch (error) {
7
- if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
8
+ if (hasErrnoCode(error, 'ENOENT')) {
8
9
  return;
9
10
  }
10
11
  throw error;
@@ -73,7 +73,7 @@ export function mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage) {
73
73
  if (userConfig?.retiredPackages !== undefined) {
74
74
  assertRetiredPackagesDoNotCollideWithActive(workspaces, userConfig.retiredPackages);
75
75
  }
76
- const project = resolveProjectConfig(userConfig?.project, rootPackage);
76
+ const project = resolveProjectConfig(userConfig?.project, rootPackage, workspaces);
77
77
  const workTypes = resolveWorkTypes(userConfig?.workTypes);
78
78
  const versionPatterns = userConfig?.versionPatterns === undefined ? { ...DEFAULT_VERSION_PATTERNS } : { ...userConfig.versionPatterns };
79
79
  const changelogJson = mergeChangelogJsonConfig(userConfig?.changelogJson);
@@ -161,7 +161,7 @@ function assertRetiredPackagesDoNotCollideWithActive(workspaces, retiredPackages
161
161
  }
162
162
  }
163
163
  }
164
- function resolveProjectConfig(userProject, rootPackage) {
164
+ function resolveProjectConfig(userProject, rootPackage, workspaces) {
165
165
  if (userProject === undefined) {
166
166
  return undefined;
167
167
  }
@@ -171,7 +171,10 @@ function resolveProjectConfig(userProject, rootPackage) {
171
171
  if (rootPackage.version === undefined) {
172
172
  throw new Error(`project block requires root ${ROOT_PACKAGE_JSON_PATH} to have a 'version' field; add a 'version' field to your root package.json`);
173
173
  }
174
- return { tagPrefix: userProject.tagPrefix ?? DEFAULT_PROJECT_TAG_PREFIX };
174
+ return {
175
+ paths: userProject.paths ?? workspaces.flatMap((workspace) => workspace.paths),
176
+ tagPrefix: userProject.tagPrefix ?? DEFAULT_PROJECT_TAG_PREFIX,
177
+ };
175
178
  }
176
179
  function assertNoTagPrefixCollisions(workspaces, retiredPackages, project) {
177
180
  const sources = [];
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
+ import { hasErrnoCode } from '@williamthorsen/nmr-core';
3
4
  import { describeError } from '@williamthorsen/toolbelt.errors';
4
5
  export const RELEASE_TAGS_FILE = 'tmp/.release-tags';
5
6
  export const RELEASE_SUMMARY_FILE = 'tmp/.release-summary';
@@ -21,15 +22,9 @@ export function readReleaseTags() {
21
22
  }
22
23
  function describeTagsReadFailure(error) {
23
24
  const tagsPath = resolveReleaseTagsPath();
24
- if (getErrnoCode(error) === 'ENOENT') {
25
+ if (hasErrnoCode(error, 'ENOENT')) {
25
26
  return `No tags file found at ${tagsPath}. Run \`release-kit prepare\` first.`;
26
27
  }
27
28
  const detail = describeError(error);
28
29
  return `Cannot read the tags file at ${tagsPath}: ${detail}`;
29
30
  }
30
- function getErrnoCode(error) {
31
- if (error instanceof Error && 'code' in error && typeof error.code === 'string') {
32
- return error.code;
33
- }
34
- return undefined;
35
- }
@@ -25,7 +25,7 @@ export function releasePrepareProject(args) {
25
25
  const workTypes = config.workTypes ?? { ...DEFAULT_WORK_TYPES };
26
26
  const versionPatterns = config.versionPatterns ?? { ...DEFAULT_VERSION_PATTERNS };
27
27
  const breakingPolicies = config.breakingPolicies ?? DEFAULT_BREAKING_POLICIES;
28
- const contributingPaths = config.workspaces.flatMap((workspace) => workspace.paths);
28
+ const contributingPaths = project.paths;
29
29
  const { tag, commits } = getCommitsSinceTarget([project.tagPrefix], contributingPaths);
30
30
  const since = tag === undefined ? '(no previous release found)' : `since ${tag}`;
31
31
  const collector = createPolicyViolationCollector();
@@ -33,6 +33,7 @@ export interface ReleaseNotesConfig {
33
33
  shouldInjectIntoReadme: boolean;
34
34
  }
35
35
  export interface ResolvedProjectConfig {
36
+ paths: string[];
36
37
  tagPrefix: string;
37
38
  }
38
39
  export interface PropagationSource {
@@ -142,6 +143,7 @@ export declare const workspaceOverrideSchema: z.ZodObject<{
142
143
  }, z.core.$strict>;
143
144
  export type WorkspaceOverride = z.infer<typeof workspaceOverrideSchema>;
144
145
  export declare const projectConfigSchema: z.ZodObject<{
146
+ paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
145
147
  tagPrefix: z.ZodOptional<z.ZodString>;
146
148
  }, z.core.$strict>;
147
149
  export type ProjectConfig = z.infer<typeof projectConfigSchema>;
@@ -193,6 +195,7 @@ export declare const releaseKitConfigSchema: z.ZodObject<{
193
195
  cliffConfigPath: z.ZodOptional<z.ZodString>;
194
196
  formatCommand: z.ZodOptional<z.ZodString>;
195
197
  project: z.ZodOptional<z.ZodObject<{
198
+ paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
196
199
  tagPrefix: z.ZodOptional<z.ZodString>;
197
200
  }, z.core.$strict>>;
198
201
  releaseNotes: z.ZodOptional<z.ZodObject<{
package/dist/esm/types.js CHANGED
@@ -21,6 +21,7 @@ export const workspaceOverrideSchema = z
21
21
  .strict();
22
22
  export const projectConfigSchema = z
23
23
  .object({
24
+ paths: z.array(z.string().min(1)).min(1).optional(),
24
25
  tagPrefix: z.string().min(1).optional(),
25
26
  })
26
27
  .strict();
@@ -126,9 +126,8 @@ function buildMonorepoInputs(discoveredPaths, userConfig, buildEntries) {
126
126
  filePath: resolveOverridePath('.'),
127
127
  };
128
128
  if (project !== undefined) {
129
- const contributingPaths = config.workspaces.flatMap((workspace) => workspace.paths);
130
129
  const projectTagPattern = buildTagPattern([project.tagPrefix]);
131
- projectScope.hashes = flattenEntriesToHashes(buildEntries(config, projectTagPattern, contributingPaths));
130
+ projectScope.hashes = flattenEntriesToHashes(buildEntries(config, projectTagPattern, project.paths));
132
131
  }
133
132
  return { project: projectScope, workspaces };
134
133
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/release-kit",
3
- "version": "10.3.2",
3
+ "version": "10.4.0",
4
4
  "description": "Version-bumping and changelog-generation toolkit for release workflows",
5
5
  "keywords": [
6
6
  "changelog",
@@ -48,17 +48,19 @@
48
48
  "CHANGELOG.md"
49
49
  ],
50
50
  "dependencies": {
51
- "@williamthorsen/toolbelt.errors": "0.4.0",
51
+ "@williamthorsen/toolbelt.errors": "0.5.0",
52
52
  "glob": "13.0.6",
53
53
  "json-stringify-pretty-compact": "4.0.0",
54
54
  "semver": "7.8.5",
55
55
  "yaml": "2.9.0",
56
56
  "zod": "4.4.3",
57
- "@williamthorsen/nmr-core": "0.10.0"
57
+ "@williamthorsen/nmr-core": "0.10.1"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/semver": "7.8.0",
61
- "readyup": "0.27.0",
61
+ "@williamthorsen/toolbelt.testing": "0.3.0",
62
+ "@williamthorsen/toolbelt.vitest": "0.4.0",
63
+ "readyup": "0.28.0",
62
64
  "smol-toml": "1.7.1"
63
65
  },
64
66
  "engines": {