@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,30 +1,8 @@
|
|
|
1
|
-
import { determineBumpType } from "./determineBumpType.js";
|
|
2
|
-
import { parseCommitMessage, } from "./parseCommitMessage.js";
|
|
3
1
|
export function decideRelease(args) {
|
|
4
|
-
const {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
};
|
|
9
|
-
const parsedCommits = [];
|
|
10
|
-
const unparseable = [];
|
|
11
|
-
for (const commit of commits) {
|
|
12
|
-
const parsed = parseCommitMessage(commit.message, commit.hash, workTypes, scopeAliases, parseOptions);
|
|
13
|
-
if (parsed === undefined) {
|
|
14
|
-
unparseable.push(commit);
|
|
15
|
-
}
|
|
16
|
-
else {
|
|
17
|
-
parsedCommits.push(parsed);
|
|
18
|
-
}
|
|
2
|
+
const { naturalBump, commitCount, force = false, bumpOverride, skipReasons } = args;
|
|
3
|
+
if (naturalBump === undefined && !force) {
|
|
4
|
+
const skipReason = commitCount === 0 ? skipReasons.noCommits : skipReasons.noBumpWorthy;
|
|
5
|
+
return { outcome: 'skip', skipReason };
|
|
19
6
|
}
|
|
20
|
-
|
|
21
|
-
const unparseableCommits = unparseable.length > 0 ? unparseable : undefined;
|
|
22
|
-
const naturalBump = determineBumpType(parsedCommits, workTypes, versionPatterns);
|
|
23
|
-
const shouldRelease = naturalBump !== undefined || force;
|
|
24
|
-
if (!shouldRelease) {
|
|
25
|
-
const skipReason = commits.length === 0 ? skipReasons.noCommits : skipReasons.noBumpWorthy;
|
|
26
|
-
return { outcome: 'skip', skipReason, parsedCommitCount, unparseableCommits };
|
|
27
|
-
}
|
|
28
|
-
const releaseType = bumpOverride ?? naturalBump ?? 'patch';
|
|
29
|
-
return { outcome: 'release', releaseType, parsedCommitCount, unparseableCommits };
|
|
7
|
+
return { outcome: 'release', releaseType: bumpOverride ?? naturalBump ?? 'patch' };
|
|
30
8
|
}
|
package/dist/esm/defaults.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { WORK_TYPES_DATA } from './workTypesData.js';
|
|
|
6
6
|
* Compose the rendered section heading for a work-type entry as `${emoji} ${label}`.
|
|
7
7
|
*
|
|
8
8
|
* Single-sourced here so callers (DEFAULT_WORK_TYPES derivation, devOnlySections
|
|
9
|
-
* derivation
|
|
9
|
+
* derivation) cannot disagree on the composition rule.
|
|
10
10
|
*/
|
|
11
11
|
export declare function composeHeader(entry: {
|
|
12
12
|
emoji: string;
|
package/dist/esm/defaults.js
CHANGED
|
@@ -13,6 +13,9 @@ function deriveDefaultWorkTypes() {
|
|
|
13
13
|
if (entry.aliases.length > 0) {
|
|
14
14
|
config.aliases = [...entry.aliases];
|
|
15
15
|
}
|
|
16
|
+
if (entry.excludedFromChangelog === true) {
|
|
17
|
+
config.excludedFromChangelog = true;
|
|
18
|
+
}
|
|
16
19
|
result[entry.key] = config;
|
|
17
20
|
}
|
|
18
21
|
return result;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ReleaseType, VersionPatterns, WorkTypeConfig } from './types.js';
|
|
2
|
+
/** The part of a changelog item or parsed commit that decides its bump: its resolved work type and breaking flag. */
|
|
3
|
+
export interface BumpSignal {
|
|
4
|
+
type: string;
|
|
5
|
+
breaking: boolean;
|
|
6
|
+
}
|
|
2
7
|
/**
|
|
3
|
-
* Determines the overall bump type from a set of
|
|
8
|
+
* Determines the overall bump type from a set of changes, returning undefined when none has a known work type.
|
|
4
9
|
*
|
|
5
|
-
* Uses `versionPatterns` to decide which
|
|
6
|
-
* The `'!'` sentinel in `versionPatterns.major` means "any breaking
|
|
7
|
-
* Any recognized
|
|
8
|
-
*
|
|
9
|
-
* @param commits - The parsed commits to analyze.
|
|
10
|
-
* @param workTypes - The work type configurations to verify recognized types.
|
|
11
|
-
* @param versionPatterns - Rules for determining major and minor bumps.
|
|
12
|
-
* @returns The determined release type, or undefined if no commits match a known work type.
|
|
10
|
+
* Uses `versionPatterns` to decide which types trigger major/minor bumps.
|
|
11
|
+
* The `'!'` sentinel in `versionPatterns.major` means "any breaking change triggers major".
|
|
12
|
+
* Any recognized type not listed in major or minor patterns defaults to patch.
|
|
13
13
|
*/
|
|
14
|
-
export declare function determineBumpType(
|
|
14
|
+
export declare function determineBumpType(changes: readonly BumpSignal[], workTypes: Record<string, WorkTypeConfig>, versionPatterns: VersionPatterns): ReleaseType | undefined;
|
|
@@ -3,23 +3,22 @@ const RELEASE_PRIORITY = {
|
|
|
3
3
|
minor: 2,
|
|
4
4
|
patch: 1,
|
|
5
5
|
};
|
|
6
|
-
export function determineBumpType(
|
|
6
|
+
export function determineBumpType(changes, workTypes, versionPatterns) {
|
|
7
7
|
const knownTypes = new Set(Object.keys(workTypes));
|
|
8
8
|
let highestPriority = 0;
|
|
9
9
|
let result;
|
|
10
|
-
for (const
|
|
11
|
-
if (
|
|
10
|
+
for (const change of changes) {
|
|
11
|
+
if (change.breaking && versionPatterns.major.includes('!')) {
|
|
12
12
|
return 'major';
|
|
13
13
|
}
|
|
14
|
-
|
|
15
|
-
if (!knownTypes.has(commitType)) {
|
|
14
|
+
if (!knownTypes.has(change.type)) {
|
|
16
15
|
continue;
|
|
17
16
|
}
|
|
18
17
|
let bump;
|
|
19
|
-
if (versionPatterns.major.includes(
|
|
18
|
+
if (versionPatterns.major.includes(change.type)) {
|
|
20
19
|
bump = 'major';
|
|
21
20
|
}
|
|
22
|
-
else if (versionPatterns.minor.includes(
|
|
21
|
+
else if (versionPatterns.minor.includes(change.type)) {
|
|
23
22
|
bump = 'minor';
|
|
24
23
|
}
|
|
25
24
|
else {
|
|
@@ -1,13 +1,58 @@
|
|
|
1
|
+
import { type EmptyWorkspaceCause } from '@williamthorsen/nmr-core/workspace';
|
|
2
|
+
export type { EmptyWorkspaceCause } from '@williamthorsen/nmr-core/workspace';
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
4
|
+
* What release-kit's workspace discovery produced: the repo releases as a single package, it resolves to a
|
|
5
|
+
* set of package directories, or it declares a workspace that resolves to none and the cause says which
|
|
6
|
+
* condition emptied it.
|
|
3
7
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
8
|
+
* Held apart from nmr-core's `WorkspaceResolution`, whose `packageDirs` are absolute, because these are
|
|
9
|
+
* relative to the monorepo root. The two carry incompatible path contracts, and a distinct `kind` for the
|
|
10
|
+
* single-package case keeps either from standing in for the other.
|
|
11
|
+
*/
|
|
12
|
+
export type WorkspaceDiscovery = {
|
|
13
|
+
kind: 'empty';
|
|
14
|
+
cause: FailingWorkspaceCause;
|
|
15
|
+
patterns: string[];
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'packages';
|
|
18
|
+
packageDirs: string[];
|
|
19
|
+
patterns: string[];
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'single-package';
|
|
22
|
+
};
|
|
23
|
+
/** A workspace that resolved to no package directory, which every release-kit command treats as a failure. */
|
|
24
|
+
export type EmptyWorkspace = Extract<WorkspaceDiscovery, {
|
|
25
|
+
kind: 'empty';
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* The empty-workspace causes that reach a command as a failure.
|
|
29
|
+
*
|
|
30
|
+
* `no-packages-list` is absent because a manifest declaring nothing releases as a single package, so no
|
|
31
|
+
* command ever composes a message for it.
|
|
32
|
+
*/
|
|
33
|
+
export type FailingWorkspaceCause = Exclude<EmptyWorkspaceCause, 'no-packages-list'>;
|
|
34
|
+
/**
|
|
35
|
+
* Returns the sentence naming which of the five conditions left a workspace holding no package directory, and
|
|
36
|
+
* the remedy for that one. Each quotes the `packages` list the manifest declares, apart from the two that
|
|
37
|
+
* reach the matcher with no list to quote.
|
|
38
|
+
*
|
|
39
|
+
* The `package.json` requirement is stated under `no-package` because it is a divergence from pnpm, which
|
|
40
|
+
* recognizes two further manifests, and the reader of a workspace that pnpm resolves has no way to infer it.
|
|
41
|
+
*/
|
|
42
|
+
export declare function describeEmptyWorkspace(workspace: EmptyWorkspace): string;
|
|
43
|
+
/**
|
|
44
|
+
* Reads `pnpm-workspace.yaml` at `monorepoRoot` and resolves its `packages` patterns, applying pnpm's
|
|
45
|
+
* semantics — `!`-prefixed exclusions included.
|
|
6
46
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
47
|
+
* A directory holding no manifest, and one whose manifest declares no `packages` list, both release as a
|
|
48
|
+
* single package: pnpm resolves each to the root package alone, and a workspace file kept for `catalog:`,
|
|
49
|
+
* `overrides:`, or `onlyBuiltDependencies:` is the second of them. Every other empty resolution is a failure,
|
|
50
|
+
* whether the reader declared patterns that matched nothing or a `packages` value the resolver could not read
|
|
51
|
+
* as patterns at all.
|
|
10
52
|
*
|
|
11
|
-
*
|
|
53
|
+
* `packageDirs` come back relative to `monorepoRoot` as POSIX paths, because that is what every consumer
|
|
54
|
+
* needs: `WorkspaceConfig.paths` feeds `git log -- <paths>`, and `packageFiles` and `changelogPaths` are read
|
|
55
|
+
* from disk relative to the working directory. The workspace root itself relativizes to `.`, the path
|
|
56
|
+
* `deriveWorkspaceConfig` reads a root package's manifest through.
|
|
12
57
|
*/
|
|
13
|
-
export declare function discoverWorkspaces():
|
|
58
|
+
export declare function discoverWorkspaces(monorepoRoot?: string): WorkspaceDiscovery;
|
|
@@ -1,42 +1,62 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { resolveWorkspace } from '@williamthorsen/nmr-core/workspace';
|
|
3
|
+
export function describeEmptyWorkspace(workspace) {
|
|
4
|
+
const declaredClause = describeDeclaredPatterns(workspace.patterns);
|
|
5
|
+
switch (workspace.cause) {
|
|
6
|
+
case 'all-excluded':
|
|
7
|
+
return (`pnpm-workspace.yaml ${declaredClause}, whose \`!\` entries exclude every directory matched by the ` +
|
|
8
|
+
'positive patterns. Drop or narrow the exclusion.');
|
|
9
|
+
case 'no-package':
|
|
10
|
+
return (`pnpm-workspace.yaml ${declaredClause}, and the matcher found no directory holding a \`package.json\`. ` +
|
|
11
|
+
'Add one to the directory that should be a package, or declare a pattern reaching a directory that ' +
|
|
12
|
+
'holds one.');
|
|
13
|
+
case 'no-pattern':
|
|
14
|
+
return (`pnpm-workspace.yaml ${declaredClause}, so no pattern reaches the matcher. Declare a positive pattern ` +
|
|
15
|
+
'such as `packages/*`, and quote any `!` entry, which YAML reads as a tag rather than a string where ' +
|
|
16
|
+
'it stands bare.');
|
|
17
|
+
case 'unreadable-manifest':
|
|
18
|
+
return ('pnpm-workspace.yaml holds no valid YAML, so nothing it declares reaches the matcher. Repair the ' +
|
|
19
|
+
'syntax error and run the command again. An unterminated quoted string and a mis-indented entry are ' +
|
|
20
|
+
'the usual ones.');
|
|
21
|
+
case 'unreadable-packages':
|
|
22
|
+
return ('pnpm-workspace.yaml declares a `packages` value that is not a list of strings, so nothing it ' +
|
|
23
|
+
'declares reaches the matcher. Make it a list whose every entry is a quoted pattern; a bare value ' +
|
|
24
|
+
'and an entry YAML read as a number or a map are the usual ones.');
|
|
25
|
+
default: {
|
|
26
|
+
const unhandledCause = workspace.cause;
|
|
27
|
+
throw new Error(`Unhandled empty-workspace cause: ${String(unhandledCause)}`);
|
|
28
|
+
}
|
|
23
29
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
}
|
|
31
|
+
export function discoverWorkspaces(monorepoRoot = process.cwd()) {
|
|
32
|
+
const resolution = resolveWorkspace(monorepoRoot);
|
|
33
|
+
if (resolution.kind === 'not-a-workspace') {
|
|
34
|
+
return { kind: 'single-package' };
|
|
27
35
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return
|
|
36
|
+
if (resolution.kind === 'empty') {
|
|
37
|
+
const { cause, patterns } = resolution;
|
|
38
|
+
return cause === 'no-packages-list' ? { kind: 'single-package' } : { cause, kind: 'empty', patterns };
|
|
31
39
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
return {
|
|
41
|
+
...resolution,
|
|
42
|
+
packageDirs: resolution.packageDirs.map((packageDir) => relativizeToPosix(monorepoRoot, packageDir)),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function describeDeclaredPatterns(patterns) {
|
|
46
|
+
const quotablePatterns = patterns.filter((pattern) => pattern.trim() !== '');
|
|
47
|
+
const emptiedCount = patterns.length - quotablePatterns.length;
|
|
48
|
+
if (emptiedCount === 0) {
|
|
49
|
+
return `declares ${renderQuotedList(patterns)}`;
|
|
40
50
|
}
|
|
41
|
-
|
|
51
|
+
const emptiedClause = `${String(emptiedCount)} ${emptiedCount === 1 ? 'entry' : 'entries'} that YAML left empty`;
|
|
52
|
+
return quotablePatterns.length === 0
|
|
53
|
+
? `declares ${emptiedClause}`
|
|
54
|
+
: `declares ${renderQuotedList(quotablePatterns)}, beside ${emptiedClause}`;
|
|
55
|
+
}
|
|
56
|
+
function relativizeToPosix(monorepoRoot, packageDir) {
|
|
57
|
+
const relativeDir = path.relative(monorepoRoot, packageDir);
|
|
58
|
+
return relativeDir === '' ? '.' : relativeDir.split(path.sep).join('/');
|
|
59
|
+
}
|
|
60
|
+
function renderQuotedList(patterns) {
|
|
61
|
+
return patterns.map((pattern) => `\`${pattern}\``).join(', ');
|
|
42
62
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** A commit as git records it, before any parsing or filtering. */
|
|
2
|
+
export interface RawCommit {
|
|
3
|
+
/** The full 40-character hash, which `changelogOverrides` keys on. */
|
|
4
|
+
hash: string;
|
|
5
|
+
/** The first line, with a wrapped subject folded onto one line by git. */
|
|
6
|
+
subject: string;
|
|
7
|
+
/** The message after its first blank line, empty when the commit carries no body. */
|
|
8
|
+
body: string;
|
|
9
|
+
/** The raw commit message, subject and body together. */
|
|
10
|
+
message: string;
|
|
11
|
+
}
|
|
12
|
+
/** The commits one release contains, with the version and date under which they shipped. */
|
|
13
|
+
export interface ReleaseWindow {
|
|
14
|
+
/** The tag name for a released window, the caller's unreleased-tag name for the newest one. */
|
|
15
|
+
version: string;
|
|
16
|
+
/** Unix seconds: the tag's creation date for a released window, the clock for the unreleased one. */
|
|
17
|
+
timestamp: number;
|
|
18
|
+
/** Oldest first. */
|
|
19
|
+
commits: RawCommit[];
|
|
20
|
+
}
|
|
21
|
+
/** Inputs for {@link enumerateReleaseWindows}. */
|
|
22
|
+
export interface EnumerateReleaseWindowsOptions {
|
|
23
|
+
/** Git pathspecs restricting the enumeration to commits that touch them. */
|
|
24
|
+
paths?: readonly string[];
|
|
25
|
+
/** Tag prefixes to match as a union; a matching tag name continues with a digit. */
|
|
26
|
+
tagPrefixes: readonly string[];
|
|
27
|
+
/** The version that the unreleased window reports, such as the tag that the release being prepared will write. */
|
|
28
|
+
unreleasedTag: string;
|
|
29
|
+
/** Returns the current time in epoch milliseconds; dates the unreleased window. */
|
|
30
|
+
now?: () => number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Splits the history reachable from HEAD into release windows, newest first.
|
|
34
|
+
*
|
|
35
|
+
* The newest window is always the unreleased one, empty when HEAD sits on a tag. Released
|
|
36
|
+
* windows follow in descending order, one per reachable tag matching a prefix. A tag that
|
|
37
|
+
* HEAD cannot reach is dropped.
|
|
38
|
+
*
|
|
39
|
+
* A commit belongs to the oldest matching tag that contains it, and to the unreleased window
|
|
40
|
+
* when no matching tag does. Containment is read from the ancestry graph rather than from a
|
|
41
|
+
* position in a linear walk, because a branch commit authored before a tag and merged after it
|
|
42
|
+
* is not in that tag's release.
|
|
43
|
+
*
|
|
44
|
+
* Three git invocations serve any number of tags: `git rev-list --topo-order --parents` for the
|
|
45
|
+
* ancestry graph, `git for-each-ref` for the tag names with their creation dates and target
|
|
46
|
+
* commits, and one path-filtered `git log` for the commits themselves.
|
|
47
|
+
*
|
|
48
|
+
* @throws If `tagPrefixes` is empty, or if any git invocation fails.
|
|
49
|
+
*/
|
|
50
|
+
export declare function enumerateReleaseWindows(options: EnumerateReleaseWindowsOptions): ReleaseWindow[];
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { GIT_OUTPUT_LIMIT } from '@williamthorsen/nmr-core';
|
|
3
|
+
import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
|
|
4
|
+
const FIELD_SEPARATOR = '\u{1F}';
|
|
5
|
+
const RECORD_SEPARATOR = '\u{1E}';
|
|
6
|
+
export function enumerateReleaseWindows(options) {
|
|
7
|
+
const { now = Date.now, paths, tagPrefixes, unreleasedTag } = options;
|
|
8
|
+
if (tagPrefixes.length === 0) {
|
|
9
|
+
throw new Error('enumerateReleaseWindows: tagPrefixes must contain at least one entry');
|
|
10
|
+
}
|
|
11
|
+
const unreleasedWindow = {
|
|
12
|
+
version: unreleasedTag,
|
|
13
|
+
timestamp: Math.floor(now() / 1_000),
|
|
14
|
+
commits: [],
|
|
15
|
+
};
|
|
16
|
+
const ancestry = readAncestry();
|
|
17
|
+
if (ancestry.parentsByHash.size === 0) {
|
|
18
|
+
return [unreleasedWindow];
|
|
19
|
+
}
|
|
20
|
+
const boundaries = readTagBoundaries(tagPrefixes, ancestry.positionByHash);
|
|
21
|
+
const releasedWindows = boundaries.map((boundary) => ({
|
|
22
|
+
version: boundary.tag,
|
|
23
|
+
timestamp: boundary.timestamp,
|
|
24
|
+
commits: [],
|
|
25
|
+
}));
|
|
26
|
+
const windowIndexByHash = claimAncestors(boundaries, ancestry.parentsByHash);
|
|
27
|
+
for (const commit of readCommits(paths).toReversed()) {
|
|
28
|
+
const windowIndex = windowIndexByHash.get(commit.hash);
|
|
29
|
+
const window = windowIndex === undefined ? unreleasedWindow : (releasedWindows[windowIndex] ?? unreleasedWindow);
|
|
30
|
+
window.commits.push(commit);
|
|
31
|
+
}
|
|
32
|
+
return [unreleasedWindow, ...releasedWindows.toReversed()];
|
|
33
|
+
}
|
|
34
|
+
function claimAncestors(boundaries, parentsByHash) {
|
|
35
|
+
const windowIndexByHash = new Map();
|
|
36
|
+
for (const [windowIndex, boundary] of boundaries.entries()) {
|
|
37
|
+
const pending = [boundary.hash];
|
|
38
|
+
while (pending.length > 0) {
|
|
39
|
+
const hash = pending.pop();
|
|
40
|
+
if (hash === undefined || windowIndexByHash.has(hash)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
windowIndexByHash.set(hash, windowIndex);
|
|
44
|
+
pending.push(...(parentsByHash.get(hash) ?? []));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return windowIndexByHash;
|
|
48
|
+
}
|
|
49
|
+
function matchesTagPrefix(tagName, tagPrefixes) {
|
|
50
|
+
return tagPrefixes.some((prefix) => tagName.startsWith(prefix) && /^\d/.test(tagName.slice(prefix.length)));
|
|
51
|
+
}
|
|
52
|
+
function parseCommitRecord(record) {
|
|
53
|
+
const [hash, subject, message] = record.replace(/^\n+/, '').split(FIELD_SEPARATOR);
|
|
54
|
+
if (hash === undefined || subject === undefined || message === undefined || hash === '') {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const trimmedMessage = message.replace(/\s+$/, '');
|
|
58
|
+
const bodyStart = trimmedMessage.indexOf('\n\n');
|
|
59
|
+
const body = bodyStart === -1 ? '' : trimmedMessage.slice(bodyStart + 2);
|
|
60
|
+
return { hash, subject, body, message: trimmedMessage };
|
|
61
|
+
}
|
|
62
|
+
function readAncestry() {
|
|
63
|
+
const args = ['rev-list', '--topo-order', '--parents', '--ignore-missing', 'HEAD'];
|
|
64
|
+
const output = runGit(args, `'git rev-list' for HEAD`);
|
|
65
|
+
const ancestry = { parentsByHash: new Map(), positionByHash: new Map() };
|
|
66
|
+
for (const [position, line] of output.split('\n').entries()) {
|
|
67
|
+
const [hash, ...parents] = line.split(' ');
|
|
68
|
+
if (hash === undefined || hash === '') {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
ancestry.parentsByHash.set(hash, parents);
|
|
72
|
+
ancestry.positionByHash.set(hash, position);
|
|
73
|
+
}
|
|
74
|
+
return ancestry;
|
|
75
|
+
}
|
|
76
|
+
function readCommits(paths) {
|
|
77
|
+
const format = `%H${FIELD_SEPARATOR}%s${FIELD_SEPARATOR}%B${RECORD_SEPARATOR}`;
|
|
78
|
+
const args = ['log', 'HEAD', `--pretty=format:${format}`];
|
|
79
|
+
if (paths !== undefined && paths.length > 0) {
|
|
80
|
+
args.push('--', ...paths);
|
|
81
|
+
}
|
|
82
|
+
const output = runGit(args, `'git log' for HEAD`);
|
|
83
|
+
const commits = [];
|
|
84
|
+
for (const record of output.split(RECORD_SEPARATOR)) {
|
|
85
|
+
const commit = parseCommitRecord(record);
|
|
86
|
+
if (commit !== undefined) {
|
|
87
|
+
commits.push(commit);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return commits;
|
|
91
|
+
}
|
|
92
|
+
function readTagBoundaries(tagPrefixes, positionByHash) {
|
|
93
|
+
const fields = ['%(refname:strip=2)', '%(creatordate:unix)', '%(objectname)', '%(*objectname)'];
|
|
94
|
+
const args = ['for-each-ref', `--format=${fields.join('%1f')}`, 'refs/tags'];
|
|
95
|
+
const output = runGit(args, `'git for-each-ref' for refs/tags`);
|
|
96
|
+
const boundaries = [];
|
|
97
|
+
for (const line of output.split('\n')) {
|
|
98
|
+
const [tag, creatorDate, objectName, dereferencedName] = line.split(FIELD_SEPARATOR);
|
|
99
|
+
if (tag === undefined || creatorDate === undefined || objectName === undefined) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (!matchesTagPrefix(tag, tagPrefixes)) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const hash = dereferencedName || objectName;
|
|
106
|
+
const position = positionByHash.get(hash);
|
|
107
|
+
if (position !== undefined) {
|
|
108
|
+
boundaries.push({ hash, position, tag, timestamp: Number(creatorDate) });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return boundaries.toSorted((left, right) => right.position - left.position);
|
|
112
|
+
}
|
|
113
|
+
function runGit(args, description) {
|
|
114
|
+
try {
|
|
115
|
+
return execFileSync('git', args, {
|
|
116
|
+
encoding: 'utf8',
|
|
117
|
+
maxBuffer: GIT_OUTPUT_LIMIT,
|
|
118
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
119
|
+
}).trim();
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
throw chainError(`Failed to run ${description}`, error);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -1,32 +1,14 @@
|
|
|
1
1
|
import type { WorkspaceConfig } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Build a git-cliff tag pattern from one or more tag prefixes.
|
|
4
|
-
*
|
|
5
|
-
* Single-prefix input preserves the historical `<prefix>[0-9].*` shape. Multi-prefix input
|
|
6
|
-
* returns `(prefix1|prefix2|...)[0-9].*` so git-cliff's regex-based `--tag-pattern` accepts
|
|
7
|
-
* tags under any listed prefix. Each prefix is regex-escaped so metacharacters in operator-
|
|
8
|
-
* supplied legacy prefixes cannot alter the pattern's meaning.
|
|
9
|
-
*
|
|
10
|
-
* @param tagPrefixes - Tag prefixes to match as a union (must contain at least one entry).
|
|
11
|
-
*/
|
|
12
|
-
export declare function buildTagPattern(tagPrefixes: readonly string[]): string;
|
|
13
2
|
/**
|
|
14
3
|
* Return the workspace's derived tag prefix followed by each declared legacy-identity tag
|
|
15
4
|
* prefix. Shared by `releasePrepareMono` (the production prepare path) and
|
|
16
5
|
* `validateOverridesCommand` so the two compute byte-equal per-workspace tag-prefix unions.
|
|
17
6
|
*/
|
|
18
7
|
export declare function getAllTagPrefixes(workspace: WorkspaceConfig): string[];
|
|
19
|
-
/**
|
|
20
|
-
* Options for `buildChangelogEntries`-driven flows that consume git-cliff output.
|
|
21
|
-
*
|
|
22
|
-
* The fields here mirror the subset of git-cliff CLI options that the in-package pipeline
|
|
23
|
-
* still passes through (tag-pattern union, include-path filtering for monorepo workspaces).
|
|
24
|
-
* `generateChangelog` and `generateChangelogs` (the prior cliff `--output` invokers) are
|
|
25
|
-
* removed; markdown rendering is now handled by `renderChangelogMarkdown.ts`.
|
|
26
|
-
*/
|
|
8
|
+
/** Options for `buildChangelogEntries`: the tag prefixes and pathspecs that bound its release windows. */
|
|
27
9
|
export interface GenerateChangelogOptions {
|
|
28
|
-
/** Tag
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
|
|
10
|
+
/** Tag prefixes to match as a union; must contain at least one entry. */
|
|
11
|
+
tagPrefixes: readonly string[];
|
|
12
|
+
/** Git pathspecs restricting the history to commits that touch them; all paths when omitted. */
|
|
13
|
+
paths?: readonly string[];
|
|
32
14
|
}
|
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
export function buildTagPattern(tagPrefixes) {
|
|
2
|
-
if (tagPrefixes.length === 0) {
|
|
3
|
-
throw new Error('buildTagPattern: tagPrefixes must contain at least one entry');
|
|
4
|
-
}
|
|
5
|
-
if (tagPrefixes.length === 1) {
|
|
6
|
-
const single = tagPrefixes[0] ?? '';
|
|
7
|
-
return `${single}[0-9].*`;
|
|
8
|
-
}
|
|
9
|
-
const escaped = tagPrefixes.map(escapeRegex);
|
|
10
|
-
return `(${escaped.join('|')})[0-9].*`;
|
|
11
|
-
}
|
|
12
1
|
export function getAllTagPrefixes(workspace) {
|
|
13
2
|
return [workspace.tagPrefix, ...(workspace.legacyIdentities?.map((identity) => identity.tagPrefix) ?? [])];
|
|
14
3
|
}
|
|
15
|
-
function escapeRegex(value) {
|
|
16
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
|
|
17
|
-
}
|
package/dist/esm/glyphs.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/** Glyphs that name no status; a status marker comes from nmr-core's `STATUS_GLYPHS`. */
|
|
2
|
-
export declare const RELEASE_GLYPHS: Readonly<Record<"plain" | "rich", Readonly<Record<"
|
|
2
|
+
export declare const RELEASE_GLYPHS: Readonly<Record<"plain" | "rich", Readonly<Record<"tag" | "bump" | "dryRun", import("@williamthorsen/toolbelt.terminal/candidate").Glyph>>>>;
|
|
3
3
|
export type ReleaseGlyphName = keyof (typeof RELEASE_GLYPHS)['rich'];
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
* dispatcher and asserted by the help drift guard test; kept free of import-time side
|
|
4
4
|
* effects so both can load it.
|
|
5
5
|
*/
|
|
6
|
-
export declare const prepareHelpText = "\nUsage: release-kit prepare [options]\n\nRun release preparation with automatic workspace discovery.\n\nOptions:\n --dry-run Run without modifying any files\n --bump=major|minor|patch
|
|
6
|
+
export declare const prepareHelpText = "\nUsage: release-kit prepare [options]\n\nRun release preparation with automatic workspace discovery.\n\nOptions:\n --config <path> Path to config file (default: .config/release-kit.config.ts)\n --dry-run Run without modifying any files\n --bump=major|minor|patch Set the level of every release that happens; does not trigger one\n --set-version=X.Y.Z Set an explicit version; bypasses the changelog-derived bump. In monorepo\n mode it requires --only, and is rejected when a 'project' block is configured.\n --force Release even when no commits or no bump-worthy commits exist\n since the last tag. Defaults to patch when --bump is not given;\n use --bump=X for a different level.\n --no-git-checks, -n Skip the clean-working-tree check\n --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only).\n When a 'project' block is configured, the project release is skipped.\n --with-release-notes Also write per-workspace release-notes previews under <workspacePath>/docs/\n (docs/README.v<version>.md and docs/RELEASE_NOTES.v<version>.md).\n Recommended .gitignore entry: packages/*/docs/*.v*.md (or docs/*.v*.md).\n --help, -h Show this help message\n";
|
|
7
7
|
/** Prints the prepare command's help text. */
|
|
8
8
|
export declare function showPrepareHelp(): void;
|
|
@@ -4,15 +4,14 @@ Usage: release-kit prepare [options]
|
|
|
4
4
|
Run release preparation with automatic workspace discovery.
|
|
5
5
|
|
|
6
6
|
Options:
|
|
7
|
+
--config <path> Path to config file (default: .config/release-kit.config.ts)
|
|
7
8
|
--dry-run Run without modifying any files
|
|
8
|
-
--bump=major|minor|patch
|
|
9
|
-
--set-version=X.Y.Z Set an explicit version; bypasses
|
|
9
|
+
--bump=major|minor|patch Set the level of every release that happens; does not trigger one
|
|
10
|
+
--set-version=X.Y.Z Set an explicit version; bypasses the changelog-derived bump. In monorepo
|
|
10
11
|
mode it requires --only, and is rejected when a 'project' block is configured.
|
|
11
12
|
--force Release even when no commits or no bump-worthy commits exist
|
|
12
|
-
since the last tag.
|
|
13
|
-
|
|
14
|
-
level. In single-package mode, a bare --force is rejected —
|
|
15
|
-
pass --bump=major|minor|patch.
|
|
13
|
+
since the last tag. Defaults to patch when --bump is not given;
|
|
14
|
+
use --bump=X for a different level.
|
|
16
15
|
--no-git-checks, -n Skip the clean-working-tree check
|
|
17
16
|
--only=name1,name2 Only process the named workspaces (comma-separated, monorepo only).
|
|
18
17
|
When a 'project' block is configured, the project release is skipped.
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { type ChangelogOverrideScope, validateAllChangelogOverrides, type ValidateAllChangelogOverridesInputs, type ValidateAllChangelogOverridesResult, } from './changelogOverrides.js';
|
|
1
|
+
export { type ChangelogOverrideScope, type OverrideTargetItem, validateAllChangelogOverrides, type ValidateAllChangelogOverridesInputs, type ValidateAllChangelogOverridesResult, } from './changelogOverrides.js';
|
|
2
2
|
export { formatValidateOverridesResult, type ValidateOverridesCommandResult } from './validateOverridesCommand.js';
|
|
@@ -62,7 +62,7 @@ export function initCommand({ dryRun, force, styles, withConfig }) {
|
|
|
62
62
|
}
|
|
63
63
|
printStep('Next steps');
|
|
64
64
|
const configHint = withConfig
|
|
65
|
-
? '1. (Optional) Customize .config/release-kit.config.ts
|
|
65
|
+
? '1. (Optional) Customize .config/release-kit.config.ts.'
|
|
66
66
|
: '1. (Optional) Run again with --with-config to scaffold config files.';
|
|
67
67
|
console.info(`
|
|
68
68
|
${configHint}
|
|
@@ -6,8 +6,6 @@ interface ScaffoldOptions {
|
|
|
6
6
|
overwrite: boolean;
|
|
7
7
|
withConfig: boolean;
|
|
8
8
|
}
|
|
9
|
-
/** Copy the bundled cliff.toml.template to `.config/git-cliff.toml` in the target repo. */
|
|
10
|
-
export declare function copyCliffTemplate(dryRun: boolean, overwrite: boolean): WriteResult;
|
|
11
9
|
/** Scaffold release-kit files for the target repo. Returns a result for each file attempted. */
|
|
12
10
|
export declare function scaffoldFiles({ repoType, dryRun, overwrite, withConfig }: ScaffoldOptions): WriteResult[];
|
|
13
11
|
export {};
|
|
@@ -1,33 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolve } from 'node:path';
|
|
3
|
-
import { findPackageRoot, writeFileWithCheck } from '@williamthorsen/nmr-core';
|
|
4
|
-
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
1
|
+
import { writeFileWithCheck } from '@williamthorsen/nmr-core';
|
|
5
2
|
import { createGithubReleaseWorkflow, publishWorkflow, releaseConfigScript, releaseWorkflow } from "./templates.js";
|
|
6
|
-
export function copyCliffTemplate(dryRun, overwrite) {
|
|
7
|
-
const destPath = '.config/git-cliff.toml';
|
|
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}` };
|
|
15
|
-
}
|
|
16
|
-
const templatePath = resolve(root, 'cliff.toml.template');
|
|
17
|
-
let content;
|
|
18
|
-
try {
|
|
19
|
-
content = readFileSync(templatePath, 'utf8');
|
|
20
|
-
}
|
|
21
|
-
catch (error) {
|
|
22
|
-
const message = describeError(error);
|
|
23
|
-
return {
|
|
24
|
-
filePath: destPath,
|
|
25
|
-
outcome: 'failed',
|
|
26
|
-
error: `Failed to read bundled template at ${templatePath}: ${message}`,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
return writeFileWithCheck(destPath, content, { dryRun, overwrite });
|
|
30
|
-
}
|
|
31
3
|
export function scaffoldFiles({ repoType, dryRun, overwrite, withConfig }) {
|
|
32
4
|
const results = [
|
|
33
5
|
writeFileWithCheck('.github/workflows/create-github-release.yaml', createGithubReleaseWorkflow(repoType), {
|
|
@@ -38,7 +10,7 @@ export function scaffoldFiles({ repoType, dryRun, overwrite, withConfig }) {
|
|
|
38
10
|
writeFileWithCheck('.github/workflows/release.yaml', releaseWorkflow(repoType), { dryRun, overwrite }),
|
|
39
11
|
];
|
|
40
12
|
if (withConfig) {
|
|
41
|
-
results.push(writeFileWithCheck('.config/release-kit.config.ts', releaseConfigScript(repoType), { dryRun, overwrite })
|
|
13
|
+
results.push(writeFileWithCheck('.config/release-kit.config.ts', releaseConfigScript(repoType), { dryRun, overwrite }));
|
|
42
14
|
}
|
|
43
15
|
return results;
|
|
44
16
|
}
|