@williamthorsen/release-kit 18.1.1 → 18.3.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 +150 -0
- package/.readyup/kits/default.js +596 -32
- package/.readyup/kits/npm-auto-publish.js +1 -1
- package/.readyup/manifest.json +55 -33
- package/CHANGELOG.md +44 -0
- package/README.md +19 -2
- package/dist/esm/assertCleanWorkingTree.d.ts +2 -1
- package/dist/esm/assertCleanWorkingTree.js +4 -8
- package/dist/esm/assertReleasesOnNpm.js +2 -9
- package/dist/esm/assertTarballsComplete.js +2 -9
- package/dist/esm/buildChangelogEntries.d.ts +5 -3
- package/dist/esm/buildChangelogEntries.js +9 -4
- package/dist/esm/buildChangelogItem.d.ts +1 -1
- package/dist/esm/buildDependencyGraph.js +2 -16
- package/dist/esm/buildReleasedResult.d.ts +15 -3
- package/dist/esm/buildReleasedResult.js +23 -2
- package/dist/esm/changelogJsonFile.d.ts +3 -2
- package/dist/esm/changelogJsonFile.js +14 -17
- package/dist/esm/changelogJsonUtils.d.ts +22 -2
- package/dist/esm/changelogJsonUtils.js +19 -10
- package/dist/esm/cli/maintenanceCommands.js +1 -1
- package/dist/esm/commitCommand.d.ts +2 -1
- package/dist/esm/commitCommand.js +8 -11
- package/dist/esm/configSchema.d.ts +3 -1
- package/dist/esm/configSchema.js +1 -0
- package/dist/esm/createGithubRelease.d.ts +13 -10
- package/dist/esm/createGithubRelease.js +9 -13
- package/dist/esm/createGithubReleaseCommand.d.ts +1 -1
- package/dist/esm/createGithubReleaseCommand.js +25 -3
- package/dist/esm/createTags.d.ts +2 -1
- package/dist/esm/createTags.js +12 -12
- package/dist/esm/decideDirectReleases.d.ts +2 -1
- package/dist/esm/decideDirectReleases.js +5 -5
- package/dist/esm/decideRelease.d.ts +1 -1
- package/dist/esm/defaults.d.ts +4 -2
- package/dist/esm/defaults.js +1 -0
- package/dist/esm/deriveDirectoryConfig.js +5 -4
- package/dist/esm/deriveWorkspaceConfig.js +2 -9
- package/dist/esm/detectUndeclaredTagPrefixes.d.ts +2 -1
- package/dist/esm/detectUndeclaredTagPrefixes.js +4 -13
- package/dist/esm/determineBumpType.d.ts +2 -1
- package/dist/esm/determineBumpType.js +5 -0
- package/dist/esm/enumerateReleaseWindows.d.ts +3 -11
- package/dist/esm/enumerateReleaseWindows.js +17 -60
- package/dist/esm/findUnroutedEntryScopes.d.ts +1 -1
- package/dist/esm/git.d.ts +61 -0
- package/dist/esm/git.js +101 -0
- package/dist/esm/init/checks.d.ts +2 -1
- package/dist/esm/init/checks.js +5 -10
- package/dist/esm/init/eligibility.d.ts +12 -0
- package/dist/esm/init/eligibility.js +50 -0
- package/dist/esm/init/initCommand.d.ts +0 -2
- package/dist/esm/init/initCommand.js +4 -45
- package/dist/esm/init/managed-workflows.d.ts +10 -0
- package/dist/esm/init/managed-workflows.js +18 -0
- package/dist/esm/init/templates.js +2 -2
- package/dist/esm/init/updateTemplatesCommand.js +9 -42
- package/dist/esm/injectReleaseNotesIntoReadme.d.ts +29 -7
- package/dist/esm/injectReleaseNotesIntoReadme.js +13 -21
- package/dist/esm/loadConfig.js +3 -16
- package/dist/esm/mergeConfig.d.ts +2 -1
- package/dist/esm/packFromCleanCheckout.js +3 -9
- package/dist/esm/planVersionBump.js +2 -16
- package/dist/esm/prepareCommand.js +2 -1
- package/dist/esm/prepareComposite.js +1 -1
- package/dist/esm/prepareReleaseTargets.d.ts +2 -1
- package/dist/esm/prepareReleaseTargets.js +3 -2
- package/dist/esm/prepareRunContext.d.ts +2 -0
- package/dist/esm/prepareTarget.d.ts +6 -4
- package/dist/esm/prepareTarget.js +9 -3
- package/dist/esm/previewTagPrefixes.d.ts +9 -2
- package/dist/esm/previewTagPrefixes.js +22 -22
- package/dist/esm/publishCommand.d.ts +1 -1
- package/dist/esm/publishCommand.js +27 -2
- package/dist/esm/pushRelease.d.ts +2 -1
- package/dist/esm/pushRelease.js +11 -11
- package/dist/esm/readJsonFile.d.ts +2 -0
- package/dist/esm/readJsonFile.js +17 -0
- package/dist/esm/reportPrepare.js +68 -114
- package/dist/esm/resolveReleaseTags.d.ts +2 -1
- package/dist/esm/resolveReleaseTags.js +4 -12
- package/dist/esm/showTagPrefixesCommand.d.ts +6 -6
- package/dist/esm/showTagPrefixesCommand.js +94 -39
- package/dist/esm/sync-labels/generateCommand.js +0 -4
- package/dist/esm/sync-labels/initCommand.js +0 -4
- package/dist/esm/sync-labels/syncCommand.js +0 -4
- package/dist/esm/types.d.ts +8 -2
- package/docs/configuration.md +15 -6
- package/docs/labels.md +2 -2
- package/docs/releasing.md +3 -2
- package/docs/work-types.md +2 -0
- package/package.json +7 -7
- package/presets/labels/common.yaml +14 -0
- package/dist/esm/sync-labels/retiredConfig.d.ts +0 -11
- package/dist/esm/sync-labels/retiredConfig.js +0 -12
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { isRecord } from '@williamthorsen/nmr-core';
|
|
3
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
3
4
|
import { isUnknownArray } from "./typeGuards.js";
|
|
4
5
|
export function isChangelogEntry(value) {
|
|
5
6
|
return (isRecord(value) &&
|
|
@@ -11,19 +12,27 @@ export function extractVersion(tag) {
|
|
|
11
12
|
const match = /(\d+\.\d+\.\d+.*)$/.exec(tag);
|
|
12
13
|
return match?.[1] ?? tag;
|
|
13
14
|
}
|
|
14
|
-
export function
|
|
15
|
+
export function describeChangelogFileProblem(status, filePath) {
|
|
16
|
+
return status === 'missing' ? `${filePath} not found` : `could not parse ${filePath}`;
|
|
17
|
+
}
|
|
18
|
+
export function loadChangelogEntries(filePath) {
|
|
15
19
|
if (!existsSync(filePath)) {
|
|
16
|
-
return
|
|
20
|
+
return { status: 'missing' };
|
|
17
21
|
}
|
|
22
|
+
let parsedJson;
|
|
18
23
|
try {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
return parsedJson.filter(isChangelogEntry);
|
|
24
|
+
parsedJson = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return { status: 'unreadable', reason: describeError(error) };
|
|
25
28
|
}
|
|
26
|
-
|
|
27
|
-
return
|
|
29
|
+
if (!isUnknownArray(parsedJson)) {
|
|
30
|
+
return { status: 'unreadable', reason: 'the top-level value is not an array' };
|
|
28
31
|
}
|
|
32
|
+
const entries = parsedJson.filter(isChangelogEntry);
|
|
33
|
+
return { status: 'loaded', entries, rejectedCount: parsedJson.length - entries.length };
|
|
34
|
+
}
|
|
35
|
+
export function readChangelogEntries(filePath) {
|
|
36
|
+
const result = loadChangelogEntries(filePath);
|
|
37
|
+
return result.status === 'loaded' ? result.entries : undefined;
|
|
29
38
|
}
|
|
@@ -12,7 +12,7 @@ import { CONFIG_FLAG, runReportingFailures } from "./commandKit.js";
|
|
|
12
12
|
const { defineCommand, defineGroup } = createCli();
|
|
13
13
|
export const showTagPrefixes = defineCommand({
|
|
14
14
|
summary: 'Show derived and declared legacy tag prefixes per workspace',
|
|
15
|
-
description: 'Print
|
|
15
|
+
description: 'Print one block per workspace with its derived tag prefix, declared legacy identities, and tag counts, followed by the declared retired packages. Reports any release-shaped tags whose prefix is neither derived nor declared, with a copy-pasteable config snippet.',
|
|
16
16
|
flags: CONFIG_FLAG,
|
|
17
17
|
run: ({ flags, context }) => runReportingFailures(context.styles, () => {
|
|
18
18
|
enterRepoRoot();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Git } from './git.js';
|
|
1
2
|
export interface CommitCommandOptions {
|
|
2
3
|
isDryRun: boolean;
|
|
3
4
|
}
|
|
@@ -5,4 +6,4 @@ export interface CommitCommandOptions {
|
|
|
5
6
|
* Runs the CLI `commit` command: reads the tags and summary files written by `prepare`, stages all changes, and
|
|
6
7
|
* creates the release commit.
|
|
7
8
|
*/
|
|
8
|
-
export declare function commitCommand({ isDryRun }: CommitCommandOptions): void;
|
|
9
|
+
export declare function commitCommand({ isDryRun }: CommitCommandOptions, git?: Git): void;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
1
|
import { readFileSync } from 'node:fs';
|
|
3
|
-
import {
|
|
2
|
+
import { hasErrnoCode, UserError } from '@williamthorsen/nmr-core';
|
|
3
|
+
import { realGit } from "./git.js";
|
|
4
4
|
import { readReleaseTags, RELEASE_SUMMARY_FILE, resolveReleaseTagsPath } from "./releaseFiles.js";
|
|
5
5
|
import { asUserError } from "./user-error-chaining.js";
|
|
6
|
-
export function commitCommand({ isDryRun }) {
|
|
6
|
+
export function commitCommand({ isDryRun }, git = realGit) {
|
|
7
7
|
const tags = readReleaseTags();
|
|
8
8
|
if (tags.length === 0) {
|
|
9
9
|
throw new UserError(`Tags file at ${resolveReleaseTagsPath()} is empty. Run \`release-kit prepare\` first.`);
|
|
@@ -25,13 +25,10 @@ export function commitCommand({ isDryRun }) {
|
|
|
25
25
|
console.info('[dry-run] Would create commit with message:\n');
|
|
26
26
|
console.info(message);
|
|
27
27
|
try {
|
|
28
|
-
const status =
|
|
29
|
-
|
|
30
|
-
maxBuffer: GIT_OUTPUT_LIMIT_BYTES,
|
|
31
|
-
});
|
|
32
|
-
if (status.trim().length > 0) {
|
|
28
|
+
const status = git.readStatus();
|
|
29
|
+
if (status.length > 0) {
|
|
33
30
|
console.info('\nUncommitted changes:');
|
|
34
|
-
console.info(status
|
|
31
|
+
console.info(status);
|
|
35
32
|
}
|
|
36
33
|
}
|
|
37
34
|
catch {
|
|
@@ -40,8 +37,8 @@ export function commitCommand({ isDryRun }) {
|
|
|
40
37
|
return;
|
|
41
38
|
}
|
|
42
39
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
git.addAll();
|
|
41
|
+
git.commit(message);
|
|
45
42
|
}
|
|
46
43
|
catch (error) {
|
|
47
44
|
throw asUserError(error);
|
|
@@ -105,10 +105,11 @@ export declare const workTypeConfigSchema: z.ZodObject<{
|
|
|
105
105
|
aliases: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
106
106
|
excludedFromChangelog: z.ZodOptional<z.ZodBoolean>;
|
|
107
107
|
}, z.core.$strict>;
|
|
108
|
-
/** Schema for the `versionPatterns` block.
|
|
108
|
+
/** Schema for the `versionPatterns` block. `major` and `minor` are required when the field is provided. */
|
|
109
109
|
export declare const versionPatternsSchema: z.ZodObject<{
|
|
110
110
|
major: z.ZodArray<z.ZodString>;
|
|
111
111
|
minor: z.ZodArray<z.ZodString>;
|
|
112
|
+
none: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
112
113
|
}, z.core.$strict>;
|
|
113
114
|
/** Schema for a single `breakingPolicies` value. */
|
|
114
115
|
export declare const breakingPolicyValueSchema: z.ZodEnum<{
|
|
@@ -204,6 +205,7 @@ export declare const releaseKitConfigSchema: z.ZodObject<{
|
|
|
204
205
|
versionPatterns: z.ZodOptional<z.ZodObject<{
|
|
205
206
|
major: z.ZodArray<z.ZodString>;
|
|
206
207
|
minor: z.ZodArray<z.ZodString>;
|
|
208
|
+
none: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
207
209
|
}, z.core.$strict>>;
|
|
208
210
|
workspaces: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
209
211
|
dir: z.ZodString;
|
package/dist/esm/configSchema.js
CHANGED
|
@@ -7,10 +7,10 @@ export interface CreateGithubReleaseOptions {
|
|
|
7
7
|
sectionOrder?: string[];
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Why a tag was skipped. `'
|
|
11
|
-
*
|
|
10
|
+
* Why a tag was skipped. `'missing-file'` and `'unreadable-file'` concern the `changelog.json` itself; the others mean
|
|
11
|
+
* that the file does not contain releasable content for the tag.
|
|
12
12
|
*/
|
|
13
|
-
export type CreateReleaseSkipReason = 'no-entry' | 'no-audience-content' | 'empty-body';
|
|
13
|
+
export type CreateReleaseSkipReason = 'missing-file' | 'unreadable-file' | 'no-entry' | 'no-audience-content' | 'empty-body';
|
|
14
14
|
/** Discriminated outcome of attempting to create a single GitHub Release. */
|
|
15
15
|
export type CreateReleaseResult = {
|
|
16
16
|
status: 'created';
|
|
@@ -22,19 +22,22 @@ export type CreateReleaseResult = {
|
|
|
22
22
|
* Creates a GitHub Release for a tag through the `gh` CLI, with the all-audience notes of the tag's `changelog.json`
|
|
23
23
|
* entry as its body.
|
|
24
24
|
*
|
|
25
|
-
* Returns a skip when the
|
|
26
|
-
* body. Throws when `gh` fails, so that the caller does not exit 0 on a failed release.
|
|
25
|
+
* Returns a skip when the changelog is missing or unreadable, the entry is missing, doesn't have any all-audience
|
|
26
|
+
* content, or renders an empty body. Throws when `gh` fails, so that the caller does not exit 0 on a failed release.
|
|
27
27
|
*/
|
|
28
28
|
export declare function createGithubRelease(options: CreateGithubReleaseOptions): CreateReleaseResult;
|
|
29
29
|
/** Outcome of a `createGithubReleases` invocation. */
|
|
30
30
|
export interface CreateGithubReleasesOutcome {
|
|
31
31
|
/** Tags for which a Release was created (or would be, under `--dry-run`). */
|
|
32
32
|
createdTags: string[];
|
|
33
|
-
/** Tags that were skipped,
|
|
34
|
-
skippedTags:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
/** Tags that were skipped, each with the reason and the `changelog.json` path that was read for it. */
|
|
34
|
+
skippedTags: SkippedReleaseTag[];
|
|
35
|
+
}
|
|
36
|
+
/** A tag that `createGithubReleases` skipped. */
|
|
37
|
+
export interface SkippedReleaseTag {
|
|
38
|
+
tag: string;
|
|
39
|
+
reason: CreateReleaseSkipReason;
|
|
40
|
+
changelogJsonPath: string;
|
|
38
41
|
}
|
|
39
42
|
/** Creates a GitHub Release for each tag and records each skip with its reason; throws at once when `gh` fails. */
|
|
40
43
|
export declare function createGithubReleases(tags: Array<{
|
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
3
2
|
import { join } from 'node:path';
|
|
4
|
-
import { extractVersion,
|
|
3
|
+
import { extractVersion, loadChangelogEntries } from "./changelogJsonUtils.js";
|
|
5
4
|
import { matchesAudience, renderReleaseNotesSingle } from "./renderReleaseNotes.js";
|
|
6
5
|
import { asUserError } from "./user-error-chaining.js";
|
|
7
6
|
export function createGithubRelease(options) {
|
|
8
7
|
const { tag, changelogJsonPath, isDryRun, sectionOrder } = options;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return { status: 'skipped', reason: '
|
|
8
|
+
const loaded = loadChangelogEntries(changelogJsonPath);
|
|
9
|
+
if (loaded.status === 'missing') {
|
|
10
|
+
return { status: 'skipped', reason: 'missing-file' };
|
|
12
11
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (entries === undefined) {
|
|
16
|
-
console.warn(`Warning: could not parse ${changelogJsonPath}; skipping GitHub Release creation`);
|
|
17
|
-
return { status: 'skipped', reason: 'no-entry' };
|
|
12
|
+
if (loaded.status === 'unreadable') {
|
|
13
|
+
return { status: 'skipped', reason: 'unreadable-file' };
|
|
18
14
|
}
|
|
19
|
-
const
|
|
15
|
+
const version = extractVersion(tag);
|
|
16
|
+
const entry = loaded.entries.find((e) => e.version === version);
|
|
20
17
|
if (entry === undefined) {
|
|
21
|
-
console.warn(`Warning: no changelog entry for version ${version}; skipping GitHub Release creation`);
|
|
22
18
|
return { status: 'skipped', reason: 'no-entry' };
|
|
23
19
|
}
|
|
24
20
|
if (!entry.sections.some(matchesAudience('all'))) {
|
|
@@ -60,7 +56,7 @@ export function createGithubReleases(tags, changelogJsonOutputPath, isDryRun, se
|
|
|
60
56
|
createdTags.push(tag);
|
|
61
57
|
}
|
|
62
58
|
else {
|
|
63
|
-
skippedTags.push({ tag, reason: result.reason });
|
|
59
|
+
skippedTags.push({ tag, reason: result.reason, changelogJsonPath });
|
|
64
60
|
}
|
|
65
61
|
}
|
|
66
62
|
return { createdTags, skippedTags };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { formatStatusLine } from '@williamthorsen/nmr-core';
|
|
2
|
+
import { describeChangelogFileProblem } from "./changelogJsonUtils.js";
|
|
1
3
|
import { createGithubReleases } from "./createGithubRelease.js";
|
|
2
4
|
import { deriveReleaseNotesConfig } from "./deriveReleaseNotesConfig.js";
|
|
3
5
|
import { formatPrivateSkip } from "./formatPrivateSkip.js";
|
|
@@ -24,11 +26,31 @@ export async function createGithubReleaseCommand({ configPath, isDryRun, tags, s
|
|
|
24
26
|
catch (error) {
|
|
25
27
|
throw prefixUserError('Failed to create GitHub Releases', error);
|
|
26
28
|
}
|
|
27
|
-
|
|
29
|
+
const contentSkips = [];
|
|
30
|
+
for (const skippedTag of outcome.skippedTags) {
|
|
31
|
+
const fileProblem = describeFileProblem(skippedTag);
|
|
32
|
+
if (fileProblem === undefined) {
|
|
33
|
+
contentSkips.push(skippedTag);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
console.warn(` ${formatStatusLine(styles.stderr, 'warning', `Skipping GitHub Release for ${skippedTag.tag}: ${fileProblem}`)}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (contentSkips.length === 0) {
|
|
28
40
|
return;
|
|
29
41
|
}
|
|
30
|
-
const formattedSkips =
|
|
31
|
-
console.info(`Skipped ${
|
|
42
|
+
const formattedSkips = contentSkips.map((s) => `${s.tag} (${s.reason})`).join(', ');
|
|
43
|
+
console.info(`Skipped ${contentSkips.length} tag(s) without releasable content: ${formattedSkips}.`);
|
|
44
|
+
}
|
|
45
|
+
function describeFileProblem(skippedTag) {
|
|
46
|
+
switch (skippedTag.reason) {
|
|
47
|
+
case 'missing-file':
|
|
48
|
+
return describeChangelogFileProblem('missing', skippedTag.changelogJsonPath);
|
|
49
|
+
case 'unreadable-file':
|
|
50
|
+
return describeChangelogFileProblem('unreadable', skippedTag.changelogJsonPath);
|
|
51
|
+
default:
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
32
54
|
}
|
|
33
55
|
function isReleasable(resolvedTag) {
|
|
34
56
|
return resolvedTag.kind !== 'package' || resolvedTag.isPublishable;
|
package/dist/esm/createTags.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
2
|
+
import { type Git } from './git.js';
|
|
2
3
|
export interface CreateTagsOptions {
|
|
3
4
|
isDryRun: boolean;
|
|
4
5
|
shouldSkipGitChecks: boolean;
|
|
@@ -8,4 +9,4 @@ export interface CreateTagsOptions {
|
|
|
8
9
|
* Creates an annotated git tag for each name in the tags file written by `prepare`, then deletes the tags and summary
|
|
9
10
|
* files, and returns the tag names. In dry-run mode, it only lists the names.
|
|
10
11
|
*/
|
|
11
|
-
export declare function createTags(options: CreateTagsOptions): string[];
|
|
12
|
+
export declare function createTags(options: CreateTagsOptions, git?: Git): string[];
|
package/dist/esm/createTags.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { formatGlyphLine, GIT_OUTPUT_LIMIT_BYTES, hasErrnoCode, UserError, } from '@williamthorsen/nmr-core';
|
|
1
|
+
import { formatGlyphLine, UserError } from '@williamthorsen/nmr-core';
|
|
3
2
|
import { deleteFileIfExists } from "./deleteFileIfExists.js";
|
|
3
|
+
import { realGit } from "./git.js";
|
|
4
4
|
import { RELEASE_GLYPHS } from "./glyphs.js";
|
|
5
5
|
import { readReleaseTags, RELEASE_SUMMARY_FILE, RELEASE_TAGS_FILE } from "./releaseFiles.js";
|
|
6
6
|
import { asUserError } from "./user-error-chaining.js";
|
|
7
|
-
export function createTags(options) {
|
|
7
|
+
export function createTags(options, git = realGit) {
|
|
8
8
|
const { isDryRun, shouldSkipGitChecks, style } = options;
|
|
9
9
|
const tags = readReleaseTags();
|
|
10
10
|
if (tags.length === 0) {
|
|
11
11
|
return [];
|
|
12
12
|
}
|
|
13
13
|
if (!isDryRun && !shouldSkipGitChecks) {
|
|
14
|
-
assertCleanWorkingTree();
|
|
14
|
+
assertCleanWorkingTree(git);
|
|
15
15
|
}
|
|
16
16
|
if (isDryRun) {
|
|
17
17
|
console.info('[dry-run] Would create tags:');
|
|
@@ -23,7 +23,7 @@ export function createTags(options) {
|
|
|
23
23
|
const createdTags = [];
|
|
24
24
|
for (const tag of tags) {
|
|
25
25
|
try {
|
|
26
|
-
|
|
26
|
+
git.createAnnotatedTag(tag);
|
|
27
27
|
createdTags.push(tag);
|
|
28
28
|
}
|
|
29
29
|
catch (error) {
|
|
@@ -45,15 +45,15 @@ export function createTags(options) {
|
|
|
45
45
|
deleteFileIfExists(RELEASE_SUMMARY_FILE);
|
|
46
46
|
return tags;
|
|
47
47
|
}
|
|
48
|
-
function assertCleanWorkingTree() {
|
|
48
|
+
function assertCleanWorkingTree(git) {
|
|
49
|
+
let isDirty;
|
|
49
50
|
try {
|
|
50
|
-
|
|
51
|
-
execFileSync('git', ['diff', '--quiet', '--cached'], { maxBuffer: GIT_OUTPUT_LIMIT_BYTES });
|
|
51
|
+
isDirty = git.hasTrackedChanges();
|
|
52
52
|
}
|
|
53
53
|
catch (error) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
throw new UserError('Working tree is dirty. Commit or stash changes before tagging, or use `--no-git-checks` to skip this check.'
|
|
54
|
+
throw asUserError(error);
|
|
55
|
+
}
|
|
56
|
+
if (isDirty) {
|
|
57
|
+
throw new UserError('Working tree is dirty. Commit or stash changes before tagging, or use `--no-git-checks` to skip this check.');
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { UntaggedBaseline } from './assertTaggedBaseline.js';
|
|
2
2
|
import type { ReleaseHistory } from './buildChangelogEntries.js';
|
|
3
|
+
import type { Git } from './git.js';
|
|
3
4
|
import { type MemberTarget } from './prepareRunContext.js';
|
|
4
5
|
import { type TargetDecision, type TargetPrepareSettings } from './prepareTarget.js';
|
|
5
6
|
import type { ReleaseEntry } from './propagateBumps.js';
|
|
@@ -20,7 +21,7 @@ export declare function decideDirectReleases(plannedTargets: readonly MemberTarg
|
|
|
20
21
|
shouldForce?: boolean;
|
|
21
22
|
bumpOverride?: ReleaseType;
|
|
22
23
|
versionOverridesById?: ReadonlyMap<string, string>;
|
|
23
|
-
}, startIds: ReadonlySet<string
|
|
24
|
+
}, startIds: ReadonlySet<string>, git: Git): DirectReleases;
|
|
24
25
|
/**
|
|
25
26
|
* Records on each planned workspace's and directory's history the entry scopes that route nowhere in its unreleased
|
|
26
27
|
* window, which only a pass across every target's window can find. The windows go to the finder with the releasing
|
|
@@ -3,7 +3,7 @@ import { findUnroutedEntryScopes } from "./findUnroutedEntryScopes.js";
|
|
|
3
3
|
import { getAllTagPrefixes } from "./generateChangelogs.js";
|
|
4
4
|
import { describeStage, isRepoRootTarget, readHistory, runStage } from "./prepareRunContext.js";
|
|
5
5
|
import { decideTargetRelease, readTargetHistory, } from "./prepareTarget.js";
|
|
6
|
-
export function decideDirectReleases(plannedTargets, settings, options, startIds) {
|
|
6
|
+
export function decideDirectReleases(plannedTargets, settings, options, startIds, git) {
|
|
7
7
|
const { shouldForce, bumpOverride, versionOverridesById } = options;
|
|
8
8
|
const directReleases = {
|
|
9
9
|
readings: new Map(),
|
|
@@ -14,13 +14,13 @@ export function decideDirectReleases(plannedTargets, settings, options, startIds
|
|
|
14
14
|
const hintState = { emitted: false };
|
|
15
15
|
const knownPrefixes = plannedTargets.flatMap(getAllTagPrefixes);
|
|
16
16
|
for (const target of plannedTargets) {
|
|
17
|
-
const { history, untaggedBaseline } = runStage(describeStage(target), () => readTargetHistory(target, settings));
|
|
17
|
+
const { history, untaggedBaseline } = runStage(describeStage(target), () => readTargetHistory(target, settings, git));
|
|
18
18
|
directReleases.readings.set(target.id, history);
|
|
19
19
|
if (untaggedBaseline !== undefined) {
|
|
20
20
|
directReleases.untaggedBaselines.set(target.id, untaggedBaseline);
|
|
21
21
|
}
|
|
22
22
|
if (history.previousTag === undefined && target.kind === 'package' && target.isWorkspace) {
|
|
23
|
-
emitBaselineHintOnce(target, knownPrefixes, hintState);
|
|
23
|
+
emitBaselineHintOnce(target, knownPrefixes, hintState, git);
|
|
24
24
|
}
|
|
25
25
|
const versionOverride = versionOverridesById?.get(target.id);
|
|
26
26
|
const decision = decideTargetRelease(target, history, {
|
|
@@ -51,10 +51,10 @@ export function reportUnroutedEntryScopes(plannedTargets, directReleases, settin
|
|
|
51
51
|
: (findings.get(target.id) ?? []);
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
-
function emitBaselineHintOnce(target, knownPrefixes, state) {
|
|
54
|
+
function emitBaselineHintOnce(target, knownPrefixes, state, git) {
|
|
55
55
|
if (state.emitted || (target.legacyIdentities?.length ?? 0) > 0)
|
|
56
56
|
return;
|
|
57
|
-
const candidates = detectUndeclaredTagPrefixes(knownPrefixes);
|
|
57
|
+
const candidates = detectUndeclaredTagPrefixes(knownPrefixes, git);
|
|
58
58
|
if (candidates.length === 0)
|
|
59
59
|
return;
|
|
60
60
|
const totalTags = candidates.reduce((sum, candidate) => sum + candidate.tagCount, 0);
|
|
@@ -11,7 +11,7 @@ export interface DecideReleaseArgs {
|
|
|
11
11
|
bumpOverride: ReleaseType | undefined;
|
|
12
12
|
/**
|
|
13
13
|
* Skip wordings to use when the window doesn't contain any commits (`noCommits`) or when it contains commits but none
|
|
14
|
-
*
|
|
14
|
+
* calls for a bump (`noBumpWorthy`). Each path passes its own pre-rendered wordings.
|
|
15
15
|
*/
|
|
16
16
|
skipReasons: {
|
|
17
17
|
noCommits: string;
|
package/dist/esm/defaults.d.ts
CHANGED
|
@@ -9,8 +9,10 @@ export declare function composeHeader(entry: {
|
|
|
9
9
|
export declare const DEFAULT_WORK_TYPES: Record<string, WorkTypeConfig>;
|
|
10
10
|
/** The breaking policy of each canonical work type, which decides whether a `!` on it is a policy violation. */
|
|
11
11
|
export declare const DEFAULT_BREAKING_POLICIES: Record<string, BreakingPolicy>;
|
|
12
|
-
/** Default version bump patterns. */
|
|
13
|
-
export declare const DEFAULT_VERSION_PATTERNS: VersionPatterns
|
|
12
|
+
/** Default version bump patterns. The `none` types do not change the code that a consumer runs. */
|
|
13
|
+
export declare const DEFAULT_VERSION_PATTERNS: VersionPatterns & {
|
|
14
|
+
none: string[];
|
|
15
|
+
};
|
|
14
16
|
/** Default configuration for structured changelog JSON generation. */
|
|
15
17
|
export declare const DEFAULT_CHANGELOG_JSON_CONFIG: ChangelogJsonConfig;
|
|
16
18
|
/** Default configuration for release notes consumption. */
|
package/dist/esm/defaults.js
CHANGED
|
@@ -34,6 +34,7 @@ export const DEFAULT_BREAKING_POLICIES = Object.fromEntries(CANONICAL_TAXONOMY.t
|
|
|
34
34
|
export const DEFAULT_VERSION_PATTERNS = {
|
|
35
35
|
major: ['!'],
|
|
36
36
|
minor: ['feat'],
|
|
37
|
+
none: ['ai', 'ci', 'docs', 'tests', 'tooling'],
|
|
37
38
|
};
|
|
38
39
|
export const DEFAULT_CHANGELOG_JSON_CONFIG = {
|
|
39
40
|
enabled: true,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { isRecord, UserError } from '@williamthorsen/nmr-core';
|
|
4
|
-
import {
|
|
4
|
+
import { readJsonFile } from "./readJsonFile.js";
|
|
5
|
+
import { prefixUserError } from "./user-error-chaining.js";
|
|
5
6
|
export const DIRECTORY_VERSION_FILE = 'version.json';
|
|
6
7
|
const TAG_PREFIX_PATTERN = /^[\w.-]+$/;
|
|
7
8
|
export function deriveDirectoryConfig(declaredDirectory, root = process.cwd()) {
|
|
@@ -32,10 +33,10 @@ function assertVersionField(label, versionFile, root) {
|
|
|
32
33
|
}
|
|
33
34
|
let parsedJson;
|
|
34
35
|
try {
|
|
35
|
-
parsedJson =
|
|
36
|
+
parsedJson = readJsonFile(absolutePath);
|
|
36
37
|
}
|
|
37
38
|
catch (error) {
|
|
38
|
-
throw
|
|
39
|
+
throw prefixUserError(label, error);
|
|
39
40
|
}
|
|
40
41
|
if (!isRecord(parsedJson) || typeof parsedJson['version'] !== 'string') {
|
|
41
42
|
throw new UserError(`${label}: ${versionFile} does not contain a string 'version' field; add one`);
|
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
1
|
import { basename } from 'node:path';
|
|
3
2
|
import { isRecord, UserError } from '@williamthorsen/nmr-core';
|
|
4
3
|
import { isPublishableManifest } from "./isPublishableManifest.js";
|
|
5
|
-
import {
|
|
4
|
+
import { readJsonFile } from "./readJsonFile.js";
|
|
6
5
|
export function deriveWorkspaceConfig(workspacePath) {
|
|
7
6
|
const dir = basename(workspacePath);
|
|
8
7
|
const packageJsonPath = `${workspacePath}/package.json`;
|
|
9
|
-
|
|
10
|
-
try {
|
|
11
|
-
parsedJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
12
|
-
}
|
|
13
|
-
catch (error) {
|
|
14
|
-
throw chainUserError(`Failed to read ${packageJsonPath}`, error);
|
|
15
|
-
}
|
|
8
|
+
const parsedJson = readJsonFile(packageJsonPath);
|
|
16
9
|
const name = isRecord(parsedJson) ? parsedJson['name'] : undefined;
|
|
17
10
|
if (typeof name !== 'string' || name.length === 0) {
|
|
18
11
|
throw new UserError(`${packageJsonPath} is missing a 'name' field (required for tag derivation).`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Git } from './git.js';
|
|
1
2
|
/** A candidate tag prefix found in the repo that is not in the known-prefix set. */
|
|
2
3
|
export interface UndeclaredTagPrefix {
|
|
3
4
|
/** The extracted prefix, including the trailing `-v` (e.g., `'core-v'`). */
|
|
@@ -20,4 +21,4 @@ export interface UndeclaredTagPrefix {
|
|
|
20
21
|
* Reads only the local tag list, so a remote tag that has not been fetched is not reported. Returns an empty array
|
|
21
22
|
* when the repo doesn't have any tags, or any candidate-shaped tags outside the known set.
|
|
22
23
|
*/
|
|
23
|
-
export declare function detectUndeclaredTagPrefixes(knownPrefixes: readonly string[]): UndeclaredTagPrefix[];
|
|
24
|
+
export declare function detectUndeclaredTagPrefixes(knownPrefixes: readonly string[], git: Git): UndeclaredTagPrefix[];
|
|
@@ -1,25 +1,16 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { GIT_OUTPUT_LIMIT_BYTES } from '@williamthorsen/nmr-core';
|
|
3
1
|
const EXAMPLE_TAG_LIMIT = 3;
|
|
4
2
|
const CANDIDATE_TAG_PATTERN = /^(?<prefix>[a-z][a-z0-9-]*-v)\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/;
|
|
5
|
-
export function detectUndeclaredTagPrefixes(knownPrefixes) {
|
|
3
|
+
export function detectUndeclaredTagPrefixes(knownPrefixes, git) {
|
|
6
4
|
const knownPrefixSet = new Set(knownPrefixes);
|
|
7
|
-
let
|
|
5
|
+
let tags;
|
|
8
6
|
try {
|
|
9
|
-
|
|
10
|
-
encoding: 'utf8',
|
|
11
|
-
maxBuffer: GIT_OUTPUT_LIMIT_BYTES,
|
|
12
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
13
|
-
});
|
|
7
|
+
tags = git.listTags();
|
|
14
8
|
}
|
|
15
9
|
catch {
|
|
16
10
|
return [];
|
|
17
11
|
}
|
|
18
12
|
const tagsByPrefix = new Map();
|
|
19
|
-
for (const
|
|
20
|
-
const tag = line.trim();
|
|
21
|
-
if (tag === '')
|
|
22
|
-
continue;
|
|
13
|
+
for (const tag of tags) {
|
|
23
14
|
const match = CANDIDATE_TAG_PATTERN.exec(tag);
|
|
24
15
|
if (match === null)
|
|
25
16
|
continue;
|
|
@@ -9,6 +9,7 @@ export interface BumpSignal {
|
|
|
9
9
|
*
|
|
10
10
|
* Uses `versionPatterns` to decide which types trigger major/minor bumps.
|
|
11
11
|
* The `'!'` sentinel in `versionPatterns.major` means "any breaking change triggers major".
|
|
12
|
-
*
|
|
12
|
+
* A recognized type listed in `versionPatterns.none` (the default list when omitted) triggers no bump; any other
|
|
13
|
+
* recognized type not listed in major or minor patterns defaults to patch.
|
|
13
14
|
*/
|
|
14
15
|
export declare function determineBumpType(changes: readonly BumpSignal[], workTypes: Record<string, WorkTypeConfig>, versionPatterns: VersionPatterns): ReleaseType | undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_VERSION_PATTERNS } from "./defaults.js";
|
|
1
2
|
const RELEASE_PRIORITY = {
|
|
2
3
|
major: 3,
|
|
3
4
|
minor: 2,
|
|
@@ -5,6 +6,7 @@ const RELEASE_PRIORITY = {
|
|
|
5
6
|
};
|
|
6
7
|
export function determineBumpType(changes, workTypes, versionPatterns) {
|
|
7
8
|
const knownTypes = new Set(Object.keys(workTypes));
|
|
9
|
+
const nonBumpingTypes = new Set(versionPatterns.none ?? DEFAULT_VERSION_PATTERNS.none);
|
|
8
10
|
let highestPriority = 0;
|
|
9
11
|
let result;
|
|
10
12
|
for (const change of changes) {
|
|
@@ -21,6 +23,9 @@ export function determineBumpType(changes, workTypes, versionPatterns) {
|
|
|
21
23
|
else if (versionPatterns.minor.includes(change.type)) {
|
|
22
24
|
bump = 'minor';
|
|
23
25
|
}
|
|
26
|
+
else if (nonBumpingTypes.has(change.type)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
24
29
|
else {
|
|
25
30
|
bump = 'patch';
|
|
26
31
|
}
|
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export interface RawCommit {
|
|
3
|
-
/** The full 40-character hash. */
|
|
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 does not have a body. */
|
|
8
|
-
body: string;
|
|
9
|
-
/** The raw commit message, subject and body together. */
|
|
10
|
-
message: string;
|
|
11
|
-
}
|
|
1
|
+
import type { Git, RawCommit } from './git.js';
|
|
12
2
|
/** The commits that one release contains, with the version and date under which they were released. */
|
|
13
3
|
export interface ReleaseWindow {
|
|
14
4
|
/** The tag name for a released window, the caller's unreleased-tag name for the newest one. */
|
|
@@ -20,6 +10,8 @@ export interface ReleaseWindow {
|
|
|
20
10
|
}
|
|
21
11
|
/** Inputs for {@link enumerateReleaseWindows}. */
|
|
22
12
|
export interface EnumerateReleaseWindowsOptions {
|
|
13
|
+
/** The git through which the history is read. */
|
|
14
|
+
git: Git;
|
|
23
15
|
/** Git pathspecs restricting the enumeration to commits that touch them. */
|
|
24
16
|
paths?: readonly string[];
|
|
25
17
|
/** Tag prefixes to match as a union; a matching tag name continues with a complete SemVer version. */
|