@williamthorsen/release-kit 4.7.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 (87) hide show
  1. package/CHANGELOG.md +80 -0
  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 +67 -12
  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 +9 -3
  30. package/dist/esm/index.js +12 -3
  31. package/dist/esm/init/detectRepoType.js +1 -2
  32. package/dist/esm/init/initCommand.js +1 -1
  33. package/dist/esm/init/scaffold.d.ts +1 -1
  34. package/dist/esm/init/scaffold.js +8 -5
  35. package/dist/esm/init/templates.d.ts +1 -0
  36. package/dist/esm/init/templates.js +40 -10
  37. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +6 -1
  38. package/dist/esm/injectReleaseNotesIntoReadme.js +20 -7
  39. package/dist/esm/loadConfig.d.ts +2 -1
  40. package/dist/esm/loadConfig.js +65 -12
  41. package/dist/esm/parseRequestedTags.d.ts +1 -0
  42. package/dist/esm/parseRequestedTags.js +10 -0
  43. package/dist/esm/prepareCommand.d.ts +3 -1
  44. package/dist/esm/prepareCommand.js +75 -27
  45. package/dist/esm/previewTagPrefixes.d.ts +30 -0
  46. package/dist/esm/previewTagPrefixes.js +120 -0
  47. package/dist/esm/propagateBumps.d.ts +1 -0
  48. package/dist/esm/propagateBumps.js +1 -1
  49. package/dist/esm/publishCommand.js +8 -13
  50. package/dist/esm/pushCommand.d.ts +1 -0
  51. package/dist/esm/pushCommand.js +47 -0
  52. package/dist/esm/pushRelease.d.ts +11 -0
  53. package/dist/esm/pushRelease.js +26 -0
  54. package/dist/esm/readCurrentVersion.d.ts +1 -0
  55. package/dist/esm/readCurrentVersion.js +21 -0
  56. package/dist/esm/releasePrepare.d.ts +2 -0
  57. package/dist/esm/releasePrepare.js +72 -30
  58. package/dist/esm/releasePrepareMono.js +235 -112
  59. package/dist/esm/renderReleaseNotes.d.ts +1 -0
  60. package/dist/esm/renderReleaseNotes.js +29 -2
  61. package/dist/esm/reportPrepare.js +100 -73
  62. package/dist/esm/resolveCliffConfigPath.js +1 -1
  63. package/dist/esm/resolveCommandTags.d.ts +1 -1
  64. package/dist/esm/resolveCommandTags.js +17 -13
  65. package/dist/esm/resolveReleaseNotesConfig.d.ts +8 -1
  66. package/dist/esm/resolveReleaseNotesConfig.js +17 -7
  67. package/dist/esm/resolveReleaseTags.d.ts +2 -1
  68. package/dist/esm/resolveReleaseTags.js +19 -14
  69. package/dist/esm/showTagPrefixesCommand.d.ts +1 -0
  70. package/dist/esm/showTagPrefixesCommand.js +84 -0
  71. package/dist/esm/sync-labels/initCommand.js +1 -1
  72. package/dist/esm/sync-labels/presets.js +1 -1
  73. package/dist/esm/sync-labels/templates.js +1 -1
  74. package/dist/esm/tagCommand.js +1 -1
  75. package/dist/esm/types.d.ts +22 -7
  76. package/dist/esm/validateConfig.js +179 -36
  77. package/dist/esm/version.d.ts +1 -1
  78. package/dist/esm/version.js +1 -1
  79. package/dist/esm/writeReleaseNotesPreviews.d.ts +18 -0
  80. package/dist/esm/writeReleaseNotesPreviews.js +65 -0
  81. package/package.json +2 -2
  82. package/dist/esm/component.d.ts +0 -2
  83. package/dist/esm/component.js +0 -14
  84. package/dist/esm/findPackageRoot.d.ts +0 -1
  85. package/dist/esm/findPackageRoot.js +0 -17
  86. package/dist/esm/githubReleaseCommand.d.ts +0 -1
  87. package/dist/esm/githubReleaseCommand.js +0 -35
