@williamthorsen/release-kit 10.6.0 → 10.8.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 +96 -0
- package/.readyup/kits/default.js +17 -3
- package/.readyup/kits/npm-auto-publish.js +87 -23
- package/.readyup/manifest.json +9 -9
- package/CHANGELOG.md +61 -0
- package/README.md +53 -1010
- package/dist/esm/bin/release-kit.js +7 -6
- package/dist/esm/buildChangelogEntries.d.ts +1 -1
- package/dist/esm/buildChangelogEntries.js +14 -5
- package/dist/esm/buildDependencyGraph.d.ts +6 -5
- package/dist/esm/buildDependencyGraph.js +5 -1
- package/dist/esm/checkWorkTypesDrift.d.ts +2 -3
- package/dist/esm/checkWorkTypesDrift.js +1 -1
- package/dist/esm/init/scaffold.js +14 -6
- package/dist/esm/parseCommitMessage.d.ts +4 -5
- 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 -13
- package/dist/esm/releasePrepareMono.js +3 -20
- 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 +12 -9
- package/dist/esm/validateConfig.js +1 -1
- package/dist/esm/work-types.json +68 -52
- package/dist/esm/work-types.schema.json +12 -2
- package/dist/esm/workTypesData.d.ts +10 -9
- package/dist/esm/workTypesData.js +97 -11
- 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 +116 -0
- package/package.json +7 -4
- package/dist/esm/readCurrentVersion.d.ts +0 -2
- package/dist/esm/readCurrentVersion.js +0 -18
|
@@ -35,8 +35,9 @@ Commands:
|
|
|
35
35
|
work-types Check for or sync work-type taxonomy drift against the upstream canonical
|
|
36
36
|
|
|
37
37
|
Options:
|
|
38
|
-
--dry-run
|
|
39
|
-
--help, -h
|
|
38
|
+
--dry-run Preview changes without writing files
|
|
39
|
+
--help, -h Show this help message
|
|
40
|
+
--version, -V Show the version number
|
|
40
41
|
`);
|
|
41
42
|
}
|
|
42
43
|
function showSyncLabelsHelp() {
|
|
@@ -95,7 +96,7 @@ function showInitHelp() {
|
|
|
95
96
|
Usage: release-kit init [options]
|
|
96
97
|
|
|
97
98
|
Initialize release-kit in the current repository.
|
|
98
|
-
By default, scaffolds only the GitHub Actions workflow
|
|
99
|
+
By default, scaffolds only the GitHub Actions workflow files.
|
|
99
100
|
|
|
100
101
|
Options:
|
|
101
102
|
--with-config Also scaffold .config/release-kit.config.ts and .config/git-cliff.toml
|
|
@@ -208,7 +209,7 @@ Subcommands:
|
|
|
208
209
|
sync Overwrite the local work-types.json with the upstream contents
|
|
209
210
|
|
|
210
211
|
Exit codes (check):
|
|
211
|
-
0 Match (or upstream
|
|
212
|
+
0 Match (or upstream not found, with a warning)
|
|
212
213
|
1 Drift detected
|
|
213
214
|
2 Network error
|
|
214
215
|
3 Schema mismatch
|
|
@@ -222,8 +223,8 @@ function showWorkTypesCheckHelp() {
|
|
|
222
223
|
Usage: release-kit work-types check
|
|
223
224
|
|
|
224
225
|
Compare the local work-types.json against the upstream codeassembly canonical and report
|
|
225
|
-
drift. Exit 0 on match, 1 on drift, 0 + warning when upstream is
|
|
226
|
-
|
|
226
|
+
drift. Exit 0 on match, 1 on drift, 0 + warning when upstream is not found, 2 on network
|
|
227
|
+
error, 3 on schema mismatch.
|
|
227
228
|
|
|
228
229
|
Options:
|
|
229
230
|
--help, -h Show this help message
|
|
@@ -23,4 +23,4 @@ export declare function stripGroupDecorations(group: string): string;
|
|
|
23
23
|
* not whether git-cliff runs). This means dry-run exercises the full git-cliff toolchain and surfaces missing-binary,
|
|
24
24
|
* malformed-config, and template-resolution failures earlier than before.
|
|
25
25
|
*/
|
|
26
|
-
export declare function buildChangelogEntries(config: Pick<ReleaseConfig, '
|
|
26
|
+
export declare function buildChangelogEntries(config: Pick<ReleaseConfig, 'breakingPolicies' | 'changelogJson' | 'cliffConfigPath' | 'workTypes'>, tag: string, options?: GenerateChangelogOptions): ChangelogEntry[];
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
|
|
2
2
|
import { extractVersion } from "./changelogJsonUtils.js";
|
|
3
|
-
import { DEFAULT_WORK_TYPES } from "./defaults.js";
|
|
3
|
+
import { DEFAULT_BREAKING_POLICIES, DEFAULT_WORK_TYPES } from "./defaults.js";
|
|
4
4
|
import { extractMigration } from "./extractMigration.js";
|
|
5
|
-
import { COMMIT_PREPROCESSOR_PATTERNS, PIPE_SCOPE_SOURCE } from "./parseCommitMessage.js";
|
|
5
|
+
import { COMMIT_PREPROCESSOR_PATTERNS, parseCommitMessage, PIPE_SCOPE_SOURCE } from "./parseCommitMessage.js";
|
|
6
6
|
import { resolveCliffConfigPath } from "./resolveCliffConfigPath.js";
|
|
7
7
|
import { runGitCliff } from "./runGitCliff.js";
|
|
8
8
|
import { stripEmojiPrefix } from "./stripEmojiPrefix.js";
|
|
@@ -30,7 +30,9 @@ export function buildChangelogEntries(config, tag, options) {
|
|
|
30
30
|
const contextJson = runGitCliff(resolvedConfigPath, cliffArgs);
|
|
31
31
|
const releases = parseCliffContext(contextJson);
|
|
32
32
|
const devOnlySections = new Set(config.changelogJson.devOnlySections);
|
|
33
|
-
|
|
33
|
+
const workTypes = config.workTypes ?? DEFAULT_WORK_TYPES;
|
|
34
|
+
const breakingPolicies = config.breakingPolicies ?? DEFAULT_BREAKING_POLICIES;
|
|
35
|
+
return transformReleases(releases, devOnlySections, workTypes, breakingPolicies);
|
|
34
36
|
}
|
|
35
37
|
catch (error) {
|
|
36
38
|
throw chainError(`Failed to build changelog entries for tag ${tag}`, error);
|
|
@@ -74,7 +76,7 @@ function toCliffContextCommit(value) {
|
|
|
74
76
|
}
|
|
75
77
|
return commit;
|
|
76
78
|
}
|
|
77
|
-
function transformReleases(releases, devOnlySections) {
|
|
79
|
+
function transformReleases(releases, devOnlySections, workTypes, breakingPolicies) {
|
|
78
80
|
const entries = [];
|
|
79
81
|
const devOnlyNormalised = new Set([...devOnlySections].map(stripGroupDecorations));
|
|
80
82
|
for (const release of releases) {
|
|
@@ -89,7 +91,7 @@ function transformReleases(releases, devOnlySections) {
|
|
|
89
91
|
const group = stripCommentPrefix(commit.group ?? 'Other');
|
|
90
92
|
const description = extractDescription(commit.message);
|
|
91
93
|
const body = extractBody(commit.message);
|
|
92
|
-
const breaking =
|
|
94
|
+
const breaking = isBreakingUnderPolicy(commit, workTypes, breakingPolicies);
|
|
93
95
|
let items = sectionMap.get(group);
|
|
94
96
|
if (items === undefined) {
|
|
95
97
|
items = [];
|
|
@@ -132,6 +134,13 @@ function transformReleases(releases, devOnlySections) {
|
|
|
132
134
|
function stripCommentPrefix(group) {
|
|
133
135
|
return group.replace(HTML_COMMENT_PREFIX_PATTERN, '');
|
|
134
136
|
}
|
|
137
|
+
function isBreakingUnderPolicy(commit, workTypes, breakingPolicies) {
|
|
138
|
+
if (!subjectHasBreakingMarker(commit.message)) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
const parsed = parseCommitMessage(commit.message, commit.id ?? '', workTypes, undefined, { breakingPolicies });
|
|
142
|
+
return parsed?.breaking ?? true;
|
|
143
|
+
}
|
|
135
144
|
const SUBJECT_BREAKING_MARKER_PATTERN = new RegExp(String.raw `^(?:${PIPE_SCOPE_SOURCE}\|)?\w+(?:\([^)]+\))?!:`);
|
|
136
145
|
function subjectHasBreakingMarker(message) {
|
|
137
146
|
let subject = message.split('\n', 1)[0] ?? '';
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { WorkspaceConfig } from './types.js';
|
|
2
|
-
/**
|
|
2
|
+
/** Identities, current versions, and `workspace:` dependency edges of the workspaces' packages. */
|
|
3
3
|
export interface DependencyGraph {
|
|
4
4
|
/** Resolve a package name to its workspace `dir`. */
|
|
5
5
|
packageNameToDir: Map<string, string>;
|
|
6
6
|
/** Resolve a workspace `dir` to its package name (inverse of `packageNameToDir`). */
|
|
7
7
|
dirToPackageName: Map<string, string>;
|
|
8
|
+
/** Resolve a workspace `dir` to its current version; absent when its `package.json` has no string `version`. */
|
|
9
|
+
dirToVersion: Map<string, string>;
|
|
8
10
|
/** Map a package name to the workspaces that depend on it. */
|
|
9
11
|
dependentsOf: Map<string, WorkspaceConfig[]>;
|
|
10
12
|
/**
|
|
@@ -14,11 +16,10 @@ export interface DependencyGraph {
|
|
|
14
16
|
dependenciesOf: Map<string, Set<string>>;
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
17
|
-
*
|
|
19
|
+
* Builds the dependency graph from the workspaces' `package.json` files.
|
|
18
20
|
*
|
|
19
21
|
* Reads each workspace's primary `package.json` (first entry in `packageFiles`) to discover
|
|
20
|
-
* `workspace:` references in `dependencies` and `peerDependencies
|
|
21
|
-
*
|
|
22
|
-
* package to all its dependents.
|
|
22
|
+
* `workspace:` references in `dependencies` and `peerDependencies`, and to record its current
|
|
23
|
+
* version.
|
|
23
24
|
*/
|
|
24
25
|
export declare function buildDependencyGraph(workspaces: readonly WorkspaceConfig[]): DependencyGraph;
|
|
@@ -6,6 +6,7 @@ function isPackageJsonSubset(value) {
|
|
|
6
6
|
export function buildDependencyGraph(workspaces) {
|
|
7
7
|
const packageNameToDir = new Map();
|
|
8
8
|
const dirToPackageName = new Map();
|
|
9
|
+
const dirToVersion = new Map();
|
|
9
10
|
const dependentsOf = new Map();
|
|
10
11
|
const dependenciesOf = new Map();
|
|
11
12
|
const workspacePackages = new Map();
|
|
@@ -16,6 +17,9 @@ export function buildDependencyGraph(workspaces) {
|
|
|
16
17
|
}
|
|
17
18
|
const pkg = readPackageJsonSubset(primaryPackageFile);
|
|
18
19
|
workspacePackages.set(workspace, pkg);
|
|
20
|
+
if (typeof pkg.version === 'string') {
|
|
21
|
+
dirToVersion.set(workspace.dir, pkg.version);
|
|
22
|
+
}
|
|
19
23
|
if (pkg.name === undefined) {
|
|
20
24
|
continue;
|
|
21
25
|
}
|
|
@@ -44,7 +48,7 @@ export function buildDependencyGraph(workspaces) {
|
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
|
-
return { packageNameToDir, dirToPackageName, dependentsOf, dependenciesOf };
|
|
51
|
+
return { packageNameToDir, dirToPackageName, dirToVersion, dependentsOf, dependenciesOf };
|
|
48
52
|
}
|
|
49
53
|
function readPackageJsonSubset(filePath) {
|
|
50
54
|
let content;
|
|
@@ -4,7 +4,7 @@ export declare const UPSTREAM_WORK_TYPES_URL = "https://raw.githubusercontent.co
|
|
|
4
4
|
export interface DriftCheckResult {
|
|
5
5
|
/**
|
|
6
6
|
* Process exit code semantics:
|
|
7
|
-
* - `0` — match (or upstream
|
|
7
|
+
* - `0` — match (or upstream not found, with a warning).
|
|
8
8
|
* - `1` — drift detected.
|
|
9
9
|
* - `2` — network error.
|
|
10
10
|
* - `3` — schema mismatch (upstream JSON does not parse or fails the schema invariants).
|
|
@@ -27,8 +27,7 @@ export interface CheckWorkTypesDriftDependencies {
|
|
|
27
27
|
*
|
|
28
28
|
* Failure modes:
|
|
29
29
|
* - Network error → exit 2 with a diagnostic.
|
|
30
|
-
* - Upstream 404 → exit 0 with a
|
|
31
|
-
* JSON yet; the check is informational while migration is in flight).
|
|
30
|
+
* - Upstream 404 → exit 0 with a warning.
|
|
32
31
|
* - Schema mismatch → exit 3 with a diagnostic.
|
|
33
32
|
* - Drift → exit 1 with a unified-diff-style message.
|
|
34
33
|
* - Match → exit 0.
|
|
@@ -38,7 +38,7 @@ export async function checkWorkTypesDrift(dependencies = {}) {
|
|
|
38
38
|
if (response.status === 404) {
|
|
39
39
|
return {
|
|
40
40
|
exitCode: 0,
|
|
41
|
-
message: `Upstream work-types.json not
|
|
41
|
+
message: `Upstream work-types.json not found at ${url}; skipping drift check.`,
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
if (!response.ok) {
|
|
@@ -1,22 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { findPackageRoot, writeFileWithCheck } from '@williamthorsen/nmr-core';
|
|
4
4
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
5
5
|
import { createGithubReleaseWorkflow, publishWorkflow, releaseConfigScript, releaseWorkflow } from "./templates.js";
|
|
6
6
|
export function copyCliffTemplate(dryRun, overwrite) {
|
|
7
7
|
const destPath = '.config/git-cliff.toml';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
let root;
|
|
9
|
+
try {
|
|
10
|
+
root = findPackageRoot(import.meta.url);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
const message = describeError(error);
|
|
14
|
+
return { filePath: destPath, outcome: 'failed', error: `Failed to resolve package root: ${message}` };
|
|
12
15
|
}
|
|
16
|
+
const templatePath = resolve(root, 'cliff.toml.template');
|
|
13
17
|
let content;
|
|
14
18
|
try {
|
|
15
19
|
content = readFileSync(templatePath, 'utf8');
|
|
16
20
|
}
|
|
17
21
|
catch (error) {
|
|
18
22
|
const message = describeError(error);
|
|
19
|
-
return {
|
|
23
|
+
return {
|
|
24
|
+
filePath: destPath,
|
|
25
|
+
outcome: 'failed',
|
|
26
|
+
error: `Failed to read bundled template at ${templatePath}: ${message}`,
|
|
27
|
+
};
|
|
20
28
|
}
|
|
21
29
|
return writeFileWithCheck(destPath, content, { dryRun, overwrite });
|
|
22
30
|
}
|
|
@@ -15,10 +15,9 @@ export type PolicyViolationSurface = 'prefix' | 'body';
|
|
|
15
15
|
/**
|
|
16
16
|
* Callback invoked when `parseCommitMessage` detects a `!`-policy violation.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* etc.) collect callback invocations and surface them in the release report.
|
|
18
|
+
* The parser warns and continues on a violation, so legacy log entries don't block releases.
|
|
19
|
+
* Callers (`decideRelease`, `releasePrepareMono`, etc.) collect callback invocations and
|
|
20
|
+
* surface them in the release report.
|
|
22
21
|
*/
|
|
23
22
|
export type PolicyViolationHandler = (commit: Commit, type: string, surface: PolicyViolationSurface) => void;
|
|
24
23
|
/** Optional configuration for `parseCommitMessage` beyond the core inputs. */
|
|
@@ -47,6 +46,6 @@ export interface ParseCommitMessageOptions {
|
|
|
47
46
|
* `!`-policy enforcement is **release-time tolerant**: when the resolved type's policy
|
|
48
47
|
* forbids `!`, the marker is dropped from the parse (`breaking: false`) and
|
|
49
48
|
* `onPolicyViolation` is invoked. When the policy requires `!`, a bare type triggers the
|
|
50
|
-
* same warning path.
|
|
49
|
+
* same warning path.
|
|
51
50
|
*/
|
|
52
51
|
export declare function parseCommitMessage(message: string, hash: string, workTypes: Record<string, WorkTypeConfig>, scopeAliases?: Record<string, string>, options?: ParseCommitMessageOptions): ParsedCommit | undefined;
|
|
@@ -16,6 +16,7 @@ export interface VersionBumpPlan {
|
|
|
16
16
|
export declare function planVersionBump(packageFiles: readonly string[], releaseType: ReleaseType): VersionBumpPlan;
|
|
17
17
|
/**
|
|
18
18
|
* Computes an explicit version assignment for a workspace's package files without writing them,
|
|
19
|
-
* bypassing commit-derived bump logic. Backs the `--set-version` flag
|
|
19
|
+
* bypassing commit-derived bump logic. Backs the `--set-version` flag, and so rejects a
|
|
20
|
+
* `newVersion` that is not greater than the current version.
|
|
20
21
|
*/
|
|
21
22
|
export declare function planVersionSet(packageFiles: readonly string[], newVersion: string): VersionBumpPlan;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
|
|
3
3
|
import { bumpVersion } from "./bumpVersion.js";
|
|
4
|
+
import { isForwardVersion } from "./compareVersions.js";
|
|
4
5
|
function isPackageJson(value) {
|
|
5
6
|
return typeof value === 'object' && value !== null && 'version' in value && typeof value.version === 'string';
|
|
6
7
|
}
|
|
@@ -13,6 +14,9 @@ export function planVersionBump(packageFiles, releaseType) {
|
|
|
13
14
|
export function planVersionSet(packageFiles, newVersion) {
|
|
14
15
|
const { firstFile, firstPkg } = readPrimaryPackage(packageFiles);
|
|
15
16
|
const currentVersion = firstPkg.version;
|
|
17
|
+
if (!isForwardVersion(currentVersion, newVersion)) {
|
|
18
|
+
throw new Error(`--set-version ${newVersion} is not greater than current version ${currentVersion}`);
|
|
19
|
+
}
|
|
16
20
|
return { currentVersion, newVersion, writes: renderVersionWrites(packageFiles, firstFile, firstPkg, newVersion) };
|
|
17
21
|
}
|
|
18
22
|
function readPrimaryPackage(packageFiles) {
|
|
@@ -12,8 +12,6 @@ export interface ReleaseEntry {
|
|
|
12
12
|
*/
|
|
13
13
|
newVersionOverride?: string;
|
|
14
14
|
}
|
|
15
|
-
/** Map from workspace `dir` to its current version string (read from package.json). */
|
|
16
|
-
export type CurrentVersions = Map<string, string>;
|
|
17
15
|
/**
|
|
18
16
|
* Walk upward through the dependency graph via BFS, adding `patch` bumps for dependents
|
|
19
17
|
* not already in the release set with a higher bump.
|
|
@@ -21,4 +19,4 @@ export type CurrentVersions = Map<string, string>;
|
|
|
21
19
|
* Returns the full release set (direct + propagated). Direct entries that also have a
|
|
22
20
|
* propagated dependency get `propagatedFrom` metadata without changing their bump type.
|
|
23
21
|
*/
|
|
24
|
-
export declare function propagateBumps(directBumps: Map<string, ReleaseEntry>, graph: DependencyGraph
|
|
22
|
+
export declare function propagateBumps(directBumps: Map<string, ReleaseEntry>, graph: DependencyGraph): Map<string, ReleaseEntry>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { bumpVersion } from "./bumpVersion.js";
|
|
2
|
-
export function propagateBumps(directBumps, graph
|
|
2
|
+
export function propagateBumps(directBumps, graph) {
|
|
3
3
|
const result = new Map();
|
|
4
4
|
for (const [dir, entry] of directBumps) {
|
|
5
5
|
result.set(dir, { ...entry });
|
|
@@ -19,7 +19,7 @@ export function propagateBumps(directBumps, graph, currentVersions) {
|
|
|
19
19
|
if (packageName === undefined) {
|
|
20
20
|
continue;
|
|
21
21
|
}
|
|
22
|
-
const currentVersion =
|
|
22
|
+
const currentVersion = graph.dirToVersion.get(dir);
|
|
23
23
|
const entry = result.get(dir);
|
|
24
24
|
if (currentVersion === undefined || entry === undefined) {
|
|
25
25
|
continue;
|
|
@@ -5,7 +5,6 @@ import { buildReleaseSummary } from "./buildReleaseSummary.js";
|
|
|
5
5
|
import { mergeChangelogEntriesWithDisk, renderChangelogJson, resolveChangelogJsonPath } from "./changelogJsonFile.js";
|
|
6
6
|
import { applyChangelogOverrides, formatStaleOverrideKeyWarning, loadOverridesForScopes, } from "./changelogOverrides.js";
|
|
7
7
|
import { createPolicyViolationCollector } from "./collectPolicyViolations.js";
|
|
8
|
-
import { isForwardVersion } from "./compareVersions.js";
|
|
9
8
|
import { DEFAULT_BREAKING_POLICIES, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
|
|
10
9
|
import { determineBumpFromCommits } from "./determineBumpFromCommits.js";
|
|
11
10
|
import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
|
|
@@ -13,7 +12,6 @@ import { hasPrettierConfig } from "./hasPrettierConfig.js";
|
|
|
13
12
|
import { resolveWorkTypes } from "./loadConfig.js";
|
|
14
13
|
import { planReleaseNotesPreviews } from "./planReleaseNotesPreviews.js";
|
|
15
14
|
import { planVersionBump, planVersionSet } from "./planVersionBump.js";
|
|
16
|
-
import { readCurrentVersion } from "./readCurrentVersion.js";
|
|
17
15
|
import { renderChangelogMarkdown } from "./renderChangelogMarkdown.js";
|
|
18
16
|
import { deriveSectionOrder } from "./resolveReleaseNotesConfig.js";
|
|
19
17
|
export function releasePrepare(config, options) {
|
|
@@ -34,17 +32,6 @@ export function releasePrepare(config, options) {
|
|
|
34
32
|
const collector = createPolicyViolationCollector();
|
|
35
33
|
let bump;
|
|
36
34
|
if (setVersion !== undefined) {
|
|
37
|
-
const primaryPackageFile = config.packageFiles[0];
|
|
38
|
-
if (primaryPackageFile === undefined) {
|
|
39
|
-
throw new Error('No package files specified');
|
|
40
|
-
}
|
|
41
|
-
const currentVersion = readCurrentVersion(primaryPackageFile);
|
|
42
|
-
if (currentVersion === undefined) {
|
|
43
|
-
throw new Error(`Cannot validate --set-version: failed to read current version from ${primaryPackageFile}`);
|
|
44
|
-
}
|
|
45
|
-
if (!isForwardVersion(currentVersion, setVersion)) {
|
|
46
|
-
throw new Error(`--set-version ${setVersion} is not greater than current version ${currentVersion}`);
|
|
47
|
-
}
|
|
48
35
|
bump = planVersionSet(config.packageFiles, setVersion);
|
|
49
36
|
}
|
|
50
37
|
else {
|
|
@@ -8,7 +8,6 @@ import { buildSyntheticChangelogEntry } from "./buildSyntheticChangelogEntry.js"
|
|
|
8
8
|
import { mergeChangelogEntriesWithDisk, renderChangelogJson, resolveChangelogJsonPath } from "./changelogJsonFile.js";
|
|
9
9
|
import { applyWorkspaceOverrides, createOverrideContext, formatStaleOverrideKeyWarning, } from "./changelogOverrides.js";
|
|
10
10
|
import { createPolicyViolationCollector } from "./collectPolicyViolations.js";
|
|
11
|
-
import { isForwardVersion } from "./compareVersions.js";
|
|
12
11
|
import { decideRelease } from "./decideRelease.js";
|
|
13
12
|
import { DEFAULT_BREAKING_POLICIES, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
|
|
14
13
|
import { detectUndeclaredTagPrefixes } from "./detectUndeclaredTagPrefixes.js";
|
|
@@ -19,7 +18,6 @@ import { resolveWorkTypes } from "./loadConfig.js";
|
|
|
19
18
|
import { planReleaseNotesPreviews } from "./planReleaseNotesPreviews.js";
|
|
20
19
|
import { planVersionBump, planVersionSet } from "./planVersionBump.js";
|
|
21
20
|
import { propagateBumps } from "./propagateBumps.js";
|
|
22
|
-
import { readCurrentVersion } from "./readCurrentVersion.js";
|
|
23
21
|
import { releasePrepareProject } from "./releasePrepareProject.js";
|
|
24
22
|
import { renderChangelogMarkdown } from "./renderChangelogMarkdown.js";
|
|
25
23
|
import { deriveSectionOrder } from "./resolveReleaseNotesConfig.js";
|
|
@@ -32,7 +30,7 @@ export function releasePrepareMono(config, options) {
|
|
|
32
30
|
}
|
|
33
31
|
const sectionOrder = deriveSectionOrder(resolveWorkTypes(config.workTypes));
|
|
34
32
|
const overrideContext = createOverrideContext(config.workspaces);
|
|
35
|
-
const { directBumps, directResults, skippedResults
|
|
33
|
+
const { directBumps, directResults, skippedResults } = determineDirectBumps(config, options);
|
|
36
34
|
const previousTags = new Map();
|
|
37
35
|
for (const result of directResults.values()) {
|
|
38
36
|
previousTags.set(result.workspace.dir, result.tag);
|
|
@@ -41,7 +39,7 @@ export function releasePrepareMono(config, options) {
|
|
|
41
39
|
previousTags.set(skipped.workspace.dir, skipped.tag);
|
|
42
40
|
}
|
|
43
41
|
const graph = buildDependencyGraph(config.workspaces);
|
|
44
|
-
const fullReleaseSet = propagateBumps(directBumps, graph
|
|
42
|
+
const fullReleaseSet = propagateBumps(directBumps, graph);
|
|
45
43
|
const { sorted: sortedDirs, cyclicDirs } = topologicalSort(fullReleaseSet, graph);
|
|
46
44
|
if (cyclicDirs.length > 0) {
|
|
47
45
|
warnings.push(`Circular workspace dependencies detected among: ${cyclicDirs.join(', ')}. ` +
|
|
@@ -119,7 +117,6 @@ function determineDirectBumps(config, options) {
|
|
|
119
117
|
const directBumps = new Map();
|
|
120
118
|
const directResults = new Map();
|
|
121
119
|
const skippedResults = [];
|
|
122
|
-
const currentVersions = new Map();
|
|
123
120
|
const hintState = { emitted: false };
|
|
124
121
|
const knownPrefixes = config.workspaces.flatMap(getAllTagPrefixes);
|
|
125
122
|
for (const workspace of config.workspaces) {
|
|
@@ -130,21 +127,7 @@ function determineDirectBumps(config, options) {
|
|
|
130
127
|
if (tag === undefined) {
|
|
131
128
|
maybeEmitBaselineHint(workspace, knownPrefixes, hintState);
|
|
132
129
|
}
|
|
133
|
-
const primaryPackageFile = workspace.packageFiles[0];
|
|
134
|
-
if (primaryPackageFile !== undefined) {
|
|
135
|
-
const currentVersion = tryStage(stageLabel, () => readCurrentVersion(primaryPackageFile));
|
|
136
|
-
if (currentVersion !== undefined) {
|
|
137
|
-
currentVersions.set(workspace.dir, currentVersion);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
130
|
if (setVersion !== undefined) {
|
|
141
|
-
const currentVersion = currentVersions.get(workspace.dir);
|
|
142
|
-
if (currentVersion === undefined) {
|
|
143
|
-
throw new Error(`Cannot validate --set-version: failed to read current version from ${primaryPackageFile ?? '(no package file)'}`);
|
|
144
|
-
}
|
|
145
|
-
if (!isForwardVersion(currentVersion, setVersion)) {
|
|
146
|
-
throw new Error(`--set-version ${setVersion} is not greater than current version ${currentVersion}`);
|
|
147
|
-
}
|
|
148
131
|
directBumps.set(workspace.dir, { releaseType: 'patch', newVersionOverride: setVersion });
|
|
149
132
|
directResults.set(workspace.dir, {
|
|
150
133
|
workspace,
|
|
@@ -199,7 +182,7 @@ function determineDirectBumps(config, options) {
|
|
|
199
182
|
bumpOverride,
|
|
200
183
|
});
|
|
201
184
|
}
|
|
202
|
-
return { directBumps, directResults, skippedResults
|
|
185
|
+
return { directBumps, directResults, skippedResults };
|
|
203
186
|
}
|
|
204
187
|
function collectSkippedWorkspaces(skippedResults, fullReleaseSet) {
|
|
205
188
|
const workspaces = [];
|
|
@@ -5,9 +5,11 @@ import type { LabelDefinition } from './types.js';
|
|
|
5
5
|
*
|
|
6
6
|
* Resolution is an ordered fold with last-writer-wins: presets are loaded in `extends` order (a later preset replaces
|
|
7
7
|
* an earlier preset's label of the same name), then the `labels` record is applied: An entry adds a label, replaces
|
|
8
|
-
* one an earlier layer defined, or removes it (`null`).
|
|
8
|
+
* one an earlier layer defined, or removes it (`null`). Names match case-insensitively, as GitHub matches them, and the
|
|
9
|
+
* replacing layer's spelling becomes the label's name. Replacement is wholesale, so an entry omitting `description`
|
|
9
10
|
* drops the one an earlier layer supplied rather than inheriting it.
|
|
10
|
-
* Throws
|
|
11
|
-
* would
|
|
11
|
+
* Throws on two misstatements that would otherwise leave no trace in the output diff: keys in the `labels` record that
|
|
12
|
+
* differ only in case, all but one of which the fold would discard, and a dangling `null`, a removal naming a label that
|
|
13
|
+
* no preset defined.
|
|
12
14
|
*/
|
|
13
15
|
export declare function resolveLabels(config: RepoLabelsConfig): LabelDefinition[];
|
|
@@ -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
|
@@ -11,19 +11,20 @@ export interface ChangelogItem {
|
|
|
11
11
|
/**
|
|
12
12
|
* Whether this item represents a breaking change.
|
|
13
13
|
*
|
|
14
|
-
* `true` when the commit subject carries the `!` prefix (e.g. `feat!:` or `drop(scope)!:`)
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* marker constructed from `WORK_TYPES_DATA.markers.breaking`
|
|
18
|
-
* with the canonical SSOT values) to surface them prominently
|
|
14
|
+
* `true` when the commit subject carries the `!` prefix (e.g. `feat!:` or `drop(scope)!:`) and
|
|
15
|
+
* the commit's work-type policy permits it, so the item agrees with the version bump.
|
|
16
|
+
* The `BREAKING CHANGE:` body footer is intentionally NOT considered here. Renderers prefix
|
|
17
|
+
* breaking-item bullets with the marker constructed from `WORK_TYPES_DATA.markers.breaking`
|
|
18
|
+
* (rendered as `🚨 **Breaking:** ` with the canonical SSOT values) to surface them prominently
|
|
19
|
+
* in release notes.
|
|
19
20
|
*/
|
|
20
21
|
breaking?: boolean;
|
|
21
22
|
/**
|
|
22
23
|
* Migration instruction for a consumer, taken from the `Migration:` paragraph of `body`.
|
|
23
24
|
*
|
|
24
25
|
* Derived from `body` wherever `body` is set, and absent where the body carries no labeled
|
|
25
|
-
* paragraph. Independent of `breaking`: a `
|
|
26
|
-
*
|
|
26
|
+
* paragraph. Independent of `breaking`: a `deprecate` commit cannot carry `!` under the default
|
|
27
|
+
* breaking policy and still calls for a migration. `body` keeps the paragraph, so
|
|
27
28
|
* `CHANGELOG.md` goes on rendering it in place.
|
|
28
29
|
*/
|
|
29
30
|
migration?: string;
|
|
@@ -388,8 +389,10 @@ export type LabelSpec = z.infer<typeof labelSpecSchema>;
|
|
|
388
389
|
* the GitHub repo, distinct from labels applied to PRs and issues).
|
|
389
390
|
*
|
|
390
391
|
* Resolution is an ordered fold with last-writer-wins: presets in `extends` order, then the `labels` record,
|
|
391
|
-
* where an entry adds a label, replaces one an earlier layer defined, or removes it (`null`).
|
|
392
|
-
*
|
|
392
|
+
* where an entry adds a label, replaces one an earlier layer defined, or removes it (`null`). Names match
|
|
393
|
+
* case-insensitively, as GitHub matches them, and the replacing layer's spelling wins.
|
|
394
|
+
* Two misstatements are resolve-time errors (`resolveLabels`), not schema errors: `labels` keys that differ only in
|
|
395
|
+
* case, and a `null` naming a label that no earlier layer defined.
|
|
393
396
|
*/
|
|
394
397
|
export declare const repoLabelsSchema: z.ZodObject<{
|
|
395
398
|
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;
|