versionary 0.3.0 → 0.5.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
@@ -72,7 +72,7 @@ Configuration is loaded from `versionary.jsonc` by default (or
72
72
 
73
73
  Schema URL for editor support:
74
74
 
75
- - `https://raw.githubusercontent.com/jolars/versionary/main/schemas/versionary-schema.json`
75
+ - `https://raw.githubusercontent.com/jolars/versionary/main/schemas/config.json`
76
76
 
77
77
  ## Config (manifest style)
78
78
 
@@ -82,6 +82,10 @@ For a quick trial, use:
82
82
  - `changelog-file` (default `CHANGELOG.md`) as release notes output
83
83
  - `release-type: "node"` uses `package.json` as version source and updates it
84
84
  during release PR prep
85
+ - `release-type: "r"` uses `DESCRIPTION` as version source and updates the
86
+ `Version:` field
87
+ - `release-type: "rust"` uses Cargo manifests (`Cargo.toml`) as version source;
88
+ `version-file` must point to a `Cargo.toml` (default: `Cargo.toml`)
85
89
  - simple/default strategy keeps `version.txt` as source of truth and does not
86
90
  update `package.json`
87
91
  - stable release branch (`release-branch`, default: `versionary/release`) so
@@ -97,6 +101,41 @@ For a quick trial, use:
97
101
  - `independent` computes package bumps per path
98
102
  - `fixed` computes one shared bump across configured package paths
99
103
 
104
+ Rust strategy examples:
105
+
106
+ ```jsonc
107
+ // Single crate
108
+ {
109
+ "release-type": "rust",
110
+ "version-file": "Cargo.toml"
111
+ }
112
+ ```
113
+
114
+ ```jsonc
115
+ // Workspace root (virtual or root crate + members)
116
+ {
117
+ "release-type": "rust",
118
+ "version-file": "Cargo.toml"
119
+ }
120
+ ```
121
+
122
+ Current rust auto-update behavior (phase scope):
123
+
124
+ - updates crate versions in each targeted crate `[package].version`
125
+ - updates internal workspace dependency versions when the dependency name
126
+ matches another targeted crate name
127
+ - applies dependency version rewrites in:
128
+ - `[dependencies]`, `[dev-dependencies]`, `[build-dependencies]`
129
+ - `[target.*.dependencies]`, `[target.*.dev-dependencies]`,
130
+ `[target.*.build-dependencies]`
131
+
132
+ Current rust non-goals/limits:
133
+
134
+ - does not update external dependency versions
135
+ - does not update `workspace.dependencies`
136
+ - does not add missing `version = ...` fields to dependency inline tables
137
+ - does not perform Cargo publish/release to crates.io
138
+
100
139
  ## Commit parsing and release analysis
101
140
 
102
141
  Release planning is based on Conventional Commit parsing semantics:
@@ -105,20 +144,21 @@ Release planning is based on Conventional Commit parsing semantics:
105
144
  - exposes structured parsed fields (`header`, `body`, `footer`, `type`, `scope`,
106
145
  `description`, `notes`, `references`, `mentions`, `revert`)
107
146
  - separates parser output from release policy mapping (`inferReleaseType*`)
108
- - recognizes breaking changes from `!` and `BREAKING CHANGE` /
109
- `BREAKING-CHANGE` footers
147
+ - recognizes breaking changes from `!` and `BREAKING CHANGE` / `BREAKING-CHANGE`
148
+ footers
110
149
  - maps release impact as `feat => minor`, `fix|perf => patch`, breaking => major
111
150
  - treats `revert:` as non-releasable commits
112
- - suppresses commits that are reverted within the analyzed release window so they
113
- do not affect bump/changelog output
114
- - emits parser diagnostics for malformed headers/footers/references and ambiguous
115
- revert messages
151
+ - suppresses commits that are reverted within the analyzed release window so
152
+ they do not affect bump/changelog output
153
+ - emits parser diagnostics for malformed headers/footers/references and
154
+ ambiguous revert messages
116
155
 
117
156
  Commands:
118
157
 
119
158
  - `pnpm verify`
120
159
  - `pnpm run` (default orchestration: no-op, create/update release PR, or publish
121
160
  release based on context)
161
+ - `pnpm run -- --json` (machine-readable orchestration result)
122
162
  - `pnpm plan`
123
163
  - `pnpm changelog -- --write`
124
164
  - `pnpm pr`
@@ -133,8 +173,8 @@ Subsequent runs use the baseline state file.
133
173
 
134
174
  ## Release retry and recovery behavior
135
175
 
136
- Release publish (`pnpm release` or the publish path in `pnpm run`) is
137
- idempotent by target tag:
176
+ Release publish (`pnpm release` or the publish path in `pnpm run`) is idempotent
177
+ by target tag:
138
178
 
139
179
  - if a tag already exists, Versionary reuses it rather than recreating it
140
180
  - if release metadata already exists for the tag (e.g., GitHub Release), it is
@@ -188,18 +228,10 @@ steps:
188
228
  with:
189
229
  fetch-depth: 0
190
230
  fetch-tags: true
191
- - uses: pnpm/action-setup@v6
192
- - uses: actions/setup-node@v6
231
+ - id: versionary
232
+ uses: jolars/versionary@v1
193
233
  with:
194
- node-version-file: .nvmrc
195
- cache: pnpm
196
- - run: pnpm install --frozen-lockfile --ignore-scripts
197
- - run: |
198
- git config user.name "github-actions[bot]"
199
- git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
200
- pnpm run run
201
- env:
202
- GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
234
+ github-token: ${{ secrets.RELEASE_TOKEN }}
203
235
  ```
204
236
 
205
237
  ```yaml
@@ -212,17 +244,28 @@ steps:
212
244
  with:
213
245
  fetch-depth: 0
214
246
  fetch-tags: true
215
- - uses: pnpm/action-setup@v6
216
- - uses: actions/setup-node@v6
247
+ - id: versionary
248
+ uses: jolars/versionary@v1
217
249
  with:
218
- node-version-file: .nvmrc
219
- cache: pnpm
220
- - run: pnpm install --frozen-lockfile --ignore-scripts
221
- - run: pnpm run run
222
- env:
223
- GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
250
+ github-token: ${{ secrets.RELEASE_TOKEN }}
251
+ - if: ${{ steps.versionary.outputs.release_created == 'true' }}
252
+ run: echo "Released ${{ steps.versionary.outputs.tag_name }}"
224
253
  ```
225
254
 
255
+ Action outputs:
256
+
257
+ - `action`: `noop`, `pr-prepared`, `release-published`, `release-skipped`
258
+ - `message`: human-readable summary
259
+ - `release_created`: `"true"` when at least one release was published
260
+ - `tag_name`: first published tag (for single-target flows)
261
+ - `tag_names`: JSON array of published tags
262
+ - `review_url`: review request URL when PR flow runs
263
+
264
+ For GitHub Action consumers, publish immutable tags (for example `v1.2.3`) and
265
+ maintain a moving major tag (`v1`, `v2`, ...). A small release-triggered
266
+ workflow should update `v<major>` to the latest release tag so `uses:
267
+ jolars/versionary@v1` stays current without breaking major compatibility.
268
+
226
269
  Package publication is intentionally out of scope in the current release flow.
227
270
  Use separate CI workflows for publishing after Versionary has prepared/tagged
228
271
  the release.
@@ -0,0 +1,3 @@
1
+ import type { SimplePlan } from "../../domain/release/plan.js";
2
+ import type { VersionaryConfig } from "../../types/config.js";
3
+ export declare function applyConfiguredArtifactRules(cwd: string, config: VersionaryConfig, plan: SimplePlan): string[];
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.applyConfiguredArtifactRules = applyConfiguredArtifactRules;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const toml_1 = __importDefault(require("@iarna/toml"));
10
+ const yaml_1 = __importDefault(require("yaml"));
11
+ function isRecord(value) {
12
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
13
+ }
14
+ function parseJsonPath(jsonpath) {
15
+ if (!jsonpath.startsWith("$")) {
16
+ throw new Error(`Invalid jsonpath "${jsonpath}". Must start with "$".`);
17
+ }
18
+ const tokens = [];
19
+ let index = 1;
20
+ while (index < jsonpath.length) {
21
+ const current = jsonpath[index];
22
+ if (current === ".") {
23
+ const keyMatch = jsonpath.slice(index + 1).match(/^[A-Za-z0-9_-]+/u);
24
+ if (!keyMatch) {
25
+ throw new Error(`Invalid jsonpath "${jsonpath}" near index ${index}.`);
26
+ }
27
+ tokens.push(keyMatch[0]);
28
+ index += 1 + keyMatch[0].length;
29
+ continue;
30
+ }
31
+ if (current === "[") {
32
+ const rest = jsonpath.slice(index + 1);
33
+ const numberMatch = rest.match(/^(\d+)\]/u);
34
+ if (numberMatch) {
35
+ tokens.push(Number(numberMatch[1]));
36
+ index += 2 + numberMatch[1].length;
37
+ continue;
38
+ }
39
+ const keyMatch = rest.match(/^"([^"]+)"\]/u);
40
+ if (keyMatch) {
41
+ tokens.push(keyMatch[1]);
42
+ index += 4 + keyMatch[1].length;
43
+ continue;
44
+ }
45
+ throw new Error(`Invalid jsonpath "${jsonpath}" near index ${index}.`);
46
+ }
47
+ throw new Error(`Invalid jsonpath "${jsonpath}" near index ${index}.`);
48
+ }
49
+ if (tokens.length === 0) {
50
+ throw new Error(`Invalid jsonpath "${jsonpath}". Path must target a field.`);
51
+ }
52
+ return tokens;
53
+ }
54
+ function setVersionAtJsonPath(document, jsonpath, version) {
55
+ const tokens = parseJsonPath(jsonpath);
56
+ let cursor = document;
57
+ for (let index = 0; index < tokens.length - 1; index += 1) {
58
+ const token = tokens[index];
59
+ if (typeof token === "number") {
60
+ if (!Array.isArray(cursor) || token >= cursor.length) {
61
+ throw new Error(`jsonpath "${jsonpath}" does not resolve to an existing field.`);
62
+ }
63
+ cursor = cursor[token];
64
+ continue;
65
+ }
66
+ if (!isRecord(cursor) || !(token in cursor)) {
67
+ throw new Error(`jsonpath "${jsonpath}" does not resolve to an existing field.`);
68
+ }
69
+ cursor = cursor[token];
70
+ }
71
+ const leaf = tokens.at(-1);
72
+ if (typeof leaf === "number") {
73
+ if (!Array.isArray(cursor) || leaf >= cursor.length) {
74
+ throw new Error(`jsonpath "${jsonpath}" does not resolve to an existing field.`);
75
+ }
76
+ const current = cursor[leaf];
77
+ if (typeof current !== "string" && typeof current !== "number") {
78
+ throw new Error(`jsonpath "${jsonpath}" must point to a string or number field for version updates.`);
79
+ }
80
+ cursor[leaf] = version;
81
+ return;
82
+ }
83
+ if (!isRecord(cursor) || !(leaf in cursor)) {
84
+ throw new Error(`jsonpath "${jsonpath}" does not resolve to an existing field.`);
85
+ }
86
+ const current = cursor[leaf];
87
+ if (typeof current !== "string" && typeof current !== "number") {
88
+ throw new Error(`jsonpath "${jsonpath}" must point to a string or number field for version updates.`);
89
+ }
90
+ cursor[leaf] = version;
91
+ }
92
+ function parseRegexPattern(pattern) {
93
+ const slashPattern = pattern.match(/^\/((?:\\\/|[^/])+)\/([a-z]*)$/u);
94
+ if (slashPattern) {
95
+ return new RegExp(slashPattern[1], slashPattern[2]);
96
+ }
97
+ return new RegExp(pattern, "m");
98
+ }
99
+ function applyRegexRule(content, pattern, version) {
100
+ const regex = parseRegexPattern(pattern);
101
+ const matchFlags = regex.flags.includes("g")
102
+ ? regex.flags
103
+ : `${regex.flags}g`;
104
+ const globalRegex = new RegExp(regex.source, matchFlags);
105
+ const matches = [...content.matchAll(globalRegex)];
106
+ if (matches.length !== 1) {
107
+ throw new Error(`Regex pattern must match exactly one occurrence; matched ${matches.length}.`);
108
+ }
109
+ const match = matches[0];
110
+ const start = match.index;
111
+ if (start === undefined) {
112
+ throw new Error("Regex match did not include an index.");
113
+ }
114
+ const full = match[0];
115
+ const groupOne = match[1];
116
+ const replacement = typeof groupOne === "string" ? full.replace(groupOne, version) : version;
117
+ return `${content.slice(0, start)}${replacement}${content.slice(start + full.length)}`;
118
+ }
119
+ function applyArtifactRuleToContent(content, rule, version) {
120
+ if (rule.type === "regex") {
121
+ return applyRegexRule(content, rule.pattern, version);
122
+ }
123
+ if (rule.type === "json") {
124
+ const parsed = JSON.parse(content);
125
+ setVersionAtJsonPath(parsed, rule.jsonpath, version);
126
+ return `${JSON.stringify(parsed, null, 2)}\n`;
127
+ }
128
+ if (rule.type === "toml") {
129
+ const parsed = toml_1.default.parse(content);
130
+ setVersionAtJsonPath(parsed, rule.jsonpath, version);
131
+ return `${toml_1.default.stringify(parsed)}\n`;
132
+ }
133
+ const parsed = yaml_1.default.parse(content);
134
+ setVersionAtJsonPath(parsed, rule.jsonpath, version);
135
+ return `${yaml_1.default.stringify(parsed)}`;
136
+ }
137
+ function normalizeRelative(base, target) {
138
+ return node_path_1.default.relative(base, target).replaceAll("\\", "/");
139
+ }
140
+ function applyArtifactRulesForPackage(cwd, packagePath, packageConfig, version) {
141
+ const rules = packageConfig["extra-files"] ?? [];
142
+ if (rules.length === 0) {
143
+ return [];
144
+ }
145
+ const packageBase = node_path_1.default.join(cwd, packagePath);
146
+ const updated = [];
147
+ for (const rule of rules) {
148
+ const targetPath = node_path_1.default.join(packageBase, rule.path);
149
+ if (!node_fs_1.default.existsSync(targetPath)) {
150
+ throw new Error(`Artifact rule target missing for package "${packagePath}": ${rule.path}`);
151
+ }
152
+ const existing = node_fs_1.default.readFileSync(targetPath, "utf8");
153
+ let next;
154
+ try {
155
+ next = applyArtifactRuleToContent(existing, rule, version);
156
+ }
157
+ catch (error) {
158
+ const message = error instanceof Error ? error.message : String(error);
159
+ throw new Error(`Failed applying artifact rule (${rule.type}) for package "${packagePath}" file "${rule.path}": ${message}`);
160
+ }
161
+ node_fs_1.default.writeFileSync(targetPath, next, "utf8");
162
+ updated.push(normalizeRelative(cwd, targetPath));
163
+ }
164
+ return updated;
165
+ }
166
+ function applyConfiguredArtifactRules(cwd, config, plan) {
167
+ const packageConfigs = config.packages ?? {};
168
+ if (!plan.packages || plan.packages.length === 0) {
169
+ return [];
170
+ }
171
+ const updated = new Set();
172
+ for (const packagePlan of plan.packages) {
173
+ if (!packagePlan.nextVersion) {
174
+ continue;
175
+ }
176
+ const packageConfig = packageConfigs[packagePlan.path];
177
+ if (!packageConfig?.["extra-files"]?.length) {
178
+ continue;
179
+ }
180
+ const files = applyArtifactRulesForPackage(cwd, packagePlan.path, packageConfig, packagePlan.nextVersion);
181
+ for (const file of files) {
182
+ updated.add(file);
183
+ }
184
+ }
185
+ return [...updated].sort((a, b) => a.localeCompare(b));
186
+ }
@@ -1,10 +1,13 @@
1
1
  import { type SimplePlan } from "../../domain/release/plan.js";
2
2
  import type { ParsedCommit } from "../../infra/git/commits.js";
3
+ import type { VersionaryPluginContext } from "../../types/plugins.js";
3
4
  export declare function splitSafeDirtyFiles(files: string[]): {
4
5
  ignored: string[];
5
6
  blocking: string[];
6
7
  };
7
- export declare function prepareSimpleReleasePr(cwd?: string): {
8
+ export declare function prepareSimpleReleasePr(cwd?: string, options?: {
9
+ logger?: VersionaryPluginContext["logger"];
10
+ }): {
8
11
  branch: string;
9
12
  title: string;
10
13
  version: string;
@@ -13,6 +16,8 @@ export declare function prepareSimpleReleasePr(cwd?: string): {
13
16
  plan: SimplePlan;
14
17
  };
15
18
  export declare function renderSimpleReviewRequestBody(version: string, previousVersion: string, commits: ParsedCommit[], plan?: SimplePlan | null, cwd?: string): string;
16
- export declare function openOrUpdateSimpleReviewRequest(cwd: string, branch: string, title: string, version: string, previousVersion: string, commits: ParsedCommit[], plan?: SimplePlan | null): Promise<string>;
19
+ export declare function openOrUpdateSimpleReviewRequest(cwd: string, branch: string, title: string, version: string, previousVersion: string, commits: ParsedCommit[], plan?: SimplePlan | null, options?: {
20
+ logger?: VersionaryPluginContext["logger"];
21
+ }): Promise<string>;
17
22
  export declare function pushReleaseBranch(cwd: string, branch: string): void;
18
23
  export declare function isReleaseCommitMessage(subject: string): boolean;
@@ -14,9 +14,12 @@ const node_path_1 = __importDefault(require("node:path"));
14
14
  const load_config_js_1 = require("../../config/load-config.js");
15
15
  const changelog_js_1 = require("../../domain/release/changelog.js");
16
16
  const plan_js_1 = require("../../domain/release/plan.js");
17
+ const package_context_js_1 = require("../../domain/strategy/package-context.js");
17
18
  const resolve_js_1 = require("../../domain/strategy/resolve.js");
19
+ const rust_js_1 = require("../../domain/strategy/rust.js");
18
20
  const capabilities_js_1 = require("../../plugins/capabilities.js");
19
21
  const runtime_js_1 = require("../../plugins/runtime.js");
22
+ const artifact_rules_js_1 = require("./artifact-rules.js");
20
23
  const state_js_1 = require("./state.js");
21
24
  const SAFE_DIRTY_FILES = new Set([
22
25
  "pnpm-lock.yaml",
@@ -55,25 +58,46 @@ function splitSafeDirtyFiles(files) {
55
58
  }
56
59
  return { ignored, blocking };
57
60
  }
58
- function ensureCleanWorktree(cwd) {
61
+ function ensureCleanWorktree(cwd, logger) {
59
62
  const dirtyFiles = listTrackedDirtyFiles(cwd);
60
63
  const { ignored, blocking } = splitSafeDirtyFiles(dirtyFiles);
61
64
  if (blocking.length > 0) {
62
65
  throw new Error(`Working tree has tracked modifications before versionary pr:\n${blocking.join("\n")}\nCommit/stash tracked changes first.`);
63
66
  }
64
67
  if (ignored.length > 0) {
65
- console.warn(`Ignoring safe tracked changes before versionary pr:\n${ignored.join("\n")}`);
68
+ logger?.warn(`Ignoring safe tracked changes before versionary pr:\n${ignored.join("\n")}`);
66
69
  }
67
70
  }
68
- function prepareSimpleReleasePr(cwd = process.cwd()) {
71
+ function prepareSimpleReleasePr(cwd = process.cwd(), options = {}) {
69
72
  const plan = (0, plan_js_1.createSimplePlan)(cwd);
70
73
  const loaded = (0, load_config_js_1.loadConfig)(cwd);
71
74
  const strategy = (0, resolve_js_1.resolveVersionStrategy)(loaded.config);
72
75
  if (!plan.nextVersion) {
73
76
  throw new Error("No releasable commits found. Nothing to open a release PR for.");
74
77
  }
75
- ensureCleanWorktree(cwd);
76
- const updatedVersionFiles = strategy.writeVersion(cwd, loaded.config, plan.nextVersion);
78
+ ensureCleanWorktree(cwd, options.logger);
79
+ const updatedVersionFiles = [];
80
+ const rustManifestVersionTargets = {};
81
+ if (plan.packages && plan.packages.length > 0) {
82
+ for (const packagePlan of plan.packages) {
83
+ if (!packagePlan.nextVersion) {
84
+ continue;
85
+ }
86
+ const packageConfig = loaded.config.packages?.[packagePlan.path] ?? {};
87
+ const packageContext = (0, package_context_js_1.resolvePackageStrategyContext)(loaded.config, packagePlan.path, packageConfig);
88
+ const packageUpdated = packageContext.strategy.writeVersion(cwd, packageContext.config, packagePlan.nextVersion);
89
+ updatedVersionFiles.push(...packageUpdated);
90
+ if (packageContext.strategy.name === rust_js_1.rustVersionStrategy.name) {
91
+ rustManifestVersionTargets[packageContext.versionFile] =
92
+ packagePlan.nextVersion;
93
+ }
94
+ }
95
+ updatedVersionFiles.push(...(0, rust_js_1.applyRustWorkspaceDependencyUpdates)(cwd, rustManifestVersionTargets));
96
+ }
97
+ else {
98
+ updatedVersionFiles.push(...strategy.writeVersion(cwd, loaded.config, plan.nextVersion));
99
+ }
100
+ const updatedArtifactFiles = (0, artifact_rules_js_1.applyConfiguredArtifactRules)(cwd, loaded.config, plan);
77
101
  const section = (0, changelog_js_1.renderSimpleChangelog)(plan);
78
102
  (0, changelog_js_1.prependChangelog)(cwd, plan.changelogFile, section);
79
103
  const branch = plan.releaseBranchPrefix;
@@ -82,7 +106,13 @@ function prepareSimpleReleasePr(cwd = process.cwd()) {
82
106
  cwd,
83
107
  stdio: ["ignore", "pipe", "ignore"],
84
108
  });
85
- const filesToAdd = [...new Set([...updatedVersionFiles, plan.changelogFile])];
109
+ const filesToAdd = [
110
+ ...new Set([
111
+ ...updatedVersionFiles,
112
+ ...updatedArtifactFiles,
113
+ plan.changelogFile,
114
+ ]),
115
+ ];
86
116
  (0, node_child_process_1.execFileSync)("git", ["add", ...filesToAdd], {
87
117
  cwd,
88
118
  stdio: ["ignore", "pipe", "ignore"],
@@ -100,24 +130,12 @@ function prepareSimpleReleasePr(cwd = process.cwd()) {
100
130
  tag: pkg.path === "."
101
131
  ? `v${pkg.nextVersion ?? ""}`
102
132
  : `${pkg.path.replaceAll("/", "-")}-v${pkg.nextVersion ?? ""}`,
103
- notes: (0, changelog_js_1.renderSimpleReleaseNotes)({
104
- currentVersion: pkg.currentVersion,
105
- nextVersion: pkg.nextVersion ?? "",
106
- commits: pkg.commits,
107
- cwd,
108
- }, { includeFooter: false }),
109
133
  }))
110
134
  : [
111
135
  {
112
136
  path: ".",
113
137
  version: plan.nextVersion,
114
138
  tag: `v${plan.nextVersion}`,
115
- notes: (0, changelog_js_1.renderSimpleReleaseNotes)({
116
- currentVersion: plan.currentVersion,
117
- nextVersion: plan.nextVersion,
118
- commits: plan.commits,
119
- cwd,
120
- }, { includeFooter: false }),
121
139
  },
122
140
  ];
123
141
  (0, state_js_1.writeBaselineSha)(cwd, undefined, releaseTargets);
@@ -171,7 +189,7 @@ function renderSimpleReviewRequestBody(version, previousVersion, commits, plan =
171
189
  cwd,
172
190
  }, { includeFooter: true });
173
191
  }
174
- async function openOrUpdateSimpleReviewRequest(cwd, branch, title, version, previousVersion, commits, plan = null) {
192
+ async function openOrUpdateSimpleReviewRequest(cwd, branch, title, version, previousVersion, commits, plan = null, options = {}) {
175
193
  const loaded = (0, load_config_js_1.loadConfig)(cwd);
176
194
  const releaseFlow = loaded.config["review-mode"] ?? "direct";
177
195
  if (releaseFlow !== "review") {
@@ -194,7 +212,7 @@ async function openOrUpdateSimpleReviewRequest(cwd, branch, title, version, prev
194
212
  labels: ["release"],
195
213
  }, {
196
214
  cwd,
197
- logger: console,
215
+ logger: options.logger,
198
216
  });
199
217
  return result.url;
200
218
  }
@@ -7,6 +7,9 @@ export interface ReleaseTargetInput {
7
7
  }
8
8
  export interface ReleaseExecutionContext {
9
9
  createReleaseMetadata: (input: ReleaseTargetInput) => Promise<VersionaryScmReleaseMetadataResult>;
10
+ logger?: {
11
+ info: (message: string) => void;
12
+ };
10
13
  }
11
14
  export interface ReleaseTargetOutcome {
12
15
  tag: string;
@@ -87,7 +87,7 @@ async function executeIdempotentReleaseTarget(cwd, target, context) {
87
87
  const metadata = await context.createReleaseMetadata(target);
88
88
  const metadataStatus = metadata.status ?? "created";
89
89
  if (tagStatus === "exists" && metadataStatus === "created") {
90
- console.info(`Recovered drift for ${target.tag}: tag already existed and release metadata has now been created.`);
90
+ context.logger?.info(`Recovered drift for ${target.tag}: tag already existed and release metadata has now been created.`);
91
91
  }
92
92
  if (!metadata.url) {
93
93
  throw new Error(`Release metadata for "${target.tag}" did not return a URL. Verify SCM API permissions and retry.`);
@@ -1 +1,19 @@
1
+ import type { VersionaryPluginContext } from "../../types/plugins.js";
1
2
  export declare function runSimpleRelease(cwd?: string): Promise<string>;
3
+ export type SimpleRunReleaseResult = {
4
+ action: "release-skipped";
5
+ reason: string;
6
+ } | {
7
+ action: "release-published";
8
+ message: string;
9
+ releases: {
10
+ tag: string;
11
+ url: string;
12
+ tagStatus: "created" | "exists";
13
+ metadataStatus: "created" | "exists";
14
+ }[];
15
+ };
16
+ export interface RunSimpleReleaseOptions {
17
+ logger?: VersionaryPluginContext["logger"];
18
+ }
19
+ export declare function runSimpleReleaseDetailed(cwd?: string, options?: RunSimpleReleaseOptions): Promise<SimpleRunReleaseResult>;
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runSimpleRelease = runSimpleRelease;
7
+ exports.runSimpleReleaseDetailed = runSimpleReleaseDetailed;
7
8
  const node_child_process_1 = require("node:child_process");
8
9
  const node_fs_1 = __importDefault(require("node:fs"));
9
10
  const node_path_1 = __importDefault(require("node:path"));
@@ -46,9 +47,19 @@ function readReleaseNotes(cwd, version, changelogFile) {
46
47
  return notes.length > 0 ? notes : `Automated release for v${version}`;
47
48
  }
48
49
  async function runSimpleRelease(cwd = process.cwd()) {
50
+ const result = await runSimpleReleaseDetailed(cwd, { logger: console });
51
+ if (result.action === "release-skipped") {
52
+ return result.reason;
53
+ }
54
+ return result.message;
55
+ }
56
+ async function runSimpleReleaseDetailed(cwd = process.cwd(), options = {}) {
49
57
  const subject = getHeadCommitSubject(cwd);
50
58
  if (!(0, pr_js_1.isReleaseCommitMessage)(subject)) {
51
- return "No release commit context detected; skipping release stage.";
59
+ return {
60
+ action: "release-skipped",
61
+ reason: "No release commit context detected; skipping release stage.",
62
+ };
52
63
  }
53
64
  const loaded = (0, load_config_js_1.loadConfig)(cwd);
54
65
  const strategy = (0, resolve_js_1.resolveVersionStrategy)(loaded.config);
@@ -72,19 +83,29 @@ async function runSimpleRelease(cwd = process.cwd()) {
72
83
  path: ".",
73
84
  version,
74
85
  tag: defaultTag,
75
- notes: readReleaseNotes(cwd, version, changelogFile),
76
86
  },
77
87
  ];
78
- const published = [];
88
+ const releases = [];
79
89
  for (const target of targets) {
80
90
  const outcome = await (0, recovery_js_1.executeIdempotentReleaseTarget)(cwd, {
81
91
  tag: target.tag,
82
92
  version: target.version,
83
- notes: target.notes ?? readReleaseNotes(cwd, target.version, changelogFile),
93
+ notes: readReleaseNotes(cwd, target.version, changelogFile),
84
94
  }, {
85
- createReleaseMetadata: (input) => plugin.createReleaseMetadata(input, { cwd, logger: console }),
95
+ createReleaseMetadata: (input) => plugin.createReleaseMetadata(input, { cwd, logger: options.logger }),
96
+ logger: options.logger,
97
+ });
98
+ releases.push({
99
+ tag: outcome.tag,
100
+ url: outcome.url,
101
+ tagStatus: outcome.tagStatus,
102
+ metadataStatus: outcome.metadataStatus,
86
103
  });
87
- published.push(`${outcome.tag}: ${outcome.url} (tag=${outcome.tagStatus}, metadata=${outcome.metadataStatus})`);
88
104
  }
89
- return `Published releases ${published.join(", ")}`;
105
+ const published = releases.map((outcome) => `${outcome.tag}: ${outcome.url} (tag=${outcome.tagStatus}, metadata=${outcome.metadataStatus})`);
106
+ return {
107
+ action: "release-published",
108
+ releases,
109
+ message: `Published releases ${published.join(", ")}`,
110
+ };
90
111
  }
@@ -2,7 +2,6 @@ export interface ReleaseTargetState {
2
2
  path: string;
3
3
  version: string;
4
4
  tag: string;
5
- notes?: string;
6
5
  }
7
6
  export declare function getBaselineStatePath(cwd: string): string;
8
7
  export declare function readBaselineSha(cwd?: string): string | null;