@@ -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
28
- --force Bypass the "no commits since last tag" check (monorepo only, requires --bump)
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.
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;
@@ -0,0 +1,30 @@
1
+ import type { UndeclaredTagPrefix } from './detectUndeclaredTagPrefixes.ts';
2
+ export interface TagPrefixPreviewRow {
3
+ workspacePath: string;
4
+ dir: string;
5
+ derivedPrefix: string | null;
6
+ derivationError: string | null;
7
+ derivedTagCount: number;
8
+ legacyEntries: LegacyTagPrefixEntry[];
9
+ }
10
+ export interface LegacyTagPrefixEntry {
11
+ prefix: string;
12
+ tagCount: number;
13
+ }
14
+ export interface RetiredPackagePreviewEntry {
15
+ name: string;
16
+ tagPrefix: string;
17
+ successor?: string;
18
+ tagCount: number;
19
+ }
20
+ export interface TagPrefixCollision {
21
+ tagPrefix: string;
22
+ workspacePaths: string[];
23
+ }
24
+ export interface TagPrefixPreview {
25
+ workspaces: TagPrefixPreviewRow[];
26
+ collisions: TagPrefixCollision[];
27
+ undeclaredCandidates: UndeclaredTagPrefix[];
28
+ retiredPackages: RetiredPackagePreviewEntry[];
29
+ }
30
+ export declare function previewTagPrefixes(): Promise<TagPrefixPreview>;
@@ -0,0 +1,120 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { basename } from "node:path";
3
+ import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
4
+ import { detectUndeclaredTagPrefixes } from "./detectUndeclaredTagPrefixes.js";
5
+ import { discoverWorkspaces } from "./discoverWorkspaces.js";
6
+ import { loadConfig } from "./loadConfig.js";
7
+ import { validateConfig } from "./validateConfig.js";
8
+ async function previewTagPrefixes() {
9
+ const workspacePaths = await discoverWorkspaces() ?? [];
10
+ const userConfig = await loadUserConfig();
11
+ const overridesByDir = buildOverrideMap(userConfig);
12
+ const workspaces = [];
13
+ for (const workspacePath of workspacePaths) {
14
+ workspaces.push(buildPreviewRow(workspacePath, overridesByDir));
15
+ }
16
+ const retiredPackages = buildRetiredPreviewEntries(userConfig?.retiredPackages ?? []);
17
+ const collisions = detectCollisions(workspaces);
18
+ const knownPrefixes = collectKnownPrefixes(workspaces, retiredPackages);
19
+ const undeclaredCandidates = detectUndeclaredTagPrefixes(knownPrefixes);
20
+ return { workspaces, collisions, undeclaredCandidates, retiredPackages };
21
+ }
22
+ async function loadUserConfig() {
23
+ let raw;
24
+ try {
25
+ raw = await loadConfig();
26
+ } catch {
27
+ return void 0;
28
+ }
29
+ if (raw === void 0) return void 0;
30
+ const { config, errors } = validateConfig(raw);
31
+ return errors.length === 0 ? config : void 0;
32
+ }
33
+ function buildOverrideMap(userConfig) {
34
+ const map = /* @__PURE__ */ new Map();
35
+ if (userConfig?.workspaces === void 0) return map;
36
+ for (const entry of userConfig.workspaces) {
37
+ if (entry.legacyIdentities !== void 0) {
38
+ map.set(entry.dir, entry.legacyIdentities);
39
+ }
40
+ }
41
+ return map;
42
+ }
43
+ function buildPreviewRow(workspacePath, overridesByDir) {
44
+ const dir = basename(workspacePath);
45
+ let derivedPrefix = null;
46
+ let derivationError = null;
47
+ try {
48
+ derivedPrefix = deriveWorkspaceConfig(workspacePath).tagPrefix;
49
+ } catch (error) {
50
+ derivationError = error instanceof Error ? error.message : String(error);
51
+ }
52
+ const derivedTagCount = derivedPrefix === null ? 0 : countTagsMatching(derivedPrefix);
53
+ const declaredIdentities = overridesByDir.get(dir) ?? [];
54
+ const legacyEntries = declaredIdentities.map((identity) => ({
55
+ prefix: identity.tagPrefix,
56
+ tagCount: countTagsMatching(identity.tagPrefix)
57
+ }));
58
+ return {
59
+ workspacePath,
60
+ dir,
61
+ derivedPrefix,
62
+ derivationError,
63
+ derivedTagCount,
64
+ legacyEntries
65
+ };
66
+ }
67
+ function countTagsMatching(prefix) {
68
+ try {
69
+ const output = execFileSync("git", ["tag", "--list", `${prefix}*`], {
70
+ encoding: "utf8",
71
+ stdio: ["pipe", "pipe", "pipe"]
72
+ });
73
+ return output.split("\n").filter((line) => line.trim() !== "").length;
74
+ } catch {
75
+ return 0;
76
+ }
77
+ }
78
+ function detectCollisions(rows) {
79
+ const pathsByPrefix = /* @__PURE__ */ new Map();
80
+ for (const row of rows) {
81
+ if (row.derivedPrefix === null) continue;
82
+ const existing = pathsByPrefix.get(row.derivedPrefix);
83
+ if (existing === void 0) {
84
+ pathsByPrefix.set(row.derivedPrefix, [row.workspacePath]);
85
+ } else {
86
+ existing.push(row.workspacePath);
87
+ }
88
+ }
89
+ const collisions = [];
90
+ for (const [tagPrefix, workspacePaths] of pathsByPrefix) {
91
+ if (workspacePaths.length > 1) {
92
+ collisions.push({ tagPrefix, workspacePaths });
93
+ }
94
+ }
95
+ return collisions;
96
+ }
97
+ function collectKnownPrefixes(rows, retiredPackages) {
98
+ const known = /* @__PURE__ */ new Set();
99
+ for (const row of rows) {
100
+ if (row.derivedPrefix !== null) known.add(row.derivedPrefix);
101
+ for (const entry of row.legacyEntries) {
102
+ known.add(entry.prefix);
103
+ }
104
+ }
105
+ for (const retired of retiredPackages) {
106
+ known.add(retired.tagPrefix);
107
+ }
108
+ return [...known];
109
+ }
110
+ function buildRetiredPreviewEntries(retiredPackages) {
111
+ return retiredPackages.map((retired) => ({
112
+ name: retired.name,
113
+ tagPrefix: retired.tagPrefix,
114
+ tagCount: countTagsMatching(retired.tagPrefix),
115
+ ...retired.successor !== void 0 ? { successor: retired.successor } : {}
116
+ }));
117
+ }
118
+ export {
119
+ previewTagPrefixes
120
+ };
@@ -3,6 +3,7 @@ import type { PropagationSource, ReleaseType } from './types.ts';
3
3
  export interface ReleaseEntry {
4
4
  releaseType: ReleaseType;
5
5
  propagatedFrom?: PropagationSource[];
6
+ newVersionOverride?: string;
6
7
  }
