@williamthorsen/release-kit 4.8.0 → 5.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +74 -4
  2. package/README.md +310 -40
  3. package/cliff.toml.template +2 -1
  4. package/dist/esm/.cache +1 -1
  5. package/dist/esm/bin/release-kit.js +44 -13
  6. package/dist/esm/buildDependencyGraph.d.ts +3 -3
  7. package/dist/esm/buildDependencyGraph.js +10 -10
  8. package/dist/esm/buildReleaseSummary.js +4 -4
  9. package/dist/esm/bumpAllVersions.d.ts +1 -0
  10. package/dist/esm/bumpAllVersions.js +16 -2
  11. package/dist/esm/bumpVersion.js +3 -0
  12. package/dist/esm/commitCommand.js +1 -1
  13. package/dist/esm/compareVersions.d.ts +1 -0
  14. package/dist/esm/compareVersions.js +27 -0
  15. package/dist/esm/createGithubRelease.d.ts +6 -2
  16. package/dist/esm/createGithubRelease.js +17 -17
  17. package/dist/esm/createGithubReleaseCommand.d.ts +1 -0
  18. package/dist/esm/createGithubReleaseCommand.js +41 -0
  19. package/dist/esm/defaults.js +5 -3
  20. package/dist/esm/deriveWorkspaceConfig.d.ts +2 -0
  21. package/dist/esm/deriveWorkspaceConfig.js +37 -0
  22. package/dist/esm/detectUndeclaredTagPrefixes.d.ts +7 -0
  23. package/dist/esm/detectUndeclaredTagPrefixes.js +46 -0
  24. package/dist/esm/generateChangelogJson.js +37 -1
  25. package/dist/esm/generateChangelogs.d.ts +1 -1
  26. package/dist/esm/generateChangelogs.js +14 -3
  27. package/dist/esm/getCommitsSinceTarget.d.ts +1 -1
  28. package/dist/esm/getCommitsSinceTarget.js +8 -4
  29. package/dist/esm/index.d.ts +7 -3
  30. package/dist/esm/index.js +10 -3
  31. package/dist/esm/init/initCommand.js +1 -1
  32. package/dist/esm/init/scaffold.d.ts +1 -1
  33. package/dist/esm/init/scaffold.js +8 -5
  34. package/dist/esm/init/templates.d.ts +1 -0
  35. package/dist/esm/init/templates.js +33 -3
  36. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +6 -1
  37. package/dist/esm/injectReleaseNotesIntoReadme.js +20 -7
  38. package/dist/esm/loadConfig.d.ts +2 -1
  39. package/dist/esm/loadConfig.js +65 -12
  40. package/dist/esm/parseRequestedTags.d.ts +1 -0
  41. package/dist/esm/parseRequestedTags.js +10 -0
  42. package/dist/esm/prepareCommand.d.ts +3 -1
  43. package/dist/esm/prepareCommand.js +74 -26
  44. package/dist/esm/previewTagPrefixes.d.ts +30 -0
  45. package/dist/esm/previewTagPrefixes.js +120 -0
  46. package/dist/esm/propagateBumps.d.ts +1 -0
  47. package/dist/esm/propagateBumps.js +1 -1
  48. package/dist/esm/publishCommand.js +8 -13
  49. package/dist/esm/pushCommand.js +5 -4
  50. package/dist/esm/readCurrentVersion.d.ts +1 -0
  51. package/dist/esm/readCurrentVersion.js +21 -0
  52. package/dist/esm/releasePrepare.d.ts +2 -0
  53. package/dist/esm/releasePrepare.js +72 -30
  54. package/dist/esm/releasePrepareMono.js +235 -112
  55. package/dist/esm/renderReleaseNotes.d.ts +1 -0
  56. package/dist/esm/renderReleaseNotes.js +29 -2
  57. package/dist/esm/reportPrepare.js +100 -73
  58. package/dist/esm/resolveCliffConfigPath.js +1 -1
  59. package/dist/esm/resolveCommandTags.d.ts +1 -1
  60. package/dist/esm/resolveCommandTags.js +17 -13
  61. package/dist/esm/resolveReleaseNotesConfig.d.ts +8 -1
  62. package/dist/esm/resolveReleaseNotesConfig.js +17 -7
  63. package/dist/esm/resolveReleaseTags.d.ts +2 -1
  64. package/dist/esm/resolveReleaseTags.js +19 -14
  65. package/dist/esm/showTagPrefixesCommand.d.ts +1 -0
  66. package/dist/esm/showTagPrefixesCommand.js +84 -0
  67. package/dist/esm/sync-labels/initCommand.js +1 -1
  68. package/dist/esm/sync-labels/presets.js +1 -1
  69. package/dist/esm/tagCommand.js +1 -1
  70. package/dist/esm/types.d.ts +22 -7
  71. package/dist/esm/validateConfig.js +179 -36
  72. package/dist/esm/version.d.ts +1 -1
  73. package/dist/esm/version.js +1 -1
  74. package/dist/esm/writeReleaseNotesPreviews.d.ts +18 -0
  75. package/dist/esm/writeReleaseNotesPreviews.js +65 -0
  76. package/package.json +2 -2
  77. package/dist/esm/component.d.ts +0 -2
  78. package/dist/esm/component.js +0 -14
  79. package/dist/esm/findPackageRoot.d.ts +0 -1
  80. package/dist/esm/findPackageRoot.js +0 -17
  81. package/dist/esm/githubReleaseCommand.d.ts +0 -1
  82. package/dist/esm/githubReleaseCommand.js +0 -35
