@williamthorsen/release-kit 8.0.1 ā 9.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.
- package/CHANGELOG.md +40 -0
- package/README.md +93 -18
- package/cliff.toml.template +20 -20
- package/dist/esm/bin/release-kit.js +12 -9
- package/dist/esm/buildChangelogEntries.js +5 -4
- package/dist/esm/changelogOverrides.js +4 -3
- package/dist/esm/createGithubRelease.js +2 -2
- package/dist/esm/defineConfig.d.ts +2 -0
- package/dist/esm/defineConfig.js +3 -0
- package/dist/esm/detectPackageManager.js +1 -1
- package/dist/esm/format.js +2 -2
- package/dist/esm/getCommitsSinceTarget.js +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +1 -0
- package/dist/esm/init/templates.js +6 -10
- package/dist/esm/injectReleaseNotesIntoReadme.js +1 -1
- package/dist/esm/loadValidatedConfig.d.ts +10 -0
- package/dist/esm/loadValidatedConfig.js +26 -0
- package/dist/esm/parseCommitMessage.d.ts +1 -0
- package/dist/esm/parseCommitMessage.js +3 -1
- package/dist/esm/prepareCommand.js +7 -6
- package/dist/esm/previewTagPrefixes.js +2 -5
- package/dist/esm/releasePrepareMono.js +6 -5
- package/dist/esm/renderReleaseNotes.js +2 -2
- package/dist/esm/resolveCommandTags.js +4 -3
- package/dist/esm/stripScope.js +3 -2
- package/dist/esm/sync-labels/generateCommand.d.ts +7 -1
- package/dist/esm/sync-labels/generateCommand.js +45 -15
- package/dist/esm/sync-labels/initCommand.js +47 -7
- package/dist/esm/sync-labels/resolveLabels.d.ts +3 -2
- package/dist/esm/sync-labels/resolveLabels.js +14 -26
- package/dist/esm/sync-labels/retiredConfig.d.ts +2 -0
- package/dist/esm/sync-labels/retiredConfig.js +11 -0
- package/dist/esm/sync-labels/syncCommand.js +4 -0
- package/dist/esm/sync-labels/templates.d.ts +3 -2
- package/dist/esm/sync-labels/templates.js +44 -16
- package/dist/esm/sync-labels/types.d.ts +0 -4
- package/dist/esm/types.d.ts +20 -0
- package/dist/esm/types.js +13 -0
- package/dist/esm/writeReleaseNotesPreviews.js +1 -1
- package/package.json +3 -3
- package/presets/labels/common.yaml +3 -0
- package/dist/esm/sync-labels/loadSyncLabelsConfig.d.ts +0 -3
- package/dist/esm/sync-labels/loadSyncLabelsConfig.js +0 -53
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 9.0.0 ā 2026-07-28
|
|
6
|
+
|
|
7
|
+
### š Features
|
|
8
|
+
|
|
9
|
+
- šØ **Breaking:** Unify label configuration in release-kit.config.ts (#505)
|
|
10
|
+
|
|
11
|
+
Repositories now declare their label set alongside the rest of their release-kit configuration in `.config/release-kit.config.ts`, and the declaration is validated and completed in the editor as it is written. A label supplied by a bundled preset can now be recolored, reworded, or dropped. `sync-labels generate --check` now reports when the committed label file has fallen behind the declaration, writing nothing. The standalone `.config/sync-labels.config.ts` is no longer read; while it remains in place, every `sync-labels` command refuses to run, naming what to move and where.
|
|
12
|
+
|
|
13
|
+
### š Bug fixes
|
|
14
|
+
|
|
15
|
+
- Include commits scoped to `*` in the changelog (#496)
|
|
16
|
+
|
|
17
|
+
Fixes an issue where commits scoped to `*` raised the version but were silently dropped from the generated changelog.
|
|
18
|
+
|
|
19
|
+
## 8.1.0 ā 2026-07-22
|
|
20
|
+
|
|
21
|
+
### š Features
|
|
22
|
+
|
|
23
|
+
- Make label deletions reviewable and apply labels on merge (#493)
|
|
24
|
+
|
|
25
|
+
Pull requests that touch a repository's label file now get a check listing every label change the sync would make, including deletions of labels the file never mentions, which a diff of the file cannot show. Merging the pull request now applies those labels. The sync's deletion and rename handling now changes only through a reviewed upgrade to a verified release. Repositories that already scaffolded this workflow adopt the new triggers by re-running `release-kit sync-labels init --force`.
|
|
26
|
+
|
|
27
|
+
### š¦ Dependencies
|
|
28
|
+
|
|
29
|
+
- Upgrade dependencies and align the Node support policy (#483)
|
|
30
|
+
|
|
31
|
+
All four published packages (`nmr`, `nmr-core`, `release-kit`, and `v11y-check`) now require Node.js 24 or later, up from Node 18.17. Separately, `nmr-compile` now rebuilds when the TypeScript version changes.
|
|
32
|
+
|
|
5
33
|
## 8.0.1 ā 2026-07-18
|
|
6
34
|
|
|
7
35
|
### š Bug fixes
|
|
@@ -32,12 +60,24 @@ All notable changes to this project will be documented in this file.
|
|
|
32
60
|
|
|
33
61
|
### š Features
|
|
34
62
|
|
|
63
|
+
- šØ **Breaking:** Rewrite parseArgs on node:util with typed error and exit API (#428)
|
|
64
|
+
|
|
65
|
+
`@williamthorsen/nmr-core` gains a typed error API for CLI parsing: `parseArgsOrExit` (parse, or print a usage error and exit) and `ParseError` replace the per-command boilerplate, so every bundled CLI reports a flag mistake the same way. Breaking: the `translateParseError` export is removed and `parseArgs` now throws `ParseError`.
|
|
66
|
+
|
|
35
67
|
- šØ **Breaking:** Auto-activate integration test variant from config presence (#448)
|
|
36
68
|
|
|
37
69
|
A package can now separate its integration tests from its standalone suite simply by including a `vitest.integration.config.ts` (alongside a `vitest.standalone.config.ts`). The `--int-test` flag that previously enabled this is removed ā that config-file pairing is now the only way to activate the separation. In such a package, `test` and `test:coverage` run only the standalone suite and skip integration tests, while a new `test:all` runs both suites together. The separation now holds even when tests run across every package at once, so a full-workspace `test` run still keeps integration tests out of the default suite. Packages that previously hand-copied these test scripts no longer need to.
|
|
38
70
|
|
|
39
71
|
### ā»ļø Refactoring
|
|
40
72
|
|
|
73
|
+
- Route all CLI error reporting through a single stderr helper (#437)
|
|
74
|
+
|
|
75
|
+
Consolidates error reporting across the CLIs so every command reports errors the same way, and guards against the previous inconsistency returning. The error messages and exit codes users see are unchanged.
|
|
76
|
+
|
|
77
|
+
- Normalize CLI error wording to the canonical Error: format (#439)
|
|
78
|
+
|
|
79
|
+
Single-line error messages from the `nmr` and `release-kit` commands now print in one uniform shape, so the same class of failure reads the same way no matter which command produced it. Previously the wording varied, which made error output harder to scan and harder for scripts to match against. Richer output, such as multi-line validation reports and the stack trace from an unexpected crash, is unchanged.
|
|
80
|
+
|
|
41
81
|
- Consolidate prepare parsing onto parseArgsOrExit (#441)
|
|
42
82
|
|
|
43
83
|
Makes `release-kit`'s `prepare` command report invalid arguments the same way its other commands already do. The error message and exit code a user sees for a bad argument are unchanged.
|
package/README.md
CHANGED
|
@@ -5,17 +5,25 @@ 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 from `git-cliff --context` output rendered in-process (with optional [editorial overrides](#editorial-overrides)).
|
|
6
6
|
|
|
7
7
|
<!-- section:release-notes -->
|
|
8
|
-
## Release notes ā
|
|
8
|
+
## Release notes ā v9.0.0 (2026-07-28)
|
|
9
|
+
|
|
10
|
+
### š Features
|
|
11
|
+
|
|
12
|
+
- šØ **Breaking:** Unify label configuration in release-kit.config.ts (#505)
|
|
13
|
+
|
|
14
|
+
Repositories now declare their label set alongside the rest of their release-kit configuration in `.config/release-kit.config.ts`, and the declaration is validated and completed in the editor as it is written. A label supplied by a bundled preset can now be recolored, reworded, or dropped. `sync-labels generate --check` now reports when the committed label file has fallen behind the declaration, writing nothing. The standalone `.config/sync-labels.config.ts` is no longer read; while it remains in place, every `sync-labels` command refuses to run, naming what to move and where.
|
|
9
15
|
|
|
10
16
|
### š Bug fixes
|
|
11
17
|
|
|
12
|
-
-
|
|
18
|
+
- Include commits scoped to `*` in the changelog (#496)
|
|
13
19
|
|
|
14
|
-
Fixes an issue where
|
|
20
|
+
Fixes an issue where commits scoped to `*` raised the version but were silently dropped from the generated changelog.
|
|
15
21
|
<!-- /section:release-notes -->
|
|
16
22
|
|
|
17
23
|
## Installation
|
|
18
24
|
|
|
25
|
+
Requires Node.js 24 or later.
|
|
26
|
+
|
|
19
27
|
```bash
|
|
20
28
|
pnpm add -D @williamthorsen/release-kit
|
|
21
29
|
```
|
|
@@ -92,9 +100,9 @@ Configuration is optional. The CLI works out of the box by auto-discovering work
|
|
|
92
100
|
### Config file
|
|
93
101
|
|
|
94
102
|
```typescript
|
|
95
|
-
import
|
|
103
|
+
import { defineConfig } from '@williamthorsen/release-kit';
|
|
96
104
|
|
|
97
|
-
|
|
105
|
+
export default defineConfig({
|
|
98
106
|
// Exclude a workspace from release processing
|
|
99
107
|
workspaces: [{ dir: 'internal-tools', shouldExclude: true }],
|
|
100
108
|
|
|
@@ -106,12 +114,10 @@ const config: ReleaseKitConfig = {
|
|
|
106
114
|
|
|
107
115
|
// Add or override work types (merged with defaults by key)
|
|
108
116
|
workTypes: { perf: { header: 'Performance' } },
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
export default config;
|
|
117
|
+
});
|
|
112
118
|
```
|
|
113
119
|
|
|
114
|
-
|
|
120
|
+
`defineConfig` is a type-safe identity function: it gives the config object validation and completion in `.ts` files and full inference in `.js` ones. The loader also accepts a plain `export default config` or `export const config = { ... }`.
|
|
115
121
|
|
|
116
122
|
### `ReleaseKitConfig` reference
|
|
117
123
|
|
|
@@ -126,6 +132,7 @@ The config file supports both `export default config` and `export const config =
|
|
|
126
132
|
| `breakingPolicies` | `Record<string, 'forbidden' \| 'optional' \| 'required'>` | Per-type `!`-policy lookup. Defaults to `DEFAULT_BREAKING_POLICIES`. Replaces the default entirely when provided. Set to `{}` to disable enforcement |
|
|
127
133
|
| `retiredPackages` | `RetiredPackage[]` | Packages that once lived in this repo but have been extracted or removed; suppresses undeclared-tag-prefix warnings |
|
|
128
134
|
| `project` | `ProjectConfig` | Opt-in project-level release block. Declaring `project: {}` (even empty) enables a project-release stage in `prepare` |
|
|
135
|
+
| `repoLabels` | `RepoLabelsConfig` | Declares the repository's label registry for `sync-labels`; see [Label configuration](#label-configuration) |
|
|
129
136
|
|
|
130
137
|
All fields are optional.
|
|
131
138
|
|
|
@@ -161,13 +168,11 @@ interface RetiredPackage {
|
|
|
161
168
|
Worked example ā `preflight` was extracted from this monorepo and continues as the standalone `readyup` project. Its tags stay in this repo as historical anchors:
|
|
162
169
|
|
|
163
170
|
```typescript
|
|
164
|
-
import
|
|
171
|
+
import { defineConfig } from '@williamthorsen/release-kit';
|
|
165
172
|
|
|
166
|
-
|
|
173
|
+
export default defineConfig({
|
|
167
174
|
retiredPackages: [{ name: '@scope/preflight', tagPrefix: 'preflight-v', successor: 'readyup' }],
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
export default config;
|
|
175
|
+
});
|
|
171
176
|
```
|
|
172
177
|
|
|
173
178
|
Validation rules:
|
|
@@ -718,15 +723,85 @@ The token needs `contents: read` on the codeassembly repo (fine-grained PAT scop
|
|
|
718
723
|
|
|
719
724
|
### `release-kit sync-labels`
|
|
720
725
|
|
|
721
|
-
Manage GitHub label definitions via config-
|
|
726
|
+
Manage GitHub label definitions via the `repoLabels` block of `.config/release-kit.config.ts`.
|
|
722
727
|
|
|
723
728
|
| Subcommand | Description | Flags |
|
|
724
729
|
| ---------- | -------------------------------------------------------------- | ---------------------- |
|
|
725
|
-
| `init` | Scaffold
|
|
726
|
-
| `generate` | Regenerate `.github/labels.yaml` from config |
|
|
730
|
+
| `init` | Scaffold the caller workflow, seed config, and generate labels | `--dry-run`, `--force` |
|
|
731
|
+
| `generate` | Regenerate `.github/labels.yaml` from config | `--check` |
|
|
727
732
|
| `sync` | Trigger the `sync-labels` GitHub Actions workflow via `gh` CLI | ā |
|
|
728
733
|
|
|
729
|
-
`init` scaffolds `.
|
|
734
|
+
`init` scaffolds the `.github/workflows/sync-labels.yaml` caller workflow and seeds a `repoLabels` block with scope labels discovered from workspaces and declared `retiredPackages`. When `.config/release-kit.config.ts` does not exist, `init` writes it; when it does, `init` prints the block for manual paste ā a hand-authored config is never rewritten. `generate` resolves the block and writes `.github/labels.yaml`; with `--check` it regenerates in memory and exits non-zero if the committed file is stale or missing, writing nothing. `sync` triggers the workflow remotely ā it requires the `gh` CLI and an existing workflow file.
|
|
735
|
+
|
|
736
|
+
Every `sync-labels` subcommand refuses to run while the retired `.config/sync-labels.config.ts` is present, so custom labels cannot be silently dropped mid-migration; see [Migrating from `.config/sync-labels.config.ts`](#migrating-from-configsync-labelsconfigts).
|
|
737
|
+
|
|
738
|
+
#### Label configuration
|
|
739
|
+
|
|
740
|
+
```typescript
|
|
741
|
+
import { defineConfig } from '@williamthorsen/release-kit';
|
|
742
|
+
|
|
743
|
+
export default defineConfig({
|
|
744
|
+
repoLabels: {
|
|
745
|
+
extends: ['common'],
|
|
746
|
+
labels: {
|
|
747
|
+
'scope:my-package': { color: '00ff96', description: 'my-package package' }, // add
|
|
748
|
+
bug: { color: 'b60205', description: 'Something broken' }, // replace the preset's `bug`
|
|
749
|
+
wontfix: null, // remove the preset's `wontfix`
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
The block declares the repository's label registry ā the set of labels defined on the GitHub repo, distinct from labels applied to PRs and issues. Resolution is an ordered fold with last-writer-wins:
|
|
756
|
+
|
|
757
|
+
1. Presets, in `extends` order ā a later preset wins on a shared name.
|
|
758
|
+
2. The `labels` record ā an entry adds a label, replaces one an earlier layer defined, or removes it (`null`). A replacing entry supplies the full `{ color, description }`.
|
|
759
|
+
|
|
760
|
+
Overlaps are never errors; order resolves them, and the committed `.github/labels.yaml` diff is where an unexpected change surfaces at review. The one config error is a dangling `null` ā removing a name no preset defines ā because that misstatement is invisible in the output diff.
|
|
761
|
+
|
|
762
|
+
#### Migrating from `.config/sync-labels.config.ts`
|
|
763
|
+
|
|
764
|
+
Label configuration formerly lived in a standalone `.config/sync-labels.config.ts` (`presets` + a `labels` array). To migrate:
|
|
765
|
+
|
|
766
|
+
1. Move the preset list to `repoLabels.extends` in `.config/release-kit.config.ts`.
|
|
767
|
+
2. Convert each `labels` array entry to a `'name': { color, description }` record entry under `repoLabels.labels`.
|
|
768
|
+
3. Delete `.config/sync-labels.config.ts` and run `release-kit sync-labels generate`.
|
|
769
|
+
|
|
770
|
+
The regenerated `.github/labels.yaml` differs only in its `# Source:` header line. Name collisions with preset labels, which the old format rejected, now mean your entry replaces the preset label.
|
|
771
|
+
|
|
772
|
+
#### When labels are applied
|
|
773
|
+
|
|
774
|
+
The scaffolded workflow carries three triggers:
|
|
775
|
+
|
|
776
|
+
| Trigger | Job | Token | Effect |
|
|
777
|
+
| --------------------------------------------------------- | ------- | --------------- | --------------------------------- |
|
|
778
|
+
| Push to the default branch touching `.github/labels.yaml` | `sync` | `issues: write` | Applies the labels |
|
|
779
|
+
| Manual dispatch (`release-kit sync-labels sync`) | `sync` | `issues: write` | Applies the labels |
|
|
780
|
+
| Pull request touching `.github/labels.yaml` | `check` | `issues: read` | Logs the diff without applying it |
|
|
781
|
+
|
|
782
|
+
Applying on merge closes the window in which a regenerated `.github/labels.yaml` sits unapplied ā a window in which a later manual dispatch would apply a label set nobody reviewed.
|
|
783
|
+
|
|
784
|
+
The `check` job runs the same sync in dry-run. Its **Label diff** log group lists every create, edit, rename, and deletion the sync would perform, including deletions of labels the file does not declare ā the destructive edits 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.
|
|
785
|
+
|
|
786
|
+
The two jobs are split because a job's permissions are fixed when the run is created and cannot vary by trigger. The split is what keeps a write-scoped token out of pull-request runs.
|
|
787
|
+
|
|
788
|
+
The push trigger filters on the path alone; the `sync` job compares `github.ref_name` against the repository's default branch. The workflow therefore needs no 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.
|
|
789
|
+
|
|
790
|
+
Manual dispatch is not a preview. It matches the `sync` job, so it applies the labels, deletions included; only the pull-request path runs in dry-run.
|
|
791
|
+
|
|
792
|
+
#### Dry-run checks on fork pull requests
|
|
793
|
+
|
|
794
|
+
GitHub issues a read-only `GITHUB_TOKEN` to pull requests from forks, and by default holds runs from first-time contributors until a maintainer approves them. The dry-run reads labels and writes nothing, so the check normally runs once approved. A repo that disables workflows on fork pull requests gets no check at all; to preview such a change, push the branch to the base repo and open the pull request from there.
|
|
795
|
+
|
|
796
|
+
#### Migrating an existing repo
|
|
797
|
+
|
|
798
|
+
The caller workflow gained the `push` and `pull_request` triggers after its first release. A repo scaffolded before that keeps working on manual dispatch alone ā nothing breaks ā but it applies labels only when someone remembers to. To adopt the new shape:
|
|
799
|
+
|
|
800
|
+
```sh
|
|
801
|
+
pnpm exec release-kit sync-labels init --force
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
`readyup` prompts for this: its `sync-labels.yaml matches template` check fails once the scaffolded workflow no longer matches the current template.
|
|
730
805
|
|
|
731
806
|
#### Published JSON Schema for `.meta/label-map.json`
|
|
732
807
|
|
package/cliff.toml.template
CHANGED
|
@@ -33,26 +33,26 @@ commit_preprocessors = []
|
|
|
33
33
|
commit_parsers = [
|
|
34
34
|
{ message = "^release:", skip = true },
|
|
35
35
|
{ message = "^Merge", skip = true },
|
|
36
|
-
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([
|
|
37
|
-
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([
|
|
38
|
-
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([
|
|
39
|
-
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([
|
|
40
|
-
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([
|
|
41
|
-
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([
|
|
42
|
-
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([
|
|
43
|
-
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([
|
|
44
|
-
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([
|
|
45
|
-
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([
|
|
46
|
-
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([
|
|
47
|
-
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([
|
|
48
|
-
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([
|
|
49
|
-
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([
|
|
50
|
-
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([
|
|
51
|
-
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([
|
|
52
|
-
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([
|
|
53
|
-
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([
|
|
54
|
-
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([
|
|
55
|
-
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([
|
|
36
|
+
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?feat(!)?:", group = "<!-- 01 -->š Features" },
|
|
37
|
+
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?feature(!)?:", group = "<!-- 01 -->š Features" },
|
|
38
|
+
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?drop(!)?:", group = "<!-- 02 -->šŖ¦ Removed" },
|
|
39
|
+
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?deprecate(!)?:", group = "<!-- 03 -->šļø Deprecated" },
|
|
40
|
+
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?fix(!)?:", group = "<!-- 04 -->š Bug fixes" },
|
|
41
|
+
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?bugfix(!)?:", group = "<!-- 04 -->š Bug fixes" },
|
|
42
|
+
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?sec(!)?:", group = "<!-- 05 -->š Security" },
|
|
43
|
+
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?security(!)?:", group = "<!-- 05 -->š Security" },
|
|
44
|
+
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?perf(!)?:", group = "<!-- 06 -->ā” Performance" },
|
|
45
|
+
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?performance(!)?:", group = "<!-- 06 -->ā” Performance" },
|
|
46
|
+
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?internal(!)?:", group = "<!-- 07 -->šļø Internal features" },
|
|
47
|
+
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?utility(!)?:", group = "<!-- 07 -->šļø Internal features" },
|
|
48
|
+
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?refactor(!)?:", group = "<!-- 08 -->ā»ļø Refactoring" },
|
|
49
|
+
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?tests?(!)?:", group = "<!-- 09 -->š§Ŗ Tests" },
|
|
50
|
+
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?tooling(!)?:", group = "<!-- 10 -->āļø Tooling" },
|
|
51
|
+
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?ci(!)?:", group = "<!-- 11 -->š· CI" },
|
|
52
|
+
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?deps?(!)?:", group = "<!-- 12 -->š¦ Dependencies" },
|
|
53
|
+
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?ai(!)?:", group = "<!-- 13 -->š¤ Agentic support" },
|
|
54
|
+
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?docs?(!)?:", group = "<!-- 14 -->š Documentation" },
|
|
55
|
+
{ message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?fmt(!)?:", skip = true },
|
|
56
56
|
# Skip unticketed commits (maintenance, deps, initial scaffolding, etc.)
|
|
57
57
|
{ message = ".*", skip = true },
|
|
58
58
|
]
|
|
@@ -57,8 +57,10 @@ function showSyncLabelsInitHelp() {
|
|
|
57
57
|
console.info(`
|
|
58
58
|
Usage: release-kit sync-labels init [options]
|
|
59
59
|
|
|
60
|
-
Scaffold the sync-labels caller workflow and
|
|
61
|
-
|
|
60
|
+
Scaffold the sync-labels caller workflow and seed the repoLabels block of
|
|
61
|
+
.config/release-kit.config.ts with scope labels discovered from workspaces and
|
|
62
|
+
retired packages, then generate .github/labels.yaml. When the config file already
|
|
63
|
+
exists, print the block for manual paste instead of rewriting the file.
|
|
62
64
|
|
|
63
65
|
Options:
|
|
64
66
|
--dry-run Preview changes without writing files
|
|
@@ -68,11 +70,12 @@ Options:
|
|
|
68
70
|
}
|
|
69
71
|
function showSyncLabelsGenerateHelp() {
|
|
70
72
|
console.info(`
|
|
71
|
-
Usage: release-kit sync-labels generate
|
|
73
|
+
Usage: release-kit sync-labels generate [options]
|
|
72
74
|
|
|
73
|
-
Regenerate .github/labels.yaml from .config/
|
|
75
|
+
Regenerate .github/labels.yaml from the repoLabels block of .config/release-kit.config.ts.
|
|
74
76
|
|
|
75
77
|
Options:
|
|
78
|
+
--check Report whether .github/labels.yaml is stale instead of writing it
|
|
76
79
|
--help, -h Show this help message
|
|
77
80
|
`);
|
|
78
81
|
}
|
|
@@ -369,11 +372,11 @@ if (command === 'sync-labels') {
|
|
|
369
372
|
showSyncLabelsGenerateHelp();
|
|
370
373
|
process.exit(0);
|
|
371
374
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
const exitCode = await generateCommand();
|
|
375
|
+
const generateFlagSchema = {
|
|
376
|
+
check: { long: '--check', type: 'boolean' },
|
|
377
|
+
};
|
|
378
|
+
const { check } = parseArgsOrExit(subflags, generateFlagSchema).flags;
|
|
379
|
+
const exitCode = await generateCommand({ check });
|
|
377
380
|
process.exit(exitCode);
|
|
378
381
|
}
|
|
379
382
|
if (subcommand === 'sync') {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { extractVersion } from "./changelogJsonUtils.js";
|
|
2
2
|
import { DEFAULT_WORK_TYPES } from "./defaults.js";
|
|
3
|
-
import { COMMIT_PREPROCESSOR_PATTERNS } from "./parseCommitMessage.js";
|
|
3
|
+
import { COMMIT_PREPROCESSOR_PATTERNS, PIPE_SCOPE_SOURCE } from "./parseCommitMessage.js";
|
|
4
4
|
import { resolveCliffConfigPath } from "./resolveCliffConfigPath.js";
|
|
5
5
|
import { runGitCliff } from "./runGitCliff.js";
|
|
6
6
|
import { stripEmojiPrefix } from "./stripEmojiPrefix.js";
|
|
@@ -9,7 +9,7 @@ const HTML_COMMENT_PREFIX_PATTERN = /^<!--[^>]*-->/;
|
|
|
9
9
|
const CANONICAL_SECTION_ORDER = new Map(Object.values(DEFAULT_WORK_TYPES).map((config, index) => [stripGroupDecorations(config.header), index]));
|
|
10
10
|
function canonicalSectionPriority(title) {
|
|
11
11
|
const index = CANONICAL_SECTION_ORDER.get(stripGroupDecorations(title));
|
|
12
|
-
return index ??
|
|
12
|
+
return index ?? Infinity;
|
|
13
13
|
}
|
|
14
14
|
export function stripGroupDecorations(group) {
|
|
15
15
|
return stripEmojiPrefix(group.replace(HTML_COMMENT_PREFIX_PATTERN, ''));
|
|
@@ -124,15 +124,16 @@ function transformReleases(releases, devOnlySections) {
|
|
|
124
124
|
function stripCommentPrefix(group) {
|
|
125
125
|
return group.replace(HTML_COMMENT_PREFIX_PATTERN, '');
|
|
126
126
|
}
|
|
127
|
+
const SUBJECT_BREAKING_MARKER_PATTERN = new RegExp(String.raw `^(?:${PIPE_SCOPE_SOURCE}\|)?\w+(?:\([^)]+\))?!:`);
|
|
127
128
|
function subjectHasBreakingMarker(message) {
|
|
128
129
|
let subject = message.split('\n', 1)[0] ?? '';
|
|
129
130
|
for (const pattern of COMMIT_PREPROCESSOR_PATTERNS) {
|
|
130
131
|
subject = subject.replace(pattern, '');
|
|
131
132
|
}
|
|
132
|
-
return
|
|
133
|
+
return SUBJECT_BREAKING_MARKER_PATTERN.test(subject);
|
|
133
134
|
}
|
|
134
135
|
function extractDescription(message) {
|
|
135
|
-
const firstLine = message.split('\n')[0] ?? message;
|
|
136
|
+
const firstLine = message.split('\n', 1)[0] ?? message;
|
|
136
137
|
const afterColon = firstLine.split(': ').slice(1).join(': ');
|
|
137
138
|
if (afterColon.length > 0) {
|
|
138
139
|
return afterColon.charAt(0).toUpperCase() + afterColon.slice(1);
|
|
@@ -59,10 +59,11 @@ function validateSingleOverride(key, rawEntry, errors) {
|
|
|
59
59
|
}
|
|
60
60
|
let entryValid = true;
|
|
61
61
|
for (const fieldName of Object.keys(rawEntry)) {
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
entryValid = false;
|
|
62
|
+
if (KNOWN_OVERRIDE_FIELDS.has(fieldName)) {
|
|
63
|
+
continue;
|
|
65
64
|
}
|
|
65
|
+
errors.push(`overrides['${key}']: unknown field '${fieldName}'`);
|
|
66
|
+
entryValid = false;
|
|
66
67
|
}
|
|
67
68
|
const result = {};
|
|
68
69
|
if (rawEntry.audience !== undefined) {
|
|
@@ -26,7 +26,7 @@ export function createGithubRelease(options) {
|
|
|
26
26
|
const body = renderReleaseNotesSingle(entry, {
|
|
27
27
|
filter: matchesAudience('all'),
|
|
28
28
|
includeHeading: false,
|
|
29
|
-
...(sectionOrder
|
|
29
|
+
...(sectionOrder !== undefined && { sectionOrder }),
|
|
30
30
|
});
|
|
31
31
|
if (body.trim() === '') {
|
|
32
32
|
return { status: 'skipped', reason: 'empty-body' };
|
|
@@ -48,7 +48,7 @@ export function createGithubReleases(tags, changelogJsonOutputPath, dryRun, sect
|
|
|
48
48
|
tag,
|
|
49
49
|
changelogJsonPath,
|
|
50
50
|
dryRun,
|
|
51
|
-
...(sectionOrder
|
|
51
|
+
...(sectionOrder !== undefined && { sectionOrder }),
|
|
52
52
|
});
|
|
53
53
|
if (result.status === 'created') {
|
|
54
54
|
created.push(tag);
|
|
@@ -7,7 +7,7 @@ export function detectPackageManager() {
|
|
|
7
7
|
const content = readFileSync(packageJsonPath, 'utf8');
|
|
8
8
|
const parsed = JSON.parse(content);
|
|
9
9
|
if (isRecord(parsed) && typeof parsed.packageManager === 'string') {
|
|
10
|
-
const [name, version] = parsed.packageManager.split('@');
|
|
10
|
+
const [name, version] = parsed.packageManager.split('@', 2);
|
|
11
11
|
if (name === 'pnpm' || name === 'npm') {
|
|
12
12
|
return name;
|
|
13
13
|
}
|
package/dist/esm/format.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export function bold(text) {
|
|
2
|
-
return `\
|
|
2
|
+
return `\u{1B}[1m${text}\u{1B}[0m`;
|
|
3
3
|
}
|
|
4
4
|
export function dim(text) {
|
|
5
|
-
return `\
|
|
5
|
+
return `\u{1B}[2m${text}\u{1B}[0m`;
|
|
6
6
|
}
|
|
7
7
|
export function sectionHeader(name) {
|
|
8
8
|
return `āāā ${name} āāā`;
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { type ChangelogOverrideScope, validateAllChangelogOverrides, type ValidateAllChangelogOverridesInputs, type ValidateAllChangelogOverridesResult, } from './changelogOverrides.js';
|
|
2
|
-
export
|
|
3
|
-
export type { ReleaseKitConfig } from './types.js';
|
|
2
|
+
export { defineConfig } from './defineConfig.js';
|
|
3
|
+
export type { LabelSpec, ReleaseKitConfig, RepoLabelsConfig } from './types.js';
|
|
4
4
|
export { formatValidateOverridesResult, type ValidateOverridesCommandResult } from './validateOverridesCommand.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export function releaseConfigScript(repoType) {
|
|
2
2
|
if (repoType === 'monorepo') {
|
|
3
|
-
return `import
|
|
3
|
+
return `import { defineConfig } from '@williamthorsen/release-kit';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
export default defineConfig({
|
|
6
6
|
releaseNotes: {
|
|
7
7
|
shouldInjectIntoReadme: true,
|
|
8
8
|
},
|
|
@@ -19,14 +19,12 @@ const config: ReleaseKitConfig = {
|
|
|
19
19
|
|
|
20
20
|
// Uncomment to add custom work types (merged with defaults):
|
|
21
21
|
// workTypes: { perf: { header: 'Performance' } },
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export default config;
|
|
22
|
+
});
|
|
25
23
|
`;
|
|
26
24
|
}
|
|
27
|
-
return `import
|
|
25
|
+
return `import { defineConfig } from '@williamthorsen/release-kit';
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
export default defineConfig({
|
|
30
28
|
releaseNotes: {
|
|
31
29
|
shouldInjectIntoReadme: true,
|
|
32
30
|
},
|
|
@@ -38,9 +36,7 @@ const config: ReleaseKitConfig = {
|
|
|
38
36
|
|
|
39
37
|
// Uncomment to add custom work types (merged with defaults):
|
|
40
38
|
// workTypes: { perf: { header: 'Performance' } },
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export default config;
|
|
39
|
+
});
|
|
44
40
|
`;
|
|
45
41
|
}
|
|
46
42
|
export function publishWorkflow(repoType) {
|
|
@@ -22,7 +22,7 @@ export function renderInjectedReadme(readme, changelogJsonPath, tag, sectionOrde
|
|
|
22
22
|
const renderedSections = renderReleaseNotesSingle(entry, {
|
|
23
23
|
filter: matchesAudience('all'),
|
|
24
24
|
includeHeading: false,
|
|
25
|
-
...(sectionOrder
|
|
25
|
+
...(sectionOrder !== undefined && { sectionOrder }),
|
|
26
26
|
});
|
|
27
27
|
if (renderedSections.trimEnd().length === 0) {
|
|
28
28
|
console.warn(`Warning: no user-facing release notes for version ${version}; skipping README injection`);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReleaseKitConfig } from './types.js';
|
|
2
|
+
export type LoadValidatedConfigResult = {
|
|
3
|
+
status: 'invalid';
|
|
4
|
+
} | {
|
|
5
|
+
status: 'missing';
|
|
6
|
+
} | {
|
|
7
|
+
status: 'ok';
|
|
8
|
+
config: ReleaseKitConfig;
|
|
9
|
+
};
|
|
10
|
+
export declare function loadValidatedConfig(): Promise<LoadValidatedConfigResult>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { reportError } from '@williamthorsen/nmr-core';
|
|
2
|
+
import { loadConfig } from "./loadConfig.js";
|
|
3
|
+
import { validateConfig } from "./validateConfig.js";
|
|
4
|
+
export async function loadValidatedConfig() {
|
|
5
|
+
let raw;
|
|
6
|
+
try {
|
|
7
|
+
raw = await loadConfig();
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
11
|
+
reportError(`Failed to load config: ${message}`);
|
|
12
|
+
return { status: 'invalid' };
|
|
13
|
+
}
|
|
14
|
+
if (raw === undefined) {
|
|
15
|
+
return { status: 'missing' };
|
|
16
|
+
}
|
|
17
|
+
const { config, errors } = validateConfig(raw);
|
|
18
|
+
if (errors.length > 0) {
|
|
19
|
+
process.stderr.write('Invalid config:\n');
|
|
20
|
+
for (const err of errors) {
|
|
21
|
+
process.stderr.write(` ā ${err}\n`);
|
|
22
|
+
}
|
|
23
|
+
return { status: 'invalid' };
|
|
24
|
+
}
|
|
25
|
+
return { status: 'ok', config };
|
|
26
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Commit, ParsedCommit, WorkTypeConfig } from './types.js';
|
|
2
2
|
export declare const COMMIT_PREPROCESSOR_PATTERNS: readonly RegExp[];
|
|
3
|
+
export declare const PIPE_SCOPE_SOURCE = "[^|]+";
|
|
3
4
|
export type PolicyViolationSurface = 'prefix' | 'body';
|
|
4
5
|
export type PolicyViolationHandler = (commit: Commit, type: string, surface: PolicyViolationSurface) => void;
|
|
5
6
|
export interface ParseCommitMessageOptions {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export const COMMIT_PREPROCESSOR_PATTERNS = [/^##\s+/, /^#\d+([.-]\d+)?\s+/, /^[A-Z]+-\d+\s+/];
|
|
2
|
+
export const PIPE_SCOPE_SOURCE = '[^|]+';
|
|
3
|
+
const COMMIT_SUBJECT_PATTERN = new RegExp(String.raw `^(?:(${PIPE_SCOPE_SOURCE})\|)?(\w+)(?:\(([^)]+)\))?(!)?:\s*(.*)$`);
|
|
2
4
|
export function parseCommitMessage(message, hash, workTypes, scopeAliases, options) {
|
|
3
5
|
const firstLine = message.split('\n', 1)[0] ?? '';
|
|
4
6
|
const stripped = stripTicketPrefix(firstLine);
|
|
5
|
-
const match = stripped.match(
|
|
7
|
+
const match = stripped.match(COMMIT_SUBJECT_PATTERN);
|
|
6
8
|
if (!match) {
|
|
7
9
|
return undefined;
|
|
8
10
|
}
|
|
@@ -85,9 +85,9 @@ export async function prepareCommand(argv) {
|
|
|
85
85
|
const options = {
|
|
86
86
|
dryRun,
|
|
87
87
|
force,
|
|
88
|
-
...(bumpOverride
|
|
89
|
-
...(setVersion
|
|
90
|
-
...(withReleaseNotes
|
|
88
|
+
...(bumpOverride !== undefined && { bumpOverride }),
|
|
89
|
+
...(setVersion !== undefined && { setVersion }),
|
|
90
|
+
...(withReleaseNotes && { withReleaseNotes: true }),
|
|
91
91
|
};
|
|
92
92
|
if (dryRun) {
|
|
93
93
|
console.info('\nš DRY RUN ā no files will be modified\n');
|
|
@@ -155,10 +155,11 @@ function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion,
|
|
|
155
155
|
if (only !== undefined) {
|
|
156
156
|
const knownNames = config.workspaces.map((w) => w.dir);
|
|
157
157
|
for (const name of only) {
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
process.exit(1);
|
|
158
|
+
if (knownNames.includes(name)) {
|
|
159
|
+
continue;
|
|
161
160
|
}
|
|
161
|
+
reportError(`Unknown workspace "${name}". Known workspaces: ${knownNames.join(', ')}`);
|
|
162
|
+
process.exit(1);
|
|
162
163
|
}
|
|
163
164
|
const graph = buildDependencyGraph(config.workspaces);
|
|
164
165
|
const violations = validateOnlyExcludesStrandedDependents(config.workspaces, only, graph, (workspace) => {
|
|
@@ -9,10 +9,7 @@ export async function previewTagPrefixes() {
|
|
|
9
9
|
const workspacePaths = (await discoverWorkspaces()) ?? [];
|
|
10
10
|
const userConfig = await loadUserConfig();
|
|
11
11
|
const overridesByDir = buildOverrideMap(userConfig);
|
|
12
|
-
const workspaces =
|
|
13
|
-
for (const workspacePath of workspacePaths) {
|
|
14
|
-
workspaces.push(buildPreviewRow(workspacePath, overridesByDir));
|
|
15
|
-
}
|
|
12
|
+
const workspaces = Array.from(workspacePaths, (workspacePath) => buildPreviewRow(workspacePath, overridesByDir));
|
|
16
13
|
const retiredPackages = buildRetiredPreviewEntries(userConfig?.retiredPackages ?? []);
|
|
17
14
|
const collisions = detectCollisions(workspaces);
|
|
18
15
|
const knownPrefixes = collectKnownPrefixes(workspaces, retiredPackages);
|
|
@@ -120,6 +117,6 @@ function buildRetiredPreviewEntries(retiredPackages) {
|
|
|
120
117
|
name: retired.name,
|
|
121
118
|
tagPrefix: retired.tagPrefix,
|
|
122
119
|
tagCount: countTagsMatching(retired.tagPrefix),
|
|
123
|
-
...(retired.successor !== undefined
|
|
120
|
+
...(retired.successor !== undefined && { successor: retired.successor }),
|
|
124
121
|
}));
|
|
125
122
|
}
|