versionary 0.8.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,7 +22,7 @@ Versionary is being built to:
22
22
 
23
23
  - support both direct releases and release-PR-gated releases
24
24
  - work across repository types (Node, Rust, docs/LaTeX, etc.)
25
- - stay SCM-agnostic at the core, with integrations via plugin capabilities
25
+ - stay SCM-agnostic at the core with built-in integration adapters
26
26
  (GitHub first; GitLab/Codeberg later)
27
27
  - keep a small, stable core with explicit extension points
28
28
  - handle trunk-based development and monorepo workflows cleanly
@@ -40,6 +40,7 @@ Out of scope (intentional):
40
40
 
41
41
  - publishing artifacts to language registries
42
42
  - replacing package-specific publish tooling
43
+ - external/user-provided plugin loading
43
44
 
44
45
  Use your CI/CD platform for registry publishing, triggered from a created
45
46
  release/tag.
@@ -95,12 +96,14 @@ For a quick trial, use:
95
96
  - pre-1.0 policy defaults to conservative major handling: for `0.y.z`, breaking
96
97
  changes bump to `0.(y+1).0`; set `allow-stable-major: true` to allow explicit
97
98
  auto-transition to `1.0.0` on a breaking release
98
- - review mode (`review-mode`): `review` (PR/MR style) or `direct` (no review
99
+ - review mode (`review-mode`): `pr` (PR/MR style) or `direct` (no review
99
100
  request)
100
101
  - optional monorepo planning with `monorepo-mode` and `packages`:
101
102
  - `independent` computes package bumps per path
102
103
  - `fixed` computes one shared bump across configured package paths
103
104
  - per-package `package-name` can override release identity (labels + tag base)
105
+ - per-package `changelog-file` writes package release notes to
106
+ `<package-path>/<changelog-file>`
104
107
 
105
108
  Rust strategy examples:
106
109
 
@@ -123,6 +126,8 @@ Rust strategy examples:
123
126
  Current rust auto-update behavior (phase scope):
124
127
 
125
128
  - updates crate versions in each targeted crate `[package].version`
129
+ - supports targeted crates using `version.workspace = true` by updating
130
+ `[workspace.package].version` in the owning workspace manifest
126
131
  - updates internal workspace dependency versions when the dependency name
127
132
  matches another targeted crate name
128
133
  - refreshes `Cargo.lock` via `cargo generate-lockfile` when `Cargo.lock` exists
@@ -235,7 +240,8 @@ Minimum GitHub token/repo permissions for Versionary-managed metadata:
235
240
 
236
241
  `review-mode` behavior:
237
242
 
238
- - `review`: `pnpm run run` prepares/updates the release branch and creates or
243
+ - `pr` (preferred; `review` is a backward-compatible alias): `pnpm run run`
244
+ prepares/updates the release branch and creates or
239
245
  updates a release PR
240
246
  - `direct`: `pnpm run run` prepares/updates the release branch and skips review
241
247
  request creation
@@ -33,13 +33,17 @@ function parseFieldPath(fieldPath) {
33
33
  const numberMatch = rest.match(/^(\d+)\]/u);
34
34
  if (numberMatch) {
35
35
  tokens.push(Number(numberMatch[1]));
36
- index += 2 + numberMatch[1].length;
36
+ index += 2 + numberMatch[1]?.length;
37
37
  continue;
38
38
  }
39
39
  const keyMatch = rest.match(/^"([^"]+)"\]/u);
40
40
  if (keyMatch) {
41
- tokens.push(keyMatch[1]);
42
- index += 4 + keyMatch[1].length;
41
+ const key = keyMatch[1];
42
+ if (!key) {
43
+ throw new Error(`Invalid field-path "${fieldPath}" near index ${index}.`);
44
+ }
45
+ tokens.push(key);
46
+ index += 4 + keyMatch[1]?.length;
43
47
  continue;
44
48
  }
45
49
  throw new Error(`Invalid field-path "${fieldPath}" near index ${index}.`);
