@williamthorsen/release-kit 13.0.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,75 @@
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
+ },
56
+ {
57
+ "version": "13.1.0",
58
+ "date": "2026-10-02",
59
+ "sections": [
60
+ {
61
+ "title": "๐ŸŽ‰ Features",
62
+ "audience": "all",
63
+ "items": [
64
+ {
65
+ "description": "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)",
66
+ "hash": "23c0e0eeaf3e59cd921632850c7771ccdd6effe1",
67
+ "entry": 1
68
+ }
69
+ ]
70
+ }
71
+ ]
72
+ },
2
73
  {
3
74
  "version": "13.0.0",
4
75
  "date": "2026-10-01",
@@ -1906,10 +1977,22 @@
1906
1977
  "audience": "all",
1907
1978
  "items": [
1908
1979
  {
1909
- "description": "Migrate release-kit from toolbelt (#18)",
1910
- "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.",
1911
- "hash": "da3a61fd335fb3e05ce20a0bffcdc1647d7aeb45"
1912
- },
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": [
1913
1996
  {
1914
1997
  "description": "Slim down release workflow by removing unnecessary pnpm install (#21)",
1915
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.",
@@ -1926,11 +2009,6 @@
1926
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`.",
1927
2010
  "breaking": true,
1928
2011
  "hash": "f3416120cfe5bf0e7dbf51389e0c2647ccf41d30"
1929
- },
1930
- {
1931
- "description": "Allow git-cliff to be used without config (#31)",
1932
- "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.",
1933
- "hash": "0cbd2605d6f5f53d1efe3dda8dd0f6e4376b499f"
1934
2012
  }
1935
2013
  ]
1936
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.0.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": "7f9ab854",
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": "2fdeff1f"
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,30 @@
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
+
23
+ ## 13.1.0 โ€” 2026-10-02
24
+
25
+ ### ๐ŸŽ‰ Features
26
+
27
+ - 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)
28
+
5
29
  ## 13.0.0 โ€” 2026-10-01
6
30
 
7
31
  ### ๐ŸŽ‰ Features
@@ -1105,9 +1129,13 @@ All notable changes to this project will be documented in this file.
1105
1129
 
1106
1130
  ### ๐ŸŽ‰ Features
1107
1131
 
1108
- - Migrate release-kit from toolbelt (#18)
1132
+ - Allow git-cliff to be used without config (#31)
1109
1133
 
1110
- 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
1111
1139
 
1112
1140
  - ๐Ÿšจ **Breaking:** Slim down release workflow by removing unnecessary pnpm install (#21)
1113
1141
 
@@ -1121,10 +1149,6 @@ All notable changes to this project will be documented in this file.
1121
1149
 
1122
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`.
1123
1151
 
1124
- - Allow git-cliff to be used without config (#31)
1125
-
1126
- 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.
1127
-
1128
1152
  ### โ™ป๏ธ Refactoring
1129
1153
 
1130
1154
  - Clean up release-kit post-migration issues (#19)
package/README.md CHANGED
@@ -7,42 +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.0.0 (2026-10-01)
10
+ ## Release notes โ€” v14.0.0 (2026-10-08)
11
11
 
12
12
  ### ๐ŸŽ‰ Features
13
13
 
14
- - Adds the `--config <path>` flag to `release-kit push`, which now reads the config as `publish` and `create-github-release` do and fails when the file exists but is invalid. (#895)
15
- - Makes `release-kit` add an item derived from a change-record entry to a workspace's changelog and version bump only when the entry's `scopes`, after `scopeAliases` resolution, name the workspace's `dir`, are empty, or contain `*`, and extends that rule to the sections of earlier releases that `release-kit prepare` rebuilds. (#896)
16
- - Adds a warning to the `release-kit prepare` report that lists each change-record entry scope matching no workspace in its commit's window, such as a misspelled scope or the old name of a renamed directory that has no `scopeAliases` entry. (#896)
17
- - Makes `release-kit overrides validate` check a workspace's overrides against only the entries routed to that workspace, so that an override keyed to an entry routed elsewhere is reported as stale. (#896)
18
- - Adds `release-kit update-templates`, which writes each scaffolded workflow that is missing or differs from the installed template, prints a diff of each updated file, refreshes `sync-labels.yaml` only when it exists, keeps `publish.yaml`'s provenance setting, and never reads or writes the config files. (#952)
19
- - Changes the readyup kits' fix messages for a workflow that differs from its template, or that references a deprecated tag, to recommend `update-templates` rather than `init --force`. (#952)
20
-
21
- ### ๐Ÿชฆ Removed
22
-
23
- - ๐Ÿšจ **Breaking:** Removes `required` as a `breakingPolicies` value, making `drop` default to `optional` so that an unmarked `drop:` commit no longer reports a policy violation. (#905)
24
- - Stops `release-kit` from parsing a commit subject that combines a pipe scope with a parenthesized scope, such as `web|feat(other): ...`, or that lacks a space and a title after the colon, because subjects are now read through `@williamthorsen/change-grammar`. (#905)
25
- - Stops publishing `schemas/label-map.json` from `release-kit`, whose identical schema `@williamthorsen/change-grammar` now publishes; URLs under existing `release-kit-v*` tags keep resolving. (#905)
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)
26
15
 
27
16
  ### ๐Ÿ› Bug fixes
28
17
 
29
- - Record a changelog entry for a forced release whose commits yield no item (#887)
30
-
31
- Makes `release-kit prepare` record the "Forced version bump." changelog entry for any release forced by `--force` or `--set-version` whose commits yield no changelog item, instead of only for a release with no commits at all. It also stops a monorepo's project release from dropping the forced-release entries that earlier releases recorded.
32
-
33
- - Stops `release-kit prepare` from reporting `commitCount` 0 and dropping the unparseable commits and change-record and policy diagnostics of a workspace released only because a dependency was bumped, and makes `prepare` list that workspace's commits in the release commit summary under its tag. (#888)
34
- - Stops `release-kit prepare` from labeling a direct release that has no commits as bumped via a dependency, by marking a propagation-only release with the new `propagatedOnly` result field, whose report line now reads `Bumped via dependency: โ€ฆ` in place of `0 commits (bumped via dependency: โ€ฆ)`. (#888)
35
- - Stops a `release-kit` command run from inside a package from releasing that package as a single-package repo with its own tag and changelog, by running every command from the repo root: the nearest directory that contains `pnpm-workspace.yaml`, or else the working directory when it contains a `package.json`. (#893)
36
- - Stops `sync-labels generate` from writing the `--config` path into the `.github/labels.yaml` header as typed, which made the header and the result of `generate --check` depend on the directory from which the command ran; the header now names the config by its path relative to the repo root. (#893)
37
- - Stops `push`, `publish`, and `create-github-release` from pushing, publishing, or creating a GitHub Release for a tag on HEAD whose workspace the config excludes with `shouldExclude: true`, which each command now skips with a warning, including when `--tags` names the tag or when every tag on HEAD is excluded. (#895)
38
- - Stops `release-kit prepare` from silently deleting a `CHANGELOG.md` section whose version is absent from both the release windows and `changelog.json`, such as history written by hand before release-kit was adopted; `prepare` now keeps that section verbatim in version order, lists the kept versions in its report, and warns about each `##` section that names no version and is dropped. (#897)
39
- - Stops `release-kit prepare` from folding every earlier commit into the new version, and deriving the bump from all of them, when the current version was released and recorded in the changelog but never tagged; `prepare` now fails before any write, under `--force` and `--set-version` too, and names each target and the tag to create. (#897)
40
- - Adds the missing `removal` and `formatting` labels to the `common` preset by defining a label for every work type in change-grammar's taxonomy, and keeps the names, colors, and descriptions of the existing type labels. (#906)
41
- - Fixes the `release.yaml` generated by `release-kit init`, which `actionlint` rejected because its `bump` choice input listed an empty string as the auto-detect option, by offering `auto` as the default option and passing it to the reusable release workflow as an empty `bump`. (#936)
42
- - Rewords the error messages, `prepare` report lines, command help, and readyup kit fix strings of `release-kit` that addressed the reader as "your", separated a detail from its subject with an em-dash, or stated an absence with "no", such as "No workspaces had release-worthy changes." (#937)
43
- - Fixes the issue that the `release.yaml` and `sync-labels.yaml` workflows generated by `release-kit init` and `release-kit sync-labels init` contained unclear wording in two comments and in the `force` input description, which the change rewrites without altering any job, trigger, or input; the release-kit kit check reports a copy generated before this change as stale until `release-kit init --force` or `release-kit sync-labels init --force` regenerates it. (#946)
44
- - Fixes the issue that `release-kit` titled a propagation-only bump's changelog section `Dependency updates` rather than with the resolved `deps` work type's header, `๐Ÿ“ฆ Dependencies` by default or a consumer's `workTypes.deps.header` override, which broke the heading convention and left the section outside the section order. (#950)
45
- - Fixes the issue that `release-kit init`'s next steps told a public repo to set `provenance: true` in `publish.yaml`, which the template already sets, and now tells a private repo to remove it instead. (#952)
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)
46
19
  <!-- /section:release-notes -->
47
20
 
48
21
  ## Installation
@@ -88,7 +61,7 @@ Commit the scaffolded workflows, then start a release from the `release` workflo
88
61
 
89
62
  ```sh
90
63
  gh workflow run release.yaml # every workspace with release-worthy changes
91
- 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
92
65
  ```
93
66
 
94
67
  After upgrading release-kit, run `release-kit update-templates` to update the scaffolded workflows to its templates; it leaves the config files alone.
@@ -167,15 +140,15 @@ release-kit publishes two [readyup](https://www.npmjs.com/package/readyup) kits:
167
140
  import { defineRdyConfig } from 'readyup';
168
141
 
169
142
  export default defineRdyConfig({
170
- packages: ['@williamthorsen/release-kit'],
143
+ sources: ['npm:@williamthorsen/release-kit'],
171
144
  });
172
145
  ```
173
146
 
174
147
  ```bash
175
- rdy run --packages
148
+ rdy run --sources
176
149
  ```
177
150
 
178
- `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.
179
152
 
180
153
  [Readiness checks](docs/readiness-checks.md) covers running each kit alone and what `npm-auto-publish` needs from the npm session.
181
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
  }
@@ -15,7 +15,12 @@ export function formatLabelsYaml(labels, presetHashes, configFilePath = CONFIG_F
15
15
  for (const [name, hash] of sortedPresetHashes) {
16
16
  headerLines.push(`# ${name} preset hash: ${hash}`);
17
17
  }
18
- const declared = labels.map((label) => ({ ...label, description: label.description ?? '' }));
18
+ const declared = labels.map(({ archived, color, description, name }) => ({
19
+ name,
20
+ color,
21
+ description: description ?? '',
22
+ ...(archived !== undefined && { archived }),
23
+ }));
19
24
  const yamlBody = stringify(declared, { singleQuote: true, lineWidth: 0 });
20
25
  return headerLines.join('\n') + '\n' + yamlBody;
21
26
  }
@@ -26,6 +26,7 @@ export function resolveLabels(config) {
26
26
  name,
27
27
  color: spec.color,
28
28
  ...(spec.description !== undefined && { description: spec.description }),
29
+ ...(spec.archived !== undefined && { archived: spec.archived }),
29
30
  });
30
31
  }
31
32
  }
@@ -1,4 +1,4 @@
1
- /** A single GitHub label definition compatible with EndBug/label-sync. */
1
+ /** A single GitHub label definition, as `sync-labels generate` writes it to `.github/labels.yaml`. */
2
2
  export interface LabelDefinition {
3
3
  /** Display name of the label. */
4
4
  name: string;
@@ -6,4 +6,6 @@ export interface LabelDefinition {
6
6
  color: string;
7
7
  /** Short description shown in the GitHub UI. Absent when the label has none. */
8
8
  description?: string;
9
+ /** Marks the label for the sync to archive rather than keep active. Absent for an active label. */
10
+ archived?: true;
9
11
  }
@@ -446,11 +446,13 @@ export declare const breakingPolicyValueSchema: z.ZodEnum<{
446
446
  export declare const labelSpecSchema: z.ZodObject<{
447
447
  color: z.ZodString;
448
448
  description: z.ZodOptional<z.ZodString>;
449
+ archived: z.ZodOptional<z.ZodLiteral<true>>;
449
450
  }, z.core.$strict>;
450
451
  /**
451
- * A label's color and optional description; the name is the record key in `repoLabels.labels`.
452
- * An omitted description resolves to empty, which the generated labels file declares explicitly so that a sync
453
- * clears whatever description the label has on the repo.
452
+ * A label's color, optional description, and optional archived mark; the name is the record key in
453
+ * `repoLabels.labels`. An omitted description resolves to empty, which the generated labels file declares explicitly
454
+ * so that a sync clears whatever description the label has on the repo. `archived: true` retires the label: The sync
455
+ * archives it, keeping it on the issues and pull requests that have it.
454
456
  */
455
457
  export type LabelSpec = z.infer<typeof labelSpecSchema>;
456
458
  /**
@@ -468,6 +470,7 @@ export declare const repoLabelsSchema: z.ZodObject<{
468
470
  labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodObject<{
469
471
  color: z.ZodString;
470
472
  description: z.ZodOptional<z.ZodString>;
473
+ archived: z.ZodOptional<z.ZodLiteral<true>>;
471
474
  }, z.core.$strict>>>>;
472
475
  }, z.core.$strict>;
473
476
  /** Consumer-facing `repoLabels` block: bundled presets to extend plus per-name adjustments. */
@@ -496,6 +499,7 @@ export declare const releaseKitConfigSchema: z.ZodObject<{
496
499
  labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodObject<{
497
500
  color: z.ZodString;
498
501
  description: z.ZodOptional<z.ZodString>;
502
+ archived: z.ZodOptional<z.ZodLiteral<true>>;
499
503
  }, z.core.$strict>>>>;
500
504
  }, z.core.$strict>>;
501
505
  retiredPackages: z.ZodOptional<z.ZodArray<z.ZodObject<{
package/dist/esm/types.js CHANGED
@@ -55,6 +55,7 @@ export const labelSpecSchema = z
55
55
  .object({
56
56
  color: z.string().min(1),
57
57
  description: z.string().optional(),
58
+ archived: z.literal(true).optional(),
58
59
  })
59
60
  .strict();
60
61
  export const repoLabelsSchema = z
package/docs/labels.md CHANGED
@@ -42,6 +42,20 @@ Names match case-insensitively, as GitHub matches them: `Bug` in the `labels` re
42
42
 
43
43
  Overlaps between layers are never errors; order resolves them, and the committed `.github/labels.yaml` diff shows any unexpected change at review. Two misstatements are config errors, because neither is visible in the output diff: a dangling `null`, which removes a name not defined by any preset, and `labels` keys that differ only in case, all but one of which the fold would discard.
44
44
 
45
+ ### Retiring a label
46
+
47
+ Mark a label `archived: true` to retire it without losing its history:
48
+
49
+ ```typescript
50
+ labels: {
51
+ 'scope:basic': { color: '00ff96', archived: true }, // retire the scope of a removed package
52
+ },
53
+ ```
54
+
55
+ The sync workflow archives a marked label rather than deleting it. An archived label stays on the issues and pull requests that have it, but it cannot be added to new ones and does not appear in the label picker. Removing the mark unarchives the label on the next sync. Removing the entry instead deletes the label, as the sync deletes every label that `.github/labels.yaml` does not declare, and the label disappears from every issue and pull request that had it.
56
+
57
+ The mark is set only in the `labels` record, not in a preset. To retire a preset's label, replace it with an entry that restates its color and adds the mark.
58
+
45
59
  ### When labels are applied
46
60
 
47
61
  The scaffolded workflow declares three triggers:
@@ -56,6 +70,8 @@ Applying on merge closes the window in which a regenerated `.github/labels.yaml`
56
70
 
57
71
  The `check` job runs the same sync in dry-run. Its **Label diff** log group lists every create, edit, rename, and deletion that the sync would perform, including deletions of labels that the file does not declare: the destructive edits that a diff of `.github/labels.yaml` alone cannot show, because a label created by hand or by another workflow is absent from the file both before and after. Read that log; the check reports success whether or not the diff is destructive, so a green check is not evidence that nothing will be deleted.
58
72
 
73
+ Its **Archive state** log group lists each label that the sync would archive or unarchive. Archiving runs after the rest of the sync, so a label that the file both adds and marks is created and then archived.
74
+
59
75
  The two jobs are split because a job's permissions are fixed when the run is created and cannot vary by trigger. The split keeps a write-scoped token out of pull-request runs.
60
76
 
61
77
  The push trigger filters on the path alone; the `sync` job compares `github.ref_name` against the repository's default branch. The workflow therefore doesn't need a per-repo edit, whatever that branch is named, and a push touching `.github/labels.yaml` on any other branch produces a run whose jobs all skip.
@@ -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.0.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
- }