@williamthorsen/release-kit 10.9.0 → 12.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/.meta/changelog.json +119 -2
- package/.readyup/kits/default.js +1 -17
- package/.readyup/kits/npm-auto-publish.js +42 -22
- package/.readyup/manifest.json +7 -7
- package/CHANGELOG.md +88 -0
- package/README.md +32 -32
- package/dist/esm/attachChangelogDiagnostics.d.ts +4 -0
- package/dist/esm/attachChangelogDiagnostics.js +11 -0
- package/dist/esm/bin/release-kit.js +34 -25
- package/dist/esm/buildChangelogEntries.d.ts +59 -17
- package/dist/esm/buildChangelogEntries.js +195 -111
- package/dist/esm/buildReleaseSummary.js +2 -2
- package/dist/esm/changelogOverrides.d.ts +31 -13
- package/dist/esm/changelogOverrides.js +138 -58
- package/dist/esm/classifyChangelogCommit.d.ts +38 -0
- package/dist/esm/classifyChangelogCommit.js +33 -0
- package/dist/esm/configFlagSchema.d.ts +12 -0
- package/dist/esm/configFlagSchema.js +3 -0
- package/dist/esm/createGithubReleaseCommand.js +6 -2
- package/dist/esm/decideRelease.d.ts +11 -37
- package/dist/esm/decideRelease.js +5 -27
- package/dist/esm/defaults.d.ts +1 -1
- package/dist/esm/defaults.js +3 -0
- package/dist/esm/determineBumpType.d.ts +11 -11
- package/dist/esm/determineBumpType.js +6 -7
- package/dist/esm/discoverWorkspaces.d.ts +53 -8
- package/dist/esm/discoverWorkspaces.js +57 -37
- package/dist/esm/enumerateReleaseWindows.d.ts +50 -0
- package/dist/esm/enumerateReleaseWindows.js +124 -0
- package/dist/esm/generateChangelogs.d.ts +5 -23
- package/dist/esm/generateChangelogs.js +0 -14
- package/dist/esm/glyphs.d.ts +1 -1
- package/dist/esm/help/prepareHelp.d.ts +1 -1
- package/dist/esm/help/prepareHelp.js +5 -6
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/init/initCommand.js +1 -1
- package/dist/esm/init/scaffold.d.ts +0 -2
- package/dist/esm/init/scaffold.js +2 -30
- package/dist/esm/loadConfig.d.ts +8 -5
- package/dist/esm/loadConfig.js +7 -7
- package/dist/esm/loadValidatedConfig.d.ts +46 -5
- package/dist/esm/loadValidatedConfig.js +32 -15
- package/dist/esm/parseChangeRecordBlock.d.ts +37 -0
- package/dist/esm/parseChangeRecordBlock.js +134 -0
- package/dist/esm/parseCommitMessage.d.ts +31 -6
- package/dist/esm/parseCommitMessage.js +6 -5
- package/dist/esm/prepareCommand.d.ts +5 -0
- package/dist/esm/prepareCommand.js +30 -47
- package/dist/esm/previewTagPrefixes.d.ts +9 -6
- package/dist/esm/previewTagPrefixes.js +12 -24
- package/dist/esm/publishCommand.d.ts +2 -2
- package/dist/esm/publishCommand.js +8 -2
- package/dist/esm/pushCommand.d.ts +1 -1
- package/dist/esm/pushCommand.js +2 -2
- package/dist/esm/releasePrepare.d.ts +7 -8
- package/dist/esm/releasePrepare.js +53 -73
- package/dist/esm/releasePrepareMono.js +48 -85
- package/dist/esm/releasePrepareProject.d.ts +1 -1
- package/dist/esm/releasePrepareProject.js +19 -37
- package/dist/esm/reportPrepare.js +45 -30
- package/dist/esm/resolveCommandTags.d.ts +3 -2
- package/dist/esm/resolveCommandTags.js +10 -6
- package/dist/esm/resolveReleaseNotesConfig.d.ts +9 -11
- package/dist/esm/resolveReleaseNotesConfig.js +9 -28
- package/dist/esm/showTagPrefixesCommand.d.ts +4 -2
- package/dist/esm/showTagPrefixesCommand.js +17 -3
- package/dist/esm/sync-labels/generateCommand.d.ts +16 -8
- package/dist/esm/sync-labels/generateCommand.js +12 -11
- package/dist/esm/sync-labels/initCommand.d.ts +9 -4
- package/dist/esm/sync-labels/initCommand.js +21 -15
- package/dist/esm/sync-labels/retiredConfig.d.ts +4 -2
- package/dist/esm/sync-labels/retiredConfig.js +2 -2
- package/dist/esm/types.d.ts +99 -55
- package/dist/esm/types.js +1 -1
- package/dist/esm/validateConfig.js +4 -0
- package/dist/esm/validateOverridesCommand.d.ts +16 -9
- package/dist/esm/validateOverridesCommand.js +39 -54
- package/docs/changelogs.md +64 -25
- package/docs/configuration.md +14 -3
- package/docs/editorial-overrides.md +75 -16
- package/docs/labels.md +2 -0
- package/docs/project-releases.md +4 -4
- package/docs/releasing.md +13 -8
- package/docs/work-types.md +13 -9
- package/package.json +5 -7
- package/schemas/changelog-overrides.json +39 -0
- package/cliff.toml.template +0 -65
- package/dist/esm/collectPolicyViolations.d.ts +0 -15
- package/dist/esm/collectPolicyViolations.js +0 -12
- package/dist/esm/determineBumpFromCommits.d.ts +0 -17
- package/dist/esm/determineBumpFromCommits.js +0 -28
- package/dist/esm/getCommitsSinceTarget.d.ts +0 -21
- package/dist/esm/getCommitsSinceTarget.js +0 -66
- package/dist/esm/resolveCliffConfigPath.d.ts +0 -11
- package/dist/esm/resolveCliffConfigPath.js +0 -20
- package/dist/esm/runGitCliff.d.ts +0 -38
- package/dist/esm/runGitCliff.js +0 -25
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { ChangelogEntry, ChangelogOverride, WorkspaceConfig } from './types.js';
|
|
2
|
+
/** Known fields on a single override entry; presence of any other field is a validation error. */
|
|
3
|
+
export declare const KNOWN_OVERRIDE_FIELDS: Set<string>;
|
|
4
|
+
/** Form of an override key: a lowercase hex commit hash or prefix, optionally followed by `:<n>`, a 1-based entry position. */
|
|
5
|
+
export declare const OVERRIDE_KEY_PATTERN: RegExp;
|
|
2
6
|
/** Result of loading an override file: either parsed overrides or a list of structured errors. */
|
|
3
7
|
export type LoadChangelogOverridesResult = {
|
|
4
8
|
overrides: Map<string, ChangelogOverride>;
|
|
@@ -26,6 +30,11 @@ export declare function validateChangelogOverrides(raw: unknown): {
|
|
|
26
30
|
overrides: Map<string, ChangelogOverride>;
|
|
27
31
|
errors: string[];
|
|
28
32
|
};
|
|
33
|
+
/** Split a valid override key into its hash prefix and, for an ordinal key, its 1-based entry position. */
|
|
34
|
+
export declare function parseOverrideKey(key: string): {
|
|
35
|
+
hashPrefix: string;
|
|
36
|
+
entry?: number;
|
|
37
|
+
};
|
|
29
38
|
/**
|
|
30
39
|
* Format the standard "stale override key" warning. Callers compute the set of stale keys
|
|
31
40
|
* (keys that didn't match anywhere) and use this helper to surface them uniformly. Centralized
|
|
@@ -35,12 +44,16 @@ export declare function formatStaleOverrideKeyWarning(key: string): string;
|
|
|
35
44
|
/**
|
|
36
45
|
* Apply overrides to a `ChangelogEntry[]`, returning a new array. Pure: no mutation, no I/O.
|
|
37
46
|
*
|
|
38
|
-
* Match algorithm: each
|
|
47
|
+
* Match algorithm: each key's hash prefix is matched against the distinct `ChangelogItem.hash` values.
|
|
39
48
|
* - 0 matches → key is recorded as unmatched in this batch (no warning emitted here).
|
|
40
|
-
* - 1 match →
|
|
49
|
+
* - 1 match → a bare key resolves to every item of that commit, and an ordinal key `<hash>:<n>` to the items whose
|
|
50
|
+
* `entry` is `n`. A key that resolves to no item is unmatched, so an ordinal key on a title-derived item goes stale.
|
|
41
51
|
* - 2+ matches → error (ambiguous prefix).
|
|
42
52
|
*
|
|
43
|
-
*
|
|
53
|
+
* Two more conditions are errors, and the keys involved apply nowhere: a bare key that sets `description` or `body` on a
|
|
54
|
+
* commit with several items, and two keys that resolve to the same item.
|
|
55
|
+
*
|
|
56
|
+
* `matchedKeys` lists the override keys that resolved to at least one item without error in this batch.
|
|
44
57
|
* Callers are responsible for computing stale-key warnings: in a monorepo run, an override
|
|
45
58
|
* may target a commit that lives in another workspace, so the per-batch zero-match signal is
|
|
46
59
|
* insufficient on its own. Single-package and monorepo orchestrators format warnings using
|
|
@@ -98,8 +111,8 @@ export declare function loadOverridesForScopes(scopes: {
|
|
|
98
111
|
*
|
|
99
112
|
* Byte-equal-key shadowing: when a workspace key string-equals a root key, the workspace
|
|
100
113
|
* entry wins entirely (no field-level merge) and supplants the root entry in the result.
|
|
101
|
-
*
|
|
102
|
-
*
|
|
114
|
+
* Non-identical keys that resolve to the same item do NOT shadow here: {@link applyChangelogOverrides}
|
|
115
|
+
* reports them as overlapping.
|
|
103
116
|
*
|
|
104
117
|
* Pure: never mutates inputs.
|
|
105
118
|
*/
|
|
@@ -127,12 +140,17 @@ export interface OverrideContext {
|
|
|
127
140
|
* is a bug, and we want the prepare run to fail before any workspace begins writing.
|
|
128
141
|
*/
|
|
129
142
|
export declare function createOverrideContext(workspaces: WorkspaceConfig[]): OverrideContext;
|
|
143
|
+
/** A changelog item as override matching sees it. `entry` is absent for a title-derived item. */
|
|
144
|
+
export interface OverrideTargetItem {
|
|
145
|
+
hash: string;
|
|
146
|
+
entry?: number;
|
|
147
|
+
}
|
|
130
148
|
/** Per-scope input to {@link validateAllChangelogOverrides}. */
|
|
131
149
|
export interface ChangelogOverrideScope {
|
|
132
150
|
/** Path to the override file (relative to the repo root). Used to load the file and to attribute findings. */
|
|
133
151
|
filePath: string;
|
|
134
|
-
/**
|
|
135
|
-
|
|
152
|
+
/** Items in this scope's history window. Each override key is matched against these. */
|
|
153
|
+
items: readonly OverrideTargetItem[];
|
|
136
154
|
}
|
|
137
155
|
/** Inputs to {@link validateAllChangelogOverrides}. */
|
|
138
156
|
export interface ValidateAllChangelogOverridesInputs {
|
|
@@ -141,16 +159,16 @@ export interface ValidateAllChangelogOverridesInputs {
|
|
|
141
159
|
*
|
|
142
160
|
* The file at `filePath` is loaded once and used in two ways:
|
|
143
161
|
* - Its overrides are composed into every workspace's apply (root-tier overrides apply globally).
|
|
144
|
-
* - When `
|
|
162
|
+
* - When `items` is provided, the project map is also applied directly to those items
|
|
145
163
|
* (project release in monorepo mode, or the package's history in single-package mode).
|
|
146
164
|
*
|
|
147
165
|
* Omit when no project file exists (rare — most repos have a root file even if empty).
|
|
148
166
|
*/
|
|
149
167
|
project?: {
|
|
150
168
|
filePath: string;
|
|
151
|
-
|
|
169
|
+
items?: readonly OverrideTargetItem[];
|
|
152
170
|
};
|
|
153
|
-
/** Per-workspace scopes. Each workspace's file applies only to its own
|
|
171
|
+
/** Per-workspace scopes. Each workspace's file applies only to its own items. */
|
|
154
172
|
workspaces?: readonly ChangelogOverrideScope[];
|
|
155
173
|
}
|
|
156
174
|
/** Result of {@link validateAllChangelogOverrides}: aggregated errors and warnings, each prefixed with the file path it pertains to. */
|
|
@@ -160,16 +178,16 @@ export interface ValidateAllChangelogOverridesResult {
|
|
|
160
178
|
}
|
|
161
179
|
/**
|
|
162
180
|
* End-to-end health check across every override file and scope. Pure: takes already-collected
|
|
163
|
-
*
|
|
181
|
+
* item universes and returns aggregated findings. The CLI command and any other consumer
|
|
164
182
|
* (programmatic library callers, future composite checks) wrap this with discovery and I/O.
|
|
165
183
|
*
|
|
166
184
|
* When invoked via the standard `validateOverridesCommand` entry point, each scope's
|
|
167
|
-
* `
|
|
185
|
+
* `items` is built by `buildChangelogEntries` — the same path `release-kit prepare` walks —
|
|
168
186
|
* so the match-set is byte-equal to what `prepare` would compute. The tier asymmetry is part
|
|
169
187
|
* of that contract: workspace-tier keys are stale if they don't match in their own workspace;
|
|
170
188
|
* root-tier keys are stale only if they don't match in any scope (no workspace AND not the
|
|
171
189
|
* project release window). Library callers that construct `inputs` directly are responsible
|
|
172
|
-
* for supplying the same
|
|
190
|
+
* for supplying the same item universes if they want this guarantee.
|
|
173
191
|
*
|
|
174
192
|
* Every returned string is prefixed with the relative override-file path it pertains to so
|
|
175
193
|
* consumers can locate the offending file without further structuring.
|
|
@@ -6,7 +6,9 @@ import { isRecord } from "./typeGuards.js";
|
|
|
6
6
|
const OVERRIDES_FILENAME = '.meta/changelog-overrides.json';
|
|
7
7
|
const VALID_AUDIENCE_VALUES = new Set(['all', 'dev', 'skip']);
|
|
8
8
|
const V1_SUPPORTED_AUDIENCE_VALUES = new Set(['skip']);
|
|
9
|
-
const KNOWN_OVERRIDE_FIELDS = new Set(['audience', 'description', 'body', 'breaking']);
|
|
9
|
+
export const KNOWN_OVERRIDE_FIELDS = new Set(['audience', 'description', 'body', 'breaking']);
|
|
10
|
+
export const OVERRIDE_KEY_PATTERN = /^[0-9a-f]+(:[1-9][0-9]*)?$/;
|
|
11
|
+
const SCHEMA_KEY = '$schema';
|
|
10
12
|
export function loadChangelogOverrides(path) {
|
|
11
13
|
if (!existsSync(path)) {
|
|
12
14
|
return { overrides: new Map() };
|
|
@@ -43,8 +45,14 @@ export function validateChangelogOverrides(raw) {
|
|
|
43
45
|
return { overrides, errors };
|
|
44
46
|
}
|
|
45
47
|
for (const [key, rawEntry] of Object.entries(raw)) {
|
|
46
|
-
if (key ===
|
|
47
|
-
|
|
48
|
+
if (key === SCHEMA_KEY) {
|
|
49
|
+
if (typeof rawEntry !== 'string') {
|
|
50
|
+
errors.push(`Override file: '${SCHEMA_KEY}' must be a string`);
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!OVERRIDE_KEY_PATTERN.test(key)) {
|
|
55
|
+
errors.push(`overrides['${key}']: key must be a lowercase hex commit hash or prefix, optionally followed by ':<n>'`);
|
|
48
56
|
continue;
|
|
49
57
|
}
|
|
50
58
|
const validated = validateSingleOverride(key, rawEntry, errors);
|
|
@@ -54,6 +62,13 @@ export function validateChangelogOverrides(raw) {
|
|
|
54
62
|
}
|
|
55
63
|
return { overrides, errors };
|
|
56
64
|
}
|
|
65
|
+
export function parseOverrideKey(key) {
|
|
66
|
+
const separatorIndex = key.indexOf(':');
|
|
67
|
+
if (separatorIndex === -1) {
|
|
68
|
+
return { hashPrefix: key };
|
|
69
|
+
}
|
|
70
|
+
return { hashPrefix: key.slice(0, separatorIndex), entry: Number(key.slice(separatorIndex + 1)) };
|
|
71
|
+
}
|
|
57
72
|
function validateSingleOverride(key, rawEntry, errors) {
|
|
58
73
|
if (!isRecord(rawEntry)) {
|
|
59
74
|
errors.push(`overrides['${key}']: must be an object`);
|
|
@@ -125,53 +140,27 @@ function validateAudience(key, value, errors) {
|
|
|
125
140
|
return 'skip';
|
|
126
141
|
}
|
|
127
142
|
export function formatStaleOverrideKeyWarning(key) {
|
|
128
|
-
return `Override key '${key}' did not match any
|
|
143
|
+
return `Override key '${key}' did not match any item in the changelog (likely a stale reference)`;
|
|
129
144
|
}
|
|
130
145
|
export function applyChangelogOverrides(entries, overrides) {
|
|
131
146
|
const warnings = [];
|
|
132
|
-
const errors = [];
|
|
133
|
-
const matchedKeys = [];
|
|
134
147
|
if (overrides.size === 0) {
|
|
135
|
-
return { entries: entries.map(cloneEntry), warnings, errors, matchedKeys };
|
|
136
|
-
}
|
|
137
|
-
const allHashes = [];
|
|
138
|
-
for (const entry of entries) {
|
|
139
|
-
for (const section of entry.sections) {
|
|
140
|
-
for (const item of section.items) {
|
|
141
|
-
if (item.hash !== undefined) {
|
|
142
|
-
allHashes.push(item.hash);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
148
|
+
return { entries: entries.map(cloneEntry), warnings, errors: [], matchedKeys: [] };
|
|
146
149
|
}
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (matches.length === 0) {
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
if (matches.length > 1) {
|
|
154
|
-
errors.push(`Override key '${overrideKey}' is ambiguous: matches multiple commits (${matches.join(', ')}). ` +
|
|
155
|
-
'Use a longer prefix or the full commit hash.');
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
keyToMatchedHashes.set(overrideKey, matches);
|
|
159
|
-
matchedKeys.push(overrideKey);
|
|
160
|
-
}
|
|
161
|
-
const hashToOverride = new Map();
|
|
162
|
-
for (const [overrideKey, matchedHashes] of keyToMatchedHashes) {
|
|
150
|
+
const { errors, itemIdToKey } = resolveOverrideKeys(indexEntryPositions(entries), overrides);
|
|
151
|
+
const itemIdToOverride = new Map();
|
|
152
|
+
for (const [itemId, overrideKey] of itemIdToKey) {
|
|
163
153
|
const override = overrides.get(overrideKey);
|
|
164
|
-
if (override
|
|
165
|
-
|
|
166
|
-
for (const hash of matchedHashes) {
|
|
167
|
-
hashToOverride.set(hash, override);
|
|
154
|
+
if (override !== undefined) {
|
|
155
|
+
itemIdToOverride.set(itemId, override);
|
|
168
156
|
}
|
|
169
157
|
}
|
|
158
|
+
const matchedKeys = [...new Set(itemIdToKey.values())];
|
|
170
159
|
const transformedEntries = [];
|
|
171
160
|
for (const entry of entries) {
|
|
172
161
|
const transformedSections = [];
|
|
173
162
|
for (const section of entry.sections) {
|
|
174
|
-
const transformedItems = applyOverridesToItems(section.items,
|
|
163
|
+
const transformedItems = applyOverridesToItems(section.items, itemIdToOverride);
|
|
175
164
|
if (transformedItems.length === 0) {
|
|
176
165
|
continue;
|
|
177
166
|
}
|
|
@@ -181,14 +170,93 @@ export function applyChangelogOverrides(entries, overrides) {
|
|
|
181
170
|
}
|
|
182
171
|
return { entries: transformedEntries, warnings, errors, matchedKeys };
|
|
183
172
|
}
|
|
184
|
-
function
|
|
173
|
+
function resolveOverrideKeys(positionsByHash, overrides) {
|
|
174
|
+
const errors = [];
|
|
175
|
+
const failedKeys = new Set();
|
|
176
|
+
const itemIdToKey = new Map();
|
|
177
|
+
for (const [overrideKey, override] of overrides) {
|
|
178
|
+
const resolution = resolveOverrideKey(overrideKey, positionsByHash);
|
|
179
|
+
if (resolution.kind === 'ambiguous') {
|
|
180
|
+
errors.push(`Override key '${overrideKey}' is ambiguous: matches multiple commits (${resolution.hashes.join(', ')}). ` +
|
|
181
|
+
'Use a longer prefix or the full commit hash.');
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (resolution.kind === 'none') {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const { hash, positions } = resolution;
|
|
188
|
+
if (parseOverrideKey(overrideKey).entry === undefined && positions.length > 1) {
|
|
189
|
+
const fieldsSet = ['description', 'body'].filter((field) => override[field] !== undefined);
|
|
190
|
+
if (fieldsSet.length > 0) {
|
|
191
|
+
const ordinalKeys = positions.map((position) => `${overrideKey}:${position}`).join(', ');
|
|
192
|
+
errors.push(`Override key '${overrideKey}' sets ${fieldsSet.join(' and ')} on a commit with several items; use ${ordinalKeys}`);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (const position of positions) {
|
|
197
|
+
const itemId = formatItemId(hash, position);
|
|
198
|
+
const claimingKey = itemIdToKey.get(itemId);
|
|
199
|
+
if (claimingKey === undefined) {
|
|
200
|
+
itemIdToKey.set(itemId, overrideKey);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const target = position === undefined ? `commit ${hash}` : `the item at ${hash}:${position}`;
|
|
204
|
+
errors.push(`Override keys '${claimingKey}' and '${overrideKey}' both match ${target}; keep one`);
|
|
205
|
+
failedKeys.add(claimingKey);
|
|
206
|
+
failedKeys.add(overrideKey);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
for (const [itemId, overrideKey] of itemIdToKey) {
|
|
210
|
+
if (failedKeys.has(overrideKey)) {
|
|
211
|
+
itemIdToKey.delete(itemId);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { errors, itemIdToKey };
|
|
215
|
+
}
|
|
216
|
+
function resolveOverrideKey(overrideKey, positionsByHash) {
|
|
217
|
+
const { hashPrefix, entry } = parseOverrideKey(overrideKey);
|
|
218
|
+
const hashes = positionsByHash
|
|
219
|
+
.keys()
|
|
220
|
+
.filter((hash) => hash.startsWith(hashPrefix))
|
|
221
|
+
.toArray();
|
|
222
|
+
const [hash] = hashes;
|
|
223
|
+
if (hash === undefined) {
|
|
224
|
+
return { kind: 'none' };
|
|
225
|
+
}
|
|
226
|
+
if (hashes.length > 1) {
|
|
227
|
+
return { kind: 'ambiguous', hashes };
|
|
228
|
+
}
|
|
229
|
+
const available = [...(positionsByHash.get(hash) ?? [])];
|
|
230
|
+
const positions = entry === undefined
|
|
231
|
+
? available.toSorted((left, right) => (left ?? 0) - (right ?? 0))
|
|
232
|
+
: available.filter((position) => position === entry);
|
|
233
|
+
return positions.length === 0 ? { kind: 'none' } : { kind: 'items', hash, positions };
|
|
234
|
+
}
|
|
235
|
+
function indexEntryPositions(entries) {
|
|
236
|
+
return indexItemPositions(entries.flatMap((entry) => entry.sections.flatMap((section) => section.items)));
|
|
237
|
+
}
|
|
238
|
+
function indexItemPositions(items) {
|
|
239
|
+
const positionsByHash = new Map();
|
|
240
|
+
for (const item of items) {
|
|
241
|
+
if (item.hash === undefined)
|
|
242
|
+
continue;
|
|
243
|
+
const positions = positionsByHash.get(item.hash) ?? new Set();
|
|
244
|
+
positions.add(item.entry);
|
|
245
|
+
positionsByHash.set(item.hash, positions);
|
|
246
|
+
}
|
|
247
|
+
return positionsByHash;
|
|
248
|
+
}
|
|
249
|
+
function formatItemId(hash, entry) {
|
|
250
|
+
return entry === undefined ? hash : `${hash}:${entry}`;
|
|
251
|
+
}
|
|
252
|
+
function applyOverridesToItems(items, itemIdToOverride) {
|
|
185
253
|
const result = [];
|
|
186
254
|
for (const item of items) {
|
|
187
255
|
if (item.hash === undefined) {
|
|
188
256
|
result.push(cloneItem(item));
|
|
189
257
|
continue;
|
|
190
258
|
}
|
|
191
|
-
const override =
|
|
259
|
+
const override = itemIdToOverride.get(formatItemId(item.hash, item.entry));
|
|
192
260
|
if (override === undefined) {
|
|
193
261
|
result.push(cloneItem(item));
|
|
194
262
|
continue;
|
|
@@ -207,6 +275,8 @@ function applyOverrideToItem(item, override) {
|
|
|
207
275
|
}
|
|
208
276
|
if (override.body !== undefined) {
|
|
209
277
|
result.body = override.body;
|
|
278
|
+
}
|
|
279
|
+
if (override.body !== undefined && item.entry === undefined) {
|
|
210
280
|
const migration = extractMigration(override.body);
|
|
211
281
|
if (migration === undefined) {
|
|
212
282
|
delete result.migration;
|
|
@@ -288,7 +358,7 @@ export function validateAllChangelogOverrides(inputs) {
|
|
|
288
358
|
const projectMap = loadScopeMap(projectFilePath, errors);
|
|
289
359
|
const workspaceMaps = (inputs.workspaces ?? []).map((scope) => ({
|
|
290
360
|
filePath: scope.filePath,
|
|
291
|
-
|
|
361
|
+
items: scope.items,
|
|
292
362
|
map: loadScopeMap(scope.filePath, errors),
|
|
293
363
|
}));
|
|
294
364
|
const globalMatchedRootKeys = new Set();
|
|
@@ -302,9 +372,9 @@ export function validateAllChangelogOverrides(inputs) {
|
|
|
302
372
|
globalMatchedRootKeys,
|
|
303
373
|
});
|
|
304
374
|
}
|
|
305
|
-
const
|
|
306
|
-
if (projectFilePath !== undefined &&
|
|
307
|
-
processProjectScope({ projectFilePath, projectMap,
|
|
375
|
+
const projectItems = inputs.project?.items;
|
|
376
|
+
if (projectFilePath !== undefined && projectItems !== undefined) {
|
|
377
|
+
processProjectScope({ projectFilePath, projectMap, projectItems, errors, globalMatchedRootKeys });
|
|
308
378
|
}
|
|
309
379
|
if (projectFilePath !== undefined) {
|
|
310
380
|
collectRootStaleWarnings(projectFilePath, projectMap, globalMatchedRootKeys, warnings);
|
|
@@ -313,39 +383,49 @@ export function validateAllChangelogOverrides(inputs) {
|
|
|
313
383
|
}
|
|
314
384
|
function processWorkspaceScope(args) {
|
|
315
385
|
const { workspace, projectFilePath, projectMap, errors, warnings, globalMatchedRootKeys } = args;
|
|
316
|
-
const { filePath,
|
|
317
|
-
const
|
|
386
|
+
const { filePath, items, map } = workspace;
|
|
387
|
+
const validationEntries = makeValidationEntries(items);
|
|
388
|
+
const positionsByHash = indexItemPositions(items);
|
|
389
|
+
const workspaceApplied = applyChangelogOverrides(validationEntries, map);
|
|
318
390
|
for (const message of workspaceApplied.errors) {
|
|
319
391
|
errors.push(prefixWithFilePath(filePath, message));
|
|
320
392
|
}
|
|
321
393
|
if (projectFilePath !== undefined && projectMap.size > 0) {
|
|
322
394
|
const projectMinusShadowed = filterShadowedKeys(projectMap, map);
|
|
323
|
-
const projectApplied = applyChangelogOverrides(
|
|
395
|
+
const projectApplied = applyChangelogOverrides(validationEntries, projectMinusShadowed);
|
|
324
396
|
for (const message of projectApplied.errors) {
|
|
325
397
|
errors.push(prefixWithFilePath(projectFilePath, message));
|
|
326
398
|
}
|
|
399
|
+
const perFileErrors = new Set([...workspaceApplied.errors, ...projectApplied.errors]);
|
|
400
|
+
const composedApplied = applyChangelogOverrides(validationEntries, composeOverrides(projectMap, map));
|
|
401
|
+
for (const message of composedApplied.errors) {
|
|
402
|
+
if (!perFileErrors.has(message)) {
|
|
403
|
+
errors.push(prefixWithFilePath(filePath, message));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
327
406
|
}
|
|
328
407
|
for (const key of map.keys()) {
|
|
329
|
-
if (!hasAnyMatch(key,
|
|
408
|
+
if (!hasAnyMatch(key, positionsByHash)) {
|
|
330
409
|
warnings.push(formatWorkspaceStaleWarning(filePath, key));
|
|
331
410
|
}
|
|
332
411
|
}
|
|
333
412
|
for (const key of projectMap.keys()) {
|
|
334
413
|
if (map.has(key))
|
|
335
414
|
continue;
|
|
336
|
-
if (hasAnyMatch(key,
|
|
415
|
+
if (hasAnyMatch(key, positionsByHash)) {
|
|
337
416
|
globalMatchedRootKeys.add(key);
|
|
338
417
|
}
|
|
339
418
|
}
|
|
340
419
|
}
|
|
341
420
|
function processProjectScope(args) {
|
|
342
|
-
const { projectFilePath, projectMap,
|
|
343
|
-
const applied = applyChangelogOverrides(makeValidationEntries(
|
|
421
|
+
const { projectFilePath, projectMap, projectItems, errors, globalMatchedRootKeys } = args;
|
|
422
|
+
const applied = applyChangelogOverrides(makeValidationEntries(projectItems), projectMap);
|
|
344
423
|
for (const message of applied.errors) {
|
|
345
424
|
errors.push(prefixWithFilePath(projectFilePath, message));
|
|
346
425
|
}
|
|
426
|
+
const positionsByHash = indexItemPositions(projectItems);
|
|
347
427
|
for (const key of projectMap.keys()) {
|
|
348
|
-
if (hasAnyMatch(key,
|
|
428
|
+
if (hasAnyMatch(key, positionsByHash)) {
|
|
349
429
|
globalMatchedRootKeys.add(key);
|
|
350
430
|
}
|
|
351
431
|
}
|
|
@@ -357,8 +437,8 @@ function collectRootStaleWarnings(projectFilePath, projectMap, globalMatchedRoot
|
|
|
357
437
|
}
|
|
358
438
|
}
|
|
359
439
|
}
|
|
360
|
-
function hasAnyMatch(key,
|
|
361
|
-
return
|
|
440
|
+
function hasAnyMatch(key, positionsByHash) {
|
|
441
|
+
return resolveOverrideKey(key, positionsByHash).kind !== 'none';
|
|
362
442
|
}
|
|
363
443
|
function filterShadowedKeys(projectMap, workspaceMap) {
|
|
364
444
|
const result = new Map();
|
|
@@ -382,7 +462,7 @@ function loadScopeMap(filePath, errors) {
|
|
|
382
462
|
}
|
|
383
463
|
return result.overrides;
|
|
384
464
|
}
|
|
385
|
-
function makeValidationEntries(
|
|
465
|
+
function makeValidationEntries(items) {
|
|
386
466
|
return [
|
|
387
467
|
{
|
|
388
468
|
version: '0.0.0',
|
|
@@ -391,7 +471,7 @@ function makeValidationEntries(hashes) {
|
|
|
391
471
|
{
|
|
392
472
|
title: 'Validation',
|
|
393
473
|
audience: 'all',
|
|
394
|
-
items:
|
|
474
|
+
items: items.map((item) => ({ ...item, description: '' })),
|
|
395
475
|
},
|
|
396
476
|
],
|
|
397
477
|
},
|
|
@@ -401,10 +481,10 @@ function prefixWithFilePath(filePath, message) {
|
|
|
401
481
|
return `${filePath}: ${message}`;
|
|
402
482
|
}
|
|
403
483
|
function formatWorkspaceStaleWarning(filePath, key) {
|
|
404
|
-
return `${filePath}: Override key '${key}' did not match any
|
|
484
|
+
return `${filePath}: Override key '${key}' did not match any item in this workspace's history (likely a stale reference)`;
|
|
405
485
|
}
|
|
406
486
|
function formatRootStaleWarning(filePath, key) {
|
|
407
|
-
return `${filePath}: Override key '${key}' did not match any
|
|
487
|
+
return `${filePath}: Override key '${key}' did not match any item in any scope (likely a stale reference)`;
|
|
408
488
|
}
|
|
409
489
|
export function applyWorkspaceOverrides(newEntries, workspacePath, overrideContext) {
|
|
410
490
|
const { project, perWorkspace, overrideWarnings, globalMatchedRootKeys } = overrideContext;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type ParseCommitMessageOptions } from './parseCommitMessage.js';
|
|
2
|
+
import type { Commit, WorkTypeConfig } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* How a commit's title places it in a changelog: under a section header, excluded by design, or unreadable.
|
|
5
|
+
*
|
|
6
|
+
* `breaking` is the parser's flag under the breaking policies passed in, which a `BREAKING CHANGE:` footer can set.
|
|
7
|
+
*/
|
|
8
|
+
export type ChangelogClassification = {
|
|
9
|
+
kind: 'header';
|
|
10
|
+
header: string;
|
|
11
|
+
type: string;
|
|
12
|
+
breaking: boolean;
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'excluded';
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'unparseable';
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Classifies a commit by its title.
|
|
20
|
+
*
|
|
21
|
+
* Four gates decide, in order: a `release:` or `Merge` subject is excluded; a subject carrying no ticket-ID prefix is
|
|
22
|
+
* unparseable; a type that `parseCommitMessage` cannot resolve against `workTypes` is unparseable; and a type that
|
|
23
|
+
* `workTypes` excludes from the changelog is excluded. A commit that passes all four takes the header its type declares.
|
|
24
|
+
*
|
|
25
|
+
* `options` reaches the parse, so a policy violation is reported for every ticketed commit whose type resolves,
|
|
26
|
+
* excluded types included.
|
|
27
|
+
*
|
|
28
|
+
* The header carries no order encoding. `buildChangelogEntries` sorts sections by canonical priority, so
|
|
29
|
+
* the position a section occupies comes from the taxonomy rather than from the header string.
|
|
30
|
+
*/
|
|
31
|
+
export declare function classifyChangelogCommit(commit: Pick<Commit, 'hash' | 'message'>, workTypes: Record<string, WorkTypeConfig>, options?: ParseCommitMessageOptions): ChangelogClassification;
|
|
32
|
+
/**
|
|
33
|
+
* Reports whether a subject belongs to a commit that never reaches a changelog: a `release:` commit, which records
|
|
34
|
+
* version bumps, or a git merge commit, whose content the merged commits already report.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isNonChangeSubject(subject: string): boolean;
|
|
37
|
+
/** Reports whether a subject belongs to a `release:` commit, which records version bumps rather than a change. */
|
|
38
|
+
export declare function isReleaseSubject(subject: string): boolean;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { COMMIT_PREPROCESSOR_PATTERNS, parseCommitMessage, } from "./parseCommitMessage.js";
|
|
2
|
+
const RELEASE_SUBJECT_PATTERN = /^release:/;
|
|
3
|
+
const MERGE_SUBJECT_PATTERN = /^Merge/;
|
|
4
|
+
export function classifyChangelogCommit(commit, workTypes, options) {
|
|
5
|
+
const subject = commit.message.split('\n', 1)[0] ?? '';
|
|
6
|
+
if (isNonChangeSubject(subject)) {
|
|
7
|
+
return { kind: 'excluded' };
|
|
8
|
+
}
|
|
9
|
+
if (!hasTicketPrefix(subject)) {
|
|
10
|
+
return { kind: 'unparseable' };
|
|
11
|
+
}
|
|
12
|
+
const parsed = parseCommitMessage(commit.message, commit.hash, workTypes, undefined, options);
|
|
13
|
+
if (parsed === undefined) {
|
|
14
|
+
return { kind: 'unparseable' };
|
|
15
|
+
}
|
|
16
|
+
const config = workTypes[parsed.type];
|
|
17
|
+
if (config === undefined) {
|
|
18
|
+
return { kind: 'unparseable' };
|
|
19
|
+
}
|
|
20
|
+
if (config.excludedFromChangelog === true) {
|
|
21
|
+
return { kind: 'excluded' };
|
|
22
|
+
}
|
|
23
|
+
return { kind: 'header', header: config.header, type: parsed.type, breaking: parsed.breaking };
|
|
24
|
+
}
|
|
25
|
+
export function isNonChangeSubject(subject) {
|
|
26
|
+
return isReleaseSubject(subject) || MERGE_SUBJECT_PATTERN.test(subject);
|
|
27
|
+
}
|
|
28
|
+
export function isReleaseSubject(subject) {
|
|
29
|
+
return RELEASE_SUBJECT_PATTERN.test(subject);
|
|
30
|
+
}
|
|
31
|
+
function hasTicketPrefix(subject) {
|
|
32
|
+
return COMMIT_PREPROCESSOR_PATTERNS.some((pattern) => pattern.test(subject));
|
|
33
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `--config` flag, shared by every subcommand that reads the config file.
|
|
3
|
+
*
|
|
4
|
+
* Spread into a subcommand's own schema rather than repeated, so that seven subcommands cannot drift to seven
|
|
5
|
+
* spellings of one flag.
|
|
6
|
+
*/
|
|
7
|
+
export declare const configFlagSchema: {
|
|
8
|
+
config: {
|
|
9
|
+
long: string;
|
|
10
|
+
type: "string";
|
|
11
|
+
};
|
|
12
|
+
};
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
|
|
2
2
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
3
|
+
import { configFlagSchema } from "./configFlagSchema.js";
|
|
3
4
|
import { createGithubReleases } from "./createGithubRelease.js";
|
|
4
5
|
import { formatPrivateSkip } from "./formatPrivateSkip.js";
|
|
6
|
+
import { assertConfigUsable } from "./loadValidatedConfig.js";
|
|
5
7
|
import { parseRequestedTags } from "./parseRequestedTags.js";
|
|
6
8
|
import { resolveCommandTags } from "./resolveCommandTags.js";
|
|
7
9
|
import { resolveReleaseNotesConfig } from "./resolveReleaseNotesConfig.js";
|
|
8
10
|
const createGithubReleaseFlagSchema = {
|
|
11
|
+
...configFlagSchema,
|
|
9
12
|
dryRun: { long: '--dry-run', type: 'boolean' },
|
|
10
13
|
tags: { long: '--tags', type: 'string' },
|
|
11
14
|
};
|
|
12
15
|
export async function createGithubReleaseCommand(argv, styles) {
|
|
13
16
|
const parsed = parseArgsOrExit(argv, createGithubReleaseFlagSchema);
|
|
14
17
|
const { dryRun } = parsed.flags;
|
|
18
|
+
await assertConfigUsable(styles.stderr, parsed.flags.config);
|
|
15
19
|
const requestedTags = parseRequestedTags(parsed.flags.tags);
|
|
16
|
-
const resolvedTags =
|
|
20
|
+
const resolvedTags = resolveCommandTags(requestedTags);
|
|
17
21
|
const publishableTags = resolvedTags.filter((resolvedTag) => resolvedTag.isPublishable);
|
|
18
22
|
for (const resolvedTag of resolvedTags) {
|
|
19
23
|
if (!resolvedTag.isPublishable) {
|
|
@@ -24,7 +28,7 @@ export async function createGithubReleaseCommand(argv, styles) {
|
|
|
24
28
|
return;
|
|
25
29
|
}
|
|
26
30
|
const { changelogJsonOutputPath, sectionOrder } = await resolveReleaseNotesConfig(styles.stderr, {
|
|
27
|
-
|
|
31
|
+
...(parsed.flags.config !== undefined && { configPath: parsed.flags.config }),
|
|
28
32
|
});
|
|
29
33
|
let outcome;
|
|
30
34
|
try {
|
|
@@ -1,23 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
/** Inputs to the unified release decision used by both pipelines. */
|
|
1
|
+
import type { ReleaseType } from './types.js';
|
|
2
|
+
/** Inputs to the release decision that every release path makes. */
|
|
4
3
|
export interface DecideReleaseArgs {
|
|
5
|
-
|
|
4
|
+
/** The bump that the window's changelog items call for; undefined when they call for none. */
|
|
5
|
+
naturalBump: ReleaseType | undefined;
|
|
6
|
+
/** The number of commits in the window, which selects the skip reason. */
|
|
7
|
+
commitCount: number;
|
|
6
8
|
/** True when `--force` was passed; treats absence of a natural bump as patch. Defaults to false. */
|
|
7
9
|
force?: boolean | undefined;
|
|
8
10
|
/** Explicit `--bump=X` override. When set, the level is X regardless of the natural bump. */
|
|
9
11
|
bumpOverride: ReleaseType | undefined;
|
|
10
|
-
workTypes: Record<string, WorkTypeConfig>;
|
|
11
|
-
versionPatterns: VersionPatterns;
|
|
12
|
-
scopeAliases: Record<string, string> | undefined;
|
|
13
|
-
/** Per-canonical-type breaking-policy lookup. When provided, drives `!` policy enforcement in `parseCommitMessage`. */
|
|
14
|
-
breakingPolicies?: Record<string, 'forbidden' | 'optional' | 'required'>;
|
|
15
|
-
/** Receives policy-violation notifications from `parseCommitMessage`. */
|
|
16
|
-
onPolicyViolation?: PolicyViolationHandler;
|
|
17
12
|
/**
|
|
18
|
-
* Skip wordings to use when no commits exist (`noCommits`) or when commits exist but
|
|
19
|
-
*
|
|
20
|
-
* own pre-rendered wordings (with `{name}` / `since` substitutions already applied).
|
|
13
|
+
* Skip wordings to use when no commits exist (`noCommits`) or when commits exist but none yields a changelog item
|
|
14
|
+
* (`noBumpWorthy`). Each path passes its own pre-rendered wordings.
|
|
21
15
|
*/
|
|
22
16
|
skipReasons: {
|
|
23
17
|
noCommits: string;
|
|
@@ -28,38 +22,18 @@ export interface DecideReleaseArgs {
|
|
|
28
22
|
export type DecideReleaseResult = {
|
|
29
23
|
outcome: 'release';
|
|
30
24
|
releaseType: ReleaseType;
|
|
31
|
-
parsedCommitCount: number;
|
|
32
|
-
unparseableCommits: Commit[] | undefined;
|
|
33
25
|
} | {
|
|
34
26
|
outcome: 'skip';
|
|
35
27
|
skipReason: string;
|
|
36
|
-
parsedCommitCount: number;
|
|
37
|
-
unparseableCommits: Commit[] | undefined;
|
|
38
28
|
};
|
|
39
29
|
/**
|
|
40
|
-
*
|
|
41
|
-
* and the per-workspace path in `releasePrepareMono`.
|
|
30
|
+
* Decides whether a window releases, and at which level.
|
|
42
31
|
*
|
|
43
32
|
* Algorithm:
|
|
44
33
|
* shouldRelease = naturalBump !== undefined OR force === true
|
|
45
34
|
* releaseLevel = bumpOverride ?? naturalBump ?? 'patch'
|
|
46
35
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* bump-worthy work type. Note that this differs from `determineBumpFromCommits`, which
|
|
50
|
-
* applies a patch floor whenever any commits exist; the patch floor is intentionally
|
|
51
|
-
* not applied here so that `--bump=X` and `--force` remain orthogonal (the floor is
|
|
52
|
-
* applied only when `--force` is set, by virtue of the `naturalBump ?? 'patch'`
|
|
53
|
-
* fallback in the release-level expression above).
|
|
54
|
-
*
|
|
55
|
-
* `parsedCommitCount` and `unparseableCommits` are returned in both branches so callers
|
|
56
|
-
* can surface diagnostic data uniformly regardless of skip/release outcome.
|
|
57
|
-
*
|
|
58
|
-
* Scope: this is the canonical algorithm for the orthogonal-flag model in the
|
|
59
|
-
* monorepo path (`releasePrepareProject` + per-workspace `determineDirectBumps`). The
|
|
60
|
-
* single-package executor (`releasePrepare`) deliberately retains
|
|
61
|
-
* `determineBumpFromCommits` and the legacy patch-floor semantics; `prepareCommand`
|
|
62
|
-
* rejects `--force` without `--bump` in single-package mode to surface that gap. A
|
|
63
|
-
* future ticket can unify the single-package path; see #313's external plan.
|
|
36
|
+
* A window without a natural bump releases only under `--force`, so `--bump=X` chooses a level without triggering a
|
|
37
|
+
* release, and `--force` triggers one without choosing a level.
|
|
64
38
|
*/
|
|
65
39
|
export declare function decideRelease(args: DecideReleaseArgs): DecideReleaseResult;
|