7
8
  export type CurrentVersions = Map<string, string>;
8
9
  export declare function propagateBumps(directBumps: Map<string, ReleaseEntry>, graph: DependencyGraph, currentVersions: CurrentVersions): Map<string, ReleaseEntry>;
@@ -24,7 +24,7 @@ function propagateBumps(directBumps, graph, currentVersions) {
24
24
  if (currentVersion === void 0 || entry === void 0) {
25
25
  continue;
26
26
  }
27
- const newVersion = bumpVersion(currentVersion, entry.releaseType);
27
+ const newVersion = entry.newVersionOverride ?? bumpVersion(currentVersion, entry.releaseType);
28
28
  const dependents = graph.dependentsOf.get(packageName);
29
29
  if (dependents === void 0) {
30
30
  continue;
@@ -1,9 +1,9 @@
1
1
  import { writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { parseArgs, translateParseError } from "@williamthorsen/node-monorepo-core";
4
- import { createGithubReleases } from "./createGithubRelease.js";
3
+ import { parseArgs, translateParseError } from "@williamthorsen/nmr-core";
5
4
  import { detectPackageManager } from "./detectPackageManager.js";
6
5
  import { injectReleaseNotesIntoReadme, resolveReadmePath } from "./injectReleaseNotesIntoReadme.js";
6
+ import { parseRequestedTags } from "./parseRequestedTags.js";
7
7
  import { publishPackage } from "./publish.js";
8
8
  import { resolveCommandTags } from "./resolveCommandTags.js";
9
9
  import { resolveReleaseNotesConfig } from "./resolveReleaseNotesConfig.js";
@@ -11,7 +11,7 @@ const publishFlagSchema = {
11
11
  dryRun: { long: "--dry-run", type: "boolean" },
12
12
  noGitChecks: { long: "--no-git-checks", type: "boolean" },
13
13
  provenance: { long: "--provenance", type: "boolean" },
14
- only: { long: "--only", type: "string" }
14
+ tags: { long: "--tags", type: "string" }
15
15
  };
16
16
  async function publishCommand(argv) {
17
17
  let parsed;
@@ -22,13 +22,13 @@ async function publishCommand(argv) {
22
22
  process.exit(1);
23
23
  }
24
24
  const { dryRun, noGitChecks, provenance } = parsed.flags;
25
- const only = parsed.flags.only?.split(",");
26
- const resolvedTags = await resolveCommandTags(only);
25
+ const requestedTags = parseRequestedTags(parsed.flags.tags);
26
+ const resolvedTags = await resolveCommandTags(requestedTags);
27
27
  if (resolvedTags.length === 0) {
28
28
  return;
29
29
  }
30
30
  const packageManager = detectPackageManager();
31
- const { releaseNotes, changelogJsonOutputPath } = await resolveReleaseNotesConfig();
31
+ const { releaseNotes, changelogJsonOutputPath, sectionOrder } = await resolveReleaseNotesConfig();
32
32
  const shouldInject = releaseNotes.shouldInjectIntoReadme;
33
33
  console.info(dryRun ? "[dry-run] Would publish:" : "Publishing:");
34
34
  for (const { tag, workspacePath } of resolvedTags) {
@@ -45,7 +45,8 @@ async function publishCommand(argv) {
45
45
  originalReadme = injectReleaseNotesIntoReadme(
46
46
  readmePath,
47
47
  join(resolvedTag.workspacePath, changelogJsonOutputPath),
48
- resolvedTag.tag
48
+ resolvedTag.tag,
49
+ sectionOrder
49
50
  );
50
51
  }
51
52
  }
@@ -68,12 +69,6 @@ async function publishCommand(argv) {
68
69
  console.error(error instanceof Error ? error.message : String(error));
69
70
  process.exit(1);
70
71
  }
71
- try {
72
- createGithubReleases(resolvedTags, releaseNotes, changelogJsonOutputPath, dryRun);
73
- } catch (error) {
74
- console.error(`Error creating GitHub Releases: ${error instanceof Error ? error.message : String(error)}`);
75
- process.exit(1);
76
- }
77
72
  }
78
73
  export {
79
74
  publishCommand
@@ -0,0 +1 @@
1
+ export declare function pushCommand(argv: string[]): Promise<void>;
@@ -0,0 +1,47 @@
1
+ import { parseArgs, translateParseError } from "@williamthorsen/nmr-core";
2
+ import { parseRequestedTags } from "./parseRequestedTags.js";
3
+ import { pushRelease } from "./pushRelease.js";
4
+ import { resolveCommandTags } from "./resolveCommandTags.js";
5
+ const pushFlagSchema = {
6
+ dryRun: { long: "--dry-run", type: "boolean" },
7
+ tags: { long: "--tags", type: "string" },
8
+ tagsOnly: { long: "--tags-only", type: "boolean" }
9
+ };
10
+ async function pushCommand(argv) {
11
+ let parsed;
12
+ try {
13
+ parsed = parseArgs(argv, pushFlagSchema);
14
+ } catch (error) {
15
+ console.error(`Error: ${translateParseError(error)}`);
16
+ process.exit(1);
17
+ }
18
+ const { dryRun, tagsOnly } = parsed.flags;
19
+ const requestedTags = parseRequestedTags(parsed.flags.tags);
20
+ const resolvedTags = await resolveCommandTags(requestedTags);
21
+ if (resolvedTags.length === 0) {
22
+ return;
23
+ }
24
+ const prefix = dryRun ? "[dry-run] Would push" : "Pushing";
25
+ if (!tagsOnly) {
26
+ console.info(`${prefix} branch and ${resolvedTags.length} tag(s):`);
27
+ } else {
28
+ console.info(`${prefix} ${resolvedTags.length} tag(s):`);
29
+ }
30
+ for (const { tag } of resolvedTags) {
31
+ console.info(` ${tag}`);
32
+ }
33
+ try {
34
+ const steps = pushRelease(resolvedTags, { dryRun, tagsOnly });
35
+ if (dryRun) {
36
+ for (const step of steps) {
37
+ console.info(`[dry-run] ${step.command.join(" ")}`);
38
+ }
39
+ }
40
+ } catch (error) {
41
+ console.error(error instanceof Error ? error.message : String(error));
42
+ process.exit(1);
43
+ }
44
+ }
45
+ export {
46
+ pushCommand
47
+ };
@@ -0,0 +1,11 @@
1
+ import type { ResolvedTag } from './resolveReleaseTags.ts';
2
+ export interface PushReleaseOptions {
3
+ dryRun?: boolean;
4
+ tagsOnly?: boolean;
5
+ }
6
+ export interface PushStep {
7
+ type: 'branch' | 'tag';
8
+ ref: string;
9
+ command: readonly [string, ...string[]];
10
+ }
11
+ export declare function pushRelease(resolvedTags: ResolvedTag[], options?: PushReleaseOptions): PushStep[];
@@ -0,0 +1,26 @@
1
+ import { execFileSync } from "node:child_process";
2
+ function pushRelease(resolvedTags, options = {}) {
3
+ const { dryRun = false, tagsOnly = false } = options;
4
+ const steps = [];
5
+ if (!tagsOnly) {
6
+ const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
7
+ encoding: "utf8",
8
+ stdio: ["pipe", "pipe", "pipe"]
9
+ }).trim();
10
+ const command = ["git", "push", "origin", branch];
11
+ steps.push({ type: "branch", ref: branch, command });
12
+ }
13
+ for (const { tag } of resolvedTags) {
14
+ const command = ["git", "push", "--no-follow-tags", "origin", tag];
15
+ steps.push({ type: "tag", ref: tag, command });
16
+ }
17
+ if (!dryRun) {
18
+ for (const step of steps) {
19
+ execFileSync(step.command[0], step.command.slice(1), { stdio: "inherit" });
20
+ }
21
+ }
22
+ return steps;
23
+ }
24
+ export {
25
+ pushRelease
26
+ };
@@ -0,0 +1 @@
1
+ export declare function readCurrentVersion(filePath: string): string | undefined;
@@ -0,0 +1,21 @@
1
+ import { readFileSync } from "node:fs";
2
+ function hasVersionField(value) {
3
+ return typeof value === "object" && value !== null && "version" in value && typeof value.version === "string";
4
+ }
5
+ function readCurrentVersion(filePath) {
6
+ try {
7
+ const content = readFileSync(filePath, "utf8");
8
+ const parsed = JSON.parse(content);
9
+ if (hasVersionField(parsed)) {
10
+ return parsed.version;
11
+ }
12
+ } catch (error) {
13
+ console.warn(
14
+ `Failed to read current version from ${filePath}: ${error instanceof Error ? error.message : String(error)}`
15
+ );
16
+ }
17
+ return void 0;
18
+ }
19
+ export {
20
+ readCurrentVersion
21
+ };
@@ -3,5 +3,7 @@ export interface ReleasePrepareOptions {
3
3
  dryRun: boolean;
4
4
  force?: boolean;
5
5
  bumpOverride?: ReleaseType;
6
+ setVersion?: string;
7
+ withReleaseNotes?: boolean;
6
8
  }
7
9
  export declare function releasePrepare(config: ReleaseConfig, options: ReleasePrepareOptions): PrepareResult;
@@ -1,46 +1,67 @@
1
1
  import { execSync } from "node:child_process";
2
- import { bumpAllVersions } from "./bumpAllVersions.js";
2
+ import { bumpAllVersions, setAllVersions } from "./bumpAllVersions.js";
3
+ import { isForwardVersion } from "./compareVersions.js";
3
4
  import { DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
4
5
  import { determineBumpFromCommits } from "./determineBumpFromCommits.js";
5
6
  import { generateChangelogJson } from "./generateChangelogJson.js";
6
7
  import { generateChangelogs } from "./generateChangelogs.js";
7
8
  import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
8
9
  import { hasPrettierConfig } from "./hasPrettierConfig.js";
10
+ import { resolveWorkTypes } from "./loadConfig.js";
11
+ import { readCurrentVersion } from "./readCurrentVersion.js";
12
+ import { deriveSectionOrder } from "./resolveReleaseNotesConfig.js";
13
+ import { writeReleaseNotesPreviews } from "./writeReleaseNotesPreviews.js";
9
14
  function releasePrepare(config, options) {
10
- const { dryRun, bumpOverride } = options;
15
+ const { dryRun, bumpOverride, setVersion, withReleaseNotes } = options;
11
16
  const workTypes = config.workTypes ?? { ...DEFAULT_WORK_TYPES };
12
17
  const versionPatterns = config.versionPatterns ?? { ...DEFAULT_VERSION_PATTERNS };
13
- const { tag, commits } = getCommitsSinceTarget(config.tagPrefix);
18
+ const { tag, commits } = getCommitsSinceTarget([config.tagPrefix]);
14
19
  let releaseType;
15
20
  let parsedCommitCount;
16
21
  let unparseableCommits;
17
- if (bumpOverride === void 0) {
18
- const determination = determineBumpFromCommits(commits, workTypes, versionPatterns, config.scopeAliases);
19
- parsedCommitCount = determination.parsedCommitCount;
20
- unparseableCommits = determination.unparseableCommits;
21
- releaseType = determination.releaseType;
22
+ let bump;
23
+ if (setVersion !== void 0) {
24
+ const primaryPackageFile = config.packageFiles[0];
25
+ if (primaryPackageFile === void 0) {
26
+ throw new Error("No package files specified");
27
+ }
28
+ const currentVersion = readCurrentVersion(primaryPackageFile);
29
+ if (currentVersion === void 0) {
30
+ throw new Error(`Cannot validate --set-version: failed to read current version from ${primaryPackageFile}`);
31
+ }
32
+ if (!isForwardVersion(currentVersion, setVersion)) {
33
+ throw new Error(`--set-version ${setVersion} is not greater than current version ${currentVersion}`);
34
+ }
35
+ bump = setAllVersions(config.packageFiles, setVersion, dryRun);
22
36
  } else {
23
- releaseType = bumpOverride;
24
- }
25
- if (releaseType === void 0) {
26
- return {
27
- components: [
28
- {
29
- status: "skipped",
30
- previousTag: tag,
31
- commitCount: commits.length,
32
- parsedCommitCount,
33
- unparseableCommits,
34
- bumpedFiles: [],
35
- changelogFiles: [],
36
- skipReason: "No release-worthy changes found. Skipping."
37
- }
38
- ],
39
- tags: [],
40
- dryRun
41
- };
37
+ if (bumpOverride === void 0) {
38
+ const determination = determineBumpFromCommits(commits, workTypes, versionPatterns, config.scopeAliases);
39
+ parsedCommitCount = determination.parsedCommitCount;
40
+ unparseableCommits = determination.unparseableCommits;
41
+ releaseType = determination.releaseType;
42
+ } else {
43
+ releaseType = bumpOverride;
44
+ }
45
+ if (releaseType === void 0) {
46
+ return {
47
+ workspaces: [
48
+ {
49
+ status: "skipped",
50
+ previousTag: tag,
51
+ commitCount: commits.length,
52
+ parsedCommitCount,
53
+ unparseableCommits,
54
+ bumpedFiles: [],
55
+ changelogFiles: [],
56
+ skipReason: "No release-worthy changes found. Skipping."
57
+ }
58
+ ],
59
+ tags: [],
60
+ dryRun
61
+ };
62
+ }
63
+ bump = bumpAllVersions(config.packageFiles, releaseType, dryRun);
42
64
  }
43
- const bump = bumpAllVersions(config.packageFiles, releaseType, dryRun);
44
65
  const newTag = `${config.tagPrefix}${bump.newVersion}`;
45
66
  const changelogFiles = generateChangelogs(config, newTag, dryRun);
46
67
  const changelogJsonFiles = [];
@@ -49,6 +70,7 @@ function releasePrepare(config, options) {
49
70
  changelogJsonFiles.push(...generateChangelogJson(config, changelogPath, newTag, dryRun));
50
71
  }
51
72
  }
73
+ maybeWriteSinglePackagePreviews(withReleaseNotes === true, config, newTag, changelogJsonFiles[0], dryRun);
52
74
  const formatCommandStr = config.formatCommand ?? (hasPrettierConfig() ? "npx prettier --write" : void 0);
53
75
  let formatCommand;
54
76
  if (formatCommandStr !== void 0) {
@@ -72,7 +94,7 @@ function releasePrepare(config, options) {
72
94
  }
73
95
  }
74
96
  return {
75
- components: [
97
+ workspaces: [
76
98
  {
77
99
  status: "released",
78
100
  previousTag: tag,
@@ -85,7 +107,8 @@ function releasePrepare(config, options) {
85
107
  bumpedFiles: bump.files,
86
108
  changelogFiles,
87
109
  commits,
88
- unparseableCommits
110
+ unparseableCommits,
111
+ ...setVersion === void 0 ? {} : { setVersion }
89
112
  }
90
113
  ],
91
114
  tags: [newTag],
@@ -93,6 +116,25 @@ function releasePrepare(config, options) {
93
116
  dryRun
94
117
  };
95
118
  }
119
+ function maybeWriteSinglePackagePreviews(withReleaseNotes, config, newTag, changelogJsonPath, dryRun) {
120
+ if (!withReleaseNotes) {
121
+ return;
122
+ }
123
+ if (!config.changelogJson.enabled) {
124
+ console.warn("Warning: --with-release-notes requires changelogJson.enabled; skipping preview generation");
125
+ return;
126
+ }
127
+ if (changelogJsonPath === void 0) {
128
+ return;
129
+ }
130
+ writeReleaseNotesPreviews({
131
+ workspacePath: process.cwd(),
132
+ tag: newTag,
133
+ changelogJsonPath,
134
+ sectionOrder: deriveSectionOrder(resolveWorkTypes(config.workTypes)),
135
+ dryRun
136
+ });
137
+ }
96
138
  export {
97
139
  releasePrepare
98
140
  };