versionary 0.32.0 → 1.0.1

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.
@@ -57,9 +57,9 @@ function setOutput(name, value) {
57
57
  appendFileSync(outputPath, `${name}<<${delimiter}\n${value}\n${delimiter}\n`, "utf8");
58
58
  }
59
59
  function main() {
60
- const token = getInput("token") || getInput("github-token");
60
+ const token = getInput("token");
61
61
  if (!token) {
62
- throw new Error("Input required and not supplied: token (or deprecated github-token).");
62
+ throw new Error("Input required and not supplied: token.");
63
63
  }
64
64
  const versionaryVersion = getInput("versionary-version") || "0.7.0";
65
65
  const cwd = getInput("working-directory") || ".";
package/dist/cli/index.js CHANGED
@@ -1,11 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFileSync } from "node:child_process";
3
- import { loadConfig } from "../config/load-config.js";
4
3
  import { prependChangelog, renderReleasePlanChangelog, } from "../release/changelog.js";
5
4
  import { createReleasePlan } from "../release/plan.js";
6
- import { closeStaleReviewRequestIfExists, consumeNextReleaseFile, isReleaseCommitMessage, openOrUpdateReviewRequest, prepareReleasePr, pushReleaseBranch, resolveReleaseHighlights, } from "../release/pr.js";
5
+ import { closeStaleReviewRequestIfExists, isReleaseCommitMessage, openOrUpdateReviewRequest, prepareReleasePr, pushReleaseBranch, resolveReleaseHighlights, } from "../release/pr.js";
7
6
  import { runRelease, runReleaseDetailed } from "../release/release.js";
8
7
  import { verifyProject } from "../release/verify-project.js";
