@williamthorsen/release-kit 10.3.2 → 10.4.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.
@@ -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.29.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.1" };
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.29.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": "bbdcc849",
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.29.0",
11
42
  "source": "kits/default.ts",
12
43
  "sourceHash": "fe239d01",
13
- "targetHash": "6b363809"
44
+ "targetHash": "6ba7f24f"
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.29.0",
23
62
  "source": "kits/npm-auto-publish.ts",
24
63
  "sourceHash": "ac484613",
25
- "targetHash": "2e324215"
64
+ "targetHash": "c7b01e0b"
26
65
  }
27
66
  ]
28
67
  }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,54 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 10.4.1 — 2026-08-18
6
+
7
+ ### 🐛 Bug fixes
8
+
9
+ - Accept `--only` under a project block, skipping the project release (#702)
10
+
11
+ Fixes an issue where `release-kit prepare --only` exited with an error in any repo whose config declares a `project` block, which left the `only` dispatch input that `release-kit init` scaffolds into `release.yaml` unusable. A narrowed run now releases the named workspaces and skips the project release, reporting the skip and the selection that caused it as a warning.
12
+
13
+ - Route release-notes preview warnings through the prepare plan (#705)
14
+
15
+ Fixes an issue where `release-kit prepare --with-release-notes` printed its diagnostics to stderr ahead of the release report, and described a skipped release-notes preview as a skipped README injection. The messages now render as `⚠️` lines inside the report and are worded for release-notes previews; a skip belonging to one workspace names that workspace.
16
+
17
+ ## 10.4.0 — 2026-08-17
18
+
19
+ ### 🎉 Features
20
+
21
+ - Let a project release cover repo-root paths (#685)
22
+
23
+ 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.
24
+
25
+ `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.
26
+
27
+ ### ♻️ Refactoring
28
+
29
+ - Adopt silenceConsole across the test suites (#677)
30
+
31
+ 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.
32
+
33
+ - Adopt `isError` and consolidate errno narrowing on a shared helper (#697)
34
+
35
+ 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.
36
+
37
+ Separately, `eslint.config.ts` now bans `instanceof Error` in every position rather than in a ternary alone.
38
+
39
+ ### 🧪 Tests
40
+
41
+ - Replace the stdio spies with captureStdio (#678)
42
+
43
+ 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.
44
+
45
+ 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.
46
+
47
+ - Adopt throwOnProcessExit and captureError across the test suites (#690)
48
+
49
+ 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.
50
+
51
+ Separately, `@williamthorsen/toolbelt.vitest` is now listed in the ReadyUp config, so that its kit is run by `rdy run --packages`.
52
+
5
53
  ## 10.3.2 — 2026-08-13
6
54
 
7
55
  ### ♻️ Refactoring
package/README.md CHANGED
@@ -4,7 +4,19 @@ 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.1 (2026-08-18)
9
+
10
+ ### 🐛 Bug fixes
11
+
12
+ - Accept `--only` under a project block, skipping the project release (#702)
13
+
14
+ Fixes an issue where `release-kit prepare --only` exited with an error in any repo whose config declares a `project` block, which left the `only` dispatch input that `release-kit init` scaffolds into `release.yaml` unusable. A narrowed run now releases the named workspaces and skips the project release, reporting the skip and the selection that caused it as a warning.
15
+
16
+ - Route release-notes preview warnings through the prepare plan (#705)
17
+
18
+ Fixes an issue where `release-kit prepare --with-release-notes` printed its diagnostics to stderr ahead of the release report, and described a skipped release-notes preview as a skipped README injection. The messages now render as `⚠️` lines inside the report and are worded for release-notes previews; a skip belonging to one workspace names that workspace.
19
+ <!-- /section:release-notes -->
8
20
 
9
21
  ## Installation
10
22
 
@@ -201,33 +213,50 @@ export default config;
201
213
 
202
214
  When configured, each `release-kit prepare` run additionally:
203
215
 
204
- - Computes commits since the last project tag (`<tagPrefix><version>`), filtered to the union of every contributing workspace's paths.
216
+ - Computes commits since the last project tag (`<tagPrefix><version>`), filtered to `paths` (by default, the union of every contributing workspace's paths).
205
217
  - 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.
218
+ - 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
219
  - Emits `./.meta/changelog.json` (when `changelogJson.enabled`).
208
220
  - With `--with-release-notes`, additionally emits `./docs/RELEASE_NOTES.v<version>.md`.
209
221
  - Appends the project tag to `tmp/.release-tags` so `release-kit commit` and `release-kit tag` pick it up alongside per-workspace tags.
210
222
 
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.
223
+ 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
224
 
213
225
  #### `ProjectConfig`
214
226
 
215
227
  ```typescript
216
228
  interface ProjectConfig {
229
+ paths?: string[]; // Defaults to the union of every contributing workspace's paths
217
230
  tagPrefix?: string; // Defaults to 'v'
218
231
  }
219
232
  ```
220
233
 
221
- | Field | Default | Description |
222
- | ----------- | ------- | -------------------------------------------------------------------- |
223
- | `tagPrefix` | `'v'` | Prefix for project tags. The full tag is `${tagPrefix}${newVersion}` |
234
+ | Field | Default | Description |
235
+ | ----------- | ----------------------- | -------------------------------------------------------------------- |
236
+ | `paths` | Contributing-path union | Patterns selecting the commits the project release considers |
237
+ | `tagPrefix` | `'v'` | Prefix for project tags. The full tag is `${tagPrefix}${newVersion}` |
238
+
239
+ 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.
240
+
241
+ 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.
242
+
243
+ 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:
244
+
245
+ ```typescript
246
+ const config: ReleaseKitConfig = {
247
+ project: { paths: ['**'] },
248
+ };
249
+ ```
250
+
251
+ `'**'` is the whole-tree pattern, matching root-level files, dotfiles, and nested paths alike.
224
252
 
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.
253
+ 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
254
 
227
255
  Validation rules:
228
256
 
229
257
  - 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).
258
+ - 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).
259
+ - `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
260
  - Unknown fields inside `project` are rejected.
232
261
 
233
262
  CLI flag interactions:
@@ -235,7 +264,7 @@ CLI flag interactions:
235
264
  - `--dry-run` previews project artifacts alongside workspace artifacts; no files are written.
236
265
  - `--bump=major|minor|patch` propagates to the project release as a level chooser. It does not trigger a release on its own when there are no commits or no bump-worthy commits.
237
266
  - `--force` runs the project release even when no commits or no bump-worthy commits exist since the last project tag. Defaults to patch when `--bump` is not given; combine with `--bump=X` to release at a different level.
238
- - `--only` is rejected with an error when `project` is configured. `--only` is a surgical, single-workspace operation; combining it with a project release that rolls up every contributing workspace would create ambiguous semantics. To release a single workspace, use a config without a `project` block, or run a full `prepare` (no `--only`) to include the project release.
267
+ - `--only` narrows the run to the named workspaces and skips the project release, which is reported as a warning. The project release rolls up every contributing workspace, and `--only` changes which workspaces those are, so rolling up a narrowed set would release a project version covering work the run excluded. Run a full `prepare` (no `--only`) to include the project release.
239
268
  - `--set-version` is rejected with an error when `project` is configured. `--set-version` operates on a single workspace, but a project release rolls up every contributing workspace; the two semantics don't compose. To use `--set-version`, run on a config without a `project` block.
240
269
 
241
270
  `--bump` and `--force` are orthogonal: `--bump` is purely a level chooser; `--force` is purely a release trigger. Examples:
@@ -441,7 +470,7 @@ Per-entry fields are all optional, but at least one must be present per entry:
441
470
  Keys can be either the full 40-character commit SHA or a non-ambiguous prefix. The matcher walks every `ChangelogItem.hash` value present in the entry tree and resolves each override key to its set of matching hashes:
442
471
 
443
472
  - **Exact prefix match (1 hit)** — the override applies. A 7-character prefix is usually unambiguous within a single repo's history; longer prefixes are always safe.
444
- - **No matches (0 hits)** — the override is treated as a stale reference (probably from a rebase or branch deletion) and a warning is logged. The release continues.
473
+ - **No matches (0 hits)** — the override is treated as a stale reference (probably from a rebase or branch deletion) and prepare reports a warning. The release continues.
445
474
  - **Ambiguous prefix (2+ hits)** — the release aborts with an error naming the key and the matching hashes. Lengthen the prefix or use the full SHA.
446
475
 
447
476
  Override application errors abort the run with a non-zero exit; warnings (zero-match keys) are non-fatal and surface on `PrepareResult.warnings`.
@@ -554,9 +583,9 @@ Run release preparation with automatic workspace discovery.
554
583
  | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
555
584
  | `--dry-run` | Preview changes without writing files |
556
585
  | `--bump=major\|minor\|patch` | Override the bump type for all workspaces |
557
- | `--set-version=X.Y.Z` | Set an explicit canonical semver version; bypasses commit-derived bumps. Requires `--only` in monorepo mode (rejected when a `project` block is configured). |
586
+ | `--set-version=X.Y.Z` | Set an explicit canonical semver version; bypasses commit-derived bumps. In monorepo mode it requires `--only`, and is rejected when a `project` block is configured. |
558
587
  | `--force` | Release even when no commits or no bump-worthy commits exist since the last tag. In monorepo/project mode, defaults to patch (combine with `--bump=X` for a different level); in single-package mode, a bare `--force` is rejected — pass `--bump=major\|minor\|patch`. |
559
- | `--only=name1,name2` | Only process the named workspaces (monorepo only; rejected when a `project` block is configured) |
588
+ | `--only=name1,name2` | Only process the named workspaces (monorepo only). When a `project` block is configured, the project release is skipped. |
560
589
  | `--with-release-notes` | Write per-workspace release-notes previews under `<workspacePath>/docs/` |
561
590
  | `--help`, `-h` | Show help |
562
591
 
@@ -569,7 +598,7 @@ Workspace names for `--only` match the package directory name (e.g., `arrays`, `
569
598
  - `<workspacePath>/docs/README.v<version>.md` — the workspace `README.md` with release notes injected at the `<!-- section:release-notes -->` marker.
570
599
  - `<workspacePath>/docs/RELEASE_NOTES.v<version>.md` — the standalone release notes for this version.
571
600
 
572
- The publish-time inject-and-revert lifecycle is unchanged; previews are additive, deterministic, and safe to regenerate. When `changelogJson.enabled` is `false`, the flag logs a warning and skips preview generation. In dry-run mode, planned writes are logged and no files are created.
601
+ The publish-time inject-and-revert lifecycle is unchanged; previews are additive, deterministic, and safe to regenerate. When `changelogJson.enabled` is `false`, prepare reports a warning and skips preview generation. In dry-run mode, planned writes are logged and no files are created.
573
602
 
574
603
  Because preview filenames are versioned, committing them will accumulate files over time. The recommended `.gitignore` entry for monorepos is:
575
604
 
@@ -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;
@@ -1,2 +1,2 @@
1
- export declare const prepareHelpText = "\nUsage: release-kit prepare [options]\n\nRun release preparation with automatic workspace discovery.\n\nOptions:\n --dry-run Run without modifying any files\n --bump=major|minor|patch Override the bump type for all workspaces\n --set-version=X.Y.Z Set an explicit version; bypasses commit-derived bumps.\n Requires --only in monorepo mode (rejected when a 'project' block is configured).\n --force Release even when no commits or no bump-worthy commits exist\n since the last tag. In monorepo and project mode, defaults to\n patch when --bump is not given; use --bump=X for a different\n level. In single-package mode, a bare --force is rejected \u2014\n pass --bump=major|minor|patch.\n --no-git-checks, -n Skip the clean-working-tree check\n --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only;\n rejected when a 'project' block is configured)\n --with-release-notes Also write per-workspace release-notes previews under <workspacePath>/docs/\n (docs/README.v<version>.md and docs/RELEASE_NOTES.v<version>.md).\n Recommended .gitignore entry: packages/*/docs/*.v*.md (or docs/*.v*.md).\n --help, -h Show this help message\n";
1
+ export declare const prepareHelpText = "\nUsage: release-kit prepare [options]\n\nRun release preparation with automatic workspace discovery.\n\nOptions:\n --dry-run Run without modifying any files\n --bump=major|minor|patch Override the bump type for all workspaces\n --set-version=X.Y.Z Set an explicit version; bypasses commit-derived bumps. In monorepo\n mode it requires --only, and is rejected when a 'project' block is configured.\n --force Release even when no commits or no bump-worthy commits exist\n since the last tag. In monorepo and project mode, defaults to\n patch when --bump is not given; use --bump=X for a different\n level. In single-package mode, a bare --force is rejected \u2014\n pass --bump=major|minor|patch.\n --no-git-checks, -n Skip the clean-working-tree check\n --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only).\n When a 'project' block is configured, the project release is skipped.\n --with-release-notes Also write per-workspace release-notes previews under <workspacePath>/docs/\n (docs/README.v<version>.md and docs/RELEASE_NOTES.v<version>.md).\n Recommended .gitignore entry: packages/*/docs/*.v*.md (or docs/*.v*.md).\n --help, -h Show this help message\n";
2
2
  export declare function showPrepareHelp(): void;
@@ -6,16 +6,16 @@ Run release preparation with automatic workspace discovery.
6
6
  Options:
7
7
  --dry-run Run without modifying any files
8
8
  --bump=major|minor|patch Override the bump type for all workspaces
9
- --set-version=X.Y.Z Set an explicit version; bypasses commit-derived bumps.
10
- Requires --only in monorepo mode (rejected when a 'project' block is configured).
9
+ --set-version=X.Y.Z Set an explicit version; bypasses commit-derived bumps. In monorepo
10
+ mode it requires --only, and is rejected when a 'project' block is configured.
11
11
  --force Release even when no commits or no bump-worthy commits exist
12
12
  since the last tag. In monorepo and project mode, defaults to
13
13
  patch when --bump is not given; use --bump=X for a different
14
14
  level. In single-package mode, a bare --force is rejected —
15
15
  pass --bump=major|minor|patch.
16
16
  --no-git-checks, -n Skip the clean-working-tree check
17
- --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only;
18
- rejected when a 'project' block is configured)
17
+ --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only).
18
+ When a 'project' block is configured, the project release is skipped.
19
19
  --with-release-notes Also write per-workspace release-notes previews under <workspacePath>/docs/
20
20
  (docs/README.v<version>.md and docs/RELEASE_NOTES.v<version>.md).
21
21
  Recommended .gitignore entry: packages/*/docs/*.v*.md (or docs/*.v*.md).
@@ -3,7 +3,16 @@ export interface RenderedInjectedReadme {
3
3
  injectedReadme: string;
4
4
  releaseNotesMarkdown: string;
5
5
  }
6
- export declare function renderInjectedReadmeFromEntries(readme: string, entries: readonly ChangelogEntry[], tag: string, sectionOrder?: string[]): RenderedInjectedReadme | undefined;
6
+ export type RenderInjectedReadmeSkipReason = 'no-entry' | 'empty-body';
7
+ export type RenderInjectedReadmeResult = {
8
+ status: 'rendered';
9
+ rendered: RenderedInjectedReadme;
10
+ } | {
11
+ status: 'skipped';
12
+ reason: RenderInjectedReadmeSkipReason;
13
+ version: string;
14
+ };
15
+ export declare function renderInjectedReadmeFromEntries(readme: string, entries: readonly ChangelogEntry[], tag: string, sectionOrder?: string[]): RenderInjectedReadmeResult;
7
16
  export declare function renderInjectedReadme(readme: string, changelogJsonPath: string, tag: string, sectionOrder?: string[]): RenderedInjectedReadme | undefined;
8
17
  export declare function injectReleaseNotesIntoReadme(readmePath: string, changelogJsonPath: string, tag: string, sectionOrder?: string[]): string | undefined;
9
18
  export declare function resolveReadmePath(workspacePath: string): string | undefined;
@@ -7,8 +7,7 @@ export function renderInjectedReadmeFromEntries(readme, entries, tag, sectionOrd
7
7
  const version = extractVersion(tag);
8
8
  const entry = entries.find((e) => e.version === version);
9
9
  if (entry === undefined) {
10
- console.warn(`Warning: no changelog entry for version ${version}; skipping README injection`);
11
- return undefined;
10
+ return { status: 'skipped', reason: 'no-entry', version };
12
11
  }
13
12
  const renderedSections = renderReleaseNotesSingle(entry, {
14
13
  filter: matchesAudience('all'),
@@ -16,13 +15,12 @@ export function renderInjectedReadmeFromEntries(readme, entries, tag, sectionOrd
16
15
  ...(sectionOrder !== undefined && { sectionOrder }),
17
16
  });
18
17
  if (renderedSections.trimEnd().length === 0) {
19
- console.warn(`Warning: no user-facing release notes for version ${version}; skipping README injection`);
20
- return undefined;
18
+ return { status: 'skipped', reason: 'empty-body', version };
21
19
  }
22
20
  const labeledHeading = `## Release notes — v${version} (${entry.date})`;
23
21
  const releaseNotesMarkdown = `${labeledHeading}\n\n${renderedSections.trimEnd()}`;
24
22
  const injectedReadme = injectSection(readme, 'release-notes', releaseNotesMarkdown);
25
- return { injectedReadme, releaseNotesMarkdown };
23
+ return { status: 'rendered', rendered: { injectedReadme, releaseNotesMarkdown } };
26
24
  }
27
25
  export function renderInjectedReadme(readme, changelogJsonPath, tag, sectionOrder) {
28
26
  if (!existsSync(changelogJsonPath)) {
@@ -34,7 +32,12 @@ export function renderInjectedReadme(readme, changelogJsonPath, tag, sectionOrde
34
32
  console.warn(`Warning: could not parse ${changelogJsonPath}; skipping README injection`);
35
33
  return undefined;
36
34
  }
37
- return renderInjectedReadmeFromEntries(readme, entries, tag, sectionOrder);
35
+ const result = renderInjectedReadmeFromEntries(readme, entries, tag, sectionOrder);
36
+ if (result.status === 'skipped') {
37
+ console.warn(`Warning: ${describeRenderSkip(result.reason, result.version)}; skipping README injection`);
38
+ return undefined;
39
+ }
40
+ return result.rendered;
38
41
  }
39
42
  export function injectReleaseNotesIntoReadme(readmePath, changelogJsonPath, tag, sectionOrder) {
40
43
  const originalReadme = readFileSync(readmePath, 'utf8');
@@ -49,3 +52,8 @@ export function resolveReadmePath(workspacePath) {
49
52
  const readmePath = join(workspacePath, 'README.md');
50
53
  return existsSync(readmePath) ? readmePath : undefined;
51
54
  }
55
+ function describeRenderSkip(reason, version) {
56
+ return reason === 'no-entry'
57
+ ? `no changelog entry for version ${version}`
58
+ : `no user-facing release notes for version ${version}`;
59
+ }
@@ -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 = [];
@@ -2,16 +2,18 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { describeError } from '@williamthorsen/toolbelt.errors';
4
4
  import { extractVersion } from "./changelogJsonUtils.js";
5
- import { renderInjectedReadmeFromEntries } from "./injectReleaseNotesIntoReadme.js";
5
+ import { renderInjectedReadmeFromEntries, } from "./injectReleaseNotesIntoReadme.js";
6
6
  export function planReleaseNotesPreviews(options) {
7
7
  const { workspacePath, tag, entries, sectionOrder } = options;
8
8
  const warnings = [];
9
9
  const readmePath = path.join(workspacePath, 'README.md');
10
10
  const readme = readWorkspaceReadme(readmePath, warnings);
11
- const rendered = renderInjectedReadmeFromEntries(readme.content, entries, tag, sectionOrder);
12
- if (rendered === undefined) {
11
+ const result = renderInjectedReadmeFromEntries(readme.content, entries, tag, sectionOrder);
12
+ if (result.status === 'skipped') {
13
+ warnings.push(describePreviewSkip(workspacePath, result.reason, result.version));
13
14
  return { writes: [], warnings };
14
15
  }
16
+ const { rendered } = result;
15
17
  if (!readme.exists && !readme.unreadable) {
16
18
  warnings.push(`${readmePath} not found; skipping injected-README preview but still writing standalone release notes`);
17
19
  }
@@ -29,6 +31,12 @@ export function planReleaseNotesPreviews(options) {
29
31
  });
30
32
  return { writes, warnings };
31
33
  }
34
+ function describePreviewSkip(workspacePath, reason, version) {
35
+ const cause = reason === 'no-entry'
36
+ ? `no changelog entry for version ${version}`
37
+ : `no user-facing release notes for version ${version}`;
38
+ return `${workspacePath}: ${cause}; skipping release-notes previews`;
39
+ }
32
40
  function readWorkspaceReadme(readmePath, warnings) {
33
41
  if (!existsSync(readmePath)) {
34
42
  return { content: '', exists: false, unreadable: false };
@@ -137,12 +137,6 @@ function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion,
137
137
  'contributing workspace. To use --set-version, run on a config without a `project` block.');
138
138
  process.exit(1);
139
139
  }
140
- if (only !== undefined && config.project !== undefined) {
141
- reportError('--only cannot be combined with a project release. ' +
142
- 'To release a single workspace, use a config without a `project` block, ' +
143
- 'or run a full `prepare` (no --only) to include the project release.');
144
- process.exit(1);
145
- }
146
140
  if (only !== undefined) {
147
141
  const knownNames = config.workspaces.map((w) => w.dir);
148
142
  for (const name of only) {
@@ -183,7 +177,7 @@ function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion,
183
177
  process.exit(1);
184
178
  }
185
179
  }
186
- runAndReport(() => releasePrepareMono(config, options), dryRun);
180
+ runAndReport(() => releasePrepareMono(config, { ...options, ...(only !== undefined && { only }) }), dryRun);
187
181
  }
188
182
  async function loadAndValidateConfig() {
189
183
  let rawConfig;
@@ -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
- }
@@ -4,6 +4,7 @@ export interface ReleasePrepareOptions {
4
4
  force?: boolean;
5
5
  bumpOverride?: ReleaseType;
6
6
  setVersion?: string;
7
+ only?: string[];
7
8
  withReleaseNotes?: boolean;
8
9
  }
9
10
  export declare function releasePrepare(config: ReleaseConfig, options: ReleasePrepareOptions): ReleasePlan;
@@ -83,17 +83,17 @@ export function releasePrepare(config, options) {
83
83
  }
84
84
  writes.push(...bump.writes);
85
85
  const newTag = `${config.tagPrefix}${bump.newVersion}`;
86
- const overrideWarnings = [];
86
+ const planWarnings = [];
87
87
  const changelogs = planSinglePackageChangelogs({
88
88
  config,
89
89
  commits,
90
90
  newTag,
91
91
  newVersion: bump.newVersion,
92
92
  overrides,
93
- overrideWarnings,
93
+ overrideWarnings: planWarnings,
94
94
  });
95
95
  const { changelogFiles, changelogJsonFiles } = changelogs;
96
- const previewWrites = planSinglePackagePreviews(withReleaseNotes === true, config, newTag, changelogs.entries, overrideWarnings);
96
+ const previewWrites = planSinglePackagePreviews(withReleaseNotes === true, config, newTag, changelogs.entries, planWarnings);
97
97
  writes.push(...changelogs.writes, ...previewWrites);
98
98
  const formatCommandStr = config.formatCommand ?? (hasPrettierConfig() ? 'npx prettier --write' : undefined);
99
99
  let formatCommand;
@@ -125,8 +125,8 @@ export function releasePrepare(config, options) {
125
125
  summary: buildReleaseSummary({ workspaces: [released] }),
126
126
  formatCommand,
127
127
  };
128
- if (overrideWarnings.length > 0) {
129
- plan.warnings = overrideWarnings;
128
+ if (planWarnings.length > 0) {
129
+ plan.warnings = planWarnings;
130
130
  }
131
131
  return plan;
132
132
  }
@@ -229,7 +229,7 @@ function planSinglePackagePreviews(withReleaseNotes, config, newTag, entries, wa
229
229
  return [];
230
230
  }
231
231
  if (!config.changelogJson.enabled) {
232
- console.warn('Warning: --with-release-notes requires changelogJson.enabled; skipping preview generation');
232
+ warnings.push('--with-release-notes requires changelogJson.enabled; skipping preview generation');
233
233
  return [];
234
234
  }
235
235
  if (config.changelogPaths.length === 0) {
@@ -25,10 +25,11 @@ import { renderChangelogMarkdown } from "./renderChangelogMarkdown.js";
25
25
  import { deriveSectionOrder } from "./resolveReleaseNotesConfig.js";
26
26
  import { refreshGitCliffCache } from "./runGitCliff.js";
27
27
  export function releasePrepareMono(config, options) {
28
- const { withReleaseNotes } = options;
28
+ const { only, withReleaseNotes } = options;
29
29
  const writes = [];
30
+ const warnings = [];
30
31
  if (withReleaseNotes === true && !config.changelogJson.enabled) {
31
- console.warn('Warning: --with-release-notes requires changelogJson.enabled; skipping preview generation');
32
+ warnings.push('--with-release-notes requires changelogJson.enabled; skipping preview generation');
32
33
  }
33
34
  const sectionOrder = deriveSectionOrder(resolveWorkTypes(config.workTypes));
34
35
  const overrideContext = createOverrideContext(config.workspaces);
@@ -43,7 +44,6 @@ export function releasePrepareMono(config, options) {
43
44
  const graph = buildDependencyGraph(config.workspaces);
44
45
  const fullReleaseSet = propagateBumps(directBumps, graph, currentVersions);
45
46
  const { sorted: sortedDirs, cyclicDirs } = topologicalSort(fullReleaseSet, graph);
46
- const warnings = [];
47
47
  if (cyclicDirs.length > 0) {
48
48
  warnings.push(`Circular workspace dependencies detected among: ${cyclicDirs.join(', ')}. ` +
49
49
  'Propagation metadata may be incomplete for these workspaces.');
@@ -77,17 +77,23 @@ export function releasePrepareMono(config, options) {
77
77
  });
78
78
  let project;
79
79
  if (config.project !== undefined) {
80
- project = tryStage('project release stage', () => releasePrepareProject({
81
- config,
82
- options,
83
- modifiedFiles,
84
- writes,
85
- tags,
86
- warnings,
87
- rootOverrides: overrideContext.project,
88
- overrideWarnings: overrideContext.overrideWarnings,
89
- globalMatchedRootKeys: overrideContext.globalMatchedRootKeys,
90
- }));
80
+ if (only === undefined) {
81
+ project = tryStage('project release stage', () => releasePrepareProject({
82
+ config,
83
+ options,
84
+ modifiedFiles,
85
+ writes,
86
+ tags,
87
+ warnings,
88
+ rootOverrides: overrideContext.project,
89
+ overrideWarnings: overrideContext.overrideWarnings,
90
+ globalMatchedRootKeys: overrideContext.globalMatchedRootKeys,
91
+ }));
92
+ }
93
+ else {
94
+ warnings.push(`Project release skipped: --only narrows this run to ${only.join(', ')}. ` +
95
+ 'Run `release-kit prepare` without --only to include the project release.');
96
+ }
91
97
  }
92
98
  const formatCommand = planFormatCommand(config, tags, modifiedFiles);
93
99
  for (const overrideKey of overrideContext.project.keys()) {
@@ -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();
@@ -22,6 +22,7 @@ function formatSingleWorkspace(result, options) {
22
22
  formatPolicyViolations(lines, workspace.policyViolations);
23
23
  if (workspace.status === 'skipped') {
24
24
  lines.push(`⏭️ ${workspace.skipReason}`);
25
+ formatWarnings(lines, result);
25
26
  return lines.join('\n');
26
27
  }
27
28
  if (workspace.setVersion !== undefined) {
@@ -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.1",
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.29.0",
62
64
  "smol-toml": "1.7.1"
63
65
  },
64
66
  "engines": {