@@ -56,6 +60,9 @@ function setVersionAtJsonPath(document, fieldPath, version) {
56
60
  let cursor = document;
57
61
  for (let index = 0; index < tokens.length - 1; index += 1) {
58
62
  const token = tokens[index];
63
+ if (token === undefined) {
64
+ throw new Error(`field-path "${fieldPath}" does not resolve to an existing field.`);
65
+ }
59
66
  if (typeof token === "number") {
60
67
  if (!Array.isArray(cursor) || token >= cursor.length) {
61
68
  throw new Error(`field-path "${fieldPath}" does not resolve to an existing field.`);
@@ -69,6 +76,9 @@ function setVersionAtJsonPath(document, fieldPath, version) {
69
76
  cursor = cursor[token];
70
77
  }
71
78
  const leaf = tokens.at(-1);
79
+ if (leaf === undefined) {
80
+ throw new Error(`field-path "${fieldPath}" does not resolve to an existing field.`);
81
+ }
72
82
  if (typeof leaf === "number") {
73
83
  if (!Array.isArray(cursor) || leaf >= cursor.length) {
74
84
  throw new Error(`field-path "${fieldPath}" does not resolve to an existing field.`);
@@ -99,7 +109,12 @@ function resolveFieldPath(rule) {
99
109
  function parseRegexPattern(pattern) {
100
110
  const slashPattern = pattern.match(/^\/((?:\\\/|[^/])+)\/([a-z]*)$/u);
101
111
  if (slashPattern) {
102
- return new RegExp(slashPattern[1], slashPattern[2]);
112
+ const source = slashPattern[1];
113
+ const flags = slashPattern[2];
114
+ if (!source || flags === undefined) {
115
+ throw new Error(`Invalid regex pattern "${pattern}".`);
116
+ }
117
+ return new RegExp(source, flags);
103
118
  }
104
119
  return new RegExp(pattern, "m");
105
120
  }
@@ -114,6 +129,9 @@ function applyRegexRule(content, pattern, version) {
114
129
  throw new Error(`Regex pattern must match exactly one occurrence; matched ${matches.length}.`);
115
130
  }
116
131
  const match = matches[0];
132
+ if (!match) {
133
+ throw new Error("Regex match result missing.");
134
+ }
117
135
  const start = match.index;
118
136
  if (start === undefined) {
119
137
  throw new Error("Regex match did not include an index.");
@@ -131,6 +149,9 @@ function applyTomlRulePreservingFormatting(content, fieldPath, version) {
131
149
  return `${toml_1.default.stringify(parsed)}\n`;
132
150
  }
133
151
  const key = simplePath[1];
152
+ if (!key) {
153
+ throw new Error(`field-path "${fieldPath}" does not resolve to an existing field.`);
154
+ }
134
155
  const linePattern = new RegExp(`^(\\s*${key}\\s*=\\s*)(["'])([^"']*)(\\2)(\\s*(?:#.*)?)$`, "mu");
135
156
  const match = content.match(linePattern);
136
157
  if (!match) {
@@ -141,6 +162,9 @@ function applyTomlRulePreservingFormatting(content, fieldPath, version) {
141
162
  }
142
163
  function applyArtifactRuleToContent(content, rule, version) {
143
164
  if (rule.type === "regex") {
165
+ if (!rule.pattern) {
166
+ throw new Error('regex artifact rules require "pattern".');
167
+ }
144
168
  return applyRegexRule(content, rule.pattern, version);
145
169
  }
146
170
  if (rule.type === "json") {
@@ -71,24 +71,60 @@ function ensureCleanWorktree(cwd, logger) {
71
71
  logger?.warn(`Ignoring safe tracked changes before versionary pr:\n${ignored.join("\n")}`);
72
72
  }
73
73
  }
74
+ function normalizeSlashPath(input) {
75
+ return input.replaceAll("\\", "/");
76
+ }
77
+ function listCargoLockFiles(cwd) {
78
+ const lockfiles = [];
79
+ const queue = [cwd];
80
+ while (queue.length > 0) {
81
+ const currentDir = queue.shift();
82
+ if (!currentDir) {
83
+ continue;
84
+ }
85
+ const entries = node_fs_1.default.readdirSync(currentDir, { withFileTypes: true });
86
+ for (const entry of entries) {
87
+ if (entry.name === ".git") {
88
+ continue;
89
+ }
90
+ const fullPath = node_path_1.default.join(currentDir, entry.name);
91
+ if (entry.isDirectory()) {
92
+ queue.push(fullPath);
93
+ continue;
94
+ }
95
+ if (!entry.isFile() || entry.name !== "Cargo.lock") {
96
+ continue;
97
+ }
98
+ lockfiles.push(normalizeSlashPath(node_path_1.default.relative(cwd, fullPath)));
99
+ }
100
+ }
101
+ return lockfiles.sort((a, b) => a.localeCompare(b));
102
+ }
74
103
  function ensureCargoLockUpToDate(cwd) {
75
- const lockfilePath = node_path_1.default.join(cwd, "Cargo.lock");
76
- if (!node_fs_1.default.existsSync(lockfilePath)) {
104
+ const lockfiles = listCargoLockFiles(cwd);
105
+ if (lockfiles.length === 0) {
77
106
  return [];
78
107
  }
79
- const before = node_fs_1.default.readFileSync(lockfilePath, "utf8");
80
- try {
81
- (0, node_child_process_1.execFileSync)("cargo", ["generate-lockfile"], {
82
- cwd,
83
- stdio: ["ignore", "pipe", "pipe"],
84
- });
85
- }
86
- catch (error) {
87
- const message = error instanceof Error ? error.message : String(error);
88
- throw new Error(`Failed to refresh Cargo.lock via "cargo generate-lockfile". Ensure cargo is installed and available in PATH. Details: ${message}`);
108
+ const updatedLockfiles = [];
109
+ for (const lockfile of lockfiles) {
110
+ const lockfilePath = node_path_1.default.join(cwd, lockfile);
111
+ const before = node_fs_1.default.readFileSync(lockfilePath, "utf8");
112
+ try {
113
+ (0, node_child_process_1.execFileSync)("cargo", ["generate-lockfile"], {
114
+ cwd: node_path_1.default.dirname(lockfilePath),
115
+ stdio: ["ignore", "pipe", "pipe"],
116
+ });
117
+ }
118
+ catch (error) {
119
+ const message = error instanceof Error ? error.message : String(error);
120
+ throw new Error(`Failed to refresh ${lockfile} via "cargo generate-lockfile". Ensure cargo is installed and available in PATH. Details: ${message}`);
121
+ }
122
+ const after = node_fs_1.default.readFileSync(lockfilePath, "utf8");
123
+ if (after !== before) {
124
+ updatedLockfiles.push(lockfile);
125
+ }
89
126
  }
90
- const after = node_fs_1.default.readFileSync(lockfilePath, "utf8");
91
- return after !== before ? ["Cargo.lock"] : [];
127
+ return updatedLockfiles;
92
128
  }
93
129
  function normalizeReleaseNameForTag(releaseName) {
94
130
  return releaseName
@@ -178,12 +214,31 @@ function buildReleaseTargets(cwd, plan, loadedConfig) {
178
214
  }
179
215
  return releaseTargets;
180
216
  }
217
+ function buildPackageReleaseMetadata(cwd, plan, loadedConfig) {
218
+ const metadataByPath = {};
219
+ for (const pkg of plan.packages ?? []) {
220
+ if (!pkg.nextVersion || pkg.path === ".") {
221
+ continue;
222
+ }
223
+ const packageConfig = loadedConfig.packages?.[pkg.path] ?? {};
224
+ const packageContext = (0, package_context_js_1.resolvePackageStrategyContext)(loadedConfig, pkg.path, packageConfig);
225
+ const releaseName = resolveReleaseName(cwd, pkg.path, packageConfig, packageContext.strategy.name, packageContext.versionFile);
226
+ metadataByPath[pkg.path] = {
227
+ releaseName,
228
+ tagPrefix: normalizeReleaseNameForTag(releaseName),
229
+ };
230
+ }
231
+ return metadataByPath;
232
+ }
181
233
  function formatReleaseCommitTitle(releaseTargets) {
182
234
  if (releaseTargets.length === 0) {
183
235
  return "chore(release): v0.0.0";
184
236
  }
185
237
  const tags = releaseTargets.map((target) => target.tag);
186
- return `chore(release): ${tags.join(", ")}`;
238
+ if (tags.length === 1) {
239
+ return `chore(release): ${tags[0]}`;
240
+ }
241
+ return `chore(release): ${tags[0]} (+${tags.length - 1} more)`;
187
242
  }
188
243
  function prepareSimpleReleasePr(cwd = process.cwd(), options = {}) {
189
244
  const plan = (0, plan_js_1.createSimplePlan)(cwd);
@@ -216,8 +271,33 @@ function prepareSimpleReleasePr(cwd = process.cwd(), options = {}) {
216
271
  }
217
272
  const updatedArtifactFiles = (0, artifact_rules_js_1.applyConfiguredArtifactRules)(cwd, loaded.config, plan);
218
273
  const updatedRustLockFiles = ensureCargoLockUpToDate(cwd);
274
+ const packageReleaseMetadata = buildPackageReleaseMetadata(cwd, plan, loaded.config);
219
275
  const section = (0, changelog_js_1.renderSimpleChangelog)(plan);
220
276
  (0, changelog_js_1.prependChangelog)(cwd, plan.changelogFile, section);
277
+ const updatedChangelogFiles = [plan.changelogFile];
278
+ for (const packagePlan of plan.packages ?? []) {
279
+ if (!packagePlan.nextVersion || packagePlan.path === ".") {
280
+ continue;
281
+ }
282
+ const packageConfig = loaded.config.packages?.[packagePlan.path] ?? {};
283
+ const packageChangelogFile = packageConfig["changelog-file"];
284
+ if (!packageChangelogFile) {
285
+ continue;
286
+ }
287
+ const packageMetadata = packageReleaseMetadata[packagePlan.path];
288
+ if (!packageMetadata) {
289
+ continue;
290
+ }
291
+ const packageSection = (0, changelog_js_1.renderPackageChangelogSection)({
292
+ currentVersion: packagePlan.currentVersion,
293
+ nextVersion: packagePlan.nextVersion,
294
+ commits: packagePlan.commits,
295
+ tagPrefix: packageMetadata.tagPrefix,
296
+ cwd,
297
+ });
298
+ (0, changelog_js_1.prependChangelog)(cwd, node_path_1.default.posix.join(packagePlan.path, packageChangelogFile), packageSection);
299
+ updatedChangelogFiles.push(node_path_1.default.posix.join(packagePlan.path, packageChangelogFile));
300
+ }
221
301
  const releaseTargets = buildReleaseTargets(cwd, plan, loaded.config);
222
302
  const branch = plan.releaseBranchPrefix;
223
303
  const title = formatReleaseCommitTitle(releaseTargets);
@@ -230,7 +310,7 @@ function prepareSimpleReleasePr(cwd = process.cwd(), options = {}) {
230
310
  ...updatedVersionFiles,
231
311
  ...updatedArtifactFiles,
232
312
  ...updatedRustLockFiles,
233
- plan.changelogFile,
313
+ ...updatedChangelogFiles,
234
314
  ]),
235
315
  ];
236
316
  (0, node_child_process_1.execFileSync)("git", ["add", ...filesToAdd], {
@@ -297,14 +377,14 @@ function renderSimpleReviewRequestBody(version, previousVersion, commits, plan =
297
377
  }
298
378
  async function openOrUpdateSimpleReviewRequest(cwd, branch, title, version, previousVersion, commits, plan = null, options = {}) {
299
379
  const loaded = (0, load_config_js_1.loadConfig)(cwd);
300
- const releaseFlow = loaded.config["review-mode"] ?? "direct";
301
- if (releaseFlow !== "review") {
380
+ const releaseFlow = loaded.config["review-mode"] ?? "pr";
381
+ if (releaseFlow === "direct") {
302
382
  return "Release flow mode is direct; skipping review request creation.";
303
383
  }
304
384
  const plugins = (0, runtime_js_1.loadRuntimePlugins)();
305
385
  const scmPlugins = (0, capabilities_js_1.findPluginsByCapability)(plugins, "scm.reviewRequest");
306
386
  if (scmPlugins.length === 0) {
307
- throw new Error("review-mode is review but no scm.reviewRequest plugin is available.");
387
+ throw new Error("review-mode is pr but no scm.reviewRequest plugin is available.");
308
388
  }
309
389
  const plugin = scmPlugins[0];
310
390
  if (!plugin?.createOrUpdateReviewRequest) {
@@ -333,5 +413,5 @@ function isReleaseCommitMessage(commitMessage) {
333
413
  return true;
334
414
  }
335
415
  const subject = commitMessage.split("\n")[0]?.trim() ?? "";
336
- return /^chore\(release\):\s+(?:v\d+\.\d+\.\d+|\S+-v\d+\.\d+\.\d+)(?:,\s+(?:v\d+\.\d+\.\d+|\S+-v\d+\.\d+\.\d+))*(?:\s+\(#\d+\))?$/u.test(subject);
416
+ return /^chore\(release\):\s+(?:(?:v\d+\.\d+\.\d+|\S+-v\d+\.\d+\.\d+)(?:,\s+(?:v\d+\.\d+\.\d+|\S+-v\d+\.\d+\.\d+))*|(?:v\d+\.\d+\.\d+|\S+-v\d+\.\d+\.\d+)\s+\(\+\d+\s+more\))(?:\s+\(#\d+\))?$/u.test(subject);
337
417
  }
@@ -3,6 +3,13 @@ export declare function runSimpleRelease(cwd?: string): Promise<string>;
3
3
  export type SimpleRunReleaseResult = {
4
4
  action: "release-skipped";
5
5
  reason: string;
6
+ } | {
7
+ action: "release-dry-run";
8
+ message: string;
9
+ targets: {
10
+ tag: string;
11
+ version: string;
12
+ }[];
6
13
  } | {
7
14
  action: "release-published";
8
15
  message: string;
@@ -15,5 +22,6 @@ export type SimpleRunReleaseResult = {
15
22
  };
16
23
  export interface RunSimpleReleaseOptions {
17
24
  logger?: VersionaryPluginContext["logger"];
25
+ "dry-run"?: boolean;
18
26
  }
19
27
  export declare function runSimpleReleaseDetailed(cwd?: string, options?: RunSimpleReleaseOptions): Promise<SimpleRunReleaseResult>;
@@ -66,15 +66,6 @@ async function runSimpleReleaseDetailed(cwd = process.cwd(), options = {}) {
66
66
  const changelogFile = loaded.config["changelog-file"] ?? "CHANGELOG.md";
67
67
  const version = strategy.readVersion(cwd, loaded.config);
68
68
  const defaultTag = `v${version}`;
69
- const plugins = (0, runtime_js_1.loadRuntimePlugins)();
70
- const scmPlugins = (0, capabilities_js_1.findPluginsByCapability)(plugins, "scm.releaseMetadata");
71
- if (scmPlugins.length === 0) {
72
- throw new Error("No scm.releaseMetadata plugin is available.");
73
- }
74
- const plugin = scmPlugins[0];
75
- if (!plugin?.createReleaseMetadata) {
76
- throw new Error(`Plugin "${plugin?.name ?? "unknown"}" does not implement createReleaseMetadata.`);
77
- }
78
69
  const releaseTargets = (0, state_js_1.readReleaseTargets)(cwd);
79
70
  const targets = releaseTargets.length > 0
80
71
  ? releaseTargets
@@ -85,6 +76,27 @@ async function runSimpleReleaseDetailed(cwd = process.cwd(), options = {}) {
85
76
  tag: defaultTag,
86
77
  },
87
78
  ];
79
+ if (options["dry-run"]) {
80
+ const targetList = targets.map((target) => `${target.tag} (${target.version})`);
81
+ return {
82
+ action: "release-dry-run",
83
+ targets: targets.map((target) => ({
84
+ tag: target.tag,
85
+ version: target.version,
86
+ })),
87
+ message: `Dry run: would publish releases ${targetList.join(", ")}`,
88
+ };
89
+ }
90
+ const plugins = (0, runtime_js_1.loadRuntimePlugins)();
91
+ const scmPlugins = (0, capabilities_js_1.findPluginsByCapability)(plugins, "scm.releaseMetadata");
92
+ if (scmPlugins.length === 0) {
93
+ throw new Error("No scm.releaseMetadata plugin is available.");
94
+ }
95
+ const plugin = scmPlugins[0];
96
+ if (!plugin?.createReleaseMetadata) {
97
+ throw new Error(`Plugin "${plugin?.name ?? "unknown"}" does not implement createReleaseMetadata.`);
98
+ }
99
+ const createReleaseMetadata = plugin.createReleaseMetadata;
88
100
  const releases = [];
89
101
  for (const target of targets) {
90
102
  const outcome = await (0, recovery_js_1.executeIdempotentReleaseTarget)(cwd, {
@@ -92,7 +104,10 @@ async function runSimpleReleaseDetailed(cwd = process.cwd(), options = {}) {
92
104
  version: target.version,
93
105
  notes: readReleaseNotes(cwd, target.version, changelogFile),
94
106
  }, {
95
- createReleaseMetadata: (input) => plugin.createReleaseMetadata(input, { cwd, logger: options.logger }),
107
+ createReleaseMetadata: (input) => createReleaseMetadata(input, {
108
+ cwd,
109
+ logger: options.logger,
110
+ }),
96
111
  logger: options.logger,
97
112
  });
98
113
  releases.push({
@@ -79,7 +79,7 @@ function readReleaseTargets(cwd = process.cwd()) {
79
79
  const parsed = parseStateFile(node_fs_1.default.readFileSync(filePath, "utf8"), filePath);
80
80
  return parsed[RELEASE_TARGETS_KEY] ?? [];
81
81
  }
82
- function writeBaselineSha(cwd = process.cwd(), sha, releaseTargets = []) {
82
+ function writeBaselineSha(cwd = process.cwd(), sha, releaseTargets) {
83
83
  const baselineShaValue = sha ??
84
84
  (0, node_child_process_1.execFileSync)("git", ["rev-parse", "HEAD"], {
85
85
  cwd,
@@ -87,10 +87,22 @@ function writeBaselineSha(cwd = process.cwd(), sha, releaseTargets = []) {
87
87
  stdio: ["ignore", "pipe", "ignore"],
88
88
  }).trim();
89
89
  const filePath = getBaselineStatePath(cwd);
90
+ const existing = node_fs_1.default.existsSync(filePath)
91
+ ? parseStateFile(node_fs_1.default.readFileSync(filePath, "utf8"), filePath)
92
+ : {};
93
+ const existingTargets = existing[RELEASE_TARGETS_KEY] ?? [];
94
+ const nextTargets = releaseTargets === undefined
95
+ ? existingTargets
96
+ : [
97
+ ...new Map([...existingTargets, ...releaseTargets].map((target) => [
98
+ target.path,
99
+ target,
100
+ ])).values(),
101
+ ].sort((a, b) => a.path.localeCompare(b.path));
90
102
  const next = {
91
103
  [MANIFEST_VERSION_KEY]: 1,
92
104
  [BASELINE_SHA_KEY]: baselineShaValue,
93
- [RELEASE_TARGETS_KEY]: releaseTargets,
105
+ [RELEASE_TARGETS_KEY]: nextTargets,
94
106
  };
95
107
  node_fs_1.default.writeFileSync(filePath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
96
108
  }
package/dist/cli/index.js CHANGED
@@ -14,15 +14,33 @@ const changelog_js_1 = require("../domain/release/changelog.js");
14
14
  const plan_js_1 = require("../domain/release/plan.js");
15
15
  function printVerifyResult() {
16
16
  const result = (0, verify_js_1.verifyProject)();
17
- for (const check of result.checks) {
18
- const status = check.ok ? "OK" : "FAIL";
19
- console.log(`[${status}] ${check.name} - ${check.details}`);
17
+ const categories = [
18
+ { key: "config", title: "Config" },
19
+ { key: "paths", title: "Paths" },
20
+ { key: "version-files", title: "Version files" },
21
+ ];
22
+ for (const category of categories) {
23
+ const checks = result.checks.filter((check) => check.category === category.key);
24
+ if (checks.length === 0) {
25
+ continue;
26
+ }
27
+ console.log(`${category.title}:`);
28
+ for (const check of checks) {
29
+ const status = check.ok ? "OK" : "FAIL";
30
+ console.log(` [${status}] ${check.name} - ${check.details}`);
31
+ if (!check.ok && check.remediation) {
32
+ console.log(` Fix: ${check.remediation}`);
33
+ }
34
+ }
35
+ console.log("");
20
36
  }
37
+ console.log(result.ok ? "Summary: all checks passed." : "Summary: checks failed.");
21
38
  return result.ok ? 0 : 1;
22
39
  }
23
40
  function parseFlags(args) {
24
41
  return {
25
42
  json: args.includes("--json"),
43
+ "dry-run": args.includes("--dry-run"),
26
44
  };
27
45
  }
28
46
  function emitJson(payload) {
@@ -38,9 +56,20 @@ async function main() {
38
56
  stdio: ["ignore", "pipe", "ignore"],
39
57
  }).trim();
40
58
  if ((0, pr_js_1.isReleaseCommitMessage)(commitMessage)) {
59
+ if (flags["dry-run"] && !flags.json) {
60
+ const release = await (0, release_js_1.runSimpleReleaseDetailed)(process.cwd(), {
61
+ logger,
62
+ "dry-run": true,
63
+ });
64
+ if (release.action === "release-dry-run") {
65
+ console.log(release.message);
66
+ return 0;
67
+ }
68
+ }
41
69
  if (flags.json) {
42
70
  const release = await (0, release_js_1.runSimpleReleaseDetailed)(process.cwd(), {
43
71
  logger,
72
+ "dry-run": flags["dry-run"],
44
73
  });
45
74
  if (release.action === "release-skipped") {
46
75
  emitJson({
@@ -51,6 +80,16 @@ async function main() {
51
80
  });
52
81
  return 0;
53
82
  }
83
+ if (release.action === "release-dry-run") {
84
+ emitJson({
85
+ action: "release-dry-run",
86
+ message: release.message,
87
+ releaseCreated: false,
88
+ tagNames: release.targets.map((target) => target.tag),
89
+ targets: release.targets,
90
+ });
91
+ return 0;
92
+ }
54
93
  emitJson({
55
94
  action: "release-published",
56
95
  message: release.message,
@@ -78,6 +117,34 @@ async function main() {
78
117
  console.log(message);
79
118
  return 0;
80
119
  }
120
+ if (flags["dry-run"]) {
121
+ const dryRunMessage = `Dry run: would prepare release PR branch ${plan.releaseBranchPrefix} for ${plan.nextVersion}`;
122
+ if (flags.json) {
123
+ emitJson({
124
+ action: "pr-dry-run",
125
+ message: dryRunMessage,
126
+ releaseCreated: false,
127
+ tagNames: [],
128
+ branch: plan.releaseBranchPrefix,
129
+ targets: plan.packages
130
+ ?.filter((pkg) => pkg.nextVersion)
131
+ .map((pkg) => ({
132
+ tag: pkg.path === "."
133
+ ? `v${pkg.nextVersion ?? ""}`
134
+ : `${pkg.path}-v${pkg.nextVersion ?? ""}`,
135
+ version: pkg.nextVersion ?? "",
136
+ })) ?? [
137
+ {
138
+ tag: `v${plan.nextVersion}`,
139
+ version: plan.nextVersion,
140
+ },
141
+ ],
142
+ });
143
+ return 0;
144
+ }
145
+ console.log(dryRunMessage);
146
+ return 0;
147
+ }
81
148
  const pr = (0, pr_js_1.prepareSimpleReleasePr)(process.cwd(), { logger });
82
149
  (0, pr_js_1.pushReleaseBranch)(process.cwd(), pr.branch);
83
150
  const reviewResult = await (0, pr_js_1.openOrUpdateSimpleReviewRequest)(process.cwd(), pr.branch, pr.title, pr.version, pr.previousVersion, pr.commits, pr.plan, { logger });
@@ -125,11 +192,20 @@ async function main() {
125
192
  : "";
126
193
  const heading = "# Changelog\n\n";
127
194
  const body = existing.replace(/^# Changelog\s*/u, "");
128
- node_fs_1.default.writeFileSync(changelogPath, `${heading}${section}\n${body}`.trimEnd() + "\n", "utf8");
195
+ node_fs_1.default.writeFileSync(changelogPath, `${`${heading}${section}\n${body}`.trimEnd()}\n`, "utf8");
129
196
  console.log(`Updated ${plan.changelogFile}`);
130
197
  return 0;
131
198
  }
132
199
  if (command === "pr") {
200
+ if (flags["dry-run"]) {
201
+ const plan = (0, plan_js_1.createSimplePlan)();
202
+ if (!plan.nextVersion) {
203
+ console.log("No releasable commits found. Nothing to do.");
204
+ return 0;
205
+ }
206
+ console.log(`Dry run: would prepare release PR branch ${plan.releaseBranchPrefix} for ${plan.nextVersion}`);
207
+ return 0;
208
+ }
133
209
  const pr = (0, pr_js_1.prepareSimpleReleasePr)(process.cwd(), { logger: console });
134
210
  (0, pr_js_1.pushReleaseBranch)(process.cwd(), pr.branch);
135
211
  const reviewResult = await (0, pr_js_1.openOrUpdateSimpleReviewRequest)(process.cwd(), pr.branch, pr.title, pr.version, pr.previousVersion, pr.commits, pr.plan);
@@ -139,18 +215,34 @@ async function main() {
139
215
  return 0;
140
216
  }
141
217
  if (command === "release") {
218
+ if (flags["dry-run"]) {
219
+ const result = await (0, release_js_1.runSimpleReleaseDetailed)(process.cwd(), {
220
+ logger,
221
+ "dry-run": true,
222
+ });
223
+ if (result.action === "release-dry-run") {
224
+ console.log(result.message);
225
+ }
226
+ else if (result.action === "release-skipped") {
227
+ console.log(result.reason);
228
+ }
229
+ else {
230
+ console.log(result.message);
231
+ }
232
+ return 0;
233
+ }
142
234
  const message = await (0, release_js_1.runSimpleRelease)(process.cwd());
143
235
  console.log(message);
144
236
  return 0;
145
237
  }
146
238
  console.log("Usage: versionary <command>");
147
239
  console.log("Commands:");
148
- console.log(" run [--json] Auto-dispatch release PR/update or release publish by context");
240
+ console.log(" run [--json] [--dry-run] Auto-dispatch release PR/update or release publish by context");
149
241
  console.log(" verify Validate config and basic repository shape");
150
242
  console.log(" plan Print release plan (simple mode)");
151
243
  console.log(" changelog [--write] Print or write changelog section");
152
- console.log(" pr Prepare release PR commit and branch");
153
- console.log(" release Publish release metadata for release commit context");
244
+ console.log(" pr [--dry-run] Prepare release PR commit and branch");
245
+ console.log(" release [--dry-run] Publish release metadata for release commit context");
154
246
  return 1;
155
247
  }
156
248
  main()
@@ -41,6 +41,9 @@ function loadConfig(cwd = process.cwd()) {
41
41
  if (!isRecord(parsed)) {
42
42
  throw new Error("Invalid config: expected an object at the root.");
43
43
  }
44
+ if (Object.hasOwn(parsed, "plugins")) {
45
+ throw new Error('The "plugins" config key is no longer supported. Versionary uses built-in integrations only.');
46
+ }
44
47
  const validated = schema_js_1.configSchema.parse(parsed);
45
48
  return {
46
49
  path: found.path,
@@ -4,6 +4,7 @@ export declare const configSchema: z.ZodObject<{
4
4
  version: z.ZodLiteral<1>;
5
5
  "review-mode": z.ZodOptional<z.ZodEnum<{
6
6
  direct: "direct";
7
+ pr: "pr";
7
8
  review: "review";
8
9
  }>>;
9
10
  "version-file": z.ZodOptional<z.ZodString>;
@@ -22,6 +23,7 @@ export declare const configSchema: z.ZodObject<{
22
23
  packages: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
23
24
  "release-type": z.ZodOptional<z.ZodString>;
24
25
  "package-name": z.ZodOptional<z.ZodString>;
26
+ "changelog-file": z.ZodOptional<z.ZodString>;
25
27
  "exclude-paths": z.ZodOptional<z.ZodArray<z.ZodString>>;
26
28
  "extra-files": z.ZodOptional<z.ZodArray<z.ZodObject<{
27
29
  type: z.ZodEnum<{
@@ -36,6 +38,5 @@ export declare const configSchema: z.ZodObject<{
36
38
  pattern: z.ZodOptional<z.ZodString>;
37
39
  }, z.core.$strip>>>;
38
40
  }, z.core.$strict>>>;
39
- plugins: z.ZodOptional<z.ZodArray<z.ZodString>>;
40
41
  }, z.core.$strict>;
41
42
  export type ConfigSchema = z.infer<typeof configSchema>;
@@ -53,6 +53,7 @@ const packageSchema = zod_1.z
53
53
  .object({
54
54
  "release-type": zod_1.z.string().optional(),
55
55
  "package-name": zod_1.z.string().optional(),
56
+ "changelog-file": zod_1.z.string().optional(),
56
57
  "exclude-paths": zod_1.z.array(zod_1.z.string()).optional(),
57
58
  "extra-files": zod_1.z.array(artifactRuleSchema).optional(),
58
59
  })
@@ -61,7 +62,7 @@ exports.configSchema = zod_1.z
61
62
  .object({
62
63
  $schema: zod_1.z.string().optional(),
63
64
  version: zod_1.z.literal(1),
64
- "review-mode": zod_1.z.enum(["direct", "review"]).optional(),
65
+ "review-mode": zod_1.z.enum(["direct", "pr", "review"]).optional(),
65
66
  "version-file": zod_1.z.string().optional(),
66
67
  "changelog-file": zod_1.z.string().optional(),
67
68
  "release-branch": zod_1.z.string().optional(),
@@ -73,6 +74,5 @@ exports.configSchema = zod_1.z
73
74
  "include-commit-authors": zod_1.z.boolean().optional(),
74
75
  "release-type": zod_1.z.string().optional(),
75
76
  packages: zod_1.z.record(zod_1.z.string().min(1), packageSchema).optional(),
76
- plugins: zod_1.z.array(zod_1.z.string().min(1)).optional(),
77
77
  })
78
78
  .strict();
@@ -9,4 +9,11 @@ export declare function renderSimpleReleaseNotes(input: {
9
9
  includeFooter?: boolean;
10
10
  }): string;
11
11
  export declare function renderSimpleChangelog(plan: SimplePlan): string;
12
+ export declare function renderPackageChangelogSection(input: {
13
+ currentVersion: string;
14
+ nextVersion: string;
15
+ commits: ParsedCommit[];
16
+ tagPrefix: string;
17
+ cwd?: string;
18
+ }): string;
12
19
  export declare function prependChangelog(cwd: string, changelogFile: string, section: string): void;
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.renderSimpleReleaseNotes = renderSimpleReleaseNotes;
7
7
  exports.renderSimpleChangelog = renderSimpleChangelog;
8
+ exports.renderPackageChangelogSection = renderPackageChangelogSection;
8
9
  exports.prependChangelog = prependChangelog;
9
10
  const node_fs_1 = __importDefault(require("node:fs"));
10
11
  const node_path_1 = __importDefault(require("node:path"));
@@ -110,6 +111,24 @@ function renderSimpleChangelog(plan) {
110
111
  cwd: process.cwd(),
111
112
  });
112
113
  }
114
+ function renderPackageChangelogSection(input) {
115
+ const repoUrl = (0, repo_url_js_1.resolveRepositoryWebBaseUrl)(input.cwd ?? process.cwd());
116
+ const header = repoUrl
117
+ ? `## [${input.nextVersion}](${repoUrl}/compare/${input.tagPrefix}-v${input.currentVersion}...${input.tagPrefix}-v${input.nextVersion}) (${formatDate()})`
118
+ : `## ${input.nextVersion} (${formatDate()})`;
119
+ const grouped = groupCommitLines(input.commits, repoUrl);
120
+ const sections = [];
121
+ if (grouped.breaking.length > 0) {
122
+ sections.push("### Breaking changes", ...grouped.breaking, "");
123
+ }
124
+ if (grouped.features.length > 0) {
125
+ sections.push("### Features", ...grouped.features, "");
126
+ }
127
+ if (grouped.fixes.length > 0) {
128
+ sections.push("### Bug Fixes", ...grouped.fixes, "");
129
+ }
130
+ return [header, "", ...sections].join("\n");
131
+ }
113
132
  function prependChangelog(cwd, changelogFile, section) {
114
133
  const changelogPath = node_path_1.default.join(cwd, changelogFile);
115
134
  const existing = node_fs_1.default.existsSync(changelogPath)
@@ -8,7 +8,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  function readDescriptionVersion(content, versionFile) {
10
10
  const match = content.match(/^Version:\s*(.+)\s*$/mu);
11
- if (!match || !match[1]) {
11
+ if (!match?.[1]) {
12
12
  throw new Error(`${versionFile} is missing a valid "Version:" field required by release-type "r".`);
13
13
  }
14
14
  return match[1].trim();
@@ -202,19 +202,73 @@ function collectRustTargetManifests(cwd, versionFile, includeWorkspaceMembers) {
202
202
  }
203
203
  throw new Error(`Configured rust target "${versionFile}" is not a Rust crate manifest. Expected [package].version or [workspace].members with crate Cargo.toml files.`);
204
204
  }
205
- function readCargoVersion(cargoTomlRaw, versionFile) {
206
- const { packageTable } = parseCargoManifest(versionFile, cargoTomlRaw);
205
+ function isWorkspaceInheritedVersion(rawVersion) {
206
+ if (!rawVersion || typeof rawVersion !== "object") {
207
+ return false;
208
+ }
209
+ const versionRecord = rawVersion;
210
+ return versionRecord.workspace === true;
211
+ }
212
+ function readWorkspacePackageVersion(cargoTomlRaw, versionFile) {
213
+ const { workspaceTable } = parseCargoManifest(versionFile, cargoTomlRaw);
214
+ if (!workspaceTable || typeof workspaceTable !== "object") {
215
+ throw new Error(`${versionFile} is missing [workspace.package].version required by members using version.workspace = true.`);
216
+ }
217
+ const workspacePackage = workspaceTable.package && typeof workspaceTable.package === "object"
218
+ ? workspaceTable.package
219
+ : null;
220
+ const rawVersion = workspacePackage?.version;
221
+ if (typeof rawVersion !== "string" || rawVersion.trim().length === 0) {
222
+ throw new Error(`${versionFile} is missing [workspace.package].version required by members using version.workspace = true.`);
223
+ }
224
+ return rawVersion.trim();
225
+ }
226
+ function findWorkspaceManifestForMember(cwd, memberManifest) {
227
+ const cwdAbs = node_path_1.default.resolve(cwd);
228
+ let currentDir = node_path_1.default.resolve(cwd, node_path_1.default.dirname(memberManifest));
229
+ while (true) {
230
+ const relativeDir = node_path_1.default.relative(cwdAbs, currentDir);
231
+ if (relativeDir.startsWith("..")) {
232
+ break;
233
+ }
234
+ const candidatePath = node_path_1.default.join(currentDir, "Cargo.toml");
235
+ if (node_fs_1.default.existsSync(candidatePath)) {
236
+ const relativeManifest = normalizeSlashPath(node_path_1.default.relative(cwdAbs, candidatePath));
237
+ const cargoTomlRaw = node_fs_1.default.readFileSync(candidatePath, "utf8");
238
+ const parsed = parseCargoManifest(relativeManifest, cargoTomlRaw);
239
+ if (parsed.workspaceTable) {
240
+ return relativeManifest;
241
+ }
242
+ }
243
+ if (currentDir === cwdAbs) {
244
+ break;
245
+ }
246
+ const parentDir = node_path_1.default.dirname(currentDir);
247
+ if (parentDir === currentDir) {
248
+ break;
249
+ }
250
+ currentDir = parentDir;
251
+ }
252
+ throw new Error(`${memberManifest} uses version.workspace = true, but no workspace Cargo.toml with [workspace.package].version was found between that crate and repository root.`);
253
+ }
254
+ function readResolvedCargoVersion(cwd, manifest, cargoTomlRaw) {
255
+ const { packageTable } = parseCargoManifest(manifest, cargoTomlRaw);
207
256
  if (!packageTable || typeof packageTable !== "object") {
208
- throw new Error(`${versionFile} is missing [package].version. Add [package] with a SemVer version.`);
257
+ throw new Error(`${manifest} is missing [package].version. Add [package] with a SemVer version.`);
209
258
  }
210
259
  const rawVersion = packageTable.version;
211
260
  if (rawVersion === undefined) {
212
- throw new Error(`${versionFile} is missing [package].version. Add [package] with a SemVer version.`);
261
+ throw new Error(`${manifest} is missing [package].version. Add [package] with a SemVer version.`);
213
262
  }
214
- if (typeof rawVersion !== "string" || rawVersion.trim().length === 0) {
215
- throw new Error(`${versionFile} has invalid [package].version. Expected a non-empty SemVer string.`);
263
+ if (typeof rawVersion === "string" && rawVersion.trim().length > 0) {
264
+ return rawVersion.trim();
216
265
  }
217
- return rawVersion.trim();
266
+ if (!isWorkspaceInheritedVersion(rawVersion)) {
267
+ throw new Error(`${manifest} has invalid [package].version. Expected a non-empty SemVer string or version.workspace = true.`);
268
+ }
269
+ const workspaceManifest = findWorkspaceManifestForMember(cwd, manifest);
270
+ const workspaceRaw = node_fs_1.default.readFileSync(node_path_1.default.join(cwd, workspaceManifest), "utf8");
271
+ return readWorkspacePackageVersion(workspaceRaw, workspaceManifest);
218
272
  }
219
273
  function readCargoPackageName(cargoTomlRaw, versionFile) {
220
274
  const { packageTable } = parseCargoManifest(versionFile, cargoTomlRaw);
@@ -272,6 +326,56 @@ function writeCargoVersion(cargoTomlRaw, versionFile, version) {
272
326
  }
273
327
  return updated;
274
328
  }
329
+ function writeWorkspacePackageVersion(cargoTomlRaw, versionFile, version) {
330
+ const lineEnding = cargoTomlRaw.includes("\r\n") ? "\r\n" : "\n";
331
+ const hasFinalLineEnding = cargoTomlRaw.endsWith("\n") || cargoTomlRaw.endsWith("\r\n");
332
+ const lines = cargoTomlRaw.split(/\r?\n/u);
333
+ let inWorkspacePackageSection = false;
334
+ let foundWorkspacePackageSection = false;
335
+ let replacedVersion = false;
336
+ for (let index = 0; index < lines.length; index += 1) {
337
+ const line = lines[index] ?? "";
338
+ const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/u);
339
+ if (sectionMatch) {
340
+ const section = sectionMatch[1]?.trim();
341
+ inWorkspacePackageSection = section === "workspace.package";
342
+ if (inWorkspacePackageSection) {
343
+ foundWorkspacePackageSection = true;
344
+ }
345
+ continue;
346
+ }
347
+ if (!inWorkspacePackageSection) {
348
+ continue;
349
+ }
350
+ const versionMatch = line.match(/^(\s*version\s*=\s*)(["'])([^"']*)(\2)(\s*(?:#.*)?)?$/u);
351
+ if (!versionMatch) {
352
+ continue;
353
+ }
354
+ const [, prefix = "", quote = '"', , , suffix = ""] = versionMatch;
355
+ lines[index] = `${prefix}${quote}${version}${quote}${suffix}`;
356
+ replacedVersion = true;
357
+ break;
358
+ }
359
+ if (!foundWorkspacePackageSection || !replacedVersion) {
360
+ throw new Error(`${versionFile} is missing [workspace.package].version required by members using version.workspace = true.`);
361
+ }
362
+ let updated = lines.join(lineEnding);
363
+ if (hasFinalLineEnding && !updated.endsWith(lineEnding)) {
364
+ updated += lineEnding;
365
+ }
366
+ if (!hasFinalLineEnding && updated.endsWith(lineEnding)) {
367
+ updated = updated.slice(0, -lineEnding.length);
368
+ }
369
+ return updated;
370
+ }
371
+ function usesWorkspaceInheritedVersion(cargoTomlRaw, versionFile) {
372
+ const { packageTable } = parseCargoManifest(versionFile, cargoTomlRaw);
373
+ if (!packageTable || typeof packageTable !== "object") {
374
+ return false;
375
+ }
376
+ const rawVersion = packageTable.version;
377
+ return isWorkspaceInheritedVersion(rawVersion);
378
+ }
275
379
  function isDependencySection(section) {
276
380
  if (ROOT_DEPENDENCY_SECTIONS.has(section)) {
277
381
  return true;
@@ -450,13 +554,14 @@ exports.rustVersionStrategy = {
450
554
  throw new Error(`Configured rust target "${versionFile}" did not resolve to a Rust crate manifest.`);
451
555
  }
452
556
  const cargoTomlRaw = node_fs_1.default.readFileSync(node_path_1.default.join(cwd, selectedManifest), "utf8");
453
- return readCargoVersion(cargoTomlRaw, selectedManifest);
557
+ return readResolvedCargoVersion(cwd, selectedManifest, cargoTomlRaw);
454
558
  },
455
559
  writeVersion(cwd, config, version) {
456
560
  const versionFile = this.getVersionFile(config);
457
561
  const manifests = collectRustTargetManifests(cwd, versionFile, !config.packages);
458
562
  const updatedFiles = [];
459
563
  const internalCrates = new Set();
564
+ const workspaceManifestsToUpdate = new Set();
460
565
  for (const manifest of manifests) {
461
566
  const versionPath = node_path_1.default.join(cwd, manifest);
462
567
  if (!node_fs_1.default.existsSync(versionPath)) {
@@ -467,6 +572,7 @@ exports.rustVersionStrategy = {
467
572
  continue;
468
573
  }
469
574
  internalCrates.add(readCargoPackageName(cargoTomlRaw, manifest));
575
+ readResolvedCargoVersion(cwd, manifest, cargoTomlRaw);
470
576
  }
471
577
  for (const manifest of manifests) {
472
578
  const versionPath = node_path_1.default.join(cwd, manifest);
@@ -477,10 +583,30 @@ exports.rustVersionStrategy = {
477
583
  if (!isCrateManifest(manifest, cargoTomlRaw)) {
478
584
  continue;
479
585
  }
480
- readCargoVersion(cargoTomlRaw, manifest);
481
- const updatedCargoToml = writeInternalDependencyVersions(writeCargoVersion(cargoTomlRaw, manifest, version), internalCrates, version);
482
- node_fs_1.default.writeFileSync(versionPath, updatedCargoToml, "utf8");
483
- updatedFiles.push(manifest);
586
+ let updatedCargoToml = cargoTomlRaw;
587
+ if (usesWorkspaceInheritedVersion(cargoTomlRaw, manifest)) {
588
+ workspaceManifestsToUpdate.add(findWorkspaceManifestForMember(cwd, manifest));
589
+ }
590
+ else {
591
+ updatedCargoToml = writeCargoVersion(updatedCargoToml, manifest, version);
592
+ }
593
+ updatedCargoToml = writeInternalDependencyVersions(updatedCargoToml, internalCrates, version);
594
+ if (updatedCargoToml !== cargoTomlRaw) {
595
+ node_fs_1.default.writeFileSync(versionPath, updatedCargoToml, "utf8");
596
+ updatedFiles.push(manifest);
597
+ }
598
+ }
599
+ for (const workspaceManifest of workspaceManifestsToUpdate) {
600
+ const workspacePath = node_path_1.default.join(cwd, workspaceManifest);
601
+ if (!node_fs_1.default.existsSync(workspacePath)) {
602
+ continue;
603
+ }
604
+ const workspaceRaw = node_fs_1.default.readFileSync(workspacePath, "utf8");
605
+ const updatedWorkspaceToml = writeInternalDependencyVersions(writeWorkspacePackageVersion(workspaceRaw, workspaceManifest, version), internalCrates, version);
606
+ if (updatedWorkspaceToml !== workspaceRaw) {
607
+ node_fs_1.default.writeFileSync(workspacePath, updatedWorkspaceToml, "utf8");
608
+ updatedFiles.push(workspaceManifest);
609
+ }
484
610
  }
485
611
  if (updatedFiles.length === 0) {
486
612
  throw new Error(`Configured rust target "${versionFile}" did not resolve to a Rust crate manifest to update.`);
@@ -1,2 +1,2 @@
1
1
  import type { VersionaryPluginRuntime } from "../../types/plugins.js";
2
- export declare function loadRuntimePlugins(cwd?: string): VersionaryPluginRuntime[];
2
+ export declare function loadRuntimePlugins(): VersionaryPluginRuntime[];
@@ -1,24 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.loadRuntimePlugins = loadRuntimePlugins;
4
- const load_config_js_1 = require("../../config/load-config.js");
5
4
  const plugin_js_1 = require("./github/plugin.js");
6
- const BUILTIN_PLUGIN_FACTORIES = {
7
- github: plugin_js_1.createGitHubPlugin,
8
- };
9
- function loadRuntimePlugins(cwd = process.cwd()) {
10
- const loaded = (0, load_config_js_1.loadConfig)(cwd);
11
- const configured = loaded.config.plugins ?? [];
12
- const plugins = [(0, plugin_js_1.createGitHubPlugin)()];
13
- const seen = new Set(plugins.map((plugin) => plugin.name));
14
- for (const name of configured) {
15
- const factory = BUILTIN_PLUGIN_FACTORIES[name];
16
- if (!factory || seen.has(name)) {
17
- continue;
18
- }
19
- const plugin = factory();
20
- plugins.push(plugin);
21
- seen.add(plugin.name);
22
- }
23
- return plugins;
5
+ function loadRuntimePlugins() {
6
+ return [(0, plugin_js_1.createGitHubPlugin)()];
24
7
  }
@@ -9,12 +9,13 @@ export interface VersionaryArtifactRule {
9
9
  export interface VersionaryPackage {
10
10
  "release-type"?: string;
11
11
  "package-name"?: string;
12
+ "changelog-file"?: string;
12
13
  "exclude-paths"?: string[];
13
14
  "extra-files"?: VersionaryArtifactRule[];
14
15
  }
15
16
  export interface VersionaryConfig {
16
17
  version: 1;
17
- "review-mode"?: "direct" | "review";
18
+ "review-mode"?: "direct" | "pr" | "review";
18
19
  "version-file"?: string;
19
20
  "changelog-file"?: string;
20
21
  "release-branch"?: string;
@@ -26,7 +27,6 @@ export interface VersionaryConfig {
26
27
  "include-commit-authors"?: boolean;
27
28
  "release-type"?: string;
28
29
  packages?: Record<string, VersionaryPackage>;
29
- plugins?: string[];
30
30
  }
31
31
  export interface LoadedConfig {
32
32
  path: string;
@@ -4,6 +4,8 @@ export interface VerifyResult {
4
4
  name: string;
5
5
  ok: boolean;
6
6
  details: string;
7
+ category: "config" | "paths" | "version-files";
8
+ remediation?: string;
7
9
  }>;
8
10
  }
9
11
  export declare function verifyProject(cwd?: string): VerifyResult;
@@ -16,6 +16,7 @@ function verifyProject(cwd = process.cwd()) {
16
16
  name: "config-load",
17
17
  ok: true,
18
18
  details: `Loaded ${node_path_1.default.basename(config.path)} (${config.format})`,
19
+ category: "config",
19
20
  });
20
21
  const strategy = (0, resolve_js_1.resolveVersionStrategy)(config.config);
21
22
  const versionFile = strategy.getVersionFile(config.config);
@@ -24,6 +25,10 @@ function verifyProject(cwd = process.cwd()) {
24
25
  name: `version-file:${versionFile}`,
25
26
  ok: exists,
26
27
  details: exists ? "Version file exists" : `Missing ${versionFile}`,
28
+ category: "version-files",
29
+ remediation: exists
30
+ ? undefined
31
+ : `Create ${versionFile} or set "version-file" to the correct path for your release strategy.`,
27
32
  });
28
33
  if (config.config.packages) {
29
34
  for (const [pkgPathRaw, packageConfig] of Object.entries(config.config.packages)) {
@@ -33,6 +38,10 @@ function verifyProject(cwd = process.cwd()) {
33
38
  name: `package-path:${pkgPathRaw}`,
34
39
  ok: exists,
35
40
  details: exists ? "Path exists" : `Missing path: ${pkgPathRaw}`,
41
+ category: "paths",
42
+ remediation: exists
43
+ ? undefined
44
+ : `Create ${pkgPathRaw} or remove/rename this entry under "packages" in versionary config.`,
36
45
  });
37
46
  if (exists) {
38
47
  const packageContext = (0, package_context_js_1.resolvePackageStrategyContext)(config.config, pkgPathRaw, packageConfig);
@@ -44,6 +53,10 @@ function verifyProject(cwd = process.cwd()) {
44
53
  details: packageVersionExists
45
54
  ? "Version file exists"
46
55
  : `Missing ${packageVersionFile}`,
56
+ category: "version-files",
57
+ remediation: packageVersionExists
58
+ ? undefined
59
+ : `Create ${packageVersionFile} or adjust package release settings ("release-type"/"version-file") for ${pkgPathRaw}.`,
47
60
  });
48
61
  }
49
62
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "versionary",
3
- "version": "0.8.2",
3
+ "version": "0.10.0",
4
4
  "description": "Automatic release framework based on conventional commits and semantic versioning",
5
5
  "keywords": [
6
6
  "releasing",