@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.
Files changed (95) hide show
  1. package/.meta/changelog.json +150 -0
  2. package/.readyup/kits/default.js +596 -32
  3. package/.readyup/kits/npm-auto-publish.js +1 -1
  4. package/.readyup/manifest.json +55 -33
  5. package/CHANGELOG.md +44 -0
  6. package/README.md +19 -2
  7. package/dist/esm/assertCleanWorkingTree.d.ts +2 -1
  8. package/dist/esm/assertCleanWorkingTree.js +4 -8
  9. package/dist/esm/assertReleasesOnNpm.js +2 -9
  10. package/dist/esm/assertTarballsComplete.js +2 -9
  11. package/dist/esm/buildChangelogEntries.d.ts +5 -3
  12. package/dist/esm/buildChangelogEntries.js +9 -4
  13. package/dist/esm/buildChangelogItem.d.ts +1 -1
  14. package/dist/esm/buildDependencyGraph.js +2 -16
  15. package/dist/esm/buildReleasedResult.d.ts +15 -3
  16. package/dist/esm/buildReleasedResult.js +23 -2
  17. package/dist/esm/changelogJsonFile.d.ts +3 -2
  18. package/dist/esm/changelogJsonFile.js +14 -17
  19. package/dist/esm/changelogJsonUtils.d.ts +22 -2
  20. package/dist/esm/changelogJsonUtils.js +19 -10
  21. package/dist/esm/cli/maintenanceCommands.js +1 -1
  22. package/dist/esm/commitCommand.d.ts +2 -1
  23. package/dist/esm/commitCommand.js +8 -11
  24. package/dist/esm/configSchema.d.ts +3 -1
  25. package/dist/esm/configSchema.js +1 -0
  26. package/dist/esm/createGithubRelease.d.ts +13 -10
  27. package/dist/esm/createGithubRelease.js +9 -13
  28. package/dist/esm/createGithubReleaseCommand.d.ts +1 -1
  29. package/dist/esm/createGithubReleaseCommand.js +25 -3
  30. package/dist/esm/createTags.d.ts +2 -1
  31. package/dist/esm/createTags.js +12 -12
  32. package/dist/esm/decideDirectReleases.d.ts +2 -1
  33. package/dist/esm/decideDirectReleases.js +5 -5
  34. package/dist/esm/decideRelease.d.ts +1 -1
  35. package/dist/esm/defaults.d.ts +4 -2
  36. package/dist/esm/defaults.js +1 -0
  37. package/dist/esm/deriveDirectoryConfig.js +5 -4
  38. package/dist/esm/deriveWorkspaceConfig.js +2 -9
  39. package/dist/esm/detectUndeclaredTagPrefixes.d.ts +2 -1
  40. package/dist/esm/detectUndeclaredTagPrefixes.js +4 -13
  41. package/dist/esm/determineBumpType.d.ts +2 -1
  42. package/dist/esm/determineBumpType.js +5 -0
  43. package/dist/esm/enumerateReleaseWindows.d.ts +3 -11
  44. package/dist/esm/enumerateReleaseWindows.js +17 -60
  45. package/dist/esm/findUnroutedEntryScopes.d.ts +1 -1
  46. package/dist/esm/git.d.ts +61 -0
  47. package/dist/esm/git.js +101 -0
  48. package/dist/esm/init/checks.d.ts +2 -1
  49. package/dist/esm/init/checks.js +5 -10
  50. package/dist/esm/init/eligibility.d.ts +12 -0
  51. package/dist/esm/init/eligibility.js +50 -0
  52. package/dist/esm/init/initCommand.d.ts +0 -2
  53. package/dist/esm/init/initCommand.js +4 -45
  54. package/dist/esm/init/managed-workflows.d.ts +10 -0
  55. package/dist/esm/init/managed-workflows.js +18 -0
  56. package/dist/esm/init/templates.js +2 -2
  57. package/dist/esm/init/updateTemplatesCommand.js +9 -42
  58. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +29 -7
  59. package/dist/esm/injectReleaseNotesIntoReadme.js +13 -21
  60. package/dist/esm/loadConfig.js +3 -16
  61. package/dist/esm/mergeConfig.d.ts +2 -1
  62. package/dist/esm/packFromCleanCheckout.js +3 -9
  63. package/dist/esm/planVersionBump.js +2 -16
  64. package/dist/esm/prepareCommand.js +2 -1
  65. package/dist/esm/prepareComposite.js +1 -1
  66. package/dist/esm/prepareReleaseTargets.d.ts +2 -1
  67. package/dist/esm/prepareReleaseTargets.js +3 -2
  68. package/dist/esm/prepareRunContext.d.ts +2 -0
  69. package/dist/esm/prepareTarget.d.ts +6 -4
  70. package/dist/esm/prepareTarget.js +9 -3
  71. package/dist/esm/previewTagPrefixes.d.ts +9 -2
  72. package/dist/esm/previewTagPrefixes.js +22 -22
  73. package/dist/esm/publishCommand.d.ts +1 -1
  74. package/dist/esm/publishCommand.js +27 -2
  75. package/dist/esm/pushRelease.d.ts +2 -1
  76. package/dist/esm/pushRelease.js +11 -11
  77. package/dist/esm/readJsonFile.d.ts +2 -0
  78. package/dist/esm/readJsonFile.js +17 -0
  79. package/dist/esm/reportPrepare.js +68 -114
  80. package/dist/esm/resolveReleaseTags.d.ts +2 -1
  81. package/dist/esm/resolveReleaseTags.js +4 -12
  82. package/dist/esm/showTagPrefixesCommand.d.ts +6 -6
  83. package/dist/esm/showTagPrefixesCommand.js +94 -39
  84. package/dist/esm/sync-labels/generateCommand.js +0 -4
  85. package/dist/esm/sync-labels/initCommand.js +0 -4
  86. package/dist/esm/sync-labels/syncCommand.js +0 -4
  87. package/dist/esm/types.d.ts +8 -2
  88. package/docs/configuration.md +15 -6
  89. package/docs/labels.md +2 -2
  90. package/docs/releasing.md +3 -2
  91. package/docs/work-types.md +2 -0
  92. package/package.json +7 -7
  93. package/presets/labels/common.yaml +14 -0
  94. package/dist/esm/sync-labels/retiredConfig.d.ts +0 -11
  95. package/dist/esm/sync-labels/retiredConfig.js +0 -12
