@williamthorsen/release-kit 5.0.0 → 5.1.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +137 -43
  3. package/dist/esm/.cache +1 -1
  4. package/dist/esm/assertCleanWorkingTree.js +1 -1
  5. package/dist/esm/bin/release-kit.js +1 -1
  6. package/dist/esm/buildChangelogEntries.d.ts +3 -0
  7. package/dist/esm/{generateChangelogJson.js → buildChangelogEntries.js} +7 -80
  8. package/dist/esm/buildDependencyGraph.d.ts +1 -0
  9. package/dist/esm/buildDependencyGraph.js +8 -1
  10. package/dist/esm/buildReleaseSummary.js +9 -1
  11. package/dist/esm/buildSyntheticChangelogEntry.d.ts +5 -0
  12. package/dist/esm/buildSyntheticChangelogEntry.js +13 -0
  13. package/dist/esm/changelogJsonFile.d.ts +4 -0
  14. package/dist/esm/changelogJsonFile.js +68 -0
  15. package/dist/esm/decideRelease.d.ts +25 -0
  16. package/dist/esm/decideRelease.js +28 -0
  17. package/dist/esm/defaults.d.ts +1 -0
  18. package/dist/esm/defaults.js +2 -0
  19. package/dist/esm/index.d.ts +2 -43
  20. package/dist/esm/index.js +0 -82
  21. package/dist/esm/init/templates.js +2 -2
  22. package/dist/esm/loadConfig.d.ts +10 -1
  23. package/dist/esm/loadConfig.js +96 -2
  24. package/dist/esm/prepareCommand.js +51 -9
  25. package/dist/esm/publish.d.ts +0 -1
  26. package/dist/esm/publish.js +3 -3
  27. package/dist/esm/publishCommand.js +10 -1
  28. package/dist/esm/releasePrepare.js +83 -39
  29. package/dist/esm/releasePrepareMono.js +133 -87
  30. package/dist/esm/releasePrepareProject.d.ts +9 -0
  31. package/dist/esm/releasePrepareProject.js +109 -0
  32. package/dist/esm/reportPrepare.js +70 -24
  33. package/dist/esm/types.d.ts +57 -14
  34. package/dist/esm/validateConfig.js +26 -0
  35. package/dist/esm/validateOnlyExcludesStrandedDependents.d.ts +14 -0
  36. package/dist/esm/validateOnlyExcludesStrandedDependents.js +109 -0
  37. package/dist/esm/version.d.ts +1 -1
  38. package/dist/esm/version.js +1 -1
  39. package/package.json +4 -1
  40. package/presets/labels/common.yaml +9 -6
  41. package/schemas/label-map.json +24 -0
  42. package/dist/esm/generateChangelogJson.d.ts +0 -7