@@ -6,9 +6,13 @@ function isNoTagError(err) {
6
6
  function errorMessage(err) {
7
7
  return err instanceof Error ? err.message : String(err);
8
8
  }
9
- function findLatestTag(tagPrefix) {
9
+ function findLatestTag(tagPrefixes) {
10
+ if (tagPrefixes.length === 0) {
11
+ throw new Error("findLatestTag: tagPrefixes must contain at least one entry");
12
+ }
13
+ const matchArgs = tagPrefixes.map((prefix) => `--match=${prefix}*`);
10
14
  try {
11
- const tagResult = execFileSync("git", ["describe", "--tags", "--abbrev=0", `--match=${tagPrefix}*`], {
15
+ const tagResult = execFileSync("git", ["describe", "--tags", "--abbrev=0", ...matchArgs], {
12
16
  encoding: "utf8",
13
17
  stdio: ["pipe", "pipe", "pipe"]
14
18
  }).trim();
@@ -35,8 +39,8 @@ function parseLogOutput(logOutput) {
35
39
  }
36
40
  return commits;
37
41
  }
38
- function getCommitsSinceTarget(tagPrefix, paths) {
39
- const tag = findLatestTag(tagPrefix);
42
+ function getCommitsSinceTarget(tagPrefixes, paths) {
43
+ const tag = findLatestTag(tagPrefixes);
40
44
  const range = tag === void 0 ? "HEAD" : `${tag}..HEAD`;
41
45
  const format = `%s${FIELD_SEPARATOR}%H`;
42
46
  const args = ["log", range, `--pretty=format:${format}`];
@@ -1,28 +1,30 @@
1
1
  export type { CreateTagsOptions } from './createTags.ts';
2
2
  export type { PackageManager } from './detectPackageManager.ts';
3
3
  export type { GenerateChangelogOptions } from './generateChangelogs.ts';
4
+ export type { RetiredPackagePreviewEntry } from './previewTagPrefixes.ts';
4
5
  export type { PublishOptions } from './publish.ts';
5
6
  export type { ReleasePrepareOptions } from './releasePrepare.ts';
6
7
  export type { ResolvedTag } from './resolveReleaseTags.ts';
7
8
  export type { LabelDefinition, SyncLabelsConfig } from './sync-labels/types.ts';
8
- export type { BumpResult, ChangelogAudience, ChangelogEntry, ChangelogItem, ChangelogJsonConfig, ChangelogSection, Commit, ComponentConfig, ComponentOverride, ComponentPrepareResult, MonorepoReleaseConfig, ParsedCommit, PrepareResult, ReleaseConfig, ReleaseKitConfig, ReleaseNotesConfig, ReleaseType, VersionPatterns, WorkTypeConfig, } from './types.ts';
9
+ export type { BumpResult, ChangelogAudience, ChangelogEntry, ChangelogItem, ChangelogJsonConfig, ChangelogSection, Commit, LegacyIdentity, MonorepoReleaseConfig, ParsedCommit, PrepareResult, ReleaseConfig, ReleaseKitConfig, ReleaseNotesConfig, ReleaseType, RetiredPackage, VersionPatterns, WorkspaceConfig, WorkspaceOverride, WorkspacePrepareResult, WorkTypeConfig, } from './types.ts';
9
10
  export { DEFAULT_CHANGELOG_JSON_CONFIG, DEFAULT_RELEASE_NOTES_CONFIG, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES, } from './defaults.ts';
10
11
  export { buildReleaseSummary } from './buildReleaseSummary.ts';
11
12
  export { bumpAllVersions } from './bumpAllVersions.ts';
12
13
  export { bumpVersion } from './bumpVersion.ts';
13
14
  export { commitCommand } from './commitCommand.ts';
14
- export { component } from './component.ts';
15
15
  export type { CreateGithubReleaseOptions } from './createGithubRelease.ts';
16
16
  export { createGithubRelease, createGithubReleases } from './createGithubRelease.ts';
17
17
  export { createTags } from './createTags.ts';
18
18
  export { deleteFileIfExists } from './deleteFileIfExists.ts';
19
+ export { deriveWorkspaceConfig } from './deriveWorkspaceConfig.ts';
19
20
  export { detectPackageManager } from './detectPackageManager.ts';
20
21
  export { determineBumpType } from './determineBumpType.ts';
21
22
  export { discoverWorkspaces } from './discoverWorkspaces.ts';
22
23
  export { generateChangelogJson, generateSyntheticChangelogJson } from './generateChangelogJson.ts';
23
24
  export { generateChangelog, generateChangelogs } from './generateChangelogs.ts';
24
25
  export { getCommitsSinceTarget } from './getCommitsSinceTarget.ts';
25
- export { injectReleaseNotesIntoReadme, resolveReadmePath } from './injectReleaseNotesIntoReadme.ts';
26
+ export type { RenderedInjectedReadme } from './injectReleaseNotesIntoReadme.ts';
27
+ export { injectReleaseNotesIntoReadme, renderInjectedReadme, resolveReadmePath, } from './injectReleaseNotesIntoReadme.ts';
26
28
  export { injectSection } from './injectSection.ts';
27
29
  export { COMMIT_PREPROCESSOR_PATTERNS, parseCommitMessage } from './parseCommitMessage.ts';
28
30
  export { RELEASE_SUMMARY_FILE, RELEASE_TAGS_FILE, writeReleaseTags } from './prepareCommand.ts';
@@ -37,3 +39,5 @@ export { reportPrepare } from './reportPrepare.ts';
37
39
  export { resolveCommandTags } from './resolveCommandTags.ts';
38
40
  export { resolveReleaseTags } from './resolveReleaseTags.ts';
39
41
  export { stripScope } from './stripScope.ts';
42
+ export type { PreviewFileResult, WriteReleaseNotesPreviewsOptions, WriteReleaseNotesPreviewsResult, } from './writeReleaseNotesPreviews.ts';
43
+ export { writeReleaseNotesPreviews } from './writeReleaseNotesPreviews.ts';
package/dist/esm/index.js CHANGED
@@ -8,17 +8,21 @@ import { buildReleaseSummary } from "./buildReleaseSummary.js";
8
8
  import { bumpAllVersions } from "./bumpAllVersions.js";
9
9
  import { bumpVersion } from "./bumpVersion.js";
10
10
  import { commitCommand } from "./commitCommand.js";
11
- import { component } from "./component.js";
12
11
  import { createGithubRelease, createGithubReleases } from "./createGithubRelease.js";
13
12
  import { createTags } from "./createTags.js";
14
13
  import { deleteFileIfExists } from "./deleteFileIfExists.js";
14
+ import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
15
15
  import { detectPackageManager } from "./detectPackageManager.js";
16
16
  import { determineBumpType } from "./determineBumpType.js";
17
17
  import { discoverWorkspaces } from "./discoverWorkspaces.js";
18
18
  import { generateChangelogJson, generateSyntheticChangelogJson } from "./generateChangelogJson.js";
19
19
  import { generateChangelog, generateChangelogs } from "./generateChangelogs.js";
20
20
  import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
21
- import { injectReleaseNotesIntoReadme, resolveReadmePath } from "./injectReleaseNotesIntoReadme.js";
21
+ import {
22
+ injectReleaseNotesIntoReadme,
23
+ renderInjectedReadme,
24
+ resolveReadmePath
25
+ } from "./injectReleaseNotesIntoReadme.js";
22
26
  import { injectSection } from "./injectSection.js";
23
27
  import { COMMIT_PREPROCESSOR_PATTERNS, parseCommitMessage } from "./parseCommitMessage.js";
24
28
  import { RELEASE_SUMMARY_FILE, RELEASE_TAGS_FILE, writeReleaseTags } from "./prepareCommand.js";
@@ -31,6 +35,7 @@ import { reportPrepare } from "./reportPrepare.js";
31
35
  import { resolveCommandTags } from "./resolveCommandTags.js";
32
36
  import { resolveReleaseTags } from "./resolveReleaseTags.js";
33
37
  import { stripScope } from "./stripScope.js";
38
+ import { writeReleaseNotesPreviews } from "./writeReleaseNotesPreviews.js";
34
39
  export {
35
40
  COMMIT_PREPROCESSOR_PATTERNS,
36
41
  DEFAULT_CHANGELOG_JSON_CONFIG,
@@ -43,11 +48,11 @@ export {
43
48
  bumpAllVersions,
44
49
  bumpVersion,
45
50
  commitCommand,
46
- component,
47
51
  createGithubRelease,
48
52
  createGithubReleases,
49
53
  createTags,
50
54
  deleteFileIfExists,
55
+ deriveWorkspaceConfig,
51
56
  detectPackageManager,
52
57
  determineBumpType,
53
58
  discoverWorkspaces,
@@ -64,6 +69,7 @@ export {
64
69
  pushRelease,
65
70
  releasePrepare,
66
71
  releasePrepareMono,
72
+ renderInjectedReadme,
67
73
  renderReleaseNotesMulti,
68
74
  renderReleaseNotesSingle,
69
75
  reportPrepare,
@@ -71,5 +77,6 @@ export {
71
77
  resolveReadmePath,
72
78
  resolveReleaseTags,
73
79
  stripScope,
80
+ writeReleaseNotesPreviews,
74
81
  writeReleaseTags
75
82
  };
@@ -1,4 +1,4 @@
1
- import { printError, printStep, printSuccess, reportWriteResult } from "@williamthorsen/node-monorepo-core";
1
+ import { printError, printStep, printSuccess, reportWriteResult } from "@williamthorsen/nmr-core";
2
2
  import { hasPackageJson, isGitRepo, usesPnpm } from "./checks.js";
3
3
  import { detectRepoType } from "./detectRepoType.js";
4
4
  import { scaffoldFiles } from "./scaffold.js";
@@ -1,4 +1,4 @@
1
- import type { WriteResult } from '@williamthorsen/node-monorepo-core';
1
+ import type { WriteResult } from '@williamthorsen/nmr-core';
2
2
  import type { RepoType } from './detectRepoType.ts';
3
3
  interface ScaffoldOptions {
4
4
  repoType: RepoType;
@@ -1,8 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
- import { writeFileWithCheck } from "@williamthorsen/node-monorepo-core";
4
- import { findPackageRoot } from "../findPackageRoot.js";
5
- import { publishWorkflow, releaseConfigScript, releaseWorkflow } from "./templates.js";
3
+ import { findPackageRoot, writeFileWithCheck } from "@williamthorsen/nmr-core";
4
+ import { createGithubReleaseWorkflow, publishWorkflow, releaseConfigScript, releaseWorkflow } from "./templates.js";
6
5
  function copyCliffTemplate(dryRun, overwrite) {
7
6
  const destPath = ".config/git-cliff.toml";
8
7
  const root = findPackageRoot(import.meta.url);
@@ -21,8 +20,12 @@ function copyCliffTemplate(dryRun, overwrite) {
21
20
  }
22
21
  function scaffoldFiles({ repoType, dryRun, overwrite, withConfig }) {
23
22
  const results = [
24
- writeFileWithCheck(".github/workflows/release.yaml", releaseWorkflow(repoType), { dryRun, overwrite }),
25
- writeFileWithCheck(".github/workflows/publish.yaml", publishWorkflow(repoType), { dryRun, overwrite })
23
+ writeFileWithCheck(".github/workflows/create-github-release.yaml", createGithubReleaseWorkflow(repoType), {
24
+ dryRun,
25
+ overwrite
26
+ }),
27
+ writeFileWithCheck(".github/workflows/publish.yaml", publishWorkflow(repoType), { dryRun, overwrite }),
28
+ writeFileWithCheck(".github/workflows/release.yaml", releaseWorkflow(repoType), { dryRun, overwrite })
26
29
  ];
27
30
  if (withConfig) {
28
31
  results.push(
@@ -1,4 +1,5 @@
1
1
  import type { RepoType } from './detectRepoType.ts';
2
2
  export declare function releaseConfigScript(repoType: RepoType): string;
3
3
  export declare function publishWorkflow(repoType: RepoType): string;
4
+ export declare function createGithubReleaseWorkflow(repoType: RepoType): string;
4
5
  export declare function releaseWorkflow(repoType: RepoType): string;
@@ -3,8 +3,12 @@ function releaseConfigScript(repoType) {
3
3
  return `import type { ReleaseKitConfig } from '@williamthorsen/release-kit';
4
4
 
5
5
  const config: ReleaseKitConfig = {
6
- // Uncomment to exclude components from release processing:
7
- // components: [
6
+ releaseNotes: {
7
+ shouldInjectIntoReadme: true,
8
+ },
9
+
10
+ // Uncomment to exclude workspaces from release processing:
11
+ // workspaces: [
8
12
  // { dir: 'my-package', shouldExclude: true },
9
13
  // ],
10
14
 
@@ -23,6 +27,10 @@ export default config;
23
27
  return `import type { ReleaseKitConfig } from '@williamthorsen/release-kit';
24
28
 
25
29
  const config: ReleaseKitConfig = {
30
+ releaseNotes: {
31
+ shouldInjectIntoReadme: true,
32
+ },
33
+
26
34
  // Formatting: prettier is auto-detected. Set formatCommand to override.
27
35
 
28
36
  // Uncomment to override the default version patterns:
@@ -54,6 +62,27 @@ jobs:
54
62
  uses: williamthorsen/node-monorepo-tools/.github/workflows/publish.reusable.yaml@workflow/publish-v1
55
63
  with:
56
64
  provenance: true
65
+ tags: \${{ github.ref_name }}
66
+ `;
67
+ }
68
+ function createGithubReleaseWorkflow(repoType) {
69
+ const tagPattern = repoType === "monorepo" ? "'*-v[0-9]*.[0-9]*.[0-9]*'" : "'v[0-9]*.[0-9]*.[0-9]*'";
70
+ return `# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
71
+ name: Create GitHub Release
72
+
73
+ on:
74
+ push:
75
+ tags:
76
+ - ${tagPattern}
77
+
78
+ permissions:
79
+ contents: write
80
+
81
+ jobs:
82
+ create-github-release:
83
+ uses: williamthorsen/node-monorepo-tools/.github/workflows/create-github-release.reusable.yaml@workflow/create-github-release-v1
84
+ with:
85
+ tag: \${{ github.ref_name }}
57
86
  `;
58
87
  }
59
88
  function releaseWorkflow(repoType) {
@@ -65,7 +94,7 @@ on:
65
94
  workflow_dispatch:
66
95
  inputs:
67
96
  only:
68
- description: 'Components to release (comma-separated, leave empty for all)'
97
+ description: 'Workspaces to release (comma-separated, leave empty for all)'
69
98
  required: false
70
99
  type: string
71
100
  bump:
@@ -130,6 +159,7 @@ jobs:
130
159
  `;
131
160
  }
132
161
  export {
162
+ createGithubReleaseWorkflow,
133
163
  publishWorkflow,
134
164
  releaseConfigScript,
135
165
  releaseWorkflow
@@ -1,2 +1,7 @@
1
- export declare function injectReleaseNotesIntoReadme(readmePath: string, changelogJsonPath: string, tag: string): string | undefined;
1
+ export interface RenderedInjectedReadme {
2
+ injectedReadme: string;
3
+ releaseNotesMarkdown: string;
4
+ }
5
+ export declare function renderInjectedReadme(readme: string, changelogJsonPath: string, tag: string, sectionOrder?: string[]): RenderedInjectedReadme | undefined;
6
+ export declare function injectReleaseNotesIntoReadme(readmePath: string, changelogJsonPath: string, tag: string, sectionOrder?: string[]): string | undefined;
2
7
  export declare function resolveReadmePath(workspacePath: string): string | undefined;
@@ -3,12 +3,11 @@ import { join } from "node:path";
3
3
  import { extractVersion, readChangelogEntries } from "./changelogJsonUtils.js";
4
4
  import { injectSection } from "./injectSection.js";
5
5
  import { matchesAudience, renderReleaseNotesSingle } from "./renderReleaseNotes.js";
6
- function injectReleaseNotesIntoReadme(readmePath, changelogJsonPath, tag) {
6
+ function renderInjectedReadme(readme, changelogJsonPath, tag, sectionOrder) {
7
7
  if (!existsSync(changelogJsonPath)) {
8
8
  console.warn(`Warning: ${changelogJsonPath} not found; skipping README injection`);
9
9
  return void 0;
10
10
  }
11
- const originalReadme = readFileSync(readmePath, "utf8");
12
11
  const version = extractVersion(tag);
13
12
  const entries = readChangelogEntries(changelogJsonPath);
14
13
  if (entries === void 0) {
@@ -20,16 +19,29 @@ function injectReleaseNotesIntoReadme(readmePath, changelogJsonPath, tag) {
20
19
  console.warn(`Warning: no changelog entry for version ${version}; skipping README injection`);
21
20
  return void 0;
22
21
  }
23
- const releaseNotesMarkdown = renderReleaseNotesSingle(entry, {
22
+ const renderedSections = renderReleaseNotesSingle(entry, {
24
23
  filter: matchesAudience("all"),
25
- includeHeading: false
24
+ includeHeading: false,
25
+ ...sectionOrder === void 0 ? {} : { sectionOrder }
26
26
  });
27
- if (releaseNotesMarkdown.trimEnd().length === 0) {
27
+ if (renderedSections.trimEnd().length === 0) {
28
28
  console.warn(`Warning: no user-facing release notes for version ${version}; skipping README injection`);
29
29
  return void 0;
30
30
  }
31
- const injected = injectSection(originalReadme, "release-notes", releaseNotesMarkdown.trimEnd());
32
- writeFileSync(readmePath, injected, "utf8");
31
+ const labeledHeading = `## Release notes \u2014 v${version} (${entry.date})`;
32
+ const releaseNotesMarkdown = `${labeledHeading}
33
+
34
+ ${renderedSections.trimEnd()}`;
35
+ const injectedReadme = injectSection(readme, "release-notes", releaseNotesMarkdown);
36
+ return { injectedReadme, releaseNotesMarkdown };
37
+ }
38
+ function injectReleaseNotesIntoReadme(readmePath, changelogJsonPath, tag, sectionOrder) {
39
+ const originalReadme = readFileSync(readmePath, "utf8");
40
+ const rendered = renderInjectedReadme(originalReadme, changelogJsonPath, tag, sectionOrder);
41
+ if (rendered === void 0) {
42
+ return void 0;
43
+ }
44
+ writeFileSync(readmePath, rendered.injectedReadme, "utf8");
33
45
  return originalReadme;
34
46
  }
35
47
  function resolveReadmePath(workspacePath) {
@@ -38,5 +50,6 @@ function resolveReadmePath(workspacePath) {
38
50
  }
39
51
  export {
40
52
  injectReleaseNotesIntoReadme,
53
+ renderInjectedReadme,
41
54
  resolveReadmePath
42
55
  };
@@ -1,5 +1,6 @@
1
- import type { MonorepoReleaseConfig, ReleaseConfig, ReleaseKitConfig } from './types.ts';
1
+ import type { MonorepoReleaseConfig, ReleaseConfig, ReleaseKitConfig, WorkTypeConfig } from './types.ts';
2
2
  export declare const CONFIG_FILE_PATH = ".config/release-kit.config.ts";
3
3
  export declare function loadConfig(): Promise<unknown>;
4
4
  export declare function mergeMonorepoConfig(discoveredPaths: string[], userConfig: ReleaseKitConfig | undefined): MonorepoReleaseConfig;
5
5
  export declare function mergeSinglePackageConfig(userConfig: ReleaseKitConfig | undefined): ReleaseConfig;
6
+ export declare function resolveWorkTypes(userWorkTypes?: Record<string, WorkTypeConfig>): Record<string, WorkTypeConfig>;
@@ -1,12 +1,12 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import path from "node:path";
3
- import { component } from "./component.js";
4
3
  import {
5
4
  DEFAULT_CHANGELOG_JSON_CONFIG,
6
5
  DEFAULT_RELEASE_NOTES_CONFIG,
7
6
  DEFAULT_VERSION_PATTERNS,
8
7
  DEFAULT_WORK_TYPES
9
8
  } from "./defaults.js";
9
+ import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
10
10
  import { isRecord } from "./typeGuards.js";
11
11
  const CONFIG_FILE_PATH = ".config/release-kit.config.ts";
12
12
  async function loadConfig() {
@@ -29,20 +29,31 @@ async function loadConfig() {
29
29
  return resolved;
30
30
  }
31
31
  function mergeMonorepoConfig(discoveredPaths, userConfig) {
32
- let components = discoveredPaths.map((workspacePath) => component(workspacePath));
33
- if (userConfig?.components !== void 0) {
34
- const overrides = new Map(userConfig.components.map((c) => [c.dir, c]));
35
- components = components.filter((c) => {
36
- const override = overrides.get(c.dir);
32
+ let workspaces = discoveredPaths.map((workspacePath) => deriveWorkspaceConfig(workspacePath));
33
+ assertUniqueTagPrefixes(workspaces);
34
+ if (userConfig?.workspaces !== void 0) {
35
+ const overrides = new Map(userConfig.workspaces.map((w) => [w.dir, w]));
36
+ workspaces = workspaces.filter((w) => {
37
+ const override = overrides.get(w.dir);
37
38
  return override?.shouldExclude !== true;
39
+ }).map((w) => {
40
+ const override = overrides.get(w.dir);
41
+ if (override?.legacyIdentities === void 0) {
42
+ return w;
43
+ }
44
+ assertLegacyIdentityDoesNotMatchCurrent(w.dir, w.name, w.tagPrefix, override.legacyIdentities);
45
+ return { ...w, legacyIdentities: override.legacyIdentities.map((identity) => ({ ...identity })) };
38
46
  });
39
47
  }
40
- const workTypes = userConfig?.workTypes === void 0 ? { ...DEFAULT_WORK_TYPES } : { ...DEFAULT_WORK_TYPES, ...userConfig.workTypes };
48
+ if (userConfig?.retiredPackages !== void 0) {
49
+ assertRetiredPackagesDoNotCollideWithActive(workspaces, userConfig.retiredPackages);
50
+ }
51
+ const workTypes = resolveWorkTypes(userConfig?.workTypes);
41
52
  const versionPatterns = userConfig?.versionPatterns === void 0 ? { ...DEFAULT_VERSION_PATTERNS } : { ...userConfig.versionPatterns };
42
53
  const changelogJson = mergeChangelogJsonConfig(userConfig?.changelogJson);
43
54
  const releaseNotes = mergeReleaseNotesConfig(userConfig?.releaseNotes);
44
55
  const result = {
45
- components,
56
+ workspaces,
46
57
  workTypes,
47
58
  versionPatterns,
48
59
  changelogJson,
@@ -63,7 +74,7 @@ function mergeMonorepoConfig(discoveredPaths, userConfig) {
63
74
  return result;
64
75
  }
65
76
  function mergeSinglePackageConfig(userConfig) {
66
- const workTypes = userConfig?.workTypes === void 0 ? { ...DEFAULT_WORK_TYPES } : { ...DEFAULT_WORK_TYPES, ...userConfig.workTypes };
77
+ const workTypes = resolveWorkTypes(userConfig?.workTypes);
67
78
  const versionPatterns = userConfig?.versionPatterns === void 0 ? { ...DEFAULT_VERSION_PATTERNS } : { ...userConfig.versionPatterns };
68
79
  const changelogJson = mergeChangelogJsonConfig(userConfig?.changelogJson);
69
80
  const releaseNotes = mergeReleaseNotesConfig(userConfig?.releaseNotes);
@@ -90,6 +101,9 @@ function mergeSinglePackageConfig(userConfig) {
90
101
  }
91
102
  return result;
92
103
  }
104
+ function resolveWorkTypes(userWorkTypes) {
105
+ return userWorkTypes === void 0 ? { ...DEFAULT_WORK_TYPES } : { ...DEFAULT_WORK_TYPES, ...userWorkTypes };
106
+ }
93
107
  function mergeChangelogJsonConfig(partial) {
94
108
  if (partial === void 0) {
95
109
  return { ...DEFAULT_CHANGELOG_JSON_CONFIG };
@@ -100,18 +114,57 @@ function mergeChangelogJsonConfig(partial) {
100
114
  devOnlySections: partial.devOnlySections ?? [...DEFAULT_CHANGELOG_JSON_CONFIG.devOnlySections]
101
115
  };
102
116
  }
117
+ function assertLegacyIdentityDoesNotMatchCurrent(dir, currentName, currentTagPrefix, legacyIdentities) {
118
+ for (const identity of legacyIdentities) {
119
+ if (identity.name === currentName && identity.tagPrefix === currentTagPrefix) {
120
+ throw new Error(
121
+ `Workspace '${dir}': legacyIdentities must not match the current identity (name='${currentName}', tagPrefix='${currentTagPrefix}'). The current identity is always searched; listing it again is a no-op.`
122
+ );
123
+ }
124
+ }
125
+ }
126
+ function assertRetiredPackagesDoNotCollideWithActive(workspaces, retiredPackages) {
127
+ const workspaceByDerivedPrefix = /* @__PURE__ */ new Map();
128
+ for (const workspace of workspaces) {
129
+ workspaceByDerivedPrefix.set(workspace.tagPrefix, workspace);
130
+ }
131
+ for (const retired of retiredPackages) {
132
+ const active = workspaceByDerivedPrefix.get(retired.tagPrefix);
133
+ if (active !== void 0) {
134
+ throw new Error(
135
+ `retiredPackages: tagPrefix '${retired.tagPrefix}' collides with active workspace '${active.dir}' (derived prefix '${active.tagPrefix}'). A retired package's tagPrefix cannot belong to an active workspace.`
136
+ );
137
+ }
138
+ }
139
+ }
140
+ function assertUniqueTagPrefixes(workspaces) {
141
+ const pathsByPrefix = /* @__PURE__ */ new Map();
142
+ for (const workspace of workspaces) {
143
+ const existing = pathsByPrefix.get(workspace.tagPrefix);
144
+ if (existing === void 0) {
145
+ pathsByPrefix.set(workspace.tagPrefix, [workspace.workspacePath]);
146
+ } else {
147
+ existing.push(workspace.workspacePath);
148
+ }
149
+ }
150
+ for (const [prefix, paths] of pathsByPrefix) {
151
+ if (paths.length > 1) {
152
+ throw new Error(`Duplicate tag prefix '${prefix}' for workspaces: ${paths.join(", ")}`);
153
+ }
154
+ }
155
+ }
103
156
  function mergeReleaseNotesConfig(partial) {
104
157
  if (partial === void 0) {
105
158
  return { ...DEFAULT_RELEASE_NOTES_CONFIG };
106
159
  }
107
160
  return {
108
- shouldInjectIntoReadme: partial.shouldInjectIntoReadme ?? DEFAULT_RELEASE_NOTES_CONFIG.shouldInjectIntoReadme,
109
- shouldCreateGithubRelease: partial.shouldCreateGithubRelease ?? DEFAULT_RELEASE_NOTES_CONFIG.shouldCreateGithubRelease
161
+ shouldInjectIntoReadme: partial.shouldInjectIntoReadme ?? DEFAULT_RELEASE_NOTES_CONFIG.shouldInjectIntoReadme
110
162
  };
111
163
  }
112
164
  export {
113
165
  CONFIG_FILE_PATH,
114
166
  loadConfig,
115
167
  mergeMonorepoConfig,
116
- mergeSinglePackageConfig
168
+ mergeSinglePackageConfig,
169
+ resolveWorkTypes
117
170
  };
@@ -0,0 +1 @@
1
+ export declare function parseRequestedTags(flagValue: string | undefined): string[] | undefined;
@@ -0,0 +1,10 @@
1
+ function parseRequestedTags(flagValue) {
2
+ if (flagValue === void 0) {
3
+ return void 0;
4
+ }
5
+ const segments = flagValue.split(",").filter(Boolean);
6
+ return segments.length === 0 ? void 0 : segments;
7
+ }
8
+ export {
9
+ parseRequestedTags
10
+ };
@@ -1,4 +1,4 @@
1
- import type { WriteResult } from '@williamthorsen/node-monorepo-core';
1
+ import type { WriteResult } from '@williamthorsen/nmr-core';
2
2
  import type { ReleaseType } from './types.ts';
3
3
  export declare const RELEASE_TAGS_FILE = "tmp/.release-tags";
4
4
  export declare const RELEASE_SUMMARY_FILE = "tmp/.release-summary";
@@ -8,6 +8,8 @@ export declare function parseArgs(argv: string[]): {
8
8
  noGitChecks: boolean;
9
9
  bumpOverride: ReleaseType | undefined;
10
10
  only: string[] | undefined;
11
+ setVersion: string | undefined;
12
+ withReleaseNotes: boolean;
11
13
  };
12
14
  export declare function writeReleaseTags(tags: string[], dryRun: boolean): WriteResult | undefined;
13
15
  export declare function prepareCommand(argv: string[]): Promise<void>;
@@ -1,8 +1,4 @@
1
- import {
2
- parseArgs as coreParseArgs,
3
- translateParseError,
4
- writeFileWithCheck
5
- } from "@williamthorsen/node-monorepo-core";
1
+ import { parseArgs as coreParseArgs, translateParseError, writeFileWithCheck } from "@williamthorsen/nmr-core";
6
2
  import { assertCleanWorkingTree } from "./assertCleanWorkingTree.js";
7
3
  import { buildReleaseSummary } from "./buildReleaseSummary.js";
8
4
  import { discoverWorkspaces } from "./discoverWorkspaces.js";
@@ -15,6 +11,7 @@ import { validateConfig } from "./validateConfig.js";
15
11
  const RELEASE_TAGS_FILE = "tmp/.release-tags";
16
12
  const RELEASE_SUMMARY_FILE = "tmp/.release-summary";
17
13
  const VALID_BUMP_TYPES = ["major", "minor", "patch"];
14
+ const CANONICAL_SEMVER_PATTERN = /^\d+\.\d+\.\d+$/;
18
15
  function isReleaseType(value) {
19
16
  return VALID_BUMP_TYPES.includes(value);
20
17
  }
@@ -24,10 +21,14 @@ Usage: npx @williamthorsen/release-kit prepare [options]
24
21
 
25
22
  Options:
26
23
  --dry-run Run without modifying any files
27
- --bump=major|minor|patch Override the bump type for all components
24
+ --bump=major|minor|patch Override the bump type for all workspaces
25
+ --set-version=X.Y.Z Set an explicit version; bypasses commit-derived bumps. Requires --only in monorepo mode.
28
26
  --force Force a release even when there are no commits since the last tag (requires --bump)
29
27
  --no-git-checks, -n Skip the clean-working-tree check
30
- --only=name1,name2 Only process the named components (comma-separated, monorepo only)
28
+ --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only)
29
+ --with-release-notes Also write per-workspace release-notes previews under {workspacePath}/docs/
30
+ (docs/README.v{version}.md and docs/RELEASE_NOTES.v{version}.md).
31
+ Recommended .gitignore entry: packages/*/docs/*.v*.md (or docs/*.v*.md).
31
32
  --help Show this help message
32
33
  `);
33
34
  }
@@ -40,7 +41,9 @@ const prepareFlagSchema = {
40
41
  short: "-n"
41
42
  },
42
43
  bump: { long: "--bump", type: "string" },
44
+ setVersion: { long: "--set-version", type: "string" },
43
45
  only: { long: "--only", type: "string" },
46
+ withReleaseNotes: { long: "--with-release-notes", type: "boolean" },
44
47
  help: { long: "--help", type: "boolean", short: "-h" }
45
48
  };
46
49
  function parseArgs(argv) {
@@ -62,10 +65,25 @@ function parseArgs(argv) {
62
65
  }
63
66
  bumpOverride = flags.bump;
64
67
  }
68
+ let setVersion;
69
+ if (flags.setVersion !== void 0) {
70
+ if (!CANONICAL_SEMVER_PATTERN.test(flags.setVersion)) {
71
+ throw new Error(
72
+ `Invalid --set-version value "${flags.setVersion}". Must be canonical semver (N.N.N, no pre-release suffix).`
73
+ );
74
+ }
75
+ setVersion = flags.setVersion;
76
+ }
65
77
  let only;
66
78
  if (flags.only !== void 0) {
67
79
  only = flags.only.split(",");
68
80
  }
81
+ if (setVersion !== void 0 && bumpOverride !== void 0) {
82
+ throw new Error("--set-version cannot be combined with --bump");
83
+ }
84
+ if (setVersion !== void 0 && flags.force) {
85
+ throw new Error("--set-version cannot be combined with --force");
86
+ }
69
87
  if (flags.force && bumpOverride === void 0) {
70
88
  throw new Error("--force requires --bump to specify the version bump type");
71
89
  }
@@ -74,7 +92,9 @@ function parseArgs(argv) {
74
92
  force: flags.force,
75
93
  noGitChecks: flags.noGitChecks,
76
94
  bumpOverride,
77
- only
95
+ only,
96
+ setVersion,
97
+ withReleaseNotes: flags.withReleaseNotes
78
98
  };
79
99
  }
80
100
  function writeReleaseTags(tags, dryRun) {
@@ -92,8 +112,10 @@ async function prepareCommand(argv) {
92
112
  let noGitChecks;
93
113
  let bumpOverride;
94
114
  let only;
115
+ let setVersion;
116
+ let withReleaseNotes;
95
117
  try {
96
- ({ dryRun, force, noGitChecks, bumpOverride, only } = parseArgs(argv));
118
+ ({ dryRun, force, noGitChecks, bumpOverride, only, setVersion, withReleaseNotes } = parseArgs(argv));
97
119
  } catch (error) {
98
120
  console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
99
121
  process.exit(1);
@@ -101,7 +123,9 @@ async function prepareCommand(argv) {
101
123
  const options = {
102
124
  dryRun,
103
125
  force,
104
- ...bumpOverride === void 0 ? {} : { bumpOverride }
126
+ ...bumpOverride === void 0 ? {} : { bumpOverride },
127
+ ...setVersion === void 0 ? {} : { setVersion },
128
+ ...withReleaseNotes ? { withReleaseNotes: true } : {}
105
129
  };
106
130
  if (dryRun) {
107
131
  console.info("\n\u{1F50D} DRY RUN \u2014 no files will be modified\n");
@@ -123,26 +147,50 @@ async function prepareCommand(argv) {
123
147
  process.exit(1);
124
148
  }
125
149
  if (discoveredPaths === void 0) {
126
- if (only !== void 0) {
127
- console.error("Error: --only is only supported for monorepo configurations");
128
- process.exit(1);
129
- }
130
- const config = mergeSinglePackageConfig(userConfig);
131
- runAndReport(() => releasePrepare(config, options), dryRun);
150
+ runSinglePackageMode(userConfig, options, only, dryRun);
132
151
  } else {
133
- const config = mergeMonorepoConfig(discoveredPaths, userConfig);
134
- if (only !== void 0) {
135
- const knownNames = config.components.map((c) => c.dir);
136
- for (const name of only) {
137
- if (!knownNames.includes(name)) {
138
- console.error(`Error: Unknown component "${name}". Known components: ${knownNames.join(", ")}`);
139
- process.exit(1);
140
- }
152
+ runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion, dryRun);
153
+ }
154
+ }
155
+ function runSinglePackageMode(userConfig, options, only, dryRun) {
156
+ if (only !== void 0) {
157
+ console.error("Error: --only is only supported for monorepo configurations");
158
+ process.exit(1);
159
+ }
160
+ const config = mergeSinglePackageConfig(userConfig);
161
+ runAndReport(() => releasePrepare(config, options), dryRun);
162
+ }
163
+ function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion, dryRun) {
164
+ let config;
165
+ try {
166
+ config = mergeMonorepoConfig(discoveredPaths, userConfig);
167
+ } catch (error) {
168
+ console.error(`Error resolving workspaces: ${error instanceof Error ? error.message : String(error)}`);
169
+ process.exit(1);
170
+ }
171
+ if (only !== void 0) {
172
+ const knownNames = config.workspaces.map((w) => w.dir);
173
+ for (const name of only) {
174
+ if (!knownNames.includes(name)) {
175
+ console.error(`Error: Unknown workspace "${name}". Known workspaces: ${knownNames.join(", ")}`);
176
+ process.exit(1);
141
177
  }
142
- config.components = config.components.filter((c) => only.includes(c.dir));
143
178
  }
144
- runAndReport(() => releasePrepareMono(config, options), dryRun);
179
+ config.workspaces = config.workspaces.filter((w) => only.includes(w.dir));
180
+ }
181
+ if (setVersion !== void 0) {
182
+ if (only === void 0) {
183
+ console.error("Error: --set-version requires --only in monorepo mode");
184
+ process.exit(1);
185
+ }
186
+ if (config.workspaces.length !== 1) {
187
+ console.error(
188
+ `Error: --set-version requires --only to match exactly one workspace; matched ${config.workspaces.length}`
189
+ );
190
+ process.exit(1);
191
+ }
145
192
  }
193
+ runAndReport(() => releasePrepareMono(config, options), dryRun);
146
194
  }
147
195
  async function loadAndValidateConfig() {
148
196
  let rawConfig;