@williamthorsen/release-kit 10.5.0 → 10.7.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/.meta/changelog.json +1570 -0
- package/.readyup/kits/default.js +127 -4
- package/.readyup/kits/npm-auto-publish.js +87 -23
- package/.readyup/manifest.json +14 -9
- package/CHANGELOG.md +83 -0
- package/README.md +68 -963
- package/dist/esm/bin/release-kit.js +4 -3
- package/dist/esm/buildChangelogEntries.js +5 -0
- package/dist/esm/buildDependencyGraph.d.ts +6 -5
- package/dist/esm/buildDependencyGraph.js +5 -1
- package/dist/esm/changelogJsonUtils.d.ts +5 -7
- package/dist/esm/changelogJsonUtils.js +0 -13
- package/dist/esm/changelogOverrides.js +8 -0
- package/dist/esm/extractMigration.d.ts +14 -0
- package/dist/esm/extractMigration.js +18 -0
- package/dist/esm/init/scaffold.js +14 -6
- package/dist/esm/packsPath.d.ts +15 -0
- package/dist/esm/packsPath.js +47 -0
- package/dist/esm/planVersionBump.d.ts +2 -1
- package/dist/esm/planVersionBump.js +4 -0
- package/dist/esm/propagateBumps.d.ts +1 -3
- package/dist/esm/propagateBumps.js +2 -2
- package/dist/esm/releasePrepare.js +0 -16
- package/dist/esm/releasePrepareMono.js +3 -24
- package/dist/esm/reportPrepare.js +4 -2
- package/dist/esm/runGitCliff.d.ts +16 -23
- package/dist/esm/runGitCliff.js +4 -10
- package/dist/esm/sync-labels/resolveLabels.d.ts +5 -3
- package/dist/esm/sync-labels/resolveLabels.js +21 -6
- package/dist/esm/types.d.ts +13 -2
- package/dist/esm/validateConfig.js +1 -1
- package/docs/api.md +73 -0
- package/docs/changelogs.md +70 -0
- package/docs/configuration.md +105 -0
- package/docs/editorial-overrides.md +144 -0
- package/docs/labels.md +83 -0
- package/docs/project-releases.md +74 -0
- package/docs/publishing.md +30 -0
- package/docs/readiness-checks.md +18 -0
- package/docs/releasing.md +108 -0
- package/docs/work-types.md +121 -0
- package/package.json +16 -12
- package/dist/esm/readCurrentVersion.d.ts +0 -2
- package/dist/esm/readCurrentVersion.js +0 -18
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
import { loadPreset } from "./presets.js";
|
|
2
2
|
export function resolveLabels(config) {
|
|
3
|
+
const labelEntries = Object.entries(config.labels ?? {});
|
|
4
|
+
const caseVariantGroups = findCaseVariantGroups(labelEntries.map(([name]) => name));
|
|
5
|
+
if (caseVariantGroups.length > 0) {
|
|
6
|
+
const groupList = caseVariantGroups.map((group) => group.map((name) => `'${name}'`).join(' and ')).join('; ');
|
|
7
|
+
throw new Error(`Keys in 'labels' differ only in case, which GitHub treats as the same label: ${groupList}. Keep one entry per label.`);
|
|
8
|
+
}
|
|
3
9
|
const resolved = new Map();
|
|
4
10
|
const presetNames = config.extends ?? [];
|
|
5
11
|
for (const presetName of presetNames) {
|
|
6
12
|
for (const label of loadPreset(presetName)) {
|
|
7
|
-
resolved.set(label.name, label);
|
|
13
|
+
resolved.set(toLabelKey(label.name), label);
|
|
8
14
|
}
|
|
9
15
|
}
|
|
10
|
-
const labelEntries = Object.entries(config.labels ?? {});
|
|
11
16
|
for (const [name, spec] of labelEntries) {
|
|
17
|
+
const key = toLabelKey(name);
|
|
12
18
|
if (spec === null) {
|
|
13
|
-
if (!resolved.has(
|
|
19
|
+
if (!resolved.has(key)) {
|
|
14
20
|
throw new Error(`Label '${name}' is set to null (remove), but no preset in 'extends' defines it. Fix the name or delete the entry.`);
|
|
15
21
|
}
|
|
16
|
-
resolved.delete(
|
|
22
|
+
resolved.delete(key);
|
|
17
23
|
}
|
|
18
24
|
else {
|
|
19
|
-
resolved.set(
|
|
25
|
+
resolved.set(key, {
|
|
20
26
|
name,
|
|
21
27
|
color: spec.color,
|
|
22
28
|
...(spec.description !== undefined && { description: spec.description }),
|
|
@@ -25,6 +31,15 @@ export function resolveLabels(config) {
|
|
|
25
31
|
}
|
|
26
32
|
return sortLabels(resolved.values().toArray());
|
|
27
33
|
}
|
|
34
|
+
function findCaseVariantGroups(names) {
|
|
35
|
+
return Map.groupBy(names, toLabelKey)
|
|
36
|
+
.values()
|
|
37
|
+
.filter((group) => group.length > 1)
|
|
38
|
+
.toArray();
|
|
39
|
+
}
|
|
28
40
|
function sortLabels(labels) {
|
|
29
|
-
return
|
|
41
|
+
return labels.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
42
|
+
}
|
|
43
|
+
function toLabelKey(name) {
|
|
44
|
+
return name.toLowerCase();
|
|
30
45
|
}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -18,6 +18,15 @@ export interface ChangelogItem {
|
|
|
18
18
|
* with the canonical SSOT values) to surface them prominently in release notes.
|
|
19
19
|
*/
|
|
20
20
|
breaking?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Migration instruction for a consumer, taken from the `Migration:` paragraph of `body`.
|
|
23
|
+
*
|
|
24
|
+
* Derived from `body` wherever `body` is set, and absent where the body carries no labeled
|
|
25
|
+
* paragraph. Independent of `breaking`: a `fix` that tightens validation cannot carry `!` under
|
|
26
|
+
* the default breaking policy and still imposes a migration. `body` keeps the paragraph, so
|
|
27
|
+
* `CHANGELOG.md` goes on rendering it in place.
|
|
28
|
+
*/
|
|
29
|
+
migration?: string;
|
|
21
30
|
/**
|
|
22
31
|
* Full git commit SHA when known. Captured from git-cliff's `--context` output and
|
|
23
32
|
* persisted in `changelog.json` so that override files can target items by hash. Synthetic
|
|
@@ -379,8 +388,10 @@ export type LabelSpec = z.infer<typeof labelSpecSchema>;
|
|
|
379
388
|
* the GitHub repo, distinct from labels applied to PRs and issues).
|
|
380
389
|
*
|
|
381
390
|
* Resolution is an ordered fold with last-writer-wins: presets in `extends` order, then the `labels` record,
|
|
382
|
-
* where an entry adds a label, replaces one an earlier layer defined, or removes it (`null`).
|
|
383
|
-
*
|
|
391
|
+
* where an entry adds a label, replaces one an earlier layer defined, or removes it (`null`). Names match
|
|
392
|
+
* case-insensitively, as GitHub matches them, and the replacing layer's spelling wins.
|
|
393
|
+
* Two misstatements are resolve-time errors (`resolveLabels`), not schema errors: `labels` keys that differ only in
|
|
394
|
+
* case, and a `null` naming a label that no earlier layer defined.
|
|
384
395
|
*/
|
|
385
396
|
export declare const repoLabelsSchema: z.ZodObject<{
|
|
386
397
|
extends: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -31,7 +31,7 @@ function preprocessDeprecatedKeys(raw) {
|
|
|
31
31
|
const errors = [];
|
|
32
32
|
const cleaned = { ...raw };
|
|
33
33
|
if (isRecord(cleaned['releaseNotes']) && Object.hasOwn(cleaned['releaseNotes'], 'shouldCreateGithubRelease')) {
|
|
34
|
-
errors.push('releaseNotes.shouldCreateGithubRelease is no longer supported. Adoption is now signaled by installing the create-github-release workflow. Remove this field from your config
|
|
34
|
+
errors.push('releaseNotes.shouldCreateGithubRelease is no longer supported. Adoption is now signaled by installing the create-github-release workflow, which `release-kit init` scaffolds. Remove this field from your config.');
|
|
35
35
|
const releaseNotesCopy = { ...cleaned['releaseNotes'] };
|
|
36
36
|
delete releaseNotesCopy['shouldCreateGithubRelease'];
|
|
37
37
|
cleaned['releaseNotes'] = releaseNotesCopy;
|
package/docs/api.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Programmatic API
|
|
2
|
+
|
|
3
|
+
How to build a release config by hand with `deriveWorkspaceConfig()`, the script-based approach that predates the CLI, and the workspace records that `resolveReleaseTags` takes.
|
|
4
|
+
|
|
5
|
+
## Using `deriveWorkspaceConfig()` for manual configuration
|
|
6
|
+
|
|
7
|
+
If you need to build a `MonorepoReleaseConfig` manually (e.g., for the legacy script-based approach), the exported `deriveWorkspaceConfig()` helper creates a `WorkspaceConfig` from a workspace-relative path. It reads the workspace's `package.json` to derive the tag prefix from the package name:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { deriveWorkspaceConfig } from '@williamthorsen/release-kit';
|
|
11
|
+
|
|
12
|
+
// packages/arrays/package.json contains `"name": "@scope/arrays"`
|
|
13
|
+
deriveWorkspaceConfig('packages/arrays');
|
|
14
|
+
// => {
|
|
15
|
+
// dir: 'arrays',
|
|
16
|
+
// name: '@scope/arrays',
|
|
17
|
+
// tagPrefix: 'arrays-v',
|
|
18
|
+
// workspacePath: 'packages/arrays',
|
|
19
|
+
// packageFiles: ['packages/arrays/package.json'],
|
|
20
|
+
// changelogPaths: ['packages/arrays'],
|
|
21
|
+
// paths: ['packages/arrays/**'],
|
|
22
|
+
// }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`dir` is the basename of the workspace path and is the stable internal identifier used by `--only`, `WorkspaceOverride.dir`, and the dependency graph. `tagPrefix` is derived from the unscoped `package.json` `name` — any leading `@scope/` is stripped — so tags reflect the package identity rather than the directory layout. For example, a workspace at `packages/core` with `"name": "@williamthorsen/nmr-core"` produces `tagPrefix: 'nmr-core-v'`, yielding tags like `nmr-core-v1.3.0`.
|
|
26
|
+
|
|
27
|
+
The workspace's `package.json` must declare a non-empty `name` field; `deriveWorkspaceConfig()` throws otherwise. If two workspaces produce the same `tagPrefix` (because their unscoped names collide), `mergeMonorepoConfig()` throws and names the colliding workspaces so you can rename one.
|
|
28
|
+
|
|
29
|
+
## Legacy script-based approach
|
|
30
|
+
|
|
31
|
+
The CLI-driven approach is recommended for new setups. The script-based approach (using `runReleasePrepare` with a manually maintained config) is still supported for backward compatibility.
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// .github/scripts/release.config.ts
|
|
35
|
+
import type { MonorepoReleaseConfig } from '@williamthorsen/release-kit';
|
|
36
|
+
import { deriveWorkspaceConfig } from '@williamthorsen/release-kit';
|
|
37
|
+
|
|
38
|
+
export const config: MonorepoReleaseConfig = {
|
|
39
|
+
workspaces: [deriveWorkspaceConfig('packages/arrays'), deriveWorkspaceConfig('packages/strings')],
|
|
40
|
+
formatCommand: 'npx prettier --write',
|
|
41
|
+
};
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// .github/scripts/release-prepare.ts
|
|
46
|
+
import { runReleasePrepare } from '@williamthorsen/release-kit';
|
|
47
|
+
import { config } from './release.config.ts';
|
|
48
|
+
|
|
49
|
+
runReleasePrepare(config);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The key difference: the script-based approach requires manually listing every workspace, while the CLI auto-discovers them from `pnpm-workspace.yaml`.
|
|
53
|
+
|
|
54
|
+
## `resolveReleaseTags` takes workspaces; `WorkspaceConfig` requires `workspacePath`
|
|
55
|
+
|
|
56
|
+
Tag resolution is now driven by workspace records rather than a caller-supplied directory map, so `resolveReleaseTags` can report both the workspace `dir` and its `workspacePath` for every resolved tag.
|
|
57
|
+
|
|
58
|
+
- `resolveReleaseTags` signature changed from `(workspaceMap?: Map<string, string>)` to `(workspaces?: readonly WorkspaceConfig[])`.
|
|
59
|
+
- `WorkspaceConfig` gained a required `workspacePath: string` field.
|
|
60
|
+
|
|
61
|
+
Replace direct `Map`-based calls with `deriveWorkspaceConfig()`, which now populates `workspacePath` for you:
|
|
62
|
+
|
|
63
|
+
```diff
|
|
64
|
+
-import { resolveReleaseTags } from '@williamthorsen/release-kit';
|
|
65
|
+
-
|
|
66
|
+
-const workspaceMap = new Map([['core', 'packages/core']]);
|
|
67
|
+
-resolveReleaseTags(workspaceMap);
|
|
68
|
+
+import { deriveWorkspaceConfig, resolveReleaseTags } from '@williamthorsen/release-kit';
|
|
69
|
+
+
|
|
70
|
+
+resolveReleaseTags([deriveWorkspaceConfig('packages/core')]);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
If you construct `WorkspaceConfig` objects directly, add `workspacePath` alongside the other required fields.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Changelogs
|
|
2
|
+
|
|
3
|
+
What reaches a published tarball, what each `.meta/changelog.json` item contains, how the git-cliff config is resolved, and how release notes reach a README.
|
|
4
|
+
|
|
5
|
+
Neither `CHANGELOG.md` nor `.meta/changelog.json` reaches a published tarball on its own. npm stopped including `CHANGELOG` automatically in npm 7, so a package that declares a `files` field ships a changelog only where that field names `CHANGELOG.md` and `.meta/changelog.json`. release-kit's readyup kit reports a publishable workspace whose `files` field omits either.
|
|
6
|
+
|
|
7
|
+
## `changelog.json` item schema
|
|
8
|
+
|
|
9
|
+
Each item under a section in `.meta/changelog.json` carries one required field and four optional ones:
|
|
10
|
+
|
|
11
|
+
| Field | Type | Meaning |
|
|
12
|
+
| ------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| `description` | `string` | The bullet headline, taken from the commit subject with the ticket ID and type prefix stripped. |
|
|
14
|
+
| `body` | `string` | The commit body, with trailing trailer metadata stripped. |
|
|
15
|
+
| `breaking` | `boolean` | Present and `true` where the commit subject carried the `!` prefix. See [`!` (breaking change) policy](work-types.md#-breaking-change-policy). |
|
|
16
|
+
| `migration` | `string` | The migration step for a consumer. See below. |
|
|
17
|
+
| `hash` | `string` | The full commit SHA, and the key on which an override entry matches. Absent on synthetic propagation entries. |
|
|
18
|
+
|
|
19
|
+
Every optional field is omitted rather than emitted as `null`, so a consumer tests for presence.
|
|
20
|
+
|
|
21
|
+
### The `migration` field
|
|
22
|
+
|
|
23
|
+
A commit body states a migration step as a paragraph opening with the literal label `Migration:`. Where one is present, `migration` carries that paragraph with the label stripped:
|
|
24
|
+
|
|
25
|
+
```jsonc
|
|
26
|
+
{
|
|
27
|
+
"description": "Rename the workspace field",
|
|
28
|
+
"body": "Renames `name` to `id`.\n\nMigration: Change any uses of `name` to `id`.",
|
|
29
|
+
"migration": "Change any uses of `name` to `id`.",
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Three properties are worth knowing:
|
|
34
|
+
|
|
35
|
+
- **It is independent of `breaking`.** A `fix` cannot carry `!` under the default breaking policy, and a `fix` that tightens validation still imposes a migration. Filter on `migration` to find every step; filter on `breaking` to find every breaking change.
|
|
36
|
+
- **`body` keeps the paragraph.** The field is an extraction, not a move, so `CHANGELOG.md` and `.meta/changelog.json` go on agreeing.
|
|
37
|
+
- **It is derived, not authored.** `migration` is not a field an override file can set (see [File shape](editorial-overrides.md#file-shape)); it is re-derived from whatever `body` an override installs, and cleared where that body carries no labeled paragraph. To change the migration text, override `body`.
|
|
38
|
+
|
|
39
|
+
The label match is exact: `migration:` and `**Migration:**` are not recognized.
|
|
40
|
+
|
|
41
|
+
## cliff.toml setup
|
|
42
|
+
|
|
43
|
+
The package includes a bundled `cliff.toml.template` that is used automatically when no custom config is found. The resolution order:
|
|
44
|
+
|
|
45
|
+
| Priority | Path | Notes |
|
|
46
|
+
| -------- | ----------------------------- | ----------------------------------------------- |
|
|
47
|
+
| 1 | `cliffConfigPath` in config | Explicit path, returned without existence check |
|
|
48
|
+
| 2 | `.config/git-cliff.toml` | Project-level override |
|
|
49
|
+
| 3 | `cliff.toml` | Repo root fallback |
|
|
50
|
+
| 4 | Bundled `cliff.toml.template` | Automatic fallback |
|
|
51
|
+
|
|
52
|
+
The bundled template provides a generic git-cliff configuration that:
|
|
53
|
+
|
|
54
|
+
- Strips issue-ticket prefixes matching `^[A-Z]+-\d+\s+` (e.g., `TOOL-123 `, `AFG-456 `)
|
|
55
|
+
- Handles both `type: description` and `workspace|type: description` commit formats
|
|
56
|
+
- Groups commits by work type via `[git].commit_parsers`
|
|
57
|
+
|
|
58
|
+
The body template is intentionally empty: release-kit reads cliff's `--context` JSON output and renders `CHANGELOG.md` in-process via `renderChangelogMarkdown` (see [How it works](../README.md#how-it-works) for the rationale). The `[git].commit_parsers` section remains load-bearing for `--context` group assignment.
|
|
59
|
+
|
|
60
|
+
To customize, scaffold a local copy with `release-kit init --with-config` and edit `.config/git-cliff.toml`. Edit only the `[git]` section — body-template changes have no effect.
|
|
61
|
+
|
|
62
|
+
## Release-notes injection
|
|
63
|
+
|
|
64
|
+
With `releaseNotes.shouldInjectIntoReadme` set to `true`, `release-kit publish` writes each package's release notes into its `README.md` for the duration of the publish, then restores the file. The notes are the all-audience sections of the released version's entry in `.meta/changelog.json`, under a `## Release notes — v<version> (<date>)` heading, and they replace whatever lies between the marker pair:
|
|
65
|
+
|
|
66
|
+
```markdown
|
|
67
|
+
<!-- section:release-notes --><!-- /section:release-notes -->
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A README without the marker pair receives the notes at its top, above its title, and release-kit's readyup kit reports a README that lacks the markers. Injection is skipped with a warning when `changelogJson.enabled` is `false`, when the version has no entry, or when the entry has no all-audience content. `release-kit prepare --with-release-notes` writes the same injected README as a [preview](releasing.md#previewing-release-notes-with---with-release-notes).
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
The fields of `.config/release-kit.config.ts`, how a workspace declares its prior identities or a retired package, the rule against colliding tag prefixes, and the patterns that decide a bump.
|
|
4
|
+
|
|
5
|
+
A minimal config file is in the [README](../README.md#configuration).
|
|
6
|
+
|
|
7
|
+
## `ReleaseKitConfig` reference
|
|
8
|
+
|
|
9
|
+
| Field | Type | Description |
|
|
10
|
+
| ------------------ | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| `cliffConfigPath` | `string` | Explicit path to cliff config. If omitted, resolved automatically: `.config/git-cliff.toml` → `cliff.toml` → bundled template |
|
|
12
|
+
| `workspaces` | `WorkspaceOverride[]` | Override or exclude discovered workspaces (matched by `dir`) |
|
|
13
|
+
| `formatCommand` | `string` | Shell command to run after changelog generation; modified file paths are appended as arguments |
|
|
14
|
+
| `versionPatterns` | `VersionPatterns` | Rules for which commit types trigger major/minor bumps |
|
|
15
|
+
| `scopeAliases` | `Record<string, string>` | Maps shorthand scope names to canonical names in commits |
|
|
16
|
+
| `workTypes` | `Record<string, WorkTypeConfig>` | Work type definitions, merged with defaults by key |
|
|
17
|
+
| `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 |
|
|
18
|
+
| `retiredPackages` | `RetiredPackage[]` | Packages that once lived in this repo but have been extracted or removed; suppresses undeclared-tag-prefix warnings |
|
|
19
|
+
| `project` | `ProjectConfig` | Opt-in project-level release block. Declaring `project: {}` (even empty) enables a project-release stage in `prepare` |
|
|
20
|
+
| `repoLabels` | `RepoLabelsConfig` | Declares the repository's label registry for `sync-labels`; see [Label configuration](labels.md#label-configuration) |
|
|
21
|
+
| `changelogJson` | `ChangelogJsonConfig` | Controls `.meta/changelog.json`: `enabled` (default `true`), `outputPath` (default `'.meta/changelog.json'`), and `devOnlySections`; see [Custom work types](work-types.md#custom-work-types) |
|
|
22
|
+
| `releaseNotes` | `ReleaseNotesConfig` | `shouldInjectIntoReadme` (default `false`) injects release notes into each package's README while it publishes; see [Release-notes injection](changelogs.md#release-notes-injection) |
|
|
23
|
+
|
|
24
|
+
All fields are optional.
|
|
25
|
+
|
|
26
|
+
## `WorkspaceOverride`
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
interface WorkspaceOverride {
|
|
30
|
+
dir: string; // Package directory name (e.g., 'arrays')
|
|
31
|
+
shouldExclude?: boolean; // If true, exclude from release processing
|
|
32
|
+
legacyIdentities?: LegacyIdentity[]; // Prior `(name, tagPrefix)` identities for this workspace
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface LegacyIdentity {
|
|
36
|
+
name: string; // Full scoped npm name at the time (e.g., '@scope/pkg')
|
|
37
|
+
tagPrefix: string; // Tag prefix under which historical tags were published (e.g., 'core-v')
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`legacyIdentities` captures prior identities of a workspace as complete `(name, tagPrefix)` snapshots. The union of the current `tagPrefix` and each identity's `tagPrefix` is consulted when release-kit searches for the most recent baseline tag and when generating changelogs. Use it when a workspace's historical tags were published under a different npm name, a different tag prefix, or both — typically across a package rename. Both fields are required per identity: each entry must be a complete historical snapshot that stays valid regardless of subsequent renames. Run `release-kit show-tag-prefixes` to detect undeclared candidates and produce a paste-ready config snippet. Listing the current identity (full `(name, tagPrefix)` match) is rejected as a no-op duplicate; an identity whose `tagPrefix` matches the current but whose `name` differs is valid and documents a prior rename that reused the same tag shape. If the workspace no longer exists in this repo at all (the package was extracted or removed), use [`retiredPackages`](#retiredpackage) instead.
|
|
42
|
+
|
|
43
|
+
## `RetiredPackage`
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
interface RetiredPackage {
|
|
47
|
+
name: string; // Final scoped npm name while the package lived in this repo
|
|
48
|
+
tagPrefix: string; // Tag prefix under which the package's historical tags were published
|
|
49
|
+
successor?: string; // Optional successor package name (e.g., 'readyup')
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`retiredPackages` is the repo-level complement to `legacyIdentities`. Use `legacyIdentities` when the workspace still exists in this repo under a new identity; use `retiredPackages` when no workspace for this package exists in this repo anymore — the package was extracted to another repo or removed outright. Retired entries are inert: release-kit never consults them for baseline lookup or changelog attribution. Their declared `tagPrefix` values are recognized as historical, so `show-tag-prefixes` stops flagging them under "Undeclared tag prefixes."
|
|
54
|
+
|
|
55
|
+
Worked example — `preflight` was extracted from this monorepo and continues as the standalone `readyup` project. Its tags stay in this repo as historical anchors:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { defineConfig } from '@williamthorsen/release-kit/config';
|
|
59
|
+
|
|
60
|
+
export default defineConfig({
|
|
61
|
+
retiredPackages: [{ name: '@scope/preflight', tagPrefix: 'preflight-v', successor: 'readyup' }],
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Validation rules:
|
|
66
|
+
|
|
67
|
+
- `name` and `tagPrefix` are required per entry and must be non-empty strings.
|
|
68
|
+
- `successor` is optional; if present, it must be a non-empty string.
|
|
69
|
+
- Full-tuple `(name, tagPrefix)` duplicates within `retiredPackages` are rejected.
|
|
70
|
+
- Two entries sharing the same `tagPrefix` but different `name`s are accepted — this documents a package renamed within the repo before being retired.
|
|
71
|
+
|
|
72
|
+
`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.
|
|
73
|
+
|
|
74
|
+
## Tag prefix collisions
|
|
75
|
+
|
|
76
|
+
Tag prefixes from distinct owners must not be identical or be a strict prefix of one another. An owner is one of:
|
|
77
|
+
|
|
78
|
+
- 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).
|
|
79
|
+
- A `retiredPackages[]` entry (one owner per entry).
|
|
80
|
+
- The `project` block, when configured.
|
|
81
|
+
|
|
82
|
+
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.
|
|
83
|
+
|
|
84
|
+
The rule is enforced at config load; the resulting error identifies both colliding declarations.
|
|
85
|
+
|
|
86
|
+
## `release-kit show-tag-prefixes`
|
|
87
|
+
|
|
88
|
+
Print a per-workspace table of derived tag prefixes, tag counts, and declared legacy prefixes. Also surfaces any release-shaped tag prefix in the repo that is neither a derived prefix nor declared via `legacyIdentities`, along with a copy-pasteable `workspaces: [...]` config snippet. The snippet uses a `TODO-fill-in-legacy-npm-name` placeholder for each identity's `name`; replace it with the package's prior npm name before pasting.
|
|
89
|
+
|
|
90
|
+
Exits `0` when every workspace derives a prefix and there are no cross-workspace collisions; exits `1` on any derivation failure or collision. Undeclared candidates do not affect the exit code — they surface as a warning via the `legacy tag prefixes are declared` readyup check.
|
|
91
|
+
|
|
92
|
+
In single-package mode, prints a single row with `workspacePath = .` and `derivedPrefix = v`; legacy entries and undeclared-candidate scanning are not applicable.
|
|
93
|
+
|
|
94
|
+
## `VersionPatterns`
|
|
95
|
+
|
|
96
|
+
Defines which commit types trigger major or minor bumps. Any recognized type not listed defaults to a patch bump.
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
interface VersionPatterns {
|
|
100
|
+
major: string[]; // Patterns triggering a major bump ('!' = any breaking change)
|
|
101
|
+
minor: string[]; // Commit types triggering a minor bump
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Default: `{ major: ['!'], minor: ['feat'] }`
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Editorial overrides
|
|
2
|
+
|
|
3
|
+
How to correct a generated changelog entry without rewriting history: where override files live, how they compose, what an entry may set, and how to validate the files.
|
|
4
|
+
|
|
5
|
+
Generated changelogs occasionally need editorial correction — typos, redacted scope, reworded entries, or historical commits whose bodies carry verbatim PR-template scaffolding (`## What`, `## Why`, etc.) that renders as literal text in user-facing release notes. Rewriting git history is not viable, and any in-place edit to `CHANGELOG.md` or `.meta/changelog.json` is overwritten on the next release because release-kit regenerates both artifacts from scratch.
|
|
6
|
+
|
|
7
|
+
Override files are the supported escape hatch. Drop a checked-in JSON file at the conventional path for the scope you want to influence, keyed by commit hash, and `release-kit prepare` applies the overrides between `buildChangelogEntries` and serialization. Both `CHANGELOG.md` and `.meta/changelog.json` reflect the post-override view, so downstream consumers (the GitHub Release body, the in-app release-notes page, etc.) see the same content.
|
|
8
|
+
|
|
9
|
+
## File-location convention
|
|
10
|
+
|
|
11
|
+
| Scope | Path | Applies to |
|
|
12
|
+
| ------------------- | ---------------------------------------------- | ------------------------------------------------------- |
|
|
13
|
+
| Project (root) | `.meta/changelog-overrides.json` | The project changelog and every workspace's changelog |
|
|
14
|
+
| Workspace | `packages/<ws>/.meta/changelog-overrides.json` | Only that workspace's changelog |
|
|
15
|
+
| Single-package mode | `.meta/changelog-overrides.json` | The package's changelog (collapses to the project case) |
|
|
16
|
+
|
|
17
|
+
Filenames have no leading dot — the `.meta/` directory already provides the visibility property and parallels its sibling artifacts (`changelog.json`, `label-map.json`).
|
|
18
|
+
|
|
19
|
+
## Composition: per-key shadowing
|
|
20
|
+
|
|
21
|
+
When a workspace's changelog is rendered, both files are consulted:
|
|
22
|
+
|
|
23
|
+
- The root file's overrides apply globally.
|
|
24
|
+
- The workspace file's overrides apply only to that workspace.
|
|
25
|
+
- When the **same hash key** (string-equal, byte-for-byte) appears in both files, the **workspace entry wins entirely** for that workspace's changelog — no field-level merge, the workspace entry replaces the root entry.
|
|
26
|
+
- Different prefix strings that happen to resolve to the same commit do **not** shadow; they fall through to the existing ambiguous-prefix error so you can correct your override file.
|
|
27
|
+
- Other keys in the root file still apply for that workspace.
|
|
28
|
+
|
|
29
|
+
The project-level changelog applies only the root file. Per-workspace files describe per-workspace editorial intent and have no meaning at the aggregated project tier.
|
|
30
|
+
|
|
31
|
+
## Stale-key warnings
|
|
32
|
+
|
|
33
|
+
A key that doesn't match any commit gets a stale-reference warning. The warning's scope mirrors the file's scope:
|
|
34
|
+
|
|
35
|
+
- **Per-workspace files** are warned against their own apply context. A key in `packages/foo/.meta/changelog-overrides.json` that doesn't match any commit in foo's changelog is unambiguously stale and is warned immediately.
|
|
36
|
+
- **Root file** keys are aggregated globally — a root key that matches in any workspace or in the project changelog is non-stale; a root key matched nowhere is warned exactly once after all batches complete.
|
|
37
|
+
|
|
38
|
+
## File shape
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"82962311": {
|
|
43
|
+
"audience": "skip"
|
|
44
|
+
},
|
|
45
|
+
"abc1234d": {
|
|
46
|
+
"body": "Cleaned-up prose without the original PR-template scaffolding."
|
|
47
|
+
},
|
|
48
|
+
"ef567890": {
|
|
49
|
+
"description": "Rewritten headline that fixes the typo",
|
|
50
|
+
"body": "Optional replacement body."
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Per-entry fields are all optional, but at least one must be present per entry:
|
|
56
|
+
|
|
57
|
+
| Field | Type | Effect |
|
|
58
|
+
| ------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
59
|
+
| `audience` | `'all' \| 'dev' \| 'skip'` | `'skip'` removes the entry entirely. `'all'` and `'dev'` are reserved for a future audience-reclassification feature (see below). |
|
|
60
|
+
| `description` | `string` | Replaces the entry's bullet headline. Other fields are preserved. |
|
|
61
|
+
| `body` | `string` | Replaces the entry's body (the prose that renders below the bullet). Other fields are preserved. |
|
|
62
|
+
| `breaking` | `boolean` | Toggles the `🚨 **Breaking:** ` marker on the bullet. |
|
|
63
|
+
|
|
64
|
+
There is no `migration` key: the field is derived from `body`, so an override that replaces `body` re-derives it. See [The `migration` field](changelogs.md#the-migration-field).
|
|
65
|
+
|
|
66
|
+
## Hash-prefix matching
|
|
67
|
+
|
|
68
|
+
Keys can be either the full 40-character commit SHA or a non-ambiguous prefix. The matcher walks every `ChangelogItem.hash` value present in the entry tree and resolves each override key to its set of matching hashes:
|
|
69
|
+
|
|
70
|
+
- **Exact prefix match (1 hit)** — the override applies. A 7-character prefix is usually unambiguous within a single repo's history; longer prefixes are always safe.
|
|
71
|
+
- **No matches (0 hits)** — the override is treated as a stale reference (probably from a rebase or branch deletion) and prepare reports a warning. The release continues.
|
|
72
|
+
- **Ambiguous prefix (2+ hits)** — the release aborts with an error naming the key and the matching hashes. Lengthen the prefix or use the full SHA.
|
|
73
|
+
|
|
74
|
+
Override application errors abort the run with a non-zero exit; warnings (zero-match keys) are non-fatal and surface on `PrepareResult.warnings`.
|
|
75
|
+
|
|
76
|
+
## Validation
|
|
77
|
+
|
|
78
|
+
The override file is validated when `release-kit prepare` loads it. Each error names the offending key so you can locate it in your file:
|
|
79
|
+
|
|
80
|
+
- Missing file → empty map, no error (the no-op default — projects that do not need overrides skip the file entirely).
|
|
81
|
+
- Malformed JSON → error.
|
|
82
|
+
- Wrong top-level shape (e.g., array, primitive) → error.
|
|
83
|
+
- Unknown fields on an entry → error.
|
|
84
|
+
- Wrong field types (e.g., `description` as a number) → error.
|
|
85
|
+
- An entry with no fields set → error (a copy-paste mistake more often than not).
|
|
86
|
+
- `audience: 'all'` or `audience: 'dev'` → error in the current release: only `'skip'` is supported (see below).
|
|
87
|
+
|
|
88
|
+
### Standalone validation: `release-kit overrides validate`
|
|
89
|
+
|
|
90
|
+
For a focused overrides-only health check (locally or as a CI gate), run:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
pnpm exec release-kit overrides validate
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
This walks every `.meta/changelog-overrides.json` file across the project tier and per-workspace tier, reporting three classes of finding:
|
|
97
|
+
|
|
98
|
+
| Class | Examples | Exit code |
|
|
99
|
+
| ------------------- | ------------------------------------------------------------------------------------------- | --------- |
|
|
100
|
+
| Schema/parse errors | malformed JSON, unknown fields, wrong field types, no-field entries, unsupported `audience` | `2` |
|
|
101
|
+
| Ambiguous-prefix | an override key resolves to 2+ commit hashes | `2` |
|
|
102
|
+
| Stale-key warnings | an override key resolves to no commit in its applicable scope | `1` |
|
|
103
|
+
|
|
104
|
+
Tier-aware stale-key semantics match `release-kit prepare`'s match-set exactly: a workspace-tier key is stale if it does not match in its own workspace's history; a root-tier key is stale only if it matches in **no** scope (no workspace AND not the project release window).
|
|
105
|
+
|
|
106
|
+
The same logic is also exposed programmatically via the `validateAllChangelogOverrides` function exported from `@williamthorsen/release-kit`, for callers that want to integrate the check into their own tooling.
|
|
107
|
+
|
|
108
|
+
## Audience semantics: v1 supports `'skip'` only
|
|
109
|
+
|
|
110
|
+
The on-disk format declares the full `'all' | 'dev' | 'skip'` audience vocabulary so the file format will not need to change when the v2 reclassification feature ships. In the current release, only `'skip'` is supported at runtime; `'all'` and `'dev'` are rejected with an explicit "not yet supported" error.
|
|
111
|
+
|
|
112
|
+
The eventual v2 behavior will let an override move a single item to a different audience section (e.g., reclassifying a `Documentation` entry as `Internal features` to keep it out of public-facing release notes). v1 deliberately leaves that as a separate change so the override mechanism can ship now and the section-split logic can land additively later.
|
|
113
|
+
|
|
114
|
+
## Worked example 1: cleaning up scaffolded historical commits (root file)
|
|
115
|
+
|
|
116
|
+
Suppose a year-old commit `82962311` was authored from a PR template that left `## What` / `## Why` headings in the body, and that commit now appears in your in-app release notes as literal Markdown headings. Add an override at the project tier:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
// .meta/changelog-overrides.json
|
|
120
|
+
{
|
|
121
|
+
"82962311": {
|
|
122
|
+
"body": "Add the in-app release-notes page with version-aware navigation."
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
On the next `release-kit prepare` run, the matched item's body is replaced before the JSON and Markdown artifacts are written. The original git history is untouched.
|
|
128
|
+
|
|
129
|
+
## Worked example 2: suppressing a cross-attribution spillover (workspace file)
|
|
130
|
+
|
|
131
|
+
Release-kit attributes commits to workspaces by file path, so a commit that primarily belongs to one workspace can land in another's changelog if it touched files there. Suppose commit `1ce3d2f` renamed the `audit-deps` package to `v11y-check` (scope `v11y-check`) but also edited `packages/nmr/src/default-scripts.ts` and `packages/nmr/README.md`. The commit correctly appears in `packages/v11y-check/CHANGELOG.md`, but it also spills into `packages/nmr/CHANGELOG.md` where it isn't the right editorial framing.
|
|
132
|
+
|
|
133
|
+
Drop a workspace-tier override at `packages/nmr/.meta/changelog-overrides.json`:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
// packages/nmr/.meta/changelog-overrides.json
|
|
137
|
+
{
|
|
138
|
+
"1ce3d2f": {
|
|
139
|
+
"audience": "skip"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The commit is now suppressed in nmr's changelog only — it still appears in v11y-check's, where it belongs. A root-tier `'skip'` would have removed it from both, which is the wrong outcome.
|
package/docs/labels.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Labels
|
|
2
|
+
|
|
3
|
+
How to declare the repository's labels in `.config/release-kit.config.ts`, how the scaffolded workflow applies them, and the published schema for `.meta/label-map.json`.
|
|
4
|
+
|
|
5
|
+
## `release-kit sync-labels`
|
|
6
|
+
|
|
7
|
+
Manage GitHub label definitions via the `repoLabels` block of `.config/release-kit.config.ts`.
|
|
8
|
+
|
|
9
|
+
`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.
|
|
10
|
+
|
|
11
|
+
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, and the refusal names what to move and where.
|
|
12
|
+
|
|
13
|
+
### Label configuration
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { defineConfig } from '@williamthorsen/release-kit/config';
|
|
17
|
+
|
|
18
|
+
export default defineConfig({
|
|
19
|
+
repoLabels: {
|
|
20
|
+
extends: ['common'],
|
|
21
|
+
labels: {
|
|
22
|
+
'scope:my-package': { color: '00ff96' }, // add, with no description
|
|
23
|
+
bug: { color: 'b60205', description: 'Something broken' }, // replace the preset's `bug`
|
|
24
|
+
wontfix: null, // remove the preset's `wontfix`
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
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:
|
|
31
|
+
|
|
32
|
+
1. Presets, in `extends` order — a later preset wins on a shared name.
|
|
33
|
+
2. The `labels` record — an entry adds a label, replaces one an earlier layer defined, or removes it (`null`). Replacement is wholesale: an entry omitting `description` resolves to no description rather than inheriting the one an earlier layer supplied.
|
|
34
|
+
|
|
35
|
+
Names match case-insensitively, as GitHub matches them: `Bug` in the `labels` record replaces a preset's `bug`, and `Bug: null` removes it. A label takes the spelling of the last layer to write it.
|
|
36
|
+
|
|
37
|
+
`description` is optional throughout, in a preset as in the `labels` record, and `sync-labels init` generates none for a scope label. The generated file spells an absent description `''` because `github-label-sync` reads an omitted description as "leave the label's current one alone"; the empty form clears it.
|
|
38
|
+
|
|
39
|
+
Overlaps between layers are never errors; order resolves them, and the committed `.github/labels.yaml` diff is where an unexpected change surfaces at review. Two misstatements are config errors, because neither is visible in the output diff: a dangling `null`, which removes a name that no preset defines, and `labels` keys that differ only in case, all but one of which the fold would discard.
|
|
40
|
+
|
|
41
|
+
### When labels are applied
|
|
42
|
+
|
|
43
|
+
The scaffolded workflow carries three triggers:
|
|
44
|
+
|
|
45
|
+
| Trigger | Job | Token | Effect |
|
|
46
|
+
| --------------------------------------------------------- | ------- | --------------- | --------------------------------- |
|
|
47
|
+
| Push to the default branch touching `.github/labels.yaml` | `sync` | `issues: write` | Applies the labels |
|
|
48
|
+
| Manual dispatch (`release-kit sync-labels sync`) | `sync` | `issues: write` | Applies the labels |
|
|
49
|
+
| Pull request touching `.github/labels.yaml` | `check` | `issues: read` | Logs the diff without applying it |
|
|
50
|
+
|
|
51
|
+
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.
|
|
52
|
+
|
|
53
|
+
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.
|
|
54
|
+
|
|
55
|
+
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.
|
|
56
|
+
|
|
57
|
+
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.
|
|
58
|
+
|
|
59
|
+
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.
|
|
60
|
+
|
|
61
|
+
### Dry-run checks on fork pull requests
|
|
62
|
+
|
|
63
|
+
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.
|
|
64
|
+
|
|
65
|
+
### Published JSON Schema for `.meta/label-map.json`
|
|
66
|
+
|
|
67
|
+
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:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
https://github.com/williamthorsen/node-monorepo-tools/raw/release-kit-v<version>/packages/release-kit/schemas/label-map.json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Consumers reference it from the top of their `.meta/label-map.json`:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"$schema": "https://github.com/williamthorsen/node-monorepo-tools/raw/release-kit-v<version>/packages/release-kit/schemas/label-map.json",
|
|
78
|
+
"types": { "feat": "feature", "fix": "fix" },
|
|
79
|
+
"scopes": { "audit": "scope:audit" }
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
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.
|