package/CHANGELOG.md CHANGED
@@ -2,6 +2,66 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [release-kit-v5.1.0] - 2026-04-30
6
+
7
+ ### Bug fixes
8
+
9
+ - Make publish's clean-tree safety gate reachable (#311)
10
+
11
+ Fixes an issue where `release-kit publish` failed with pnpm's "working tree is dirty" error on a clean tree whenever `releaseNotes.shouldInjectIntoReadme: true` was configured. release-kit injects the release notes into the package README before invoking `pnpm publish`, so pnpm's own working-tree check fired on a tree release-kit had just dirtied — even though the user's tree was clean at command start.
12
+
13
+ - Make `--set-version` + `project` rejection explicit (#319)
14
+
15
+ Improves the error users see when invoking `release-kit prepare --set-version` with a `project` block configured. The combination is still rejected — as before — but now produces a single, project-aware message ("--set-version cannot be combined with a project release…") rather than the previous two-step chain (`--set-version requires --only`, then `--only cannot be combined with a project release` after the user added `--only`).
16
+
17
+ - Reject `--only` that would strand excluded dependents (#321)
18
+
19
+ Fixes a silent footgun in `release-kit prepare --only=...`: an excluded internal dependent with its own changes would be left unreleased with no runtime signal, even though the targeted workspace it depends on was being released. The command now rejects such invocations up front, naming every excluded dependent whose changes would be stranded.
20
+
21
+ - Order prerelease versions correctly in changelog sort (#334)
22
+
23
+ Fixes a latent issue in `@williamthorsen/release-kit` where prerelease version tags (e.g., `1.2.3-alpha`, `1.2.3-rc.1`) were sorted as if their prerelease component were absent, causing them to appear in the wrong position relative to releases sharing the same base version. Changelog entries are now ordered per SemVer §11: prerelease versions precede the corresponding release (`1.2.3-alpha < 1.2.3`), build metadata is ignored for ordering, and entries that fail SemVer validation sort deterministically to the bottom of the list rather than collapsing into mid-list positions.
24
+
25
+ ### Documentation
26
+
27
+ - Document tag prefix collisions as general rule (#320)
28
+
29
+ Documents the strict-prefix tag-prefix collision rule as a general validation rule that applies to every release-kit consumer declaring more than one tag prefix: across active workspaces, declared legacy identities, retired packages, and the optional `project` block. Previously, the rule appeared only inside the `Project releases` validation list.
30
+
31
+ ### Features
32
+
33
+ - Add `project` block for project-level release stage (#317)
34
+
35
+ Adds support for monorepos that ship a single combined deliverable to version, changelog, and release-note the project itself rather than only its constituent workspaces.
36
+
37
+ - Publish JSON Schema for `.meta/label-map.json` (#325)
38
+
39
+ Adds a JSON Schema for `.meta/label-map.json` to release-kit, packaged at `packages/release-kit/schemas/label-map.json` and shipped to npm. Consumers reference it via the stable raw URL pattern `https://github.com/williamthorsen/node-monorepo-tools/raw/release-kit-v<version>/packages/release-kit/schemas/label-map.json` — the same shape audit-deps already uses.
40
+
41
+ - Label prepare errors with the failing stage (#326)
42
+
43
+ Adds stage attribution to errors thrown during `release-kit prepare`. Errors from per-workspace bumps and changelog generation, the project release stage, and the post-release format command now begin with a stage label that identifies the failing stage and (where relevant) the affected workspace.
44
+
45
+ - Make `--force` and `--bump` orthogonal (#328)
46
+
47
+ Decouples `--force` and `--bump` so each flag has a single responsibility, and unifies skip semantics across the per-workspace and project pipelines.
48
+
49
+ ### Refactoring
50
+
51
+ - Convert prepare results to discriminated unions (#330)
52
+
53
+ Tightens `ProjectPrepareResult` and `WorkspacePrepareResult` from flat-with-optionals types into status-discriminated unions, so consumers that have already narrowed on `status === 'released'` no longer need to re-guard each release-only field with `!== undefined`. The four new sub-types (`ReleasedProjectResult`, `ReleasedWorkspaceResult`, `SkippedProjectResult`, `SkippedWorkspaceResult`) are exported from the package so callers can name the variants directly. Renderer output is byte-identical to before.
54
+
55
+ - Split changelog.json generation into layered helpers (#333)
56
+
57
+ Reorganises `changelog.json` generation in `@williamthorsen/release-kit` so that producing entries (running git-cliff and shaping the output) is fully separated from persisting them (reading, merging, and writing the file). Removes the silent-discard parse-failure path at the project release stage by no longer reading the root `changelog.json` before overwriting it. Sharpens dry-run mode: `git-cliff` now runs even on dry-run so configuration mistakes surface in preview rather than only on a real release. Trims the public `index.ts` barrel from ~50 re-exports to the two type names actually consumed by external configs.
58
+
59
+ ### Tests
60
+
61
+ - Cover untested project-release and config branches (#329)
62
+
63
+ Closes four mechanical test-coverage gaps in the project-level release surfaces flagged in the test review of #308. New cases exercise the `(no previous release found)` rendering for released projects, the unparseable-commit warning block on the released-project rendering path, the `readFileSync` I/O failure path in `readRootPackageVersion`, and the contributing-paths invariant in `releasePrepareProject`. No production code changes — these are pure-render and pure-derivation branches that previously had no test exercising them.
64
+
5
65
  ## [release-kit-v5.0.0] - 2026-04-23
6
66
 
7
67
  ### Bug fixes
package/README.md CHANGED
@@ -5,61 +5,49 @@ Version-bumping and changelog-generation toolkit for release workflows.
5
5
  Provides a self-contained CLI that auto-discovers workspaces from `pnpm-workspace.yaml`, parses conventional commits, determines version bumps, updates `package.json` files, and generates changelogs with `git-cliff`.
6
6
 
7
7
  <!-- section:release-notes -->
8
- ## Release notes — v5.0.0 (2026-04-23)
8
+ ## Release notes — v5.1.0 (2026-04-30)
9
9
 
10
10
  ### Bug fixes
11
11
 
12
- - Derive monorepo tag prefix from unscoped `package.json` name (#278)
12
+ - Make publish's clean-tree safety gate reachable (#311)
13
13
 
14
- Fixes a long-standing mismatch between a monorepo workspace's directory basename and its publishable package identity.
14
+ Fixes an issue where `release-kit publish` failed with pnpm's "working tree is dirty" error on a clean tree whenever `releaseNotes.shouldInjectIntoReadme: true` was configured. release-kit injects the release notes into the package README before invoking `pnpm publish`, so pnpm's own working-tree check fired on a tree release-kit had just dirtied — even though the user's tree was clean at command start.
15
15
 
16
- ### Features
17
-
18
- - Improve release-notes rendering quality (#261)
19
-
20
- Improves the quality of release notes and CHANGELOG entries generated by release-kit. Release notes sections are now ordered by work-type priority (bug fixes first, then features, then internal), and each bullet now includes the commit body text for context that a one-line title cannot provide. Refactoring commits are now excluded from the release notes.
21
-
22
- - Scaffold release-notes injection and check markers (#267)
23
-
24
- Adds release-notes injection to the configs scaffolded by `release-kit init`, so newly-onboarded consumers get the feature without having to discover or toggle the flag. The release-kit readyup kit gains a check that warns when a consumer's README is missing the marker pair where injected notes should land — without those markers, injection silently prepends to the top of the file, pushing the README's title below the notes.
25
-
26
- - Split GitHub Release creation into its own workflow (#272)
27
-
28
- Splits GitHub Release creation out of release-kit publish into a dedicated release-kit create-github-release CLI command and a matching reusable GitHub Actions workflow. Consumers that do not publish to npm can now create Releases independently, and the contents: write permission required to create a Release no longer leaks into the publish path.
16
+ - Make `--set-version` + `project` rejection explicit (#319)
29
17
 
30
- - Replace --only with --tags on release-kit publish and push (#273)
18
+ Improves the error users see when invoking `release-kit prepare --set-version` with a `project` block configured. The combination is still rejected — as before — but now produces a single, project-aware message ("--set-version cannot be combined with a project release…") rather than the previous two-step chain (`--set-version requires --only`, then `--only cannot be combined with a project release` after the user added `--only`).
31
19
 
32
- `release-kit publish` and `release-kit push` now filter by full tag name via `--tags=<tag1,tag2>` instead of workspace directory name via `--only=<dir>`, matching the shape already used by `create-github-release`. Callers pass the tag they care about (e.g., `core-v1.3.0`) directly, with no translation step back to the publishing workspace's directory name. The reusable workflow gains an optional `tags:` input, and the internal `publish.yaml` caller now passes `tags: ${{ github.ref_name }}`, making the publish scope explicit rather than relying on the single-tag fetch default of `actions/checkout@v6`.
20
+ - Reject `--only` that would strand excluded dependents (#321)
33
21
 
34
- - Apply pre-1.0 bump rule and add --set-version CLI escape hatch (#274)
22
+ Fixes a silent footgun in `release-kit prepare --only=...`: an excluded internal dependent with its own changes would be left unreleased with no runtime signal, even though the targeted workspace it depends on was being released. The command now rejects such invocations up front, naming every excluded dependent whose changes would be stranded.
35
23
 
36
- Fixes an issue where a `feat!` commit on a pre-1.0 package would accidentally promote it to `1.0.0`. At pre-1.0 (`0.y.z`), a `'major'` release type now collapses to a minor bump. Adds a validated `--set-version <semver>` CLI flag on `release-kit prepare` that bypasses commit-derived bump logic and writes a specific version.
24
+ - Order prerelease versions correctly in changelog sort (#334)
37
25
 
38
- - Scaffold audit.yaml workflow from audit-deps init (#277)
26
+ Fixes a latent issue in `@williamthorsen/release-kit` where prerelease version tags (e.g., `1.2.3-alpha`, `1.2.3-rc.1`) were sorted as if their prerelease component were absent, causing them to appear in the wrong position relative to releases sharing the same base version. Changelog entries are now ordered per SemVer §11: prerelease versions precede the corresponding release (`1.2.3-alpha < 1.2.3`), build metadata is ignored for ordering, and entries that fail SemVer validation sort deterministically to the bottom of the list rather than collapsing into mid-list positions.
39
27
 
40
- Adds GitHub Actions workflow scaffolding to `audit-deps init`. Running the command now writes both `.config/audit-deps.config.json` and `.github/workflows/audit.yaml` in the target repo, so that consumers no longer have to copy the canonical caller workflow by hand from this repo. The workflow content is shipped as a bundled template that ships to npm, and the repo's own workflow is kept byte-identical to that template via a consistency test — the canonical workflow cannot silently drift from what is published.
28
+ ### Features
41
29
 
42
- - Add migrate-tag-prefixes.sh migration tool (#282)
30
+ - Add `project` block for project-level release stage (#317)
43
31
 
44
- Adds a one-shot migration tool, `migrate-tag-prefixes.sh`, shipped inside the release-kit package. The tool creates additive annotated-tag aliases under release-kit's new unscoped-package-name prefix that point at the same commits as the previous directory-basename tags, bridging the gap so post-migration `getCommitsSinceTarget` calls can resolve prior releases.
32
+ Adds support for monorepos that ship a single combined deliverable to version, changelog, and release-note the project itself rather than only its constituent workspaces.
45
33
 
46
- - Add legacyTagPrefixes config field (#289)
34
+ - Publish JSON Schema for `.meta/label-map.json` (#325)
47
35
 
48
- Replaces the v4 v5 tag-prefix migration mechanism (tag aliasing via `migrate-tag-prefixes.sh`) with a declarative `legacyTagPrefixes` config field. release-kit now searches for both legacy and modern prefixes when generating changelogs.
36
+ Adds a JSON Schema for `.meta/label-map.json` to release-kit, packaged at `packages/release-kit/schemas/label-map.json` and shipped to npm. Consumers reference it via the stable raw URL pattern `https://github.com/williamthorsen/node-monorepo-tools/raw/release-kit-v<version>/packages/release-kit/schemas/label-map.json` the same shape audit-deps already uses.
49
37
 
50
- Adds a companion `release-kit show-tag-prefixes` CLI command that renders a per-workspace table of derived and declared legacy prefixes, flags cross-workspace collisions, and surfaces undeclared candidate prefixes with a copy-pasteable config snippet. `release-kit prepare` gains a one-line hint pointing operators to `show-tag-prefixes` when a workspace has no baseline tag but the repo contains candidate-shaped tags.
38
+ - Label prepare errors with the failing stage (#326)
51
39
 
52
- - Replace `legacyTagPrefixes` with `legacyIdentities` (#297)
40
+ Adds stage attribution to errors thrown during `release-kit prepare`. Errors from per-workspace bumps and changelog generation, the project release stage, and the post-release format command now begin with a stage label that identifies the failing stage and (where relevant) the affected workspace.
53
41
 
54
- Replaces the per-workspace `legacyTagPrefixes: string[]` field with `legacyIdentities: LegacyIdentity[]`, a structured array of complete `(name, tagPrefix)` historical snapshots. Each legacy identity is now a self-consistent record of what a workspace used to be called and how its tags used to be prefixed, so a workspace that has been renamed (npm name change, tag-prefix change, or both) carries one entry per prior identity.
42
+ - Make `--force` and `--bump` orthogonal (#328)
55
43
 
56
- - Add `retiredPackages` repo-level config field (#299)
44
+ Decouples `--force` and `--bump` so each flag has a single responsibility, and unifies skip semantics across the per-workspace and project pipelines.
57
45
 
58
- Adds support for declaring packages that once lived in this repo but have been extracted or removed, so their historical tag prefixes (e.g., `preflight-v*` from the extracted `readyup` project) no longer surface as "Undeclared tag prefixes" in `release-kit show-tag-prefixes`. Declared retired packages are acknowledged as real history but never consulted for baseline lookup or changelog attribution — they complement `workspaces[].legacyIdentities`, which is used when a workspace still exists under a new identity.
46
+ ### Documentation
59
47
 
60
- - Add release-notes preview generator to `release-kit prepare` (#302)
48
+ - Document tag prefix collisions as general rule (#320)
61
49
 
62
- Adds the ability to generate release notes when running `release-kit prepare`. The `--with-release-notes` option enables the generation of per-workspace preview files so authors can verify release-note injection before publishing. Injected release notes are also now prefixed with a `## Release notes v{version} ({date})` heading to add missing context to the README.
50
+ Documents the strict-prefix tag-prefix collision rule as a general validation rule that applies to every release-kit consumer declaring more than one tag prefix: across active workspaces, declared legacy identities, retired packages, and the optional `project` block. Previously, the rule appeared only inside the `Project releases` validation list.
63
51
  <!-- /section:release-notes -->
64
52
 
65
53
  ## Installation
@@ -172,6 +160,7 @@ The config file supports both `export default config` and `export const config =
172
160
  | `scopeAliases` | `Record<string, string>` | Maps shorthand scope names to canonical names in commits |
173
161
  | `workTypes` | `Record<string, WorkTypeConfig>` | Work type definitions, merged with defaults by key |
174
162
  | `retiredPackages` | `RetiredPackage[]` | Packages that once lived in this repo but have been extracted or removed; suppresses undeclared-tag-prefix warnings |
163
+ | `project` | `ProjectConfig` | Opt-in project-level release block. Declaring `project: {}` (even empty) enables a project-release stage in `prepare` |
175
164
 
176
165
  All fields are optional.
177
166
 
@@ -222,10 +211,95 @@ Validation rules:
222
211
  - `successor` is optional; if present, it must be a non-empty string.
223
212
  - Full-tuple `(name, tagPrefix)` duplicates within `retiredPackages` are rejected.
224
213
  - Two entries sharing the same `tagPrefix` but different `name`s are accepted — this documents a package renamed within the repo before being retired.
225
- - A `tagPrefix` that collides with an active workspace's derived prefix or any declared `workspaces[].legacyIdentities[].tagPrefix` is rejected. A retired prefix cannot also be current or legacy.
226
214
 
227
215
  `show-tag-prefixes` currently does not render a dedicated "Retired packages" section (deferred). Declaring a retired entry is verifiable by confirming that its `tagPrefix` stops appearing under "Undeclared tag prefixes" in the `show-tag-prefixes` output.
228
216
 
217
+ ### Tag prefix collisions
218
+
219
+ Tag prefixes from distinct owners must not be identical or be a strict prefix of one another. An owner is one of:
220
+
221
+ - An active workspace, comprising its derived `tagPrefix` plus any declared `legacyIdentities[].tagPrefix`. Identities of the same workspace are one owner, so their prefixes are allowed to overlap (this represents the same package across renames).
222
+ - A `retiredPackages[]` entry (one owner per entry).
223
+ - The `project` block, when configured.
224
+
225
+ release-kit resolves baseline tags via `git describe --match=<prefix>*`, so a strict-prefix overlap between distinct owners would cause that glob to return cross-matches against the wrong owner's history. For example, a project prefix of `v` collides with a workspace prefix of `vue-helpers-v`, since `git describe --match=v*` would return both project tags and `vue-helpers` tags.
226
+
227
+ The rule is enforced at config load; the resulting error identifies both colliding declarations.
228
+
229
+ ### Project releases
230
+
231
+ Some monorepos ship a single combined deliverable — a Chrome extension, a CLI binary, a packaged desktop app — for which the per-workspace tags and changelogs alone do not describe what the user actually receives. Declare the optional `project` block to add a project-level release stage that runs alongside the per-workspace pipeline.
232
+
233
+ ```typescript
234
+ import type { ReleaseKitConfig } from '@williamthorsen/release-kit';
235
+
236
+ const config: ReleaseKitConfig = {
237
+ // Empty object is enough to opt in. Every non-excluded workspace contributes.
238
+ project: {},
239
+ };
240
+
241
+ export default config;
242
+ ```
243
+
244
+ When configured, each `release-kit prepare` run additionally:
245
+
246
+ - Computes commits since the last project tag (`<tagPrefix><version>`), filtered to the union of every contributing workspace's paths.
247
+ - Bumps the root `package.json`'s `version` field using the same bump-derivation rules as workspaces (or the `--bump=...` override).
248
+ - Regenerates the root `./CHANGELOG.md` via `git-cliff`, scoped to the project's `tagPrefix` and contributing paths.
249
+ - Emits `./.meta/changelog.json` (when `changelogJson.enabled`).
250
+ - With `--with-release-notes`, additionally emits `./docs/RELEASE_NOTES.v<version>.md`.
251
+ - Appends the project tag to `tmp/.release-tags` so `release-kit commit` and `release-kit tag` pick it up alongside per-workspace tags.
252
+
253
+ If no contributing workspace has commits since the last project tag, the project release is silently skipped — same behavior as a per-workspace skip.
254
+
255
+ #### `ProjectConfig`
256
+
257
+ ```typescript
258
+ interface ProjectConfig {
259
+ tagPrefix?: string; // Defaults to 'v'
260
+ }
261
+ ```
262
+
263
+ | Field | Default | Description |
264
+ | ----------- | ------- | -------------------------------------------------------------------- |
265
+ | `tagPrefix` | `'v'` | Prefix for project tags. The full tag is `${tagPrefix}${newVersion}` |
266
+
267
+ Contributing workspaces are implicit: every non-excluded discovered workspace contributes. There is no field to override the contributing set in this initial release; if a future consumer needs to release a workspace as a component but exclude it from the project release, that override can be added then.
268
+
269
+ Validation rules:
270
+
271
+ - The root `package.json` must exist and declare a `version` field. release-kit reports an error at config-load time if either is missing.
272
+ - The `project` block is rejected in single-package mode (the implicit "all non-excluded workspaces contribute" rule is meaningless in a single-package repo).
273
+ - Unknown fields inside `project` are rejected.
274
+
275
+ CLI flag interactions:
276
+
277
+ - `--dry-run` previews project artifacts alongside workspace artifacts; no files are written.
278
+ - `--bump=major|minor|patch` propagates to the project release as a level chooser. It does not trigger a release on its own when there are no commits or no bump-worthy commits.
279
+ - `--force` runs the project release even when no commits or no bump-worthy commits exist since the last project tag. Defaults to patch when `--bump` is not given; combine with `--bump=X` to release at a different level.
280
+ - `--only` is rejected with an error when `project` is configured. `--only` is a surgical, single-workspace operation; combining it with a project release that rolls up every contributing workspace would create ambiguous semantics. To release a single workspace, use a config without a `project` block, or run a full `prepare` (no `--only`) to include the project release.
281
+ - `--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.
282
+
283
+ `--bump` and `--force` are orthogonal: `--bump` is purely a level chooser; `--force` is purely a release trigger. Examples:
284
+
285
+ ```sh
286
+ # Release every target at its natural bump level (no flags).
287
+ release-kit prepare
288
+
289
+ # Force a release even when no bump-worthy commits exist; defaults to patch
290
+ # per target, with each target keeping its natural bump if one is derivable.
291
+ release-kit prepare --force
292
+
293
+ # Force a release at a uniform level across every releasing target.
294
+ release-kit prepare --force --bump=minor
295
+
296
+ # --bump=X alone is a level chooser, NOT a trigger. If a target has no
297
+ # bump-worthy commits, it skips with a "Pass --force..." reason. If it has
298
+ # bump-worthy commits, the override applies. (Behavioral change from earlier
299
+ # release-kit versions, where --bump=X alone would force a release.)
300
+ release-kit prepare --bump=minor
301
+ ```
302
+
229
303
  ### `VersionPatterns`
230
304
 
231
305
  Defines which commit types trigger major or minor bumps. Any recognized type not listed defaults to a patch bump.
@@ -277,15 +351,15 @@ Release-notes sections are rendered in the declaration order of the merged work-
277
351
 
278
352
  Run release preparation with automatic workspace discovery.
279
353
 
280
- | Flag | Description |
281
- | ---------------------------- | ------------------------------------------------------------------------------------------------------------ |
282
- | `--dry-run` | Preview changes without writing files |
283
- | `--bump=major\|minor\|patch` | Override the bump type for all workspaces |
284
- | `--set-version=X.Y.Z` | Set an explicit canonical semver version; bypasses commit-derived bumps. Requires `--only` in monorepo mode. |
285
- | `--force` | Bypass the "no commits since last tag" check (requires `--bump`) |
286
- | `--only=name1,name2` | Only process the named workspaces (monorepo only) |
287
- | `--with-release-notes` | Write per-workspace release-notes previews under `{workspacePath}/docs/` |
288
- | `--help`, `-h` | Show help |
354
+ | Flag | Description |
355
+ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
356
+ | `--dry-run` | Preview changes without writing files |
357
+ | `--bump=major\|minor\|patch` | Override the bump type for all workspaces |
358
+ | `--set-version=X.Y.Z` | Set an explicit canonical semver version; bypasses commit-derived bumps. Requires `--only` in monorepo mode. |
359
+ | `--force` | Release even when no commits or no bump-worthy commits exist since the last tag (defaults to patch; combine with `--bump=X` for a different level) |
360
+ | `--only=name1,name2` | Only process the named workspaces (monorepo only; rejected when a `project` block is configured) |
361
+ | `--with-release-notes` | Write per-workspace release-notes previews under `{workspacePath}/docs/` |
362
+ | `--help`, `-h` | Show help |
289
363
 
290
364
  Workspace names for `--only` match the package directory name (e.g., `arrays`, `release-kit`).
291
365
 
@@ -385,6 +459,26 @@ Manage GitHub label definitions via config-driven YAML files.
385
459
 
386
460
  `init` scaffolds `.config/sync-labels.config.ts` with auto-detected workspace scope labels and a `.github/workflows/sync-labels.yaml` caller workflow, then generates `.github/labels.yaml`. `generate` reads the config and writes `.github/labels.yaml`. `sync` triggers the workflow remotely — it requires the `gh` CLI and an existing workflow file.
387
461
 
462
+ #### Published JSON Schema for `.meta/label-map.json`
463
+
464
+ release-kit publishes a JSON Schema for `.meta/label-map.json` — a separate, generic data file that maps commit-prefix scopes and types to GitHub label names. The schema lives at `packages/release-kit/schemas/label-map.json` in this repo and is reachable via the stable raw URL:
465
+
466
+ ```
467
+ https://github.com/williamthorsen/node-monorepo-tools/raw/release-kit-v<version>/packages/release-kit/schemas/label-map.json
468
+ ```
469
+
470
+ Consumers reference it from the top of their `.meta/label-map.json`:
471
+
472
+ ```json
473
+ {
474
+ "$schema": "https://github.com/williamthorsen/node-monorepo-tools/raw/release-kit-v<version>/packages/release-kit/schemas/label-map.json",
475
+ "types": { "feat": "feature", "fix": "fix" },
476
+ "scopes": { "audit": "scope:audit" }
477
+ }
478
+ ```
479
+
480
+ release-kit publishes the schema only; it does not generate `.meta/label-map.json`. Generation requires commit-prefix knowledge that lives outside release-kit (in agent-conventions tooling), and is owned by those consumers.
481
+
388
482
  ## GitHub Actions workflow
389
483
 
390
484
  The `init` command scaffolds a release workflow at `.github/workflows/release.yaml` that delegates to a reusable release workflow. The scaffolded workflow accepts these inputs:
package/dist/esm/.cache CHANGED
@@ -1 +1 @@
1
- 4493cd8b695da653fc9ae7a81c0916cc0cb92c9acf3d560433b18cdd74f945a4
1
+ fca70d83414ef35352e152c9e916f24705c93c5556b14c16388a5b791752f075
@@ -6,7 +6,7 @@ function assertCleanWorkingTree() {
6
6
  }).trim();
7
7
  if (status.length > 0) {
8
8
  throw new Error(
9
- "Working tree has uncommitted changes. Commit or stash them before running prepare, or use --no-git-checks to bypass this check."
9
+ "Working tree has uncommitted changes. Commit or stash them, or use --no-git-checks to bypass this check."
10
10
  );
11
11
  }
12
12
  }
@@ -183,7 +183,7 @@ Publish packages that have release tags on HEAD.
183
183
 
184
184
  Options:
185
185
  --dry-run Preview without publishing
186
- --no-git-checks Skip git checks (pnpm only)
186
+ --no-git-checks Skip the clean-working-tree check
187
187
  --tags=tag1,tag2 Only publish the named tags (comma-separated, full tag names)
188
188
  --provenance Generate provenance statement (requires OIDC, not supported by classic yarn)
189
189
  --help, -h Show this help message
@@ -0,0 +1,3 @@
1
+ import type { GenerateChangelogOptions } from './generateChangelogs.ts';
2
+ import type { ChangelogEntry, ReleaseConfig } from './types.ts';
3
+ export declare function buildChangelogEntries(config: Pick<ReleaseConfig, 'cliffConfigPath' | 'changelogJson'>, tag: string, options?: GenerateChangelogOptions): ChangelogEntry[];
@@ -1,16 +1,11 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { copyFileSync, mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
- import { dirname, join } from "node:path";
5
- import stringify from "json-stringify-pretty-compact";
6
- import { extractVersion, isChangelogEntry } from "./changelogJsonUtils.js";
4
+ import { join } from "node:path";
5
+ import { extractVersion } from "./changelogJsonUtils.js";
7
6
  import { resolveCliffConfigPath } from "./resolveCliffConfigPath.js";
8
7
  import { isRecord, isUnknownArray } from "./typeGuards.js";
9
- function generateChangelogJson(config, changelogPath, tag, dryRun, options) {
10
- const outputFile = join(changelogPath, config.changelogJson.outputPath);
11
- if (dryRun) {
12
- return [outputFile];
13
- }
8
+ function buildChangelogEntries(config, tag, options) {
14
9
  const resolvedConfigPath = resolveCliffConfigPath(config.cliffConfigPath, import.meta.url);
15
10
  let cliffConfigPath = resolvedConfigPath;
16
11
  let tempDir;
@@ -33,15 +28,10 @@ function generateChangelogJson(config, changelogPath, tag, dryRun, options) {
33
28
  });
34
29
  const releases = parseCliffContext(contextJson);
35
30
  const devOnlySections = new Set(config.changelogJson.devOnlySections);
36
- const entries = transformReleases(releases, devOnlySections);
37
- const existingEntries = readExistingEntries(outputFile);
38
- const merged = mergeEntries(entries, existingEntries);
39
- mkdirSync(dirname(outputFile), { recursive: true });
40
- writeFileSync(outputFile, stringify(merged, { maxLength: 100 }) + "\n", "utf8");
41
- return [outputFile];
31
+ return transformReleases(releases, devOnlySections);
42
32
  } catch (error) {
43
33
  throw new Error(
44
- `Failed to generate changelog JSON for ${outputFile}: ${error instanceof Error ? error.message : String(error)}`
34
+ `Failed to build changelog entries for tag ${tag}: ${error instanceof Error ? error.message : String(error)}`
45
35
  );
46
36
  } finally {
47
37
  if (tempDir !== void 0) {
@@ -49,25 +39,6 @@ function generateChangelogJson(config, changelogPath, tag, dryRun, options) {
49
39
  }
50
40
  }
51
41
  }
52
- function generateSyntheticChangelogJson(config, changelogPath, newVersion, date, propagatedFrom, dryRun) {
53
- const outputFile = join(changelogPath, config.changelogJson.outputPath);
54
- if (dryRun) {
55
- return [outputFile];
56
- }
57
- const items = propagatedFrom.map((dep) => ({
58
- description: `Bumped \`${dep.packageName}\` to ${dep.newVersion}`
59
- }));
60
- const entry = {
61
- version: newVersion,
62
- date,
63
- sections: [{ title: "Dependency updates", audience: "dev", items }]
64
- };
65
- const existingEntries = readExistingEntries(outputFile);
66
- const merged = mergeEntries([entry], existingEntries);
67
- mkdirSync(dirname(outputFile), { recursive: true });
68
- writeFileSync(outputFile, stringify(merged, { maxLength: 100 }) + "\n", "utf8");
69
- return [outputFile];
70
- }
71
42
  function parseCliffContext(json) {
72
43
  const parsed = JSON.parse(json);
73
44
  if (!isUnknownArray(parsed)) {
@@ -183,50 +154,6 @@ function extractBody(message) {
183
154
  }
184
155
  return lines.slice(start, end).join("\n").trim();
185
156
  }
186
- function readExistingEntries(filePath) {
187
- if (!existsSync(filePath)) {
188
- return [];
189
- }
190
- try {
191
- const content = readFileSync(filePath, "utf8");
192
- const parsed = JSON.parse(content);
193
- if (!isUnknownArray(parsed)) {
194
- return [];
195
- }
196
- return parsed.filter(isChangelogEntry);
197
- } catch (error) {
198
- console.warn(
199
- `Warning: could not parse existing ${filePath}: ${error instanceof Error ? error.message : String(error)}; treating as empty`
200
- );
201
- return [];
202
- }
203
- }
204
- function mergeEntries(newEntries, existingEntries) {
205
- const versionMap = /* @__PURE__ */ new Map();
206
- for (const entry of existingEntries) {
207
- versionMap.set(entry.version, entry);
208
- }
209
- for (const entry of newEntries) {
210
- versionMap.set(entry.version, entry);
211
- }
212
- return [...versionMap.values()].sort((a, b) => compareVersionsDescending(a.version, b.version));
213
- }
214
- function parseVersionParts(version) {
215
- return version.split(".").map((s) => {
216
- const n = Number(s);
217
- return Number.isNaN(n) ? 0 : n;
218
- });
219
- }
220
- function compareVersionsDescending(a, b) {
221
- const partsA = parseVersionParts(a);
222
- const partsB = parseVersionParts(b);
223
- for (let i = 0; i < 3; i++) {
224
- const diff = (partsB[i] ?? 0) - (partsA[i] ?? 0);
225
- if (diff !== 0) return diff;
226
- }
227
- return 0;
228
- }
229
157
  export {
230
- generateChangelogJson,
231
- generateSyntheticChangelogJson
158
+ buildChangelogEntries
232
159
  };
@@ -3,5 +3,6 @@ export interface DependencyGraph {
3
3
  packageNameToDir: Map<string, string>;
4
4
  dirToPackageName: Map<string, string>;
5
5
  dependentsOf: Map<string, WorkspaceConfig[]>;
6
+ dependenciesOf: Map<string, Set<string>>;
6
7
  }
7
8
  export declare function buildDependencyGraph(workspaces: readonly WorkspaceConfig[]): DependencyGraph;
@@ -6,6 +6,7 @@ function buildDependencyGraph(workspaces) {
6
6
  const packageNameToDir = /* @__PURE__ */ new Map();
7
7
  const dirToPackageName = /* @__PURE__ */ new Map();
8
8
  const dependentsOf = /* @__PURE__ */ new Map();
9
+ const dependenciesOf = /* @__PURE__ */ new Map();
9
10
  const workspacePackages = /* @__PURE__ */ new Map();
10
11
  for (const workspace of workspaces) {
11
12
  const primaryPackageFile = workspace.packageFiles[0];
@@ -32,9 +33,15 @@ function buildDependencyGraph(workspaces) {
32
33
  } else {
33
34
  existing.push(workspace);
34
35
  }
36
+ const forward = dependenciesOf.get(workspace.dir);
37
+ if (forward === void 0) {
38
+ dependenciesOf.set(workspace.dir, /* @__PURE__ */ new Set([depName]));
39
+ } else {
40
+ forward.add(depName);
41
+ }
35
42
  }
36
43
  }
37
- return { packageNameToDir, dirToPackageName, dependentsOf };
44
+ return { packageNameToDir, dirToPackageName, dependentsOf, dependenciesOf };
38
45
  }
39
46
  function readPackageJsonSubset(filePath) {
40
47
  let content;
@@ -2,7 +2,7 @@ import { stripScope } from "./stripScope.js";
2
2
  function buildReleaseSummary(result) {
3
3
  const sections = [];
4
4
  for (const workspace of result.workspaces) {
5
- if (workspace.status !== "released" || workspace.tag === void 0) {
5
+ if (workspace.status !== "released") {
6
6
  continue;
7
7
  }
8
8
  const commits = workspace.commits;
@@ -15,6 +15,14 @@ function buildReleaseSummary(result) {
15
15
  }
16
16
  sections.push(lines.join("\n"));
17
17
  }
18
+ const project = result.project;
19
+ if (project !== void 0 && project.status === "released" && project.commits.length > 0) {
20
+ const lines = [project.tag];
21
+ for (const commit of project.commits) {
22
+ lines.push(`- ${stripScope(commit.message)}`);
23
+ }
24
+ sections.push(lines.join("\n"));
25
+ }
18
26
  return sections.join("\n\n");
19
27
  }
20
28
  export {
@@ -0,0 +1,5 @@
1
+ import type { ChangelogEntry } from './types.ts';
2
+ export declare function buildSyntheticChangelogEntry(propagatedFrom: ReadonlyArray<{
3
+ packageName: string;
4
+ newVersion: string;
5
+ }>, version: string, date: string): ChangelogEntry;
@@ -0,0 +1,13 @@
1
+ function buildSyntheticChangelogEntry(propagatedFrom, version, date) {
2
+ const items = propagatedFrom.map((dep) => ({
3
+ description: `Bumped \`${dep.packageName}\` to ${dep.newVersion}`
4
+ }));
5
+ return {
6
+ version,
7
+ date,
8
+ sections: [{ title: "Dependency updates", audience: "dev", items }]
9
+ };
10
+ }
11
+ export {
12
+ buildSyntheticChangelogEntry
13
+ };
@@ -0,0 +1,4 @@
1
+ import type { ChangelogEntry, ReleaseConfig } from './types.ts';
2
+ export declare function resolveChangelogJsonPath(config: Pick<ReleaseConfig, 'changelogJson'>, changelogPath: string): string;
3
+ export declare function writeChangelogJson(filePath: string, entries: ChangelogEntry[]): string;
4
+ export declare function upsertChangelogJson(filePath: string, entries: ChangelogEntry[]): string;
@@ -0,0 +1,68 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import stringify from "json-stringify-pretty-compact";
4
+ import semver from "semver";
5
+ import { isChangelogEntry } from "./changelogJsonUtils.js";
6
+ import { isUnknownArray } from "./typeGuards.js";
7
+ function resolveChangelogJsonPath(config, changelogPath) {
8
+ return join(changelogPath, config.changelogJson.outputPath);
9
+ }
10
+ function writeChangelogJson(filePath, entries) {
11
+ const sorted = sortNewestFirst(entries);
12
+ mkdirSync(dirname(filePath), { recursive: true });
13
+ writeFileSync(filePath, stringify(sorted, { maxLength: 100 }) + "\n", "utf8");
14
+ return filePath;
15
+ }
16
+ function upsertChangelogJson(filePath, entries) {
17
+ const existing = readExistingEntries(filePath);
18
+ const merged = mergeEntries(entries, existing);
19
+ mkdirSync(dirname(filePath), { recursive: true });
20
+ writeFileSync(filePath, stringify(merged, { maxLength: 100 }) + "\n", "utf8");
21
+ return filePath;
22
+ }
23
+ function sortNewestFirst(entries) {
24
+ return [...entries].sort((a, b) => compareVersionsDescending(a.version, b.version));
25
+ }
26
+ function readExistingEntries(filePath) {
27
+ if (!existsSync(filePath)) {
28
+ return [];
29
+ }
30
+ try {
31
+ const content = readFileSync(filePath, "utf8");
32
+ const parsed = JSON.parse(content);
33
+ if (!isUnknownArray(parsed)) {
34
+ return [];
35
+ }
36
+ return parsed.filter(isChangelogEntry);
37
+ } catch (error) {
38
+ console.warn(
39
+ `Warning: could not parse existing ${filePath}: ${error instanceof Error ? error.message : String(error)}; treating as empty`
40
+ );
41
+ return [];
42
+ }
43
+ }
44
+ function mergeEntries(newEntries, existingEntries) {
45
+ const versionMap = /* @__PURE__ */ new Map();
46
+ for (const entry of existingEntries) {
47
+ versionMap.set(entry.version, entry);
48
+ }
49
+ for (const entry of newEntries) {
50
+ versionMap.set(entry.version, entry);
51
+ }
52
+ return sortNewestFirst(versionMap.values());
53
+ }
54
+ function compareVersionsDescending(a, b) {
55
+ const aValid = semver.valid(a);
56
+ const bValid = semver.valid(b);
57
+ if (aValid && bValid) return semver.rcompare(aValid, bValid);
58
+ if (aValid) return -1;
59
+ if (bValid) return 1;
60
+ if (a > b) return -1;
61
+ if (a < b) return 1;
62
+ return 0;
63
+ }
64
+ export {
65
+ resolveChangelogJsonPath,
66
+ upsertChangelogJson,
67
+ writeChangelogJson
68
+ };