8
+ /**
9
+ * What a dry run would actually release. `plan.nextVersion` is the repository
10
+ * aggregate, which in a monorepo names no real release, so prefer the
11
+ * per-package versions whenever the plan has them.
12
+ */
13
+ function formatDryRunReleaseSubject(plan) {
14
+ const releasing = (plan.packages ?? []).filter((pkg) => pkg.nextVersion);
15
+ if (releasing.length === 0) {
16
+ return plan.nextVersion ?? "";
17
+ }
18
+ return releasing
19
+ .map((pkg) => `${pkg.path === "." ? plan.packageName : pkg.path} ${pkg.nextVersion}`)
20
+ .join(", ");
21
+ }
9
22
  function printVerifyResult() {
10
23
  const result = verifyProject();
11
24
  const categories = [
@@ -115,7 +128,7 @@ async function main() {
115
128
  return 0;
116
129
  }
117
130
  if (flags["dry-run"]) {
118
- const dryRunMessage = `Dry run: would prepare release PR branch ${plan.releaseBranchPrefix} for ${plan.nextVersion}`;
131
+ const dryRunMessage = `Dry run: would prepare release PR branch ${plan.releaseBranchPrefix} for ${formatDryRunReleaseSubject(plan)}`;
119
132
  if (flags.json) {
120
133
  emitJson({
121
134
  action: "pr-dry-run",
@@ -195,8 +208,7 @@ async function main() {
195
208
  console.log("No releasable commits found.");
196
209
  return 0;
197
210
  }
198
- const loaded = loadConfig();
199
- const highlightsResult = resolveReleaseHighlights(process.cwd(), loaded.config, plan.changelogFile, plan.changelogFormat, logger);
211
+ const highlightsResult = resolveReleaseHighlights(process.cwd(), plan.changelogFile, plan.changelogFormat);
200
212
  const section = renderReleasePlanChangelog(plan, {
201
213
  highlights: highlightsResult.highlights,
202
214
  });
@@ -205,9 +217,6 @@ async function main() {
205
217
  return 0;
206
218
  }
207
219
  prependChangelog(process.cwd(), plan.changelogFile, section, plan.changelogFormat);
208
- if (highlightsResult.source === "file" && highlightsResult.filePath) {
209
- consumeNextReleaseFile(process.cwd(), highlightsResult.filePath);
210
- }
211
220
  console.log(`Updated ${plan.changelogFile}`);
212
221
  return 0;
213
222
  }
@@ -223,7 +232,7 @@ async function main() {
223
232
  return 0;
224
233
  }
225
234
  if (flags["dry-run"]) {
226
- console.log(`Dry run: would prepare release PR branch ${plan.releaseBranchPrefix} for ${plan.nextVersion}`);
235
+ console.log(`Dry run: would prepare release PR branch ${plan.releaseBranchPrefix} for ${formatDryRunReleaseSubject(plan)}`);
227
236
  return 0;
228
237
  }
229
238
  const pr = prepareReleasePr(process.cwd(), { logger: console });
@@ -5,7 +5,6 @@ export declare const configSchema: z.ZodObject<{
5
5
  "review-mode": z.ZodOptional<z.ZodEnum<{
6
6
  direct: "direct";
7
7
  pr: "pr";
8
- review: "review";
9
8
  }>>;
10
9
  "version-file": z.ZodOptional<z.ZodString>;
11
10
  "changelog-file": z.ZodOptional<z.ZodString>;
@@ -15,17 +14,16 @@ export declare const configSchema: z.ZodObject<{
15
14
  }>>;
16
15
  "release-draft": z.ZodOptional<z.ZodBoolean>;
17
16
  "release-reference-comments": z.ZodOptional<z.ZodEnum<{
18
- off: "off";
19
17
  "best-effort": "best-effort";
18
+ off: "off";
20
19
  strict: "strict";
21
20
  }>>;
22
21
  "release-branch": z.ZodOptional<z.ZodString>;
23
22
  "baseline-file": z.ZodOptional<z.ZodString>;
24
- "next-release-file": z.ZodOptional<z.ZodString>;
25
23
  "bootstrap-sha": z.ZodOptional<z.ZodString>;
26
24
  "monorepo-mode": z.ZodOptional<z.ZodEnum<{
27
- independent: "independent";
28
25
  fixed: "fixed";
26
+ independent: "independent";
29
27
  }>>;
30
28
  "bump-minor-pre-major": z.ZodOptional<z.ZodBoolean>;
31
29
  "allow-stable-major": z.ZodOptional<z.ZodBoolean>;
@@ -45,14 +43,13 @@ export declare const configSchema: z.ZodObject<{
45
43
  "extra-files": z.ZodOptional<z.ZodArray<z.ZodObject<{
46
44
  type: z.ZodEnum<{
47
45
  json: "json";
48
- toml: "toml";
49
- yaml: "yaml";
50
46
  nix: "nix";
51
47
  regex: "regex";
48
+ toml: "toml";
49
+ yaml: "yaml";
52
50
  }>;
53
51
  path: z.ZodString;
54
52
  "field-path": z.ZodOptional<z.ZodString>;
55
- jsonpath: z.ZodOptional<z.ZodString>;
56
53
  pattern: z.ZodOptional<z.ZodString>;
57
54
  replacement: z.ZodOptional<z.ZodString>;
58
55
  }, z.core.$strip>>>;
@@ -4,44 +4,35 @@ const artifactRuleSchema = z
4
4
  type: z.enum(["json", "toml", "yaml", "nix", "regex"]),
5
5
  path: z.string().min(1),
6
6
  "field-path": z.string().optional(),
7
- jsonpath: z.string().optional(),
8
7
  pattern: z.string().optional(),
9
8
  replacement: z.string().optional(),
10
9
  })
11
10
  .superRefine((value, ctx) => {
12
- const needsJsonPath = value.type === "json" ||
11
+ const needsFieldPath = value.type === "json" ||
13
12
  value.type === "toml" ||
14
13
  value.type === "yaml" ||
15
14
  value.type === "nix";
16
- const hasFieldPath = Boolean(value["field-path"] ?? value.jsonpath);
17
- if (needsJsonPath && !hasFieldPath) {
15
+ if (needsFieldPath && !value["field-path"]) {
18
16
  ctx.addIssue({
19
17
  code: z.ZodIssueCode.custom,
20
- message: `${value.type} artifact rules require "field-path" (or deprecated "jsonpath").`,
18
+ message: `${value.type} artifact rules require "field-path".`,
21
19
  path: ["field-path"],
22
20
  });
23
21
  }
24
- if (needsJsonPath && value.pattern) {
22
+ if (needsFieldPath && value.pattern) {
25
23
  ctx.addIssue({
26
24
  code: z.ZodIssueCode.custom,
27
25
  message: `${value.type} artifact rules do not support "pattern".`,
28
26
  path: ["pattern"],
29
27
  });
30
28
  }
31
- if (needsJsonPath && value.replacement) {
29
+ if (needsFieldPath && value.replacement) {
32
30
  ctx.addIssue({
33
31
  code: z.ZodIssueCode.custom,
34
32
  message: `${value.type} artifact rules do not support "replacement".`,
35
33
  path: ["replacement"],
36
34
  });
37
35
  }
38
- if (value["field-path"] && value.jsonpath) {
39
- ctx.addIssue({
40
- code: z.ZodIssueCode.custom,
41
- message: 'Specify only one of "field-path" or deprecated "jsonpath".',
42
- path: ["field-path"],
43
- });
44
- }
45
36
  if (value.type === "regex" && !value.pattern) {
46
37
  ctx.addIssue({
47
38
  code: z.ZodIssueCode.custom,
@@ -49,10 +40,10 @@ const artifactRuleSchema = z
49
40
  path: ["pattern"],
50
41
  });
51
42
  }
52
- if (value.type === "regex" && (value["field-path"] || value.jsonpath)) {
43
+ if (value.type === "regex" && value["field-path"]) {
53
44
  ctx.addIssue({
54
45
  code: z.ZodIssueCode.custom,
55
- message: 'regex artifact rules do not support "field-path" or deprecated "jsonpath".',
46
+ message: 'regex artifact rules do not support "field-path".',
56
47
  path: ["field-path"],
57
48
  });
58
49
  }
@@ -75,7 +66,7 @@ export const configSchema = z
75
66
  .object({
76
67
  $schema: z.string().optional(),
77
68
  version: z.literal(1),
78
- "review-mode": z.enum(["direct", "pr", "review"]).optional(),
69
+ "review-mode": z.enum(["direct", "pr"]).optional(),
79
70
  "version-file": z.string().optional(),
80
71
  "changelog-file": z.string().optional(),
81
72
  "changelog-format": z.enum(["markdown-changelog", "r-news"]).optional(),
@@ -85,7 +76,6 @@ export const configSchema = z
85
76
  .optional(),
86
77
  "release-branch": z.string().optional(),
87
78
  "baseline-file": z.string().optional(),
88
- "next-release-file": z.string().optional(),
89
79
  "bootstrap-sha": z.string().optional(),
90
80
  "monorepo-mode": z.enum(["independent", "fixed"]).optional(),
91
81
  "bump-minor-pre-major": z.boolean().optional(),
@@ -1,3 +1,3 @@
1
1
  import type { VersionaryConfig } from "../types/config.js";
2
- import type { SimplePlan } from "./plan.js";
3
- export declare function applyConfiguredArtifactRules(cwd: string, config: VersionaryConfig, plan: SimplePlan): string[];
2
+ import type { ReleasePlan } from "./plan.js";
3
+ export declare function applyConfiguredArtifactRules(cwd: string, config: VersionaryConfig, plan: ReleasePlan): string[];
@@ -143,9 +143,9 @@ function setVersionAtJsonPath(document, fieldPath, version) {
143
143
  }
144
144
  }
145
145
  function resolveFieldPath(rule) {
146
- const fieldPath = rule["field-path"] ?? rule.jsonpath;
146
+ const fieldPath = rule["field-path"];
147
147
  if (!fieldPath) {
148
- throw new Error(`${rule.type} artifact rules require "field-path" (or deprecated "jsonpath").`);
148
+ throw new Error(`${rule.type} artifact rules require "field-path".`);
149
149
  }
150
150
  return fieldPath;
151
151
  }
@@ -207,8 +207,8 @@ function applyRegexRule(content, pattern, version, replacementTemplate) {
207
207
  const rendered = renderReplacementTemplate(replacementTemplate, version);
208
208
  return `${content.slice(0, start)}${rendered}${content.slice(start + full.length)}`;
209
209
  }
210
- // Legacy behavior: substitute the full version into the first capture group,
211
- // leaving the rest of the match intact. Splice by group indices rather than
210
+ // Without a template: substitute the full version into the first capture
211
+ // group, leaving the rest of the match intact. Splice by group indices rather than
212
212
  // `String.replace` so literal `$` sequences and repeated group content are
213
213
  // handled correctly.
214
214
  const groupIndices = match.indices?.[1];
@@ -1,6 +1,6 @@
1
1
  import { type ParsedCommit } from "../git/commits.js";
2
2
  import type { VersionaryChangelogFormat } from "../types/config.js";
3
- import { type ReleasePlan, type SimplePlan } from "./plan.js";
3
+ import { type ReleasePlan } from "./plan.js";
4
4
  export declare function renderReleaseNotesSection(input: {
5
5
  currentVersion: string;
6
6
  nextVersion: string;
@@ -23,8 +23,6 @@ export declare function renderReleasePlanChangelog(plan: ReleasePlan, options?:
23
23
  cwd?: string;
24
24
  highlights?: string;
25
25
  }): string;
26
- /** @deprecated Use renderReleasePlanChangelog. */
27
- export declare function renderSimpleChangelog(plan: SimplePlan): string;
28
26
  /**
29
27
  * Locate a manual-notes block at the top of an existing changelog and split it
30
28
  * out. The notes block is the first release-level heading whose text is not a
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { applyRevertSuppression, inferReleaseTypeFromParsedCommit, parseConventionalCommitMessage, } from "../git/commits.js";
4
4
  import { resolveRepositoryWebBaseUrl } from "../git/repo-url.js";
5
- import { resolvePackageDependencies, } from "./plan.js";
5
+ import { resolvePackageDependencies, resolveRootReleaseView, } from "./plan.js";
6
6
  import { isVersionHeading } from "./semver.js";
7
7
  const REVIEW_REQUEST_FOOTER = "---\n\nThis PR was generated by [Versionary](https://github.com/jolars/versionary).";
8
8
  function formatDate() {
@@ -68,6 +68,7 @@ function groupCommitLines(commits, repoUrl) {
68
68
  const fixes = [];
69
69
  const performance = [];
70
70
  const reverts = [];
71
+ const other = [];
71
72
  const effectiveCommits = applyRevertSuppression(commits);
72
73
  const getRevertedSubject = (commit) => {
73
74
  const normalizedDescription = (commit.description ?? "")
@@ -93,9 +94,6 @@ function groupCommitLines(commits, repoUrl) {
93
94
  };
94
95
  for (const commit of effectiveCommits) {
95
96
  const type = inferReleaseTypeFromParsedCommit(commit);
96
- if (!type) {
97
- continue;
98
- }
99
97
  const { label, message } = formatCommitMessage(commit.subject);
100
98
  const short = commit.hash.slice(0, 7);
101
99
  const hashLabel = repoUrl
@@ -106,6 +104,10 @@ function groupCommitLines(commits, repoUrl) {
106
104
  const line = `- ${label}${message} (${hashLabel})${referencesSuffix}`;
107
105
  const commitType = (commit.type ?? "").toLowerCase();
108
106
  const isBreaking = type === "major";
107
+ if (!type) {
108
+ other.push(line);
109
+ continue;
110
+ }
109
111
  if (commit.isRevert) {
110
112
  if (!shouldIncludeRevert(commit)) {
111
113
  continue;
@@ -128,7 +130,23 @@ function groupCommitLines(commits, repoUrl) {
128
130
  fixes.push(line);
129
131
  }
130
132
  }
131
- return { breaking, features, fixes, performance, reverts };
133
+ return { breaking, features, fixes, performance, reverts, other };
134
+ }
135
+ /**
136
+ * A forced bump — a stale dependency, or a propagated requirement rewrite —
137
+ * can carry no release-worthy commits at all. Rather than publish a bare
138
+ * version heading, such a release falls back to listing whatever else is
139
+ * shipping under it.
140
+ */
141
+ function needsOtherChangesFallback(grouped, hasHighlights, dependencyCount) {
142
+ if (hasHighlights || dependencyCount > 0 || grouped.other.length === 0) {
143
+ return false;
144
+ }
145
+ return (grouped.breaking.length === 0 &&
146
+ grouped.features.length === 0 &&
147
+ grouped.fixes.length === 0 &&
148
+ grouped.performance.length === 0 &&
149
+ grouped.reverts.length === 0);
132
150
  }
133
151
  export function renderReleaseNotesSection(input, options = {}) {
134
152
  const repoUrl = resolveRepositoryWebBaseUrl(input.cwd ?? process.cwd());
@@ -161,6 +179,9 @@ export function renderReleaseNotesSection(input, options = {}) {
161
179
  if (input.dependencies && input.dependencies.length > 0) {
162
180
  sections.push("### Dependencies", ...input.dependencies.map((dependency) => `- updated ${dependency.name} to v${dependency.version}`), "");
163
181
  }
182
+ if (needsOtherChangesFallback(grouped, trimmedHighlights.length > 0, input.dependencies?.length ?? 0)) {
183
+ sections.push("### Other changes", ...grouped.other, "");
184
+ }
164
185
  const body = [header, "", ...sections].join("\n").trimEnd();
165
186
  if (options.includeFooter) {
166
187
  return `${body}\n\n${renderReviewRequestFooter()}`;
@@ -171,24 +192,27 @@ export function renderReviewRequestFooter() {
171
192
  return REVIEW_REQUEST_FOOTER;
172
193
  }
173
194
  export function renderReleasePlanChangelog(plan, options = {}) {
174
- if (!plan.nextVersion) {
195
+ // Root's changelog describes root's own release, so it must be driven by the
196
+ // root package's version and commit set rather than the plan-level aggregate.
197
+ const root = resolveRootReleaseView(plan);
198
+ if (!root.nextVersion) {
175
199
  return "";
176
200
  }
177
201
  const dedupedCommits = [
178
- ...new Map(plan.commits.map((commit) => [commit.hash, commit])).values(),
202
+ ...new Map(root.commits.map((commit) => [commit.hash, commit])).values(),
179
203
  ];
180
204
  if (plan.changelogFormat === "r-news") {
181
205
  return renderRNewsReleaseNotes({
182
206
  packageName: plan.packageName,
183
- nextVersion: plan.nextVersion,
207
+ nextVersion: root.nextVersion,
184
208
  commits: dedupedCommits,
185
209
  cwd: process.cwd(),
186
210
  highlights: options.highlights,
187
211
  });
188
212
  }
189
213
  return renderReleaseNotesSection({
190
- currentVersion: plan.currentVersion,
191
- nextVersion: plan.nextVersion,
214
+ currentVersion: root.currentVersion,
215
+ nextVersion: root.nextVersion,
192
216
  commits: dedupedCommits,
193
217
  cwd: options.cwd ?? process.cwd(),
194
218
  dependencies: resolvePackageDependencies(plan, "."),
@@ -198,10 +222,6 @@ export function renderReleasePlanChangelog(plan, options = {}) {
198
222
  includeFooter: options.includeFooter,
199
223
  });
200
224
  }
201
- /** @deprecated Use renderReleasePlanChangelog. */
202
- export function renderSimpleChangelog(plan) {
203
- return renderReleasePlanChangelog(plan);
204
- }
205
225
  /**
206
226
  * Locate a manual-notes block at the top of an existing changelog and split it
207
227
  * out. The notes block is the first release-level heading whose text is not a
@@ -292,6 +312,9 @@ export function renderRNewsReleaseNotes(input) {
292
312
  if (grouped.reverts.length > 0) {
293
313
  sections.push("## Reverts", "", ...grouped.reverts, "");
294
314
  }
315
+ if (needsOtherChangesFallback(grouped, trimmedHighlights.length > 0, 0)) {
316
+ sections.push("## Other changes", "", ...grouped.other, "");
317
+ }
295
318
  return [`# ${input.packageName} ${normalizedVersion}`, "", ...sections]
296
319
  .join("\n")
297
320
  .trimEnd();
@@ -1,6 +1,7 @@
1
1
  import { type ParsedCommit } from "../git/commits.js";
2
2
  import type { VersionaryChangelogFormat, VersionaryConfig } from "../types/config.js";
3
3
  import { type ReleaseType } from "./semver.js";
4
+ type BumpReason = "direct" | "dependency-propagation" | "stale-dependency" | "follows" | "release-as";
4
5
  export interface ReleasePlan {
5
6
  mode: "simple";
6
7
  releaseType: ReleaseType;
@@ -18,13 +19,11 @@ export interface ReleasePlan {
18
19
  releaseType: ReleaseType;
19
20
  currentVersion: string;
20
21
  nextVersion: string | null;
21
- bumpReason?: "direct" | "dependency-propagation" | "follows";
22
+ bumpReason?: BumpReason;
22
23
  dependencySourcePaths?: string[];
23
24
  commits: ParsedCommit[];
24
25
  }>;
25
26
  }
26
- /** @deprecated Use ReleasePlan. */
27
- export type SimplePlan = ReleasePlan;
28
27
  export declare function getChangelogDefaults(config: {
29
28
  "release-type"?: VersionaryConfig["release-type"];
30
29
  "changelog-file"?: VersionaryConfig["changelog-file"];
@@ -35,9 +34,30 @@ export declare function getChangelogDefaults(config: {
35
34
  changelogFormat: VersionaryChangelogFormat;
36
35
  };
37
36
  export declare function createReleasePlan(cwd?: string): ReleasePlan;
38
- /** @deprecated Use createReleasePlan. */
39
- export declare function createSimplePlan(cwd?: string): ReleasePlan;
37
+ /**
38
+ * The root package's own release, as distinct from the plan-level aggregate.
39
+ *
40
+ * `plan.releaseType`/`plan.nextVersion`/`plan.commits` describe the repository
41
+ * as a whole: the aggregate release type folds in every package's commits and
42
+ * is then applied to root's version number. That answers "is anything
43
+ * releasing, and how large is the biggest change anywhere" — not "what is root
44
+ * releasing". A sibling's `feat` therefore lifts the aggregate to a minor even
45
+ * when root's own `exclude-paths` drop that commit, so anything that names or
46
+ * describes root's release must go through here instead. Using the aggregate
47
+ * would report a version no version file carries and list commits root
48
+ * deliberately excluded.
49
+ *
50
+ * Falls back to the aggregate when the plan has no explicit root package: the
51
+ * top-level changelog is then a repository-wide summary with no package of its
52
+ * own to describe.
53
+ */
54
+ export declare function resolveRootReleaseView(plan: ReleasePlan): {
55
+ currentVersion: string;
56
+ nextVersion: string | null;
57
+ commits: ParsedCommit[];
58
+ };
40
59
  export declare function resolvePackageDependencies(plan: ReleasePlan, packagePath: string): Array<{
41
60
  name: string;
42
61
  version: string;
43
62
  }>;
63
+ export {};