@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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { parseArgsOrExit, readPackageVersion, reportError } from '@williamthorsen/nmr-core';
|
|
3
3
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
4
4
|
import { commitCommand } from "../commitCommand.js";
|
|
5
|
+
import { configFlagSchema } from "../configFlagSchema.js";
|
|
5
6
|
import { createGithubReleaseCommand } from "../createGithubReleaseCommand.js";
|
|
6
7
|
import { showPrepareHelp } from "../help/prepareHelp.js";
|
|
7
8
|
import { initCommand } from "../init/initCommand.js";
|
|
@@ -68,9 +69,10 @@ retired packages, then generate .github/labels.yaml. When the config file alread
|
|
|
68
69
|
exists, print the block for manual paste instead of rewriting the file.
|
|
69
70
|
|
|
70
71
|
Options:
|
|
71
|
-
--
|
|
72
|
-
--
|
|
73
|
-
--
|
|
72
|
+
--config <path> Path to config file (default: .config/release-kit.config.ts)
|
|
73
|
+
--dry-run Preview changes without writing files
|
|
74
|
+
--force Overwrite existing files instead of skipping them
|
|
75
|
+
--help, -h Show this help message
|
|
74
76
|
`);
|
|
75
77
|
}
|
|
76
78
|
function showSyncLabelsGenerateHelp() {
|
|
@@ -80,8 +82,9 @@ Usage: release-kit sync-labels generate [options]
|
|
|
80
82
|
Regenerate .github/labels.yaml from the repoLabels block of .config/release-kit.config.ts.
|
|
81
83
|
|
|
82
84
|
Options:
|
|
83
|
-
--check
|
|
84
|
-
--
|
|
85
|
+
--check Report whether .github/labels.yaml is stale instead of writing it
|
|
86
|
+
--config <path> Path to config file (default: .config/release-kit.config.ts)
|
|
87
|
+
--help, -h Show this help message
|
|
85
88
|
`);
|
|
86
89
|
}
|
|
87
90
|
function showSyncLabelsSyncHelp() {
|
|
@@ -102,7 +105,7 @@ Initialize release-kit in the current repository.
|
|
|
102
105
|
By default, scaffolds only the GitHub Actions workflow files.
|
|
103
106
|
|
|
104
107
|
Options:
|
|
105
|
-
--with-config Also scaffold .config/release-kit.config.ts
|
|
108
|
+
--with-config Also scaffold .config/release-kit.config.ts
|
|
106
109
|
--force Overwrite existing files instead of skipping them
|
|
107
110
|
--dry-run Preview changes without writing files
|
|
108
111
|
--help, -h Show this help message
|
|
@@ -154,6 +157,7 @@ Create GitHub Releases from changelog.json for tags on HEAD. Private
|
|
|
154
157
|
(package.json#private) workspaces are skipped with a warning and get no Release.
|
|
155
158
|
|
|
156
159
|
Options:
|
|
160
|
+
--config <path> Path to config file (default: .config/release-kit.config.ts)
|
|
157
161
|
--dry-run Preview without creating releases
|
|
158
162
|
--tags=tag1,tag2 Only create releases for the named tags (comma-separated, full tag names)
|
|
159
163
|
--help, -h Show this help message
|
|
@@ -169,7 +173,8 @@ derived prefix nor declared in \`legacyIdentities\`, with a copy-pasteable
|
|
|
169
173
|
config snippet.
|
|
170
174
|
|
|
171
175
|
Options:
|
|
172
|
-
--
|
|
176
|
+
--config <path> Path to config file (default: .config/release-kit.config.ts)
|
|
177
|
+
--help, -h Show this help message
|
|
173
178
|
`);
|
|
174
179
|
}
|
|
175
180
|
function showOverridesHelp() {
|
|
@@ -190,15 +195,17 @@ function showOverridesValidateHelp() {
|
|
|
190
195
|
Usage: release-kit overrides validate
|
|
191
196
|
|
|
192
197
|
Validate every \`.meta/changelog-overrides.json\` file across the project and per-workspace
|
|
193
|
-
scopes. Reports schema/parse errors, ambiguous-prefix errors,
|
|
198
|
+
scopes. Reports schema/parse errors, ambiguous-prefix errors, conflicting-key errors, and
|
|
199
|
+
stale-key warnings.
|
|
194
200
|
|
|
195
201
|
Exit codes:
|
|
196
202
|
0 Clean — no errors, no stale keys
|
|
197
203
|
1 Stale-key warnings only (no errors)
|
|
198
|
-
2 Schema/parse
|
|
204
|
+
2 Schema/parse, ambiguous-prefix, or conflicting-key errors (errors dominate)
|
|
199
205
|
|
|
200
206
|
Options:
|
|
201
|
-
--
|
|
207
|
+
--config <path> Path to config file (default: .config/release-kit.config.ts)
|
|
208
|
+
--help, -h Show this help message
|
|
202
209
|
`);
|
|
203
210
|
}
|
|
204
211
|
function showPublishHelp() {
|
|
@@ -211,6 +218,7 @@ silently filtered out. With --tags, an unpublishable tag is skipped with a warni
|
|
|
211
218
|
any publishable tags still publish.
|
|
212
219
|
|
|
213
220
|
Options:
|
|
221
|
+
--config <path> Path to config file (default: .config/release-kit.config.ts)
|
|
214
222
|
--dry-run Preview without publishing
|
|
215
223
|
--no-git-checks Skip the clean-working-tree check
|
|
216
224
|
--tags=tag1,tag2 Only publish the named tags (comma-separated, full tag names)
|
|
@@ -269,7 +277,7 @@ if (command === 'push') {
|
|
|
269
277
|
showPushHelp();
|
|
270
278
|
process.exit(0);
|
|
271
279
|
}
|
|
272
|
-
|
|
280
|
+
pushCommand(flags);
|
|
273
281
|
process.exit(0);
|
|
274
282
|
}
|
|
275
283
|
if (command === 'create-github-release') {
|
|
@@ -293,11 +301,8 @@ if (command === 'show-tag-prefixes') {
|
|
|
293
301
|
showShowTagPrefixesHelp();
|
|
294
302
|
process.exit(0);
|
|
295
303
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
process.exit(1);
|
|
299
|
-
}
|
|
300
|
-
const exitCode = await showTagPrefixesCommand(styles);
|
|
304
|
+
const { config } = parseArgsOrExit(flags, configFlagSchema).flags;
|
|
305
|
+
const exitCode = await showTagPrefixesCommand(styles, config);
|
|
301
306
|
process.exit(exitCode);
|
|
302
307
|
}
|
|
303
308
|
if (command === 'init') {
|
|
@@ -327,11 +332,17 @@ if (command === 'sync-labels') {
|
|
|
327
332
|
process.exit(0);
|
|
328
333
|
}
|
|
329
334
|
const syncLabelsInitFlagSchema = {
|
|
335
|
+
...configFlagSchema,
|
|
330
336
|
dryRun: { long: '--dry-run', type: 'boolean' },
|
|
331
337
|
force: { long: '--force', type: 'boolean' },
|
|
332
338
|
};
|
|
333
|
-
const { dryRun, force } = parseArgsOrExit(subflags, syncLabelsInitFlagSchema).flags;
|
|
334
|
-
const exitCode = await syncLabelsInitCommand({
|
|
339
|
+
const { config, dryRun, force } = parseArgsOrExit(subflags, syncLabelsInitFlagSchema).flags;
|
|
340
|
+
const exitCode = await syncLabelsInitCommand({
|
|
341
|
+
...(config !== undefined && { configPath: config }),
|
|
342
|
+
dryRun,
|
|
343
|
+
force,
|
|
344
|
+
styles,
|
|
345
|
+
});
|
|
335
346
|
process.exit(exitCode);
|
|
336
347
|
}
|
|
337
348
|
if (subcommand === 'generate') {
|
|
@@ -340,10 +351,11 @@ if (command === 'sync-labels') {
|
|
|
340
351
|
process.exit(0);
|
|
341
352
|
}
|
|
342
353
|
const generateFlagSchema = {
|
|
354
|
+
...configFlagSchema,
|
|
343
355
|
check: { long: '--check', type: 'boolean' },
|
|
344
356
|
};
|
|
345
|
-
const { check } = parseArgsOrExit(subflags, generateFlagSchema).flags;
|
|
346
|
-
const exitCode = await generateCommand({ check, styles });
|
|
357
|
+
const { check, config } = parseArgsOrExit(subflags, generateFlagSchema).flags;
|
|
358
|
+
const exitCode = await generateCommand({ check, ...(config !== undefined && { configPath: config }), styles });
|
|
347
359
|
process.exit(exitCode);
|
|
348
360
|
}
|
|
349
361
|
if (subcommand === 'sync') {
|
|
@@ -374,11 +386,8 @@ if (command === 'overrides') {
|
|
|
374
386
|
showOverridesValidateHelp();
|
|
375
387
|
process.exit(0);
|
|
376
388
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
process.exit(1);
|
|
380
|
-
}
|
|
381
|
-
const result = await validateOverridesCommand(styles);
|
|
389
|
+
const { config } = parseArgsOrExit(subflags, configFlagSchema).flags;
|
|
390
|
+
const result = await validateOverridesCommand(styles, config);
|
|
382
391
|
if (result.exitCode === 0) {
|
|
383
392
|
console.info(result.message);
|
|
384
393
|
}
|
|
@@ -1,26 +1,68 @@
|
|
|
1
|
+
import { type RawCommit } from './enumerateReleaseWindows.js';
|
|
1
2
|
import type { GenerateChangelogOptions } from './generateChangelogs.js';
|
|
2
|
-
import type { ChangelogEntry, ReleaseConfig } from './types.js';
|
|
3
|
+
import type { ChangelogEntry, ChangelogSection, MalformedChangeRecordBlock, PolicyViolation, ReleaseConfig, ReleaseType, UndeclaredEntryType } from './types.js';
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
+
* Strips a section title down to its bare name.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* but downstream consumers that read the raw `group` value (changelog.json titles, the
|
|
10
|
-
* dev-vs-public classifier, the drift test) see the prefix and must strip it. The trailing
|
|
11
|
-
* `stripEmojiPrefix` keeps the helper backward-compatible with consumer overrides written
|
|
12
|
-
* as bare names.
|
|
7
|
+
* A default title carries a leading emoji (e.g. `"🐛 Bug fixes"`), while a consumer's
|
|
8
|
+
* `devOnlySections` or `workTypes` override may be written as a bare name. Comparing both sides
|
|
9
|
+
* through this helper lets the two forms match.
|
|
13
10
|
*/
|
|
14
11
|
export declare function stripGroupDecorations(group: string): string;
|
|
15
12
|
/**
|
|
16
|
-
*
|
|
13
|
+
* Builds structured changelog entries from the release windows of git history, naming the unreleased window `tag`.
|
|
17
14
|
*
|
|
18
|
-
*
|
|
19
|
-
* `
|
|
20
|
-
|
|
15
|
+
* Composes `readReleaseHistory` and `toChangelogEntries` for a caller that knows the tag before it reads. Performs no
|
|
16
|
+
* `changelog.json` I/O; callers persist the entries via `renderChangelogJson`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildChangelogEntries(config: ReleaseHistoryConfig, tag: string, options: GenerateChangelogOptions): {
|
|
19
|
+
entries: ChangelogEntry[];
|
|
20
|
+
diagnostics: ChangelogDiagnostics;
|
|
21
|
+
};
|
|
22
|
+
/** What reading the unreleased window reports beyond its items. */
|
|
23
|
+
export interface ChangelogDiagnostics {
|
|
24
|
+
malformedBlocks: MalformedChangeRecordBlock[];
|
|
25
|
+
/** Breaking-policy violations of the window's titles and change-record entries. */
|
|
26
|
+
policyViolations: PolicyViolation[];
|
|
27
|
+
undeclaredEntryTypes: UndeclaredEntryType[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Reads a scope's release windows once and returns the changelog items of every window, with what the unreleased
|
|
31
|
+
* window decides: its commits, its bump, and its diagnostics.
|
|
21
32
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
33
|
+
* A commit whose last `change-record` block records entries yields one item per entry; any other commit is
|
|
34
|
+
* classified by its title through `classifyChangelogCommit`. A `release:` or merge subject yields nothing either
|
|
35
|
+
* way. Only the unreleased window records diagnostics, since the released windows were reported when they were
|
|
36
|
+
* prepared.
|
|
25
37
|
*/
|
|
26
|
-
export declare function
|
|
38
|
+
export declare function readReleaseHistory(config: ReleaseHistoryConfig, options: GenerateChangelogOptions): ReleaseHistory;
|
|
39
|
+
/** The configuration that `readReleaseHistory` reads. */
|
|
40
|
+
export type ReleaseHistoryConfig = Pick<ReleaseConfig, 'breakingPolicies' | 'changelogJson' | 'versionPatterns' | 'workTypes'>;
|
|
41
|
+
/** One read of a scope's release windows. */
|
|
42
|
+
export interface ReleaseHistory {
|
|
43
|
+
/** The newest matching tag that HEAD reaches; undefined when none does. */
|
|
44
|
+
previousTag: string | undefined;
|
|
45
|
+
/** Entries of the released windows, newest first; a window that yields no item contributes none. */
|
|
46
|
+
releasedEntries: ChangelogEntry[];
|
|
47
|
+
unreleased: UnreleasedWindowReading;
|
|
48
|
+
}
|
|
49
|
+
/** The unreleased window: its changelog sections and commits, and the bump and diagnostics that they determine. */
|
|
50
|
+
export interface UnreleasedWindowReading {
|
|
51
|
+
/** The highest bump that the window's items call for; undefined when the window yields no item. */
|
|
52
|
+
bump: ReleaseType | undefined;
|
|
53
|
+
/** The window's commits, newest first, without release commits. */
|
|
54
|
+
commits: RawCommit[];
|
|
55
|
+
/** The date of the window's entry, from the time of the read. */
|
|
56
|
+
date: string;
|
|
57
|
+
diagnostics: ChangelogDiagnostics;
|
|
58
|
+
/** The number of commits that yield at least one item. */
|
|
59
|
+
parsedCommitCount: number;
|
|
60
|
+
sections: ChangelogSection[];
|
|
61
|
+
/**
|
|
62
|
+
* Commits that yield no item and that neither an exclusion nor a diagnostic accounts for, newest first; undefined
|
|
63
|
+
* when none.
|
|
64
|
+
*/
|
|
65
|
+
unparseableCommits: RawCommit[] | undefined;
|
|
66
|
+
}
|
|
67
|
+
/** Labels the unreleased window of `history` with `tag` and returns the entries of every window, newest first. */
|
|
68
|
+
export declare function toChangelogEntries(history: ReleaseHistory, tag: string): ChangelogEntry[];
|
|
@@ -1,145 +1,228 @@
|
|
|
1
1
|
import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
|
|
2
2
|
import { extractVersion } from "./changelogJsonUtils.js";
|
|
3
|
-
import {
|
|
3
|
+
import { classifyChangelogCommit, isNonChangeSubject, isReleaseSubject } from "./classifyChangelogCommit.js";
|
|
4
|
+
import { DEFAULT_BREAKING_POLICIES, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
|
|
5
|
+
import { determineBumpType } from "./determineBumpType.js";
|
|
6
|
+
import { enumerateReleaseWindows } from "./enumerateReleaseWindows.js";
|
|
4
7
|
import { extractMigration } from "./extractMigration.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { runGitCliff } from "./runGitCliff.js";
|
|
8
|
+
import { parseChangeRecordBlock, stripChangeRecordBlocks } from "./parseChangeRecordBlock.js";
|
|
9
|
+
import { COMMIT_PREPROCESSOR_PATTERNS, evaluateBreakingPolicy, PIPE_SCOPE_SOURCE, resolveType, } from "./parseCommitMessage.js";
|
|
8
10
|
import { stripEmojiPrefix } from "./stripEmojiPrefix.js";
|
|
9
|
-
|
|
10
|
-
const HTML_COMMENT_PREFIX_PATTERN = /^<!--[^>]*-->/;
|
|
11
|
+
const UNRELEASED_TAG = 'unreleased';
|
|
11
12
|
const CANONICAL_SECTION_ORDER = new Map(Object.values(DEFAULT_WORK_TYPES).map((config, index) => [stripGroupDecorations(config.header), index]));
|
|
12
13
|
function canonicalSectionPriority(title) {
|
|
13
14
|
const index = CANONICAL_SECTION_ORDER.get(stripGroupDecorations(title));
|
|
14
15
|
return index ?? Infinity;
|
|
15
16
|
}
|
|
16
17
|
export function stripGroupDecorations(group) {
|
|
17
|
-
return stripEmojiPrefix(group
|
|
18
|
+
return stripEmojiPrefix(group);
|
|
18
19
|
}
|
|
19
20
|
export function buildChangelogEntries(config, tag, options) {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
const includePaths = options?.includePaths ?? [];
|
|
26
|
-
for (const includePath of includePaths) {
|
|
27
|
-
cliffArgs.push('--include-path', includePath);
|
|
28
|
-
}
|
|
21
|
+
const history = readReleaseHistory(config, options);
|
|
22
|
+
return { entries: toChangelogEntries(history, tag), diagnostics: history.unreleased.diagnostics };
|
|
23
|
+
}
|
|
24
|
+
export function readReleaseHistory(config, options) {
|
|
29
25
|
try {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
const windows = enumerateReleaseWindows({
|
|
27
|
+
...(options.paths !== undefined && { paths: options.paths }),
|
|
28
|
+
tagPrefixes: options.tagPrefixes,
|
|
29
|
+
unreleasedTag: UNRELEASED_TAG,
|
|
30
|
+
});
|
|
31
|
+
const context = {
|
|
32
|
+
breakingPolicies: config.breakingPolicies ?? DEFAULT_BREAKING_POLICIES,
|
|
33
|
+
devOnlySections: new Set(config.changelogJson.devOnlySections.map(stripGroupDecorations)),
|
|
34
|
+
versionPatterns: config.versionPatterns ?? DEFAULT_VERSION_PATTERNS,
|
|
35
|
+
workTypes: config.workTypes ?? DEFAULT_WORK_TYPES,
|
|
36
|
+
};
|
|
37
|
+
return transformReleases(windows, context);
|
|
36
38
|
}
|
|
37
39
|
catch (error) {
|
|
38
|
-
throw chainError(`Failed to
|
|
40
|
+
throw chainError(`Failed to read the release history for ${options.tagPrefixes.join(', ')}`, error);
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
|
-
function
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
44
|
-
|
|
43
|
+
export function toChangelogEntries(history, tag) {
|
|
44
|
+
const { unreleased } = history;
|
|
45
|
+
if (unreleased.sections.length === 0) {
|
|
46
|
+
return [...history.releasedEntries];
|
|
45
47
|
}
|
|
46
|
-
return
|
|
48
|
+
return [
|
|
49
|
+
{ version: extractVersion(tag), date: unreleased.date, sections: unreleased.sections },
|
|
50
|
+
...history.releasedEntries,
|
|
51
|
+
];
|
|
47
52
|
}
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
function transformReleases(windows, context) {
|
|
54
|
+
const [unreleasedWindow, baselineWindow] = windows;
|
|
55
|
+
const diagnostics = { malformedBlocks: [], policyViolations: [], undeclaredEntryTypes: [] };
|
|
56
|
+
const signals = [];
|
|
57
|
+
const unparseable = [];
|
|
58
|
+
let parsedCommitCount = 0;
|
|
59
|
+
const sectionMap = new Map();
|
|
60
|
+
const unreleasedCommits = unreleasedWindow?.commits ?? [];
|
|
61
|
+
for (const commit of unreleasedCommits) {
|
|
62
|
+
const reading = readCommit(commit, context, diagnostics);
|
|
63
|
+
for (const derived of reading.items) {
|
|
64
|
+
appendItem(sectionMap, derived.header, derived.item);
|
|
65
|
+
signals.push(derived.signal);
|
|
66
|
+
}
|
|
67
|
+
if (reading.items.length > 0) {
|
|
68
|
+
parsedCommitCount += 1;
|
|
69
|
+
}
|
|
70
|
+
else if (reading.isUnparseable) {
|
|
71
|
+
unparseable.push(commit);
|
|
72
|
+
}
|
|
51
73
|
}
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
74
|
+
const releasedEntries = [];
|
|
75
|
+
for (const release of windows.slice(1)) {
|
|
76
|
+
const releaseSections = new Map();
|
|
77
|
+
for (const commit of release.commits) {
|
|
78
|
+
for (const derived of readCommit(commit, context, undefined).items) {
|
|
79
|
+
appendItem(releaseSections, derived.header, derived.item);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const sections = buildSections(releaseSections, context.devOnlySections);
|
|
83
|
+
if (sections.length > 0) {
|
|
84
|
+
releasedEntries.push({ version: extractVersion(release.version), date: formatDate(release.timestamp), sections });
|
|
85
|
+
}
|
|
55
86
|
}
|
|
56
|
-
|
|
57
|
-
|
|
87
|
+
return {
|
|
88
|
+
previousTag: baselineWindow?.version,
|
|
89
|
+
releasedEntries,
|
|
90
|
+
unreleased: {
|
|
91
|
+
bump: determineBumpType(signals, context.workTypes, context.versionPatterns),
|
|
92
|
+
commits: unreleasedCommits.filter((commit) => !isReleaseSubject(commit.subject)).toReversed(),
|
|
93
|
+
date: formatDate(unreleasedWindow?.timestamp ?? Math.floor(Date.now() / 1_000)),
|
|
94
|
+
diagnostics,
|
|
95
|
+
parsedCommitCount,
|
|
96
|
+
sections: buildSections(sectionMap, context.devOnlySections),
|
|
97
|
+
unparseableCommits: unparseable.length > 0 ? unparseable.toReversed() : undefined,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function readCommit(commit, context, diagnostics) {
|
|
102
|
+
if (isNonChangeSubject(commit.subject)) {
|
|
103
|
+
return { items: [], isUnparseable: false };
|
|
58
104
|
}
|
|
59
|
-
|
|
60
|
-
|
|
105
|
+
const reading = parseChangeRecordBlock(commit.message);
|
|
106
|
+
if (reading.kind === 'malformed') {
|
|
107
|
+
diagnostics?.malformedBlocks.push({
|
|
108
|
+
commitHash: commit.hash,
|
|
109
|
+
commitSubject: commit.subject,
|
|
110
|
+
reason: reading.reason,
|
|
111
|
+
});
|
|
61
112
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (!isRecord(value)) {
|
|
66
|
-
return { message: '' };
|
|
113
|
+
if (reading.kind === 'read' && reading.entries.length > 0) {
|
|
114
|
+
const items = reading.entries.flatMap((entry, entryIndex) => buildEntryItem(commit, entry, entryIndex + 1, reading.prNumber, context, diagnostics) ?? []);
|
|
115
|
+
return { items, isUnparseable: false };
|
|
67
116
|
}
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
117
|
+
const classification = classifyChangelogCommit(commit, context.workTypes, {
|
|
118
|
+
breakingPolicies: context.breakingPolicies,
|
|
119
|
+
...(diagnostics !== undefined && {
|
|
120
|
+
onPolicyViolation: (violating, type, surface) => {
|
|
121
|
+
diagnostics.policyViolations.push({
|
|
122
|
+
commitHash: violating.hash,
|
|
123
|
+
commitSubject: violating.subject,
|
|
124
|
+
type,
|
|
125
|
+
surface,
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
130
|
+
if (classification.kind !== 'header') {
|
|
131
|
+
return { items: [], isUnparseable: classification.kind === 'unparseable' && reading.kind !== 'malformed' };
|
|
73
132
|
}
|
|
74
|
-
|
|
75
|
-
|
|
133
|
+
const item = buildTitleItem(commit, classification.breaking);
|
|
134
|
+
return {
|
|
135
|
+
items: [
|
|
136
|
+
{ header: classification.header, item, signal: { type: classification.type, breaking: item.breaking === true } },
|
|
137
|
+
],
|
|
138
|
+
isUnparseable: false,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function buildSections(sectionMap, devOnlySections) {
|
|
142
|
+
const sections = [];
|
|
143
|
+
for (const [title, items] of sectionMap) {
|
|
144
|
+
sections.push({
|
|
145
|
+
title,
|
|
146
|
+
audience: devOnlySections.has(stripGroupDecorations(title)) ? 'dev' : 'all',
|
|
147
|
+
items,
|
|
148
|
+
});
|
|
76
149
|
}
|
|
77
|
-
return
|
|
150
|
+
return sections.toSorted((a, b) => canonicalSectionPriority(a.title) - canonicalSectionPriority(b.title));
|
|
78
151
|
}
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const date = release.timestamp !== undefined ? new Date(release.timestamp * 1_000).toISOString().slice(0, 10) : 'unreleased';
|
|
88
|
-
const sectionMap = new Map();
|
|
89
|
-
const commits = release.commits ?? [];
|
|
90
|
-
for (const commit of commits) {
|
|
91
|
-
const group = stripCommentPrefix(commit.group ?? 'Other');
|
|
92
|
-
const description = extractDescription(commit.message);
|
|
93
|
-
const body = extractBody(commit.message);
|
|
94
|
-
const breaking = isBreakingUnderPolicy(commit, workTypes, breakingPolicies);
|
|
95
|
-
let items = sectionMap.get(group);
|
|
96
|
-
if (items === undefined) {
|
|
97
|
-
items = [];
|
|
98
|
-
sectionMap.set(group, items);
|
|
99
|
-
}
|
|
100
|
-
const item = { description };
|
|
101
|
-
if (body !== undefined) {
|
|
102
|
-
item.body = body;
|
|
103
|
-
}
|
|
104
|
-
if (breaking) {
|
|
105
|
-
item.breaking = true;
|
|
106
|
-
}
|
|
107
|
-
const migration = extractMigration(body);
|
|
108
|
-
if (migration !== undefined) {
|
|
109
|
-
item.migration = migration;
|
|
110
|
-
}
|
|
111
|
-
if (commit.id !== undefined && commit.id !== '') {
|
|
112
|
-
item.hash = commit.id;
|
|
113
|
-
}
|
|
114
|
-
items.push(item);
|
|
115
|
-
}
|
|
116
|
-
const sections = [];
|
|
117
|
-
for (const [title, items] of sectionMap) {
|
|
118
|
-
if (items.length === 0) {
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
sections.push({
|
|
122
|
-
title,
|
|
123
|
-
audience: devOnlyNormalised.has(stripGroupDecorations(title)) ? 'dev' : 'all',
|
|
124
|
-
items,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
sections.sort((a, b) => canonicalSectionPriority(a.title) - canonicalSectionPriority(b.title));
|
|
128
|
-
if (sections.length > 0) {
|
|
129
|
-
entries.push({ version, date, sections });
|
|
130
|
-
}
|
|
152
|
+
function formatDate(timestamp) {
|
|
153
|
+
return new Date(timestamp * 1_000).toISOString().slice(0, 10);
|
|
154
|
+
}
|
|
155
|
+
function appendItem(sectionMap, header, item) {
|
|
156
|
+
let items = sectionMap.get(header);
|
|
157
|
+
if (items === undefined) {
|
|
158
|
+
items = [];
|
|
159
|
+
sectionMap.set(header, items);
|
|
131
160
|
}
|
|
132
|
-
|
|
161
|
+
items.push(item);
|
|
133
162
|
}
|
|
134
|
-
function
|
|
135
|
-
|
|
163
|
+
function buildEntryItem(commit, entry, position, prNumber, context, diagnostics) {
|
|
164
|
+
const { breakingPolicies, workTypes } = context;
|
|
165
|
+
const type = resolveType(entry.type, workTypes);
|
|
166
|
+
if (type === undefined) {
|
|
167
|
+
diagnostics?.undeclaredEntryTypes.push({
|
|
168
|
+
commitHash: commit.hash,
|
|
169
|
+
commitSubject: commit.subject,
|
|
170
|
+
entryPosition: position,
|
|
171
|
+
type: entry.type,
|
|
172
|
+
});
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
const config = workTypes[type];
|
|
176
|
+
if (config === undefined || config.excludedFromChangelog === true) {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
const breaking = evaluateBreakingPolicy({
|
|
180
|
+
commit: { message: commit.message, subject: commit.subject, hash: commit.hash },
|
|
181
|
+
resolvedType: type,
|
|
182
|
+
hasPrefixBreaking: entry.breaking,
|
|
183
|
+
hasFooterBreaking: false,
|
|
184
|
+
policy: breakingPolicies[type] ?? 'optional',
|
|
185
|
+
prefixSurface: 'entry',
|
|
186
|
+
onPolicyViolation: diagnostics === undefined
|
|
187
|
+
? undefined
|
|
188
|
+
: (violating, violatedType, surface) => {
|
|
189
|
+
diagnostics.policyViolations.push({
|
|
190
|
+
commitHash: violating.hash,
|
|
191
|
+
commitSubject: violating.subject,
|
|
192
|
+
type: violatedType,
|
|
193
|
+
surface,
|
|
194
|
+
entryPosition: position,
|
|
195
|
+
});
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
const item = {
|
|
199
|
+
description: prNumber === undefined ? entry.text : `${entry.text} (#${prNumber})`,
|
|
200
|
+
};
|
|
201
|
+
if (breaking) {
|
|
202
|
+
item.breaking = true;
|
|
203
|
+
}
|
|
204
|
+
if (entry.migration !== undefined) {
|
|
205
|
+
item.migration = entry.migration;
|
|
206
|
+
}
|
|
207
|
+
item.hash = commit.hash;
|
|
208
|
+
item.entry = position;
|
|
209
|
+
return { header: config.header, item, signal: { type, breaking } };
|
|
136
210
|
}
|
|
137
|
-
function
|
|
138
|
-
|
|
139
|
-
|
|
211
|
+
function buildTitleItem(commit, isParsedBreaking) {
|
|
212
|
+
const body = extractBody(stripChangeRecordBlocks(commit.message));
|
|
213
|
+
const item = { description: extractDescription(commit.message) };
|
|
214
|
+
if (body !== undefined) {
|
|
215
|
+
item.body = body;
|
|
216
|
+
}
|
|
217
|
+
if (isParsedBreaking && subjectHasBreakingMarker(commit.message)) {
|
|
218
|
+
item.breaking = true;
|
|
219
|
+
}
|
|
220
|
+
const migration = extractMigration(body);
|
|
221
|
+
if (migration !== undefined) {
|
|
222
|
+
item.migration = migration;
|
|
140
223
|
}
|
|
141
|
-
|
|
142
|
-
return
|
|
224
|
+
item.hash = commit.hash;
|
|
225
|
+
return item;
|
|
143
226
|
}
|
|
144
227
|
const SUBJECT_BREAKING_MARKER_PATTERN = new RegExp(String.raw `^(?:${PIPE_SCOPE_SOURCE}\|)?\w+(?:\([^)]+\))?!:`);
|
|
145
228
|
function subjectHasBreakingMarker(message) {
|
|
@@ -158,6 +241,7 @@ function extractDescription(message) {
|
|
|
158
241
|
return firstLine;
|
|
159
242
|
}
|
|
160
243
|
const TRAILER_PATTERNS = [
|
|
244
|
+
/^Change:/i,
|
|
161
245
|
/^Signed-off-by:/i,
|
|
162
246
|
/^Co-authored-by:/i,
|
|
163
247
|
/^(Closes|Fixes|Resolves)\s+#\d+\s*$/i,
|
|
@@ -11,7 +11,7 @@ export function buildReleaseSummary(result) {
|
|
|
11
11
|
}
|
|
12
12
|
const lines = [workspace.tag];
|
|
13
13
|
for (const commit of commits) {
|
|
14
|
-
lines.push(`- ${stripScope(commit.
|
|
14
|
+
lines.push(`- ${stripScope(commit.subject)}`);
|
|
15
15
|
}
|
|
16
16
|
sections.push(lines.join('\n'));
|
|
17
17
|
}
|
|
@@ -19,7 +19,7 @@ export function buildReleaseSummary(result) {
|
|
|
19
19
|
if (project !== undefined && project.status === 'released' && project.commits.length > 0) {
|
|
20
20
|
const lines = [project.tag];
|
|
21
21
|
for (const commit of project.commits) {
|
|
22
|
-
lines.push(`- ${stripScope(commit.
|
|
22
|
+
lines.push(`- ${stripScope(commit.subject)}`);
|
|
23
23
|
}
|
|
24
24
|
sections.push(lines.join('\n'));
|
|
25
25
|
}
|