@@ -1,11 +1,7 @@
1
- import { execFileSync } from 'node:child_process';
2
- import { GIT_OUTPUT_LIMIT_BYTES } from '@williamthorsen/nmr-core';
3
1
  import { matchesTagPrefix } from "./matchesTagPrefix.js";
4
2
  import { chainUserError } from "./user-error-chaining.js";
5
- const FIELD_SEPARATOR = '\u{1F}';
6
- const RECORD_SEPARATOR = '\u{1E}';
7
3
  export function enumerateReleaseWindows(options) {
8
- const { now = Date.now, paths, tagPrefixes, unreleasedTag } = options;
4
+ const { git, now = Date.now, paths, tagPrefixes, unreleasedTag } = options;
9
5
  if (tagPrefixes.length === 0) {
10
6
  throw new Error('enumerateReleaseWindows: tagPrefixes must contain at least one entry');
11
7
  }
@@ -14,18 +10,18 @@ export function enumerateReleaseWindows(options) {
14
10
  timestampSec: Math.floor(now() / 1000),
15
11
  commits: [],
16
12
  };
17
- const ancestry = readAncestry();
13
+ const ancestry = readAncestry(git);
18
14
  if (ancestry.parentsByHash.size === 0) {
19
15
  return [unreleasedWindow];
20
16
  }
21
- const boundaries = readTagBoundaries(tagPrefixes, ancestry.positionByHash);
17
+ const boundaries = readTagBoundaries(git, tagPrefixes, ancestry.positionByHash);
22
18
  const releasedWindows = boundaries.map((boundary) => ({
23
19
  version: boundary.tag,
24
20
  timestampSec: boundary.timestampSec,
25
21
  commits: [],
26
22
  }));
27
23
  const windowIndexByHash = claimAncestors(boundaries, ancestry.parentsByHash);
28
- for (const commit of readCommits(paths).toReversed()) {
24
+ for (const commit of readCommits(git, paths).toReversed()) {
29
25
  const windowIndex = windowIndexByHash.get(commit.hash);
30
26
  const window = windowIndex === undefined ? unreleasedWindow : (releasedWindows[windowIndex] ?? unreleasedWindow);
31
27
  window.commits.push(commit);
@@ -47,74 +43,35 @@ function claimAncestors(boundaries, parentsByHash) {
47
43
  }
48
44
  return windowIndexByHash;
49
45
  }
50
- function parseCommitRecord(record) {
51
- const [hash, subject, message] = record.replace(/^\n+/, '').split(FIELD_SEPARATOR);
52
- if (hash === undefined || subject === undefined || message === undefined || hash === '') {
53
- return undefined;
54
- }
55
- const trimmedMessage = message.replace(/\s+$/, '');
56
- const bodyStart = trimmedMessage.indexOf('\n\n');
57
- const body = bodyStart === -1 ? '' : trimmedMessage.slice(bodyStart + 2);
58
- return { hash, subject, body, message: trimmedMessage };
59
- }
60
- function readAncestry() {
61
- const args = ['rev-list', '--topo-order', '--parents', '--ignore-missing', 'HEAD'];
62
- const output = runGit(args, `'git rev-list' for HEAD`);
46
+ function readAncestry(git) {
47
+ const entries = runGitRead(() => git.readAncestry(), `'git rev-list' for HEAD`);
63
48
  const ancestry = { parentsByHash: new Map(), positionByHash: new Map() };
64
- for (const [position, line] of output.split('\n').entries()) {
65
- const [hash, ...parents] = line.split(' ');
66
- if (hash === undefined || hash === '') {
67
- continue;
68
- }
49
+ for (const [position, { hash, parents }] of entries.entries()) {
69
50
  ancestry.parentsByHash.set(hash, parents);
70
51
  ancestry.positionByHash.set(hash, position);
71
52
  }
72
53
  return ancestry;
73
54
  }
74
- function readCommits(paths) {
75
- const format = `%H${FIELD_SEPARATOR}%s${FIELD_SEPARATOR}%B${RECORD_SEPARATOR}`;
76
- const args = ['log', 'HEAD', `--pretty=format:${format}`];
77
- if (paths !== undefined && paths.length > 0) {
78
- args.push('--', ...paths);
79
- }
80
- const output = runGit(args, `'git log' for HEAD`);
81
- const commits = [];
82
- for (const record of output.split(RECORD_SEPARATOR)) {
83
- const commit = parseCommitRecord(record);
84
- if (commit !== undefined) {
85
- commits.push(commit);
86
- }
87
- }
88
- return commits;
55
+ function readCommits(git, paths) {
56
+ return runGitRead(() => git.readCommits(paths), `'git log' for HEAD`);
89
57
  }
90
- function readTagBoundaries(tagPrefixes, positionByHash) {
91
- const fields = ['%(refname:strip=2)', '%(creatordate:unix)', '%(objectname)', '%(*objectname)'];
92
- const args = ['for-each-ref', `--format=${fields.join('%1f')}`, 'refs/tags'];
93
- const output = runGit(args, `'git for-each-ref' for refs/tags`);
58
+ function readTagBoundaries(git, tagPrefixes, positionByHash) {
59
+ const refs = runGitRead(() => git.listTagRefs(), `'git for-each-ref' for refs/tags`);
94
60
  const boundaries = [];
95
- for (const line of output.split('\n')) {
96
- const [tag, creatorDate, objectName, dereferencedName] = line.split(FIELD_SEPARATOR);
97
- if (tag === undefined ||
98
- creatorDate === undefined ||
99
- objectName === undefined ||
100
- !matchesTagPrefix(tag, tagPrefixes)) {
61
+ for (const { tag, timestampSec, commitHash } of refs) {
62
+ if (!matchesTagPrefix(tag, tagPrefixes)) {
101
63
  continue;
102
64
  }
103
- const hash = dereferencedName || objectName;
104
- const position = positionByHash.get(hash);
65
+ const position = positionByHash.get(commitHash);
105
66
  if (position !== undefined) {
106
- boundaries.push({ hash, position, tag, timestampSec: Number(creatorDate) });
67
+ boundaries.push({ hash: commitHash, position, tag, timestampSec });
107
68
  }
108
69
  }
109
70
  return boundaries.toSorted((left, right) => right.position - left.position);
110
71
  }
111
- function runGit(args, description) {
72
+ function runGitRead(read, description) {
112
73
  try {
113
- return execFileSync('git', args, {
114
- encoding: 'utf8',
115
- maxBuffer: GIT_OUTPUT_LIMIT_BYTES,
116
- stdio: ['pipe', 'pipe', 'pipe'],
117
- }).trim();
74
+ return read();
118
75
  }
119
76
  catch (error) {
120
77
  throw chainUserError(`Failed to run ${description}`, error);
@@ -1,4 +1,4 @@
1
- import type { RawCommit } from './enumerateReleaseWindows.js';
1
+ import type { RawCommit } from './git.js';
2
2
  import type { UnroutedEntryScope } from './types.js';
3
3
  /** A workspace target's name, the scope that routes to it, with the commits of its unreleased window. */
4
4
  export interface WorkspaceWindow {
@@ -0,0 +1,61 @@
1
+ /** A commit reachable from HEAD and the hashes of its parents. */
2
+ export interface AncestryEntry {
3
+ hash: string;
4
+ parents: string[];
5
+ }
6
+ /** Returns the arguments of the `git push` that publishes `branch` to `origin`. */
7
+ export declare function buildBranchPushArgs(branch: string): string[];
8
+ /** Returns the arguments of the `git push` that publishes `tag` alone to `origin`. */
9
+ export declare function buildTagPushArgs(tag: string): string[];
10
+ /** The git operations that release-kit runs, each returning parsed output and throwing when git fails. */
11
+ export interface Git {
12
+ /** Stages every change in the working tree. */
13
+ addAll(): void;
14
+ /** Creates a commit with `message`. */
15
+ commit(message: string): void;
16
+ /** Creates an annotated tag at HEAD whose message is its name. */
17
+ createAnnotatedTag(tag: string): void;
18
+ /** Reports whether a tracked file has staged or unstaged changes; untracked files do not count. */
19
+ hasTrackedChanges(): boolean;
20
+ /** Reports whether the working directory is inside a git work tree; never throws. */
21
+ isInsideWorkTree(): boolean;
22
+ /** Lists the tags whose names match `pattern`, every tag when it is absent. */
23
+ listTags(pattern?: string): string[];
24
+ /** Lists the tags that point at HEAD. */
25
+ listTagsAtHead(): string[];
26
+ /** Lists every tag with its creation date and the commit at which it points. */
27
+ listTagRefs(): TagRef[];
28
+ /** Pushes `branch` to `origin`, with git's output on the terminal. */
29
+ pushBranch(branch: string): void;
30
+ /** Pushes `tag` alone to `origin`, with git's output on the terminal. */
31
+ pushTag(tag: string): void;
32
+ /** Reads every commit reachable from HEAD in topological order, newest first; empty for an unborn HEAD. */
33
+ readAncestry(): AncestryEntry[];
34
+ /** Reads every commit reachable from HEAD that touches one of `paths`, or any path when it is absent, newest first. */
35
+ readCommits(paths?: readonly string[]): RawCommit[];
36
+ /** Returns the name of the checked-out branch. */
37
+ readCurrentBranch(): string;
38
+ /** Returns the `git status --porcelain` output without its trailing newline, empty when the tree is clean. */
39
+ readStatus(): string;
40
+ }
41
+ /** A commit as git records it, before any parsing or filtering. */
42
+ export interface RawCommit {
43
+ /** The full 40-character hash. */
44
+ hash: string;
45
+ /** The first line, with a wrapped subject folded onto one line by git. */
46
+ subject: string;
47
+ /** The message after its first blank line, empty when the commit does not have a body. */
48
+ body: string;
49
+ /** The raw commit message, subject and body together. */
50
+ message: string;
51
+ }
52
+ /** The `Git` that runs git in the process's working directory. */
53
+ export declare const realGit: Git;
54
+ /** A tag, the date on which it was created, and the commit at which it points. */
55
+ export interface TagRef {
56
+ tag: string;
57
+ /** Unix seconds: the tagger date for an annotated tag, the commit date for a lightweight one. */
58
+ timestampSec: number;
59
+ /** The commit at which the tag points, dereferenced for an annotated tag. */
60
+ commitHash: string;
61
+ }
@@ -0,0 +1,101 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { runGit } from '@williamthorsen/nmr-core';
3
+ export function buildBranchPushArgs(branch) {
4
+ return ['push', 'origin', branch];
5
+ }
6
+ export function buildTagPushArgs(tag) {
7
+ return ['push', '--no-follow-tags', 'origin', tag];
8
+ }
9
+ export const realGit = {
10
+ addAll: () => {
11
+ readGitOutput(['add', '-A']);
12
+ },
13
+ commit: (message) => {
14
+ readGitOutput(['commit', '-m', message]);
15
+ },
16
+ createAnnotatedTag: (tag) => {
17
+ readGitOutput(['tag', '-a', tag, '-m', tag]);
18
+ },
19
+ hasTrackedChanges: () => readGitOutput(['status', '--porcelain', '--untracked-files=no']).trim().length > 0,
20
+ isInsideWorkTree: () => runGit(['rev-parse', '--is-inside-work-tree'], process.cwd()).ok,
21
+ listTags: (pattern) => splitLines(readGitOutput(pattern === undefined ? ['tag', '--list'] : ['tag', '--list', pattern])),
22
+ listTagsAtHead: () => splitLines(readGitOutput(['tag', '--points-at', 'HEAD'])),
23
+ listTagRefs,
24
+ pushBranch: (branch) => {
25
+ pushWithTerminalOutput(buildBranchPushArgs(branch));
26
+ },
27
+ pushTag: (tag) => {
28
+ pushWithTerminalOutput(buildTagPushArgs(tag));
29
+ },
30
+ readAncestry,
31
+ readCommits,
32
+ readCurrentBranch: () => readGitOutput(['rev-parse', '--abbrev-ref', 'HEAD']).trim(),
33
+ readStatus: () => readGitOutput(['status', '--porcelain']).trimEnd(),
34
+ };
35
+ const FIELD_SEPARATOR = '\u{1F}';
36
+ const RECORD_SEPARATOR = '\u{1E}';
37
+ function listTagRefs() {
38
+ const fields = ['%(refname:strip=2)', '%(creatordate:unix)', '%(objectname)', '%(*objectname)'];
39
+ const output = readGitOutput(['for-each-ref', `--format=${fields.join('%1f')}`, 'refs/tags']).trim();
40
+ const refs = [];
41
+ for (const line of output.split('\n')) {
42
+ const [tag, creatorDate, objectName, dereferencedName] = line.split(FIELD_SEPARATOR);
43
+ if (tag === undefined || tag === '' || creatorDate === undefined || objectName === undefined) {
44
+ continue;
45
+ }
46
+ refs.push({ tag, timestampSec: Number(creatorDate), commitHash: dereferencedName || objectName });
47
+ }
48
+ return refs;
49
+ }
50
+ function parseCommitRecord(record) {
51
+ const [hash, subject, message] = record.replace(/^\n+/, '').split(FIELD_SEPARATOR);
52
+ if (hash === undefined || subject === undefined || message === undefined || hash === '') {
53
+ return undefined;
54
+ }
55
+ const trimmedMessage = message.replace(/\s+$/, '');
56
+ const bodyStart = trimmedMessage.indexOf('\n\n');
57
+ const body = bodyStart === -1 ? '' : trimmedMessage.slice(bodyStart + 2);
58
+ return { hash, subject, body, message: trimmedMessage };
59
+ }
60
+ function pushWithTerminalOutput(args) {
61
+ execFileSync('git', args, { stdio: 'inherit' });
62
+ }
63
+ function readAncestry() {
64
+ const output = readGitOutput(['rev-list', '--topo-order', '--parents', '--ignore-missing', 'HEAD']).trim();
65
+ const entries = [];
66
+ for (const line of output.split('\n')) {
67
+ const [hash, ...parents] = line.split(' ');
68
+ if (hash !== undefined && hash !== '') {
69
+ entries.push({ hash, parents });
70
+ }
71
+ }
72
+ return entries;
73
+ }
74
+ function readCommits(paths) {
75
+ const format = `%H${FIELD_SEPARATOR}%s${FIELD_SEPARATOR}%B${RECORD_SEPARATOR}`;
76
+ const args = ['log', 'HEAD', `--pretty=format:${format}`];
77
+ if (paths !== undefined && paths.length > 0) {
78
+ args.push('--', ...paths);
79
+ }
80
+ const commits = [];
81
+ for (const record of readGitOutput(args).trim().split(RECORD_SEPARATOR)) {
82
+ const commit = parseCommitRecord(record);
83
+ if (commit !== undefined) {
84
+ commits.push(commit);
85
+ }
86
+ }
87
+ return commits;
88
+ }
89
+ function readGitOutput(args) {
90
+ const result = runGit(args, process.cwd());
91
+ if (!result.ok) {
92
+ throw new Error(result.error);
93
+ }
94
+ return result.stdout;
95
+ }
96
+ function splitLines(output) {
97
+ return output
98
+ .split('\n')
99
+ .map((line) => line.trim())
100
+ .filter((line) => line.length > 0);
101
+ }
@@ -1,10 +1,11 @@
1
+ import { type Git } from '../git.js';
1
2
  /** Result of an eligibility check. */
2
3
  export interface CheckResult {
3
4
  ok: boolean;
4
5
  message?: string;
5
6
  }
6
7
  /** Verifies that the current directory is inside a git repository. */
7
- export declare function isGitRepo(): CheckResult;
8
+ export declare function isGitRepo(git?: Git): CheckResult;
8
9
  /** Verifies that package.json exists in the current directory. */
9
10
  export declare function hasPackageJson(): CheckResult;
10
11
  /** Verifies that the project uses pnpm, signaled by pnpm-lock.yaml or a pnpm `packageManager` field. */
@@ -1,15 +1,10 @@
1
- import { execSync } from 'node:child_process';
2
1
  import { existsSync, readFileSync } from 'node:fs';
3
- import { GIT_OUTPUT_LIMIT_BYTES } from '@williamthorsen/nmr-core';
2
+ import { realGit } from "../git.js";
4
3
  import { parseJsonRecord } from "./parseJsonRecord.js";
5
- export function isGitRepo() {
6
- try {
7
- execSync('git rev-parse --is-inside-work-tree', { maxBuffer: GIT_OUTPUT_LIMIT_BYTES, stdio: 'pipe' });
8
- return { ok: true };
9
- }
10
- catch {
11
- return { ok: false, message: 'Not inside a git repository. Run `git init` first.' };
12
- }
4
+ export function isGitRepo(git = realGit) {
5
+ return git.isInsideWorkTree()
6
+ ? { ok: true }
7
+ : { ok: false, message: 'Not inside a git repository. Run `git init` first.' };
13
8
  }
14
9
  export function hasPackageJson() {
15
10
  if (existsSync('package.json')) {
@@ -0,0 +1,12 @@
1
+ import { type StreamStyles } from '@williamthorsen/nmr-core';
2
+ import { type RepoType } from './detectRepoType.js';
3
+ /** Runs the eligibility checks, stopping at the first failure, and reports whether all passed. */
4
+ export declare function checkEligibility(styles: StreamStyles): boolean;
5
+ /**
6
+ * Runs the preamble shared by the scaffolding commands and returns the detected repo type, or `undefined` once it has
7
+ * reported a failed check or an anticipated error.
8
+ */
9
+ export declare function resolveEligibleRepoType({ isDryRun, styles, }: {
10
+ isDryRun: boolean;
11
+ styles: StreamStyles;
12
+ }): RepoType | undefined;
@@ -0,0 +1,50 @@
1
+ import { isAnticipatedFailure, printError, printStep, printSuccess } from '@williamthorsen/nmr-core';
2
+ import { describeError } from '@williamthorsen/toolbelt.errors';
3
+ import { hasPackageJson, isGitRepo, usesPnpm } from "./checks.js";
4
+ import { detectRepoType } from "./detectRepoType.js";
5
+ export function checkEligibility(styles) {
6
+ printStep('Checking eligibility');
7
+ return (runRequiredCheck('Git repository detected', isGitRepo(), styles) &&
8
+ runRequiredCheck('package.json found', hasPackageJson(), styles) &&
9
+ runRequiredCheck('pnpm detected', usesPnpm(), styles));
10
+ }
11
+ export function resolveEligibleRepoType({ isDryRun, styles, }) {
12
+ if (isDryRun) {
13
+ console.info('[dry-run mode]');
14
+ }
15
+ let eligible;
16
+ try {
17
+ eligible = checkEligibility(styles);
18
+ }
19
+ catch (error) {
20
+ if (!isAnticipatedFailure(error)) {
21
+ throw error;
22
+ }
23
+ printError(`Eligibility check failed: ${describeError(error)}`, styles.stderr);
24
+ return undefined;
25
+ }
26
+ if (!eligible)
27
+ return undefined;
28
+ printStep('Detecting repo type');
29
+ let repoType;
30
+ try {
31
+ repoType = detectRepoType();
32
+ }
33
+ catch (error) {
34
+ if (!isAnticipatedFailure(error)) {
35
+ throw error;
36
+ }
37
+ printError(`Failed to detect repo type: ${describeError(error)}`, styles.stderr);
38
+ return undefined;
39
+ }
40
+ printSuccess(`Detected: ${repoType}`, styles.stdout);
41
+ return repoType;
42
+ }
43
+ function runRequiredCheck(label, result, styles) {
44
+ if (result.ok) {
45
+ printSuccess(label, styles.stdout);
46
+ return true;
47
+ }
48
+ printError(result.message ?? `${label} failed`, styles.stderr);
49
+ return false;
50
+ }
@@ -5,8 +5,6 @@ interface InitOptions {
5
5
  styles: StreamStyles;
6
6
  shouldScaffoldConfig: boolean;
7
7
  }
8
- /** Runs the eligibility checks, stopping at the first failure, and reports whether all passed. */
9
- export declare function checkEligibility(styles: StreamStyles): boolean;
10
8
  /** Runs the `release-kit init` command and returns the process exit code. */
11
9
  export declare function initCommand({ isDryRun, shouldForce, styles, shouldScaffoldConfig }: InitOptions): number;
12
10
  export {};
@@ -1,45 +1,12 @@
1
- import { isAnticipatedFailure, printError, printSkip, printStep, printSuccess, reportWriteResult, } from '@williamthorsen/nmr-core';
1
+ import { isAnticipatedFailure, printError, printSkip, printStep, reportWriteResult, } from '@williamthorsen/nmr-core';
2
2
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
- import { hasPackageJson, isGitRepo, usesPnpm } from "./checks.js";
4
- import { detectRepoType } from "./detectRepoType.js";
3
+ import { resolveEligibleRepoType } from "./eligibility.js";
5
4
  import { formatPrivateWorkflowSkip, hasPublishablePackage } from "./hasPublishablePackage.js";
6
5
  import { PUBLISHING_WORKFLOW_PATHS, scaffoldFiles } from "./scaffold.js";
7
- export function checkEligibility(styles) {
8
- printStep('Checking eligibility');
9
- return (runRequiredCheck('Git repository detected', isGitRepo(), styles) &&
10
- runRequiredCheck('package.json found', hasPackageJson(), styles) &&
11
- runRequiredCheck('pnpm detected', usesPnpm(), styles));
12
- }
13
6
  export function initCommand({ isDryRun, shouldForce, styles, shouldScaffoldConfig }) {
14
- if (isDryRun) {
15
- console.info('[dry-run mode]');
16
- }
17
- let eligible;
18
- try {
19
- eligible = checkEligibility(styles);
20
- }
21
- catch (error) {
22
- if (!isAnticipatedFailure(error)) {
23
- throw error;
24
- }
25
- printError(`Eligibility check failed: ${describeError(error)}`, styles.stderr);
26
- return 1;
27
- }
28
- if (!eligible)
7
+ const repoType = resolveEligibleRepoType({ isDryRun, styles });
8
+ if (repoType === undefined)
29
9
  return 1;
30
- printStep('Detecting repo type');
31
- let repoType;
32
- try {
33
- repoType = detectRepoType();
34
- }
35
- catch (error) {
36
- if (!isAnticipatedFailure(error)) {
37
- throw error;
38
- }
39
- printError(`Failed to detect repo type: ${describeError(error)}`, styles.stderr);
40
- return 1;
41
- }
42
- printSuccess(`Detected: ${repoType}`, styles.stdout);
43
10
  const publishable = hasPublishablePackage();
44
11
  printStep('Scaffolding files');
45
12
  if (!publishable) {
@@ -80,11 +47,3 @@ function formatNextSteps({ publishable, shouldScaffoldConfig, }) {
80
47
  ];
81
48
  return steps.map((step, index) => ` ${String(index + 1)}. ${step}`).join('\n');
82
49
  }
83
- function runRequiredCheck(label, result, styles) {
84
- if (result.ok) {
85
- printSuccess(label, styles.stdout);
86
- return true;
87
- }
88
- printError(result.message ?? `${label} failed`, styles.stderr);
89
- return false;
90
- }
@@ -0,0 +1,10 @@
1
+ import type { RepoType } from './detectRepoType.js';
2
+ /** Renders the create-github-release workflow that the repo should contain. */
3
+ export declare function renderExpectedCreateGithubReleaseWorkflow(repoType: RepoType): string;
4
+ /**
5
+ * Renders the publish workflow that the repo should contain, keeping the provenance setting of the existing
6
+ * `publish.yaml` and enabling provenance when the file is missing.
7
+ */
8
+ export declare function renderExpectedPublishWorkflow(repoType: RepoType): string;
9
+ /** Renders the release workflow that the repo should contain. */
10
+ export declare function renderExpectedReleaseWorkflow(repoType: RepoType): string;
@@ -0,0 +1,18 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { hasProvenance } from "./hasProvenance.js";
3
+ import { PUBLISH_WORKFLOW_PATH } from "./scaffold.js";
4
+ import { renderCreateGithubReleaseWorkflow, renderPublishWorkflow, renderReleaseWorkflow } from "./templates.js";
5
+ export function renderExpectedCreateGithubReleaseWorkflow(repoType) {
6
+ return renderCreateGithubReleaseWorkflow(repoType);
7
+ }
8
+ export function renderExpectedPublishWorkflow(repoType) {
9
+ return renderPublishWorkflow(repoType, { shouldGenerateProvenance: readProvenanceSetting() });
10
+ }
11
+ export function renderExpectedReleaseWorkflow(repoType) {
12
+ return renderReleaseWorkflow(repoType);
13
+ }
14
+ function readProvenanceSetting() {
15
+ if (!existsSync(PUBLISH_WORKFLOW_PATH))
16
+ return true;
17
+ return hasProvenance(readFileSync(PUBLISH_WORKFLOW_PATH, 'utf8'));
18
+ }
@@ -15,7 +15,7 @@ export default defineConfig({
15
15
  // Formatting: prettier is auto-detected. Set formatCommand to override.
16
16
 
17
17
  // Uncomment to override the default version patterns:
18
- // versionPatterns: { major: ['!'], minor: ['feat', 'feature'] },
18
+ // versionPatterns: { major: ['!'], minor: ['feat', 'feature'], none: ['ai', 'ci', 'docs', 'tests', 'tooling'] },
19
19
 
20
20
  // Uncomment to add custom work types (merged with defaults):
21
21
  // workTypes: { perf: { header: 'Performance' } },
@@ -32,7 +32,7 @@ export default defineConfig({
32
32
  // Formatting: prettier is auto-detected. Set formatCommand to override.
33
33
 
34
34
  // Uncomment to override the default version patterns:
35
- // versionPatterns: { major: ['!'], minor: ['feat', 'feature'] },
35
+ // versionPatterns: { major: ['!'], minor: ['feat', 'feature'], none: ['ai', 'ci', 'docs', 'tests', 'tooling'] },
36
36
 
37
37
  // Uncomment to add custom work types (merged with defaults):
38
38
  // workTypes: { perf: { header: 'Performance' } },
@@ -1,44 +1,16 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { isAnticipatedFailure, printError, printSkip, printStep, printSuccess, reportTemplateUpdate, updateManagedFile, } from '@williamthorsen/nmr-core';
1
+ import { existsSync } from 'node:fs';
2
+ import { isAnticipatedFailure, printError, printSkip, printStep, reportTemplateUpdate, updateManagedFile, } from '@williamthorsen/nmr-core';
3
3
  import { describeError } from '@williamthorsen/toolbelt.errors';
4
4
  import { readSyncLabelsWorkflowTemplate } from "../sync-labels/templates.js";
5
- import { detectRepoType } from "./detectRepoType.js";
6
- import { hasProvenance } from "./hasProvenance.js";
5
+ import { resolveEligibleRepoType } from "./eligibility.js";
7
6
  import { formatPrivateWorkflowSkip, hasPublishablePackage } from "./hasPublishablePackage.js";
8
- import { checkEligibility } from "./initCommand.js";
7
+ import { renderExpectedCreateGithubReleaseWorkflow, renderExpectedPublishWorkflow, renderExpectedReleaseWorkflow, } from "./managed-workflows.js";
9
8
  import { CREATE_GITHUB_RELEASE_WORKFLOW_PATH, PUBLISH_WORKFLOW_PATH, PUBLISHING_WORKFLOW_PATHS } from "./scaffold.js";
10
- import { renderCreateGithubReleaseWorkflow, renderPublishWorkflow, renderReleaseWorkflow } from "./templates.js";
11
9
  const SYNC_LABELS_WORKFLOW_PATH = '.github/workflows/sync-labels.yaml';
12
10
  export function updateTemplatesCommand({ isDryRun, styles }) {
13
- if (isDryRun) {
14
- console.info('[dry-run mode]');
15
- }
16
- let eligible;
17
- try {
18
- eligible = checkEligibility(styles);
19
- }
20
- catch (error) {
21
- if (!isAnticipatedFailure(error)) {
22
- throw error;
23
- }
24
- printError(`Eligibility check failed: ${describeError(error)}`, styles.stderr);
25
- return 1;
26
- }
27
- if (!eligible)
11
+ const repoType = resolveEligibleRepoType({ isDryRun, styles });
12
+ if (repoType === undefined)
28
13
  return 1;
29
- printStep('Detecting repo type');
30
- let repoType;
31
- try {
32
- repoType = detectRepoType();
33
- }
34
- catch (error) {
35
- if (!isAnticipatedFailure(error)) {
36
- throw error;
37
- }
38
- printError(`Failed to detect repo type: ${describeError(error)}`, styles.stderr);
39
- return 1;
40
- }
41
- printSuccess(`Detected: ${repoType}`, styles.stdout);
42
14
  printStep('Updating workflows');
43
15
  let results;
44
16
  try {
@@ -65,17 +37,12 @@ function buildManagedFiles(repoType, publishable) {
65
37
  ? []
66
38
  : PUBLISHING_WORKFLOW_PATHS.filter((filePath) => !existsSync(filePath));
67
39
  const files = [
68
- [CREATE_GITHUB_RELEASE_WORKFLOW_PATH, renderCreateGithubReleaseWorkflow(repoType)],
69
- [PUBLISH_WORKFLOW_PATH, renderPublishWorkflow(repoType, { shouldGenerateProvenance: readProvenanceSetting() })],
70
- ['.github/workflows/release.yaml', renderReleaseWorkflow(repoType)],
40
+ [CREATE_GITHUB_RELEASE_WORKFLOW_PATH, renderExpectedCreateGithubReleaseWorkflow(repoType)],
41
+ [PUBLISH_WORKFLOW_PATH, renderExpectedPublishWorkflow(repoType)],
42
+ ['.github/workflows/release.yaml', renderExpectedReleaseWorkflow(repoType)],
71
43
  ];
72
44
  if (existsSync(SYNC_LABELS_WORKFLOW_PATH)) {
73
45
  files.push([SYNC_LABELS_WORKFLOW_PATH, readSyncLabelsWorkflowTemplate()]);
74
46
  }
75
47
  return { files: files.filter(([filePath]) => !skippedPaths.includes(filePath)), skippedPaths };
76
48
  }
77
- function readProvenanceSetting() {
78
- if (!existsSync(PUBLISH_WORKFLOW_PATH))
79
- return true;
80
- return hasProvenance(readFileSync(PUBLISH_WORKFLOW_PATH, 'utf8'));
81
- }
@@ -34,19 +34,41 @@ export type RenderInjectedReadmeResult = {
34
34
  * function has derived it from the tag.
35
35
  */
36
36
  export declare function renderInjectedReadmeFromEntries(readme: string, entries: readonly ChangelogEntry[], tag: string, sectionOrder?: string[]): RenderInjectedReadmeResult;
37
+ /** Why `renderInjectedReadme` did not render a README, with the version when the reason concerns one. */
38
+ export type ReadmeInjectionSkip = {
39
+ reason: 'missing-file' | 'unreadable-file';
40
+ } | {
41
+ reason: RenderInjectedReadmeSkipReason;
42
+ version: string;
43
+ };
44
+ /** Discriminated outcome of rendering release notes from a saved `changelog.json`. */
45
+ export type RenderInjectedReadmeFromFileResult = {
46
+ status: 'rendered';
47
+ renderedReadme: RenderedInjectedReadme;
48
+ } | {
49
+ status: 'skipped';
50
+ skip: ReadmeInjectionSkip;
51
+ };
52
+ /** Discriminated outcome of injecting release notes into a README file. */
53
+ export type InjectReleaseNotesResult = {
54
+ status: 'injected';
55
+ originalReadme: string;
56
+ } | {
57
+ status: 'skipped';
58
+ skip: ReadmeInjectionSkip;
59
+ };
37
60
  /**
38
61
  * Path-taking wrapper over {@link renderInjectedReadmeFromEntries} for callers rendering from a
39
62
  * saved `changelog.json`, such as the publish-time injection flow.
40
63
  *
41
- * Adds the two skip conditions that only a file can present: The changelog is missing, or it does
42
- * not parse. Warns on every skip, its own and the renderer's alike, because the publish path does
43
- * not have any other channel for the message.
64
+ * Adds the two skip conditions that only a file can present: The changelog is missing, or it cannot
65
+ * be read as entries.
44
66
  */
45
- export declare function renderInjectedReadme(readme: string, changelogJsonPath: string, tag: string, sectionOrder?: string[]): RenderedInjectedReadme | undefined;
67
+ export declare function renderInjectedReadme(readme: string, changelogJsonPath: string, tag: string, sectionOrder?: string[]): RenderInjectedReadmeFromFileResult;
46
68
  /**
47
- * Injects release notes into a README file, returning the original content for restoration, or `undefined` when
48
- * injection was skipped.
69
+ * Injects release notes into a README file, returning the original content for restoration, or the skip when
70
+ * injection did not happen.
49
71
  */
50
- export declare function injectReleaseNotesIntoReadme(readmePath: string, changelogJsonPath: string, tag: string, sectionOrder?: string[]): string | undefined;
72
+ export declare function injectReleaseNotesIntoReadme(readmePath: string, changelogJsonPath: string, tag: string, sectionOrder?: string[]): InjectReleaseNotesResult;
51
73
  /** Returns the path of a workspace's `README.md`, or `undefined` when it has none. */
52
74
  export declare function resolveReadmePath(workspacePath: string): string | undefined;