@williamthorsen/release-kit 13.1.0 โ†’ 14.0.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,4 +1,58 @@
1
1
  [
2
+ {
3
+ "version": "14.0.0",
4
+ "date": "2026-10-08",
5
+ "sections": [
6
+ {
7
+ "title": "๐ŸŽ‰ Features",
8
+ "audience": "all",
9
+ "items": [
10
+ {
11
+ "description": "Changes monorepo `--set-version` to take comma-separated `<workspace>@N.N.N` entries, which `prepare` accepts without `--only` and together with `--bump` and `--force`, and which release each named workspace's dependents. (#970)",
12
+ "breaking": true,
13
+ "migration": "Replace `--only arrays --set-version 1.0.0` with `--set-version arrays@1.0.0 --only arrays`, and keep `--only` unless the release should cover every workspace, because a `--set-version` run without `--only` is a full run.",
14
+ "hash": "07e3e99cb03e6031619bd58dac6ee339ceac0e89",
15
+ "entry": 2
16
+ }
17
+ ]
18
+ },
19
+ {
20
+ "title": "๐Ÿ› Bug fixes",
21
+ "audience": "all",
22
+ "items": [
23
+ {
24
+ "description": "Fixes the issue that `release-kit prepare --only` left each dependent of the named workspaces published against the old version range of its dependency, by also releasing every transitive dependent: a dependent with commits of its own at its own bump, and one without at a propagated patch bump. (#970)",
25
+ "breaking": true,
26
+ "migration": "Expect each `--only` run to release the dependents of the named workspaces as well, and remove any dependents that were added to `--only` only to get past the stranded-dependents error, which `prepare` no longer reports.",
27
+ "hash": "07e3e99cb03e6031619bd58dac6ee339ceac0e89",
28
+ "entry": 1
29
+ }
30
+ ]
31
+ },
32
+ {
33
+ "title": "๐Ÿงช Tests",
34
+ "audience": "dev",
35
+ "items": [
36
+ {
37
+ "description": "Replaces the hand-rolled `console.warn` spies in release-kit's config-warning and push tests with `silenceConsole`, which restores the method when its `using` scope exits. (#964)",
38
+ "hash": "38cc179b4116bcd5aba8371b17b0816150c9e9a3",
39
+ "entry": 4
40
+ }
41
+ ]
42
+ },
43
+ {
44
+ "title": "๐Ÿ“š Documentation",
45
+ "audience": "dev",
46
+ "items": [
47
+ {
48
+ "description": "Updates the readyup examples in the `nmr`, `release-kit`, and `v11y-check` READMEs and the `nmr` rulebook to use the `sources` key and `rdy run --sources`, and states that `--sources` needs readyup 0.40 or later. (#977)",
49
+ "hash": "41177f8f562d59a4cf0aff9d24199dbd370bb1a1",
50
+ "entry": 4
51
+ }
52
+ ]
53
+ }
54
+ ]
55
+ },
2
56
  {
3
57
  "version": "13.1.0",
4
58
  "date": "2026-10-02",
@@ -1923,10 +1977,22 @@
1923
1977
  "audience": "all",
1924
1978
  "items": [
1925
1979
  {
1926
- "description": "Migrate release-kit from toolbelt (#18)",
1927
- "body": "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.",
1928
- "hash": "da3a61fd335fb3e05ce20a0bffcdc1647d7aeb45"
1929
- },
1980
+ "description": "Allow git-cliff to be used without config (#31)",
1981
+ "body": "Adds a `resolveCliffConfigPath()` function that searches for a git-cliff config in a 4-step cascade (explicit path โ†’ `.config/git-cliff.toml` โ†’ `cliff.toml` โ†’ bundled `cliff.toml.template`), eliminating the requirement for consuming repos to maintain a cliff config copy. Restructures the `init` command to scaffold only the workflow file by default, with new `--with-config` and `--force` flags. Moves `.release-tags` from `/tmp/release-kit/` to project-local `tmp/` for predictable behavior in local runs.",
1982
+ "hash": "0cbd2605d6f5f53d1efe3dda8dd0f6e4376b499f"
1983
+ }
1984
+ ]
1985
+ }
1986
+ ]
1987
+ },
1988
+ {
1989
+ "version": "2.0.0",
1990
+ "date": "2026-03-16",
1991
+ "sections": [
1992
+ {
1993
+ "title": "๐ŸŽ‰ Features",
1994
+ "audience": "all",
1995
+ "items": [
1930
1996
  {
1931
1997
  "description": "Slim down release workflow by removing unnecessary pnpm install (#21)",
1932
1998
  "body": "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.",
@@ -1943,11 +2009,6 @@
1943
2009
  "body": "Moves the reusable release workflow from `williamthorsen/.github` into this repo as `release-workflow.yaml`, stripping all pnpm-related steps since release-kit now runs git-cliff and prettier via `npx` internally. Updates this repo's caller workflow to use a relative path and update init templates to reference the new location. Establishes a naming convention (`{name}-workflow.yaml` for reusable, `{name}.yaml` for callers) and independent versioning strategy (`{name}-workflow-v{major}` tags), documented in `.github/workflows/README.md`.",
1944
2010
  "breaking": true,
1945
2011
  "hash": "f3416120cfe5bf0e7dbf51389e0c2647ccf41d30"
1946
- },
1947
- {
1948
- "description": "Allow git-cliff to be used without config (#31)",
1949
- "body": "Adds a `resolveCliffConfigPath()` function that searches for a git-cliff config in a 4-step cascade (explicit path โ†’ `.config/git-cliff.toml` โ†’ `cliff.toml` โ†’ bundled `cliff.toml.template`), eliminating the requirement for consuming repos to maintain a cliff config copy. Restructures the `init` command to scaffold only the workflow file by default, with new `--with-config` and `--force` flags. Moves `.release-tags` from `/tmp/release-kit/` to project-local `tmp/` for predictable behavior in local runs.",
1950
- "hash": "0cbd2605d6f5f53d1efe3dda8dd0f6e4376b499f"
1951
2012
  }
1952
2013
  ]
1953
2014
  },
@@ -1,6 +1,6 @@
1
1
  /** @noformat -- @generated. Do not edit. Compiled by rdy. */
2
2
  /* eslint-disable */
3
- export const __readyupVersion = "0.38.0";
3
+ export const __readyupVersion = "0.40.0";
4
4
 
5
5
 
6
6
  // .readyup/kits/default.ts
@@ -98,7 +98,7 @@ function translateGlob(pattern) {
98
98
 
99
99
  // .readyup/kits/default.ts
100
100
  function getMinVersion() {
101
- const picked = { "version": "13.1.0" };
101
+ const picked = { "version": "14.0.0" };
102
102
  if (typeof picked["version"] !== "string") {
103
103
  throw new TypeError("release-kit/package.json: 'version' must be a string");
104
104
  }
@@ -116,7 +116,7 @@ var CONFIG_EXPORT_PATTERNS = [
116
116
  ];
117
117
  var COMMON_PRESET_HASH = "618bb5cbcbdd4f4f0c2a777add7e14d992d48483350dd9651bf8cf4c1fa9d418";
118
118
  var SYNC_LABELS_WORKFLOW_HASH = "6b50015e2919ffa4b16d0b4a1f340e585ff3e5b779a2256010d3c62fa07ff8f2";
119
- var RELEASE_WORKFLOW_HASH_MONOREPO = "f35503bcfc5c131a8309305fed27f8127cb901f422cb8b62978a34b86dda7f7a";
119
+ var RELEASE_WORKFLOW_HASH_MONOREPO = "4a20ff3315155500f64a27133c54eb1bc1f07a4acc370ec0d83176109041c60d";
120
120
  var RELEASE_WORKFLOW_HASH_SINGLE = "d26dd4ca186632b388ea46bd885721124c03d19918e511baa40821d2d760da69";
121
121
  var PUBLISH_WORKFLOW_HASH_MONOREPO = "0afa9ffe914f3dc8f043e68252ebc604c8cc1a953422fcea37a909a4def370ee";
122
122
  var PUBLISH_WORKFLOW_HASH_SINGLE = "6f31183e0a1e66be791a19266c3b028dadbd9fe010f7fc4452f3f8970c937b43";
@@ -1,6 +1,6 @@
1
1
  /** @noformat -- @generated. Do not edit. Compiled by rdy. */
2
2
  /* eslint-disable */
3
- export const __readyupVersion = "0.38.0";
3
+ export const __readyupVersion = "0.40.0";
4
4
 
5
5
 
6
6
  // .readyup/kits/npm-auto-publish.ts
@@ -8,12 +8,12 @@
8
8
  "esbuildVersion": "0.28.2",
9
9
  "inputs": [
10
10
  {
11
- "hash": "91bc3638",
11
+ "hash": "7a8522cd",
12
12
  "kind": "module",
13
13
  "path": "kits/default.ts"
14
14
  },
15
15
  {
16
- "hash": "fd27a632",
16
+ "hash": "6eab98d1",
17
17
  "kind": "inline",
18
18
  "path": "../package.json",
19
19
  "paths": [
@@ -43,10 +43,10 @@
43
43
  ],
44
44
  "name": "default",
45
45
  "path": "kits/default.js",
46
- "readyupVersion": "0.38.0",
46
+ "readyupVersion": "0.40.0",
47
47
  "source": "kits/default.ts",
48
- "sourceHash": "91bc3638",
49
- "targetHash": "a3266cf2"
48
+ "sourceHash": "7a8522cd",
49
+ "targetHash": "4e21467f"
50
50
  },
51
51
  {
52
52
  "checklists": [
@@ -68,10 +68,10 @@
68
68
  ],
69
69
  "name": "npm-auto-publish",
70
70
  "path": "kits/npm-auto-publish.js",
71
- "readyupVersion": "0.38.0",
71
+ "readyupVersion": "0.40.0",
72
72
  "source": "kits/npm-auto-publish.ts",
73
73
  "sourceHash": "d9910d60",
74
- "targetHash": "8bdbefef"
74
+ "targetHash": "2a6931b6"
75
75
  }
76
76
  ]
77
77
  }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 14.0.0 โ€” 2026-10-08
6
+
7
+ ### ๐ŸŽ‰ Features
8
+
9
+ - ๐Ÿšจ **Breaking:** Changes monorepo `--set-version` to take comma-separated `<workspace>@N.N.N` entries, which `prepare` accepts without `--only` and together with `--bump` and `--force`, and which release each named workspace's dependents. (#970)
10
+
11
+ ### ๐Ÿ› Bug fixes
12
+
13
+ - ๐Ÿšจ **Breaking:** Fixes the issue that `release-kit prepare --only` left each dependent of the named workspaces published against the old version range of its dependency, by also releasing every transitive dependent: a dependent with commits of its own at its own bump, and one without at a propagated patch bump. (#970)
14
+
15
+ ### ๐Ÿงช Tests
16
+
17
+ - Replaces the hand-rolled `console.warn` spies in release-kit's config-warning and push tests with `silenceConsole`, which restores the method when its `using` scope exits. (#964)
18
+
19
+ ### ๐Ÿ“š Documentation
20
+
21
+ - Updates the readyup examples in the `nmr`, `release-kit`, and `v11y-check` READMEs and the `nmr` rulebook to use the `sources` key and `rdy run --sources`, and states that `--sources` needs readyup 0.40 or later. (#977)
22
+
5
23
  ## 13.1.0 โ€” 2026-10-02
6
24
 
7
25
  ### ๐ŸŽ‰ Features
@@ -1111,9 +1129,13 @@ All notable changes to this project will be documented in this file.
1111
1129
 
1112
1130
  ### ๐ŸŽ‰ Features
1113
1131
 
1114
- - Migrate release-kit from toolbelt (#18)
1132
+ - Allow git-cliff to be used without config (#31)
1115
1133
 
1116
- 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.
1134
+ Adds a `resolveCliffConfigPath()` function that searches for a git-cliff config in a 4-step cascade (explicit path โ†’ `.config/git-cliff.toml` โ†’ `cliff.toml` โ†’ bundled `cliff.toml.template`), eliminating the requirement for consuming repos to maintain a cliff config copy. Restructures the `init` command to scaffold only the workflow file by default, with new `--with-config` and `--force` flags. Moves `.release-tags` from `/tmp/release-kit/` to project-local `tmp/` for predictable behavior in local runs.
1135
+
1136
+ ## 2.0.0 โ€” 2026-03-16
1137
+
1138
+ ### ๐ŸŽ‰ Features
1117
1139
 
1118
1140
  - ๐Ÿšจ **Breaking:** Slim down release workflow by removing unnecessary pnpm install (#21)
1119
1141
 
@@ -1127,10 +1149,6 @@ All notable changes to this project will be documented in this file.
1127
1149
 
1128
1150
  Moves the reusable release workflow from `williamthorsen/.github` into this repo as `release-workflow.yaml`, stripping all pnpm-related steps since release-kit now runs git-cliff and prettier via `npx` internally. Updates this repo's caller workflow to use a relative path and update init templates to reference the new location. Establishes a naming convention (`{name}-workflow.yaml` for reusable, `{name}.yaml` for callers) and independent versioning strategy (`{name}-workflow-v{major}` tags), documented in `.github/workflows/README.md`.
1129
1151
 
1130
- - Allow git-cliff to be used without config (#31)
1131
-
1132
- Adds a `resolveCliffConfigPath()` function that searches for a git-cliff config in a 4-step cascade (explicit path โ†’ `.config/git-cliff.toml` โ†’ `cliff.toml` โ†’ bundled `cliff.toml.template`), eliminating the requirement for consuming repos to maintain a cliff config copy. Restructures the `init` command to scaffold only the workflow file by default, with new `--with-config` and `--force` flags. Moves `.release-tags` from `/tmp/release-kit/` to project-local `tmp/` for predictable behavior in local runs.
1133
-
1134
1152
  ### โ™ป๏ธ Refactoring
1135
1153
 
1136
1154
  - Clean up release-kit post-migration issues (#19)
package/README.md CHANGED
@@ -7,11 +7,15 @@ Version-bumping and changelog-generation toolkit for release workflows.
7
7
  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 the same commit history that decides each bump (with optional [editorial overrides](docs/editorial-overrides.md)).
8
8
 
9
9
  <!-- section:release-notes -->
10
- ## Release notes โ€” v13.1.0 (2026-10-02)
10
+ ## Release notes โ€” v14.0.0 (2026-10-08)
11
11
 
12
12
  ### ๐ŸŽ‰ Features
13
13
 
14
- - Adds an `archived: true` mark to `repoLabels.labels` entries, which `sync-labels generate` writes to `.github/labels.yaml` without changing the output for unmarked labels. (#955)
14
+ - ๐Ÿšจ **Breaking:** Changes monorepo `--set-version` to take comma-separated `<workspace>@N.N.N` entries, which `prepare` accepts without `--only` and together with `--bump` and `--force`, and which release each named workspace's dependents. (#970)
15
+
16
+ ### ๐Ÿ› Bug fixes
17
+
18
+ - ๐Ÿšจ **Breaking:** Fixes the issue that `release-kit prepare --only` left each dependent of the named workspaces published against the old version range of its dependency, by also releasing every transitive dependent: a dependent with commits of its own at its own bump, and one without at a propagated patch bump. (#970)
15
19
  <!-- /section:release-notes -->
16
20
 
17
21
  ## Installation
@@ -57,7 +61,7 @@ Commit the scaffolded workflows, then start a release from the `release` workflo
57
61
 
58
62
  ```sh
59
63
  gh workflow run release.yaml # every workspace with release-worthy changes
60
- gh workflow run release.yaml -f only=arrays # the named workspaces alone
64
+ gh workflow run release.yaml -f only=arrays # the named workspaces and their dependents
61
65
  ```
62
66
 
63
67
  After upgrading release-kit, run `release-kit update-templates` to update the scaffolded workflows to its templates; it leaves the config files alone.
@@ -136,15 +140,15 @@ release-kit publishes two [readyup](https://www.npmjs.com/package/readyup) kits:
136
140
  import { defineRdyConfig } from 'readyup';
137
141
 
138
142
  export default defineRdyConfig({
139
- packages: ['@williamthorsen/release-kit'],
143
+ sources: ['npm:@williamthorsen/release-kit'],
140
144
  });
141
145
  ```
142
146
 
143
147
  ```bash
144
- rdy run --packages
148
+ rdy run --sources
145
149
  ```
146
150
 
147
- `rdy run` needs `readyup` 0.23 or later, and `@williamthorsen/release-kit` as a _direct_ devDependency: A strict pnpm layout links nothing else into the project; therefore, a transitive copy is unreachable.
151
+ `rdy run --sources` needs `readyup` 0.40 or later, and `@williamthorsen/release-kit` as a _direct_ devDependency: A strict pnpm layout links nothing else into the project; therefore, a transitive copy is unreachable.
148
152
 
149
153
  [Readiness checks](docs/readiness-checks.md) covers running each kit alone and what `npm-auto-publish` needs from the npm session.
150
154
 
@@ -0,0 +1,3 @@
1
+ import type { DependencyGraph } from './buildDependencyGraph.js';
2
+ /** Returns the start workspace `dir`s together with the `dir` of every workspace that depends on one, transitively. */
3
+ export declare function findTransitiveDependents(graph: DependencyGraph, startDirs: Iterable<string>): Set<string>;
@@ -0,0 +1,20 @@
1
+ export function findTransitiveDependents(graph, startDirs) {
2
+ const found = new Set(startDirs);
3
+ const queue = [...found];
4
+ while (queue.length > 0) {
5
+ const dir = queue.shift();
6
+ if (dir === undefined)
7
+ break;
8
+ const packageName = graph.dirToPackageName.get(dir);
9
+ if (packageName === undefined)
10
+ continue;
11
+ const dependents = graph.dependentsOf.get(packageName) ?? [];
12
+ for (const dependent of dependents) {
13
+ if (found.has(dependent.dir))
14
+ continue;
15
+ found.add(dependent.dir);
16
+ queue.push(dependent.dir);
17
+ }
18
+ }
19
+ return found;
20
+ }
@@ -2,6 +2,6 @@
2
2
  * Help text for `release-kit prepare`. The module does not have any import-time side effects, so that a test can
3
3
  * import it without running the CLI.
4
4
  */
5
- export declare const prepareHelpText = "\nUsage: release-kit prepare [options]\n\nRun release preparation with automatic workspace discovery.\n\nOptions:\n --config <path> Config file, relative to the current directory\n (default: .config/release-kit.config.ts at the repo root)\n --dry-run Run without modifying any files\n --bump=major|minor|patch Set the level of every release that happens; does not trigger one\n --set-version=X.Y.Z Set an explicit version; bypasses the changelog-derived bump. In monorepo\n mode it requires --only, and is rejected when a 'project' block is configured.\n --force Release even when the last tag is not followed by any commits,\n or by any bump-worthy ones. Defaults to patch when --bump is not given;\n use --bump=X for a different level.\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";
5
+ export declare const prepareHelpText = "\nUsage: release-kit prepare [options]\n\nRun release preparation with automatic workspace discovery.\n\nOptions:\n --config <path> Config file, relative to the current directory\n (default: .config/release-kit.config.ts at the repo root)\n --dry-run Run without modifying any files\n --bump=major|minor|patch Set the level of every release that happens; does not trigger one\n --set-version=X.Y.Z Set an explicit version; bypasses the changelog-derived bump. In monorepo\n mode, name each workspace: --set-version=arrays@1.0.0[,strings@2.0.0].\n Rejected when a 'project' block is configured.\n --force Release even when the last tag is not followed by any commits,\n or by any bump-worthy ones. Defaults to patch when --bump is not given;\n use --bump=X for a different level.\n --no-git-checks, -n Skip the clean-working-tree check\n --only=name1,name2 Release only the named workspaces and their dependents (comma-separated,\n 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";
6
6
  /** Prints the prepare command's help text. */
7
7
  export declare function showPrepareHelp(): void;
@@ -9,12 +9,14 @@ Options:
9
9
  --dry-run Run without modifying any files
10
10
  --bump=major|minor|patch Set the level of every release that happens; does not trigger one
11
11
  --set-version=X.Y.Z Set an explicit version; bypasses the changelog-derived bump. In monorepo
12
- mode it requires --only, and is rejected when a 'project' block is configured.
12
+ mode, name each workspace: --set-version=arrays@1.0.0[,strings@2.0.0].
13
+ Rejected when a 'project' block is configured.
13
14
  --force Release even when the last tag is not followed by any commits,
14
15
  or by any bump-worthy ones. Defaults to patch when --bump is not given;
15
16
  use --bump=X for a different level.
16
17
  --no-git-checks, -n Skip the clean-working-tree check
17
- --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only).
18
+ --only=name1,name2 Release only the named workspaces and their dependents (comma-separated,
19
+ monorepo only).
18
20
  When a 'project' block is configured, the project release is skipped.
19
21
  --with-release-notes Also write per-workspace release-notes previews under <workspacePath>/docs/
20
22
  (docs/README.v<version>.md and docs/RELEASE_NOTES.v<version>.md).
@@ -89,7 +89,7 @@ on:
89
89
  workflow_dispatch:
90
90
  inputs:
91
91
  only:
92
- description: 'Workspaces to release (comma-separated, leave empty for all)'
92
+ description: 'Workspaces to release, with their dependents (comma-separated, leave empty for all)'
93
93
  required: false
94
94
  type: string
95
95
  bump:
@@ -43,6 +43,7 @@ export declare function parseArgs(argv: string[]): {
43
43
  noGitChecks: boolean;
44
44
  bumpOverride: ReleaseType | undefined;
45
45
  only: string[] | undefined;
46
+ /** The raw `--set-version` value, whose accepted form depends on the mode. */
46
47
  setVersion: string | undefined;
47
48
  withReleaseNotes: boolean;
48
49
  };
@@ -2,12 +2,9 @@ import { execSync } from 'node:child_process';
2
2
  import { formatGlyphLine, parseArgsOrExit, reportError, } from '@williamthorsen/nmr-core';
3
3
  import { describeError } from '@williamthorsen/toolbelt.errors';
4
4
  import { assertCleanWorkingTree } from "./assertCleanWorkingTree.js";
5
- import { readReleaseHistory } from "./buildChangelogEntries.js";
6
- import { buildDependencyGraph } from "./buildDependencyGraph.js";
7
5
  import { configFlagSchema } from "./configFlagSchema.js";
8
6
  import { describeEmptyWorkspace, discoverWorkspaces } from "./discoverWorkspaces.js";
9
7
  import { dim } from "./format.js";
10
- import { getAllTagPrefixes } from "./generateChangelogs.js";
11
8
  import { RELEASE_GLYPHS } from "./glyphs.js";
12
9
  import { mergeMonorepoConfig, mergeSinglePackageConfig, readRootPackageVersion } from "./loadConfig.js";
13
10
  import { loadValidatedConfig, reportConfigProblem, reportConfigWarnings } from "./loadValidatedConfig.js";
@@ -17,7 +14,6 @@ import { releasePrepare } from "./releasePrepare.js";
17
14
  import { releasePrepareMono } from "./releasePrepareMono.js";
18
15
  import { reportPrepare } from "./reportPrepare.js";
19
16
  import { resolveConfigFlag } from "./resolveConfigFlag.js";
20
- import { validateOnlyExcludesStrandedDependents } from "./validateOnlyExcludesStrandedDependents.js";
21
17
  const VALID_BUMP_TYPES = ['major', 'minor', 'patch'];
22
18
  const CANONICAL_SEMVER_PATTERN = /^\d+\.\d+\.\d+$/;
23
19
  function isReleaseType(value) {
@@ -47,26 +43,10 @@ export function parseArgs(argv) {
47
43
  }
48
44
  bumpOverride = flags.bump;
49
45
  }
50
- let setVersion;
51
- if (flags.setVersion !== undefined) {
52
- if (!CANONICAL_SEMVER_PATTERN.test(flags.setVersion)) {
53
- reportError(`Invalid --set-version value "${flags.setVersion}". Must be canonical semver (N.N.N, no pre-release suffix).`);
54
- process.exit(1);
55
- }
56
- setVersion = flags.setVersion;
57
- }
58
46
  let only;
59
47
  if (flags.only !== undefined) {
60
48
  only = flags.only.split(',');
61
49
  }
62
- if (setVersion !== undefined && bumpOverride !== undefined) {
63
- reportError('--set-version cannot be combined with --bump');
64
- process.exit(1);
65
- }
66
- if (setVersion !== undefined && flags.force) {
67
- reportError('--set-version cannot be combined with --force');
68
- process.exit(1);
69
- }
70
50
  return {
71
51
  configPath: flags.config,
72
52
  dryRun: flags.dryRun,
@@ -74,7 +54,7 @@ export function parseArgs(argv) {
74
54
  noGitChecks: flags.noGitChecks,
75
55
  bumpOverride,
76
56
  only,
77
- setVersion,
57
+ setVersion: flags.setVersion,
78
58
  withReleaseNotes: flags.withReleaseNotes,
79
59
  };
80
60
  }
@@ -83,7 +63,6 @@ export async function prepareCommand(argv, styles, invocationDir) {
83
63
  const options = {
84
64
  force,
85
65
  ...(bumpOverride !== undefined && { bumpOverride }),
86
- ...(setVersion !== undefined && { setVersion }),
87
66
  ...(withReleaseNotes && { withReleaseNotes: true }),
88
67
  };
89
68
  if (dryRun) {
@@ -121,19 +100,23 @@ export async function prepareCommand(argv, styles, invocationDir) {
121
100
  process.exit(1);
122
101
  }
123
102
  if (workspace.kind === 'single-package') {
124
- runSinglePackageMode(userConfig, options, only, dryRun, styles.stdout);
103
+ runSinglePackageMode(userConfig, options, only, setVersion, dryRun, styles.stdout);
125
104
  }
126
105
  else {
127
106
  runMonorepoMode(workspace.packageDirs, userConfig, options, only, setVersion, dryRun, styles.stdout);
128
107
  }
129
108
  }
130
- function runSinglePackageMode(userConfig, options, only, dryRun, style) {
109
+ function runSinglePackageMode(userConfig, options, only, setVersion, dryRun, style) {
131
110
  if (only !== undefined) {
132
111
  reportError('--only is only supported for monorepo configurations');
133
112
  process.exit(1);
134
113
  }
114
+ if (setVersion !== undefined && !CANONICAL_SEMVER_PATTERN.test(setVersion)) {
115
+ reportError(`Invalid --set-version value "${setVersion}". Must be canonical semver (N.N.N, no pre-release suffix).`);
116
+ process.exit(1);
117
+ }
135
118
  const config = mergeSinglePackageConfig(userConfig);
136
- runAndReport(() => releasePrepare(config, options), dryRun, style);
119
+ runAndReport(() => releasePrepare(config, { ...options, ...(setVersion !== undefined && { setVersion }) }), dryRun, style);
137
120
  }
138
121
  function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion, dryRun, style) {
139
122
  let config;
@@ -147,53 +130,48 @@ function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion,
147
130
  }
148
131
  if (setVersion !== undefined && config.project !== undefined) {
149
132
  reportError('--set-version cannot be combined with a project release. ' +
150
- '--set-version operates on a single workspace; a project release rolls up every ' +
133
+ "--set-version sets individual workspaces' versions; a project release rolls up every " +
151
134
  'contributing workspace. To use --set-version, run on a config without a `project` block.');
152
135
  process.exit(1);
153
136
  }
154
- let configuredWorkspaceDirs;
155
- if (only !== undefined) {
156
- const knownNames = config.workspaces.map((w) => w.dir);
157
- for (const name of only) {
158
- if (knownNames.includes(name)) {
159
- continue;
160
- }
161
- reportError(`Unknown workspace "${name}". Known workspaces: ${knownNames.join(', ')}`);
162
- process.exit(1);
137
+ const knownNames = config.workspaces.map((w) => w.dir);
138
+ const onlyNames = only ?? [];
139
+ for (const name of onlyNames) {
140
+ if (knownNames.includes(name)) {
141
+ continue;
163
142
  }
164
- const graph = buildDependencyGraph(config.workspaces);
165
- const violations = validateOnlyExcludesStrandedDependents(config.workspaces, only, graph, (workspace) => {
166
- const history = readReleaseHistory(config, { tagPrefixes: getAllTagPrefixes(workspace), paths: workspace.paths });
167
- return { has: history.unreleased.commits.length > 0, tag: history.previousTag };
168
- });
169
- if (violations !== undefined) {
170
- reportError('--only excludes packages with changes that would be stranded by the release.');
171
- process.stderr.write('The following packages must be added to --only or have their dependencies removed:\n');
172
- for (const violation of violations) {
173
- const since = violation.tag ?? 'the beginning';
174
- process.stderr.write(` - ${violation.dir} (downstream of ${violation.downstreamOf}; has commits since ${since})\n`);
175
- }
176
- process.stderr.write('Alternatively, run `release-kit prepare` without --only to release everything.\n');
143
+ reportError(`Unknown workspace "${name}". Known workspaces: ${knownNames.join(', ')}`);
144
+ process.exit(1);
145
+ }
146
+ const monorepoOptions = {
147
+ ...options,
148
+ ...(only !== undefined && { only }),
149
+ ...(setVersion !== undefined && { setVersions: parseSetVersions(setVersion, knownNames) }),
150
+ };
151
+ runAndReport(() => releasePrepareMono(config, monorepoOptions), dryRun, style);
152
+ }
153
+ function parseSetVersions(value, knownNames) {
154
+ const setVersions = new Map();
155
+ for (const entry of value.split(',')) {
156
+ const separatorIndex = entry.lastIndexOf('@');
157
+ const name = entry.slice(0, Math.max(0, separatorIndex));
158
+ const version = entry.slice(separatorIndex + 1);
159
+ if (separatorIndex <= 0 || !CANONICAL_SEMVER_PATTERN.test(version)) {
160
+ reportError(`Invalid --set-version entry "${entry}". In monorepo mode, each entry must be <workspace>@N.N.N ` +
161
+ '(canonical semver, no pre-release suffix), e.g. --set-version=arrays@1.0.0.');
177
162
  process.exit(1);
178
163
  }
179
- configuredWorkspaceDirs = knownNames;
180
- config.workspaces = config.workspaces.filter((w) => only.includes(w.dir));
181
- }
182
- if (setVersion !== undefined) {
183
- if (only === undefined) {
184
- reportError('--set-version requires --only in monorepo mode');
164
+ if (!knownNames.includes(name)) {
165
+ reportError(`Unknown workspace "${name}" in --set-version. Known workspaces: ${knownNames.join(', ')}`);
185
166
  process.exit(1);
186
167
  }
187
- if (config.workspaces.length !== 1) {
188
- reportError(`--set-version requires --only to match exactly one workspace; matched ${config.workspaces.length}`);
168
+ if (setVersions.has(name)) {
169
+ reportError(`--set-version names workspace "${name}" more than once`);
189
170
  process.exit(1);
190
171
  }
172
+ setVersions.set(name, version);
191
173
  }
192
- runAndReport(() => releasePrepareMono(config, {
193
- ...options,
194
- ...(only !== undefined && { only }),
195
- ...(configuredWorkspaceDirs !== undefined && { configuredWorkspaceDirs }),
196
- }), dryRun, style);
174
+ return setVersions;
197
175
  }
198
176
  function runAndReport(computePlan, dryRun, style) {
199
177
  let plan;
@@ -10,24 +10,10 @@ export interface ReleasePrepareOptions {
10
10
  /** Level of the release, in place of the level that the changelog items call for; it doesn't trigger a release. */
11
11
  bumpOverride?: ReleaseType;
12
12
  /**
13
- * Explicit target version (canonical `N.N.N`) that bypasses commit-derived bump logic.
14
- * Mutually exclusive with `bumpOverride`. In monorepo mode the caller must narrow
15
- * `config.workspaces` to a single workspace before invoking.
13
+ * Explicit target version (canonical `N.N.N`) that bypasses commit-derived bump logic and takes precedence over
14
+ * `bumpOverride` and `force`.
16
15
  */
17
16
  setVersion?: string;
18
- /**
19
- * Workspace directories to which `--only` narrowed the run (monorepo only), with
20
- * `config.workspaces` already filtered to match. Present only for a narrowed run, which
21
- * skips the project release: The project tier rolls up every contributing workspace, and
22
- * the narrowing has changed which workspaces those are.
23
- */
24
- only?: string[];
25
- /**
26
- * Every configured workspace `dir`, taken before `--only` narrowed `config.workspaces`; the unrouted-scope report
27
- * does not report a scope naming one of them that the run did not read. Defaults to the `dir`s of
28
- * `config.workspaces`.
29
- */
30
- configuredWorkspaceDirs?: readonly string[];
31
17
  /**
32
18
  * If true, plan per-workspace release-notes previews under `{workspacePath}/docs/`
33
19
  * (`README.v{version}.md` and `RELEASE_NOTES.v{version}.md`) from each workspace's
@@ -1,14 +1,29 @@
1
1
  import type { ReleasePlan } from './releasePlan.js';
2
2
  import type { ReleasePrepareOptions } from './releasePrepare.js';
3
3
  import type { MonorepoPrepareConfig } from './types.js';
4
+ /** Options for monorepo release preparation. */
5
+ export interface MonorepoPrepareOptions extends Omit<ReleasePrepareOptions, 'setVersion'> {
6
+ /**
7
+ * Workspace `dir`s from which the release starts; the run plans them and their transitive dependents. Present only
8
+ * for a narrowed run, which skips the project release: The project tier rolls up every contributing workspace, and
9
+ * the narrowing has changed which workspaces those are.
10
+ */
11
+ only?: readonly string[];
12
+ /**
13
+ * Explicit target versions (canonical `N.N.N`), keyed by workspace `dir`. Each named workspace starts the release
14
+ * and releases at its version, which takes precedence over `bumpOverride` and `force`.
15
+ */
16
+ setVersions?: ReadonlyMap<string, string>;
17
+ }
4
18
  /**
5
19
  * Orchestrates release preparation for a monorepo with multiple workspaces.
6
20
  *
21
+ * Phase 0: Selects the planned workspaces: those from which the release starts and their transitive dependents.
7
22
  * Phase 1: Determines direct bumps from commits for each workspace.
8
- * Phase 2: Builds the dependency graph and propagates bumps to dependents.
23
+ * Phase 2: Propagates bumps to dependents.
9
24
  * Phase 2b: Sorts the full release set topologically.
10
25
  * Phase 3: Plans bumps and changelogs in dependency order.
11
26
  * Phase 3b: Plans the project release.
12
27
  * Phase 4: Renders the format command.
13
28
  */
14
- export declare function releasePrepareMono(config: MonorepoPrepareConfig, options: ReleasePrepareOptions): ReleasePlan;
29
+ export declare function releasePrepareMono(fullConfig: MonorepoPrepareConfig, options: MonorepoPrepareOptions): ReleasePlan;
@@ -11,6 +11,7 @@ import { applyWorkspaceOverrides, createOverrideContext, formatStaleOverrideKeyW
11
11
  import { decideRelease } from "./decideRelease.js";
12
12
  import { deriveDependenciesHeader, deriveSectionOrder } from "./deriveReleaseNotesConfig.js";
13
13
  import { detectUndeclaredTagPrefixes } from "./detectUndeclaredTagPrefixes.js";
14
+ import { findTransitiveDependents } from "./findTransitiveDependents.js";
14
15
  import { findUnroutedEntryScopes } from "./findUnroutedEntryScopes.js";
15
16
  import { getAllTagPrefixes } from "./generateChangelogs.js";
16
17
  import { hasPrettierConfig } from "./hasPrettierConfig.js";
@@ -21,10 +22,17 @@ import { propagateBumps } from "./propagateBumps.js";
21
22
  import { planPreservedSections } from "./readChangelogSections.js";
22
23
  import { releasePrepareProject } from "./releasePrepareProject.js";
23
24
  import { renderChangelogMarkdown } from "./renderChangelogMarkdown.js";
24
- export function releasePrepareMono(config, options) {
25
+ export function releasePrepareMono(fullConfig, options) {
25
26
  const { only, withReleaseNotes } = options;
26
27
  const writes = [];
27
28
  const warnings = [];
29
+ const graph = buildDependencyGraph(fullConfig.workspaces);
30
+ const startDirs = selectStartDirs(fullConfig.workspaces, options);
31
+ const plannedDirs = findTransitiveDependents(graph, startDirs);
32
+ const config = {
33
+ ...fullConfig,
34
+ workspaces: fullConfig.workspaces.filter((workspace) => plannedDirs.has(workspace.dir)),
35
+ };
28
36
  if (withReleaseNotes === true && !config.changelogJson.enabled) {
29
37
  warnings.push('--with-release-notes requires changelogJson.enabled; skipping preview generation');
30
38
  }
@@ -32,10 +40,9 @@ export function releasePrepareMono(config, options) {
32
40
  const sectionOrder = deriveSectionOrder(workTypes);
33
41
  const dependenciesHeader = deriveDependenciesHeader(workTypes);
34
42
  const overrideContext = createOverrideContext(config.workspaces);
35
- const { directBumps, directResults, skippedResults, untaggedBaselines } = determineDirectBumps(config, options);
36
- reportUnroutedEntryScopes([...directResults.values(), ...skippedResults], config, options);
43
+ const { directBumps, directResults, skippedResults, untaggedBaselines } = determineDirectBumps(config, options, startDirs);
44
+ reportUnroutedEntryScopes([...directResults.values(), ...skippedResults], config, fullConfig.workspaces.map((workspace) => workspace.dir));
37
45
  const skippedHistories = new Map(skippedResults.map((skipped) => [skipped.workspace.dir, skipped.history]));
38
- const graph = buildDependencyGraph(config.workspaces);
39
46
  const fullReleaseSet = propagateBumps(directBumps, graph);
40
47
  assertTaggedBaseline([...untaggedBaselines]
41
48
  .filter(([dir]) => directBumps.has(dir) || !fullReleaseSet.has(dir))
@@ -107,11 +114,15 @@ export function releasePrepareMono(config, options) {
107
114
  ...(project !== undefined && { project }),
108
115
  };
109
116
  }
110
- function determineDirectBumps(config, options) {
111
- const { force, bumpOverride, setVersion } = options;
112
- if (setVersion !== undefined && config.workspaces.length !== 1) {
113
- throw new Error(`--set-version requires exactly one workspace; received ${config.workspaces.length}`);
117
+ function selectStartDirs(workspaces, options) {
118
+ const { only, setVersions } = options;
119
+ if (only === undefined) {
120
+ return new Set(workspaces.map((workspace) => workspace.dir));
114
121
  }
122
+ return new Set([...only, ...(setVersions?.keys() ?? [])]);
123
+ }
124
+ function determineDirectBumps(config, options, startDirs) {
125
+ const { force, bumpOverride, setVersions } = options;
115
126
  const directBumps = new Map();
116
127
  const directResults = new Map();
117
128
  const skippedResults = [];
@@ -138,6 +149,7 @@ function determineDirectBumps(config, options) {
138
149
  if (tag === undefined) {
139
150
  maybeEmitBaselineHint(workspace, knownPrefixes, hintState);
140
151
  }
152
+ const setVersion = setVersions?.get(workspace.dir);
141
153
  if (setVersion !== undefined) {
142
154
  directBumps.set(workspace.dir, { releaseType: 'patch', newVersionOverride: setVersion });
143
155
  directResults.set(workspace.dir, {
@@ -152,7 +164,7 @@ function determineDirectBumps(config, options) {
152
164
  const decision = decideRelease({
153
165
  naturalBump: history.unreleased.bump,
154
166
  commitCount: history.unreleased.commits.length,
155
- force,
167
+ force: force === true && startDirs.has(workspace.dir),
156
168
  bumpOverride,
157
169
  skipReasons: {
158
170
  noCommits: `No commits for ${name} ${since}. Pass --force to release at patch. Skipping.`,
@@ -173,8 +185,8 @@ function determineDirectBumps(config, options) {
173
185
  }
174
186
  return { directBumps, directResults, skippedResults, untaggedBaselines };
175
187
  }
176
- function reportUnroutedEntryScopes(results, config, options) {
177
- const findings = findUnroutedEntryScopes(results.map(({ workspace, history }) => ({ dir: workspace.dir, commits: history.unreleased.commits })), options.configuredWorkspaceDirs ?? config.workspaces.map((workspace) => workspace.dir), config.scopeAliases ?? {});
188
+ function reportUnroutedEntryScopes(results, config, configuredWorkspaceDirs) {
189
+ const findings = findUnroutedEntryScopes(results.map(({ workspace, history }) => ({ dir: workspace.dir, commits: history.unreleased.commits })), configuredWorkspaceDirs, config.scopeAliases ?? {});
178
190
  for (const { workspace, history } of results) {
179
191
  history.unreleased.diagnostics.unroutedEntryScopes = findings.get(workspace.dir) ?? [];
180
192
  }
@@ -68,7 +68,7 @@ CLI flag interactions:
68
68
  - `--dry-run` previews project artifacts alongside workspace artifacts; it doesn't write any files.
69
69
  - `--bump=major|minor|patch` propagates to the project release as a level chooser. It does not trigger a release on its own when the window doesn't have any commits or any bump-worthy commits.
70
70
  - `--force` runs the project release even when the history since the last project tag doesn't have any commits or any bump-worthy commits. Defaults to patch when `--bump` is not given; combine with `--bump=X` to release at a different level.
71
- - `--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 that the run excluded. Run a full `prepare` (without `--only`) to include the project release.
72
- - `--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.
71
+ - `--only` narrows the run to the named workspaces and their dependents 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 that the run excluded. Run a full `prepare` (without `--only`) to include the project release.
72
+ - `--set-version` is rejected with an error when `project` is configured. `--set-version` sets individual workspaces' versions, 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.
73
73
 
74
74
  How `--bump` and `--force` combine for every target is in [Releasing](releasing.md#release-kit-prepare).
@@ -7,12 +7,12 @@ release-kit publishes two [readyup](https://www.npmjs.com/package/readyup) kits
7
7
  The [README](../README.md#readiness-checks) shows how to name release-kit in the readyup config.
8
8
 
9
9
  ```bash
10
- rdy run --packages # every kit each listed package publishes
10
+ rdy run --sources # every kit each listed package publishes
11
11
  rdy run --from npm:@williamthorsen/release-kit # the default kit alone
12
12
  rdy run --from npm:@williamthorsen/release-kit npm-auto-publish # the npm-auto-publish kit
13
13
  rdy list --from npm:@williamthorsen/release-kit # what release-kit publishes
14
14
  ```
15
15
 
16
- `--packages` is the form that keeps working when release-kit publishes further kits.
16
+ `--sources` is the form that keeps working when release-kit publishes further kits.
17
17
 
18
18
  Because `npm-auto-publish` queries the npm registry for each package's trusted publisher, it needs an npm session elevated by two-factor authentication, and is meant to be invoked deliberately rather than included in an unattended run. A session that cannot answer those queries, whether it is missing a login, missing the elevation, or facing an unreachable registry, is reported once by the `npm session can answer trust queries` gate. The rows that would repeat the failed query are then skipped: the trusted-publisher rows in every case, and `published to npm` when the registry itself is unreachable. Each package's `package.json` checks report throughout, since they don't read a registry.
package/docs/releasing.md CHANGED
@@ -29,7 +29,7 @@ The workflows are managed files: release-kit renders them, and a change made by
29
29
 
30
30
  Runs release preparation with automatic workspace discovery.
31
31
 
32
- Workspace names for `--only` match the package directory name (e.g., `arrays`, `release-kit`).
32
+ `--only` names the workspaces from which the release starts, by package directory name (e.g., `arrays`, `release-kit`). The run also releases every workspace that depends on one of them, directly or transitively: A dependent with commits of its own releases at its own bump, and one without releases at a propagated patch bump, so that it publishes against the new version. Workspaces outside that set are left out of the run.
33
33
 
34
34
  `prepare` reads each target's history once and builds the changelog items of its unreleased commits. The bump is the highest level that those items call for, under [`versionPatterns`](configuration.md#versionpatterns), so the bump and the changelog describe the same release: A `feat` entry in a [change-record block](changelogs.md#change-record-blocks) bumps minor under a `docs:` title, and a breaking item bumps major.
35
35
 
@@ -82,18 +82,20 @@ docs/*.v*.md
82
82
 
83
83
  The `--set-version` flag is a first-class escape hatch for the cases in which the derived bump produces the wrong version, most notably when promoting a pre-1.0 package to 1.0.0. Pre-1.0 packages collapse a `feat!` breaking change to a minor bump (matching semantic-release's `initialMajor: false` and release-please's `bump-minor-pre-major`), so a deliberate promotion to 1.0.0 must be requested explicitly.
84
84
 
85
+ In a single-package repo, the value is the version: `--set-version 1.0.0`. In a monorepo, it names the workspace with each version, as comma-separated `<workspace>@N.N.N` entries: `--set-version arrays@1.0.0,strings@2.0.0`. Each named workspace releases at its version whether or not it has commits, and starts the release like a workspace named by `--only`.
86
+
85
87
  The flag validates that:
86
88
 
87
- - The value is canonical `N.N.N` semver (pre-release suffixes are rejected).
88
- - The target is strictly greater than the current version (numeric comparison on each component).
89
- - In monorepo mode, `--only` is set and resolves to exactly one workspace.
89
+ - Each version is canonical `N.N.N` semver (pre-release suffixes are rejected).
90
+ - In a monorepo, each entry names a known workspace, and none names it twice.
91
+ - Each version is strictly greater than the workspace's current version (numeric comparison on each component).
90
92
 
91
- `--set-version` is mutually exclusive with `--bump` and `--force`. The rest of the pipeline (changelog generation, tag creation, commit summary, propagation to dependents) runs unchanged, so dependents receive a propagated patch bump triggered by the overridden version.
93
+ The rest of the pipeline (changelog generation, tag creation, commit summary, propagation to dependents) runs unchanged, so each dependent receives a propagated patch bump that records the explicit version. Without `--only`, the run is a full run with the explicit versions applied. `--bump` and `--force` combine with `--set-version` and govern every other workspace.
92
94
 
93
- Promoting a pre-1.0 package to 1.0.0 in a monorepo:
95
+ Promoting a pre-1.0 package to 1.0.0 in a monorepo, which also releases its dependents:
94
96
 
95
97
  ```sh
96
- release-kit prepare --only arrays --set-version 1.0.0
98
+ release-kit prepare --set-version arrays@1.0.0 --only arrays
97
99
  ```
98
100
 
99
101
  An empty changelog section is expected for a bare promotion, because the changelog is generated from commits since the last tag. To include a narrative entry, merge a descriptive release commit (e.g., a `feat!` describing the stable API) before running `prepare`.
@@ -118,7 +120,7 @@ The `init` command scaffolds a release workflow at `.github/workflows/release.ya
118
120
 
119
121
  | Input | Type | Description |
120
122
  | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
121
- | `only` | string | Workspaces to release (comma-separated, leave empty for all) |
123
+ | `only` | string | Workspaces to release, with their dependents (comma-separated, leave empty for all) |
122
124
  | `bump` | choice | Bump type: `auto` (default, detects it from commits), `patch`, `minor`, or `major` |
123
125
  | `force` | boolean | Release even when the window doesn't have any commits or any bump-worthy commits (defaults to patch; combine with `bump` for a different level) |
124
126
 
@@ -130,7 +132,7 @@ For repos that need a self-contained workflow instead of the reusable one, the s
130
132
  # All workspaces
131
133
  gh workflow run release.yaml
132
134
 
133
- # Specific workspace(s)
135
+ # Specific workspace(s), with their dependents
134
136
  gh workflow run release.yaml -f only=arrays
135
137
  gh workflow run release.yaml -f only=arrays,strings -f bump=minor
136
138
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/release-kit",
3
- "version": "13.1.0",
3
+ "version": "14.0.0",
4
4
  "description": "Version-bumping and changelog-generation toolkit for release workflows",
5
5
  "keywords": [
6
6
  "changelog",
@@ -50,9 +50,9 @@
50
50
  "schemas/**"
51
51
  ],
52
52
  "dependencies": {
53
- "@williamthorsen/change-grammar": "0.1.0",
54
- "@williamthorsen/nmr-core": "0.13.1",
55
- "@williamthorsen/toolbelt.errors": "0.8.1",
53
+ "@williamthorsen/change-grammar": "0.2.0",
54
+ "@williamthorsen/nmr-core": "0.13.2",
55
+ "@williamthorsen/toolbelt.errors": "0.8.2",
56
56
  "json-stringify-pretty-compact": "4.0.0",
57
57
  "semver": "7.8.5",
58
58
  "yaml": "2.9.1",
@@ -60,12 +60,12 @@
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/semver": "7.8.0",
63
- "@williamthorsen/toolbelt.filesystem": "0.9.1",
64
- "@williamthorsen/toolbelt.testing": "0.6.1",
65
- "@williamthorsen/toolbelt.vitest": "0.10.1",
63
+ "@williamthorsen/toolbelt.filesystem": "0.9.2",
64
+ "@williamthorsen/toolbelt.testing": "0.6.2",
65
+ "@williamthorsen/toolbelt.vitest": "0.10.2",
66
66
  "actionlint": "2.0.6",
67
67
  "esbuild": "0.28.2",
68
- "readyup": "0.38.0",
68
+ "readyup": "0.40.0",
69
69
  "smol-toml": "1.9.0",
70
70
  "vitest": "4.1.11"
71
71
  },
@@ -1,37 +0,0 @@
1
- import type { DependencyGraph } from './buildDependencyGraph.js';
2
- import type { WorkspaceConfig } from './types.js';
3
- /** Result of probing a workspace's commits since its last tag. */
4
- export interface CommitsProbeResult {
5
- /** Whether the workspace has any commits since its last tag (excluding `release:` commits). */
6
- has: boolean;
7
- /**
8
- * The baseline tag from which the commits were measured; `undefined` when the workspace does not have a prior tag.
9
- */
10
- tag: string | undefined;
11
- }
12
- /** A single excluded workspace whose changes would be stranded by the `--only` invocation. */
13
- export interface StrandedDependentViolation {
14
- /** The excluded workspace's `dir`. */
15
- dir: string;
16
- /** The `dir` of the released (or anticipated-release) dependency whose release republishes this workspace. */
17
- downstreamOf: string;
18
- /**
19
- * The baseline tag from which this workspace's commits were counted; `undefined` if the workspace does not have a
20
- * prior tag.
21
- */
22
- tag: string | undefined;
23
- }
24
- /** Probe callback that returns whether a workspace has commits since its last tag. */
25
- type CommitsProbe = (workspace: WorkspaceConfig) => CommitsProbeResult;
26
- /**
27
- * Detects `--only` invocations that would silently strand changes in excluded internal dependents.
28
- *
29
- * First computes R, the `--only` workspaces that will release, then walks the reverse-dependency graph breadth-first
30
- * from R and records each excluded workspace with its own commits as a violation. The walk continues through a
31
- * violation, as though the user had added it to `--only`, which reports deeper violations in the same pass. It stops
32
- * at an excluded workspace without commits, which does not republish, so its dependents are unaffected by R.
33
- *
34
- * Returns `undefined` when the walk does not find any violation, or the violations sorted by `dir`.
35
- */
36
- export declare function validateOnlyExcludesStrandedDependents(workspaces: readonly WorkspaceConfig[], only: readonly string[], graph: DependencyGraph, hasCommits: CommitsProbe): StrandedDependentViolation[] | undefined;
37
- export {};
@@ -1,115 +0,0 @@
1
- export function validateOnlyExcludesStrandedDependents(workspaces, only, graph, hasCommits) {
2
- const probeCommits = memoizeCommitsProbe(hasCommits);
3
- const workspaceByDir = new Map(workspaces.map((w) => [w.dir, w]));
4
- const released = computeReleasedSet(only, workspaceByDir, graph, probeCommits);
5
- const violations = collectStrandedViolations(released, new Set(only), graph, probeCommits);
6
- if (violations.length === 0)
7
- return undefined;
8
- violations.sort((a, b) => a.dir.localeCompare(b.dir));
9
- return violations;
10
- }
11
- function memoizeCommitsProbe(probe) {
12
- const cache = new Map();
13
- return (workspace) => {
14
- const cached = cache.get(workspace.dir);
15
- if (cached !== undefined)
16
- return cached;
17
- const result = probe(workspace);
18
- cache.set(workspace.dir, result);
19
- return result;
20
- };
21
- }
22
- function computeReleasedSet(only, workspaceByDir, graph, probeCommits) {
23
- const released = new Set();
24
- for (const dir of only) {
25
- const workspace = workspaceByDir.get(dir);
26
- if (workspace !== undefined && probeCommits(workspace).has) {
27
- released.add(dir);
28
- }
29
- }
30
- let changed = true;
31
- while (changed) {
32
- changed = false;
33
- for (const dir of only) {
34
- if (released.has(dir))
35
- continue;
36
- if (hasDependencyIn(dir, graph, released)) {
37
- released.add(dir);
38
- changed = true;
39
- }
40
- }
41
- }
42
- return released;
43
- }
44
- function hasDependencyIn(dir, graph, released) {
45
- const forwardDeps = graph.dependenciesOf.get(dir);
46
- if (forwardDeps === undefined)
47
- return false;
48
- for (const depPackageName of forwardDeps) {
49
- const depDir = graph.packageNameToDir.get(depPackageName);
50
- if (depDir !== undefined && released.has(depDir))
51
- return true;
52
- }
53
- return false;
54
- }
55
- function collectStrandedViolations(released, onlySet, graph, probeCommits) {
56
- const violations = [];
57
- const violationDirs = new Set();
58
- const visited = new Set();
59
- const queue = [];
60
- for (const dir of released) {
61
- const packageName = graph.dirToPackageName.get(dir);
62
- if (packageName !== undefined) {
63
- queue.push({ packageName, attributionRoot: dir });
64
- }
65
- }
66
- while (queue.length > 0) {
67
- const item = queue.shift();
68
- if (item === undefined)
69
- break;
70
- if (visited.has(item.packageName))
71
- continue;
72
- visited.add(item.packageName);
73
- const dependents = graph.dependentsOf.get(item.packageName);
74
- if (dependents === undefined)
75
- continue;
76
- for (const dependent of dependents) {
77
- visitDependent(dependent, item.attributionRoot, {
78
- released,
79
- onlySet,
80
- graph,
81
- probeCommits,
82
- queue,
83
- violations,
84
- violationDirs,
85
- });
86
- }
87
- }
88
- return violations;
89
- }
90
- function visitDependent(dependent, attributionRoot, ctx) {
91
- if (ctx.onlySet.has(dependent.dir)) {
92
- if (ctx.released.has(dependent.dir)) {
93
- enqueueDependent(dependent, ctx);
94
- }
95
- return;
96
- }
97
- const probe = ctx.probeCommits(dependent);
98
- if (!probe.has)
99
- return;
100
- if (!ctx.violationDirs.has(dependent.dir)) {
101
- ctx.violationDirs.add(dependent.dir);
102
- ctx.violations.push({
103
- dir: dependent.dir,
104
- downstreamOf: attributionRoot,
105
- tag: probe.tag,
106
- });
107
- }
108
- enqueueDependent(dependent, ctx);
109
- }
110
- function enqueueDependent(dependent, ctx) {
111
- const packageName = ctx.graph.dirToPackageName.get(dependent.dir);
112
- if (packageName !== undefined) {
113
- ctx.queue.push({ packageName, attributionRoot: dependent.dir });
114
- }
115
- }