versionary 0.5.0 → 0.7.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
@@ -100,6 +100,7 @@ For a quick trial, use:
100
100
  - optional monorepo planning with `monorepo-mode` and `packages`:
101
101
  - `independent` computes package bumps per path
102
102
  - `fixed` computes one shared bump across configured package paths
103
+ - per-package `package-name` can override release identity (labels + tag base)
103
104
 
104
105
  Rust strategy examples:
105
106
 
@@ -124,6 +125,8 @@ Current rust auto-update behavior (phase scope):
124
125
  - updates crate versions in each targeted crate `[package].version`
125
126
  - updates internal workspace dependency versions when the dependency name
126
127
  matches another targeted crate name
128
+ - refreshes `Cargo.lock` via `cargo generate-lockfile` when `Cargo.lock` exists
129
+ in repo root
127
130
  - applies dependency version rewrites in:
128
131
  - `[dependencies]`, `[dev-dependencies]`, `[build-dependencies]`
129
132
  - `[target.*.dependencies]`, `[target.*.dev-dependencies]`,
@@ -136,6 +139,28 @@ Current rust non-goals/limits:
136
139
  - does not add missing `version = ...` fields to dependency inline tables
137
140
  - does not perform Cargo publish/release to crates.io
138
141
 
142
+ If `Cargo.lock` exists, `cargo` must be available in PATH during PR preparation.
143
+
144
+ ### Monorepo release names and tag naming
145
+
146
+ For independent monorepo targets, Versionary derives release tags as:
147
+
148
+ - root package (`"."`): `v<version>`
149
+ - non-root package: `<release-name>-v<version>`
150
+
151
+ `release-name` precedence is:
152
+
153
+ 1. `packages.<path>.package-name` (explicit override)
154
+ 2. strategy-native package name from version file:
155
+ - Node: `package.json` `name`
156
+ - Rust: `Cargo.toml` `[package].name`
157
+ - R: `DESCRIPTION` `Package:`
158
+ 3. package path fallback
159
+
160
+ When multiple packages resolve to the same `<release-name>` and version, the run
161
+ fails fast with a duplicate-tag error and suggests setting unique
162
+ `package-name` values.
163
+
139
164
  ## Commit parsing and release analysis
140
165
 
141
166
  Release planning is based on Conventional Commit parsing semantics:
@@ -231,7 +256,7 @@ steps:
231
256
  - id: versionary
232
257
  uses: jolars/versionary@v1
233
258
  with:
234
- github-token: ${{ secrets.RELEASE_TOKEN }}
259
+ token: ${{ secrets.RELEASE_TOKEN }}
235
260
  ```
236
261
 
237
262
  ```yaml
@@ -247,11 +272,16 @@ steps:
247
272
  - id: versionary
248
273
  uses: jolars/versionary@v1
249
274
  with:
250
- github-token: ${{ secrets.RELEASE_TOKEN }}
275
+ token: ${{ secrets.RELEASE_TOKEN }}
251
276
  - if: ${{ steps.versionary.outputs.release_created == 'true' }}
252
277
  run: echo "Released ${{ steps.versionary.outputs.tag_name }}"
253
278
  ```
254
279
 
280
+ `token` is used for both GitHub API calls and git push authentication in
281
+ the composite action. This means release-branch force-pushes are attributed to
282
+ that token and can trigger downstream workflows when using a PAT/App token.
283
+ (`github-token` remains as a deprecated alias for backward compatibility.)
284
+
255
285
  Action outputs:
256
286
 
257
287
  - `action`: `noop`, `pr-prepared`, `release-published`, `release-skipped`
@@ -11,25 +11,25 @@ const yaml_1 = __importDefault(require("yaml"));
11
11
  function isRecord(value) {
12
12
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
13
13
  }
14
- function parseJsonPath(jsonpath) {
15
- if (!jsonpath.startsWith("$")) {
16
- throw new Error(`Invalid jsonpath "${jsonpath}". Must start with "$".`);
14
+ function parseFieldPath(fieldPath) {
15
+ if (!fieldPath.startsWith("$")) {
16
+ throw new Error(`Invalid field-path "${fieldPath}". Must start with "$".`);
17
17
  }
18
18
  const tokens = [];
19
19
  let index = 1;
20
- while (index < jsonpath.length) {
21
- const current = jsonpath[index];
20
+ while (index < fieldPath.length) {
21
+ const current = fieldPath[index];
22
22
  if (current === ".") {
23
- const keyMatch = jsonpath.slice(index + 1).match(/^[A-Za-z0-9_-]+/u);
23
+ const keyMatch = fieldPath.slice(index + 1).match(/^[A-Za-z0-9_-]+/u);
24
24
  if (!keyMatch) {
25
- throw new Error(`Invalid jsonpath "${jsonpath}" near index ${index}.`);
25
+ throw new Error(`Invalid field-path "${fieldPath}" near index ${index}.`);
26
26
  }
27
27
  tokens.push(keyMatch[0]);
28
28
  index += 1 + keyMatch[0].length;
29
29
  continue;
30
30
  }
31
31
  if (current === "[") {
32
- const rest = jsonpath.slice(index + 1);
32
+ const rest = fieldPath.slice(index + 1);
33
33
  const numberMatch = rest.match(/^(\d+)\]/u);
34
34
  if (numberMatch) {
35
35
  tokens.push(Number(numberMatch[1]));
@@ -42,53 +42,60 @@ function parseJsonPath(jsonpath) {
42
42
  index += 4 + keyMatch[1].length;
43
43
  continue;
44
44
  }
45
- throw new Error(`Invalid jsonpath "${jsonpath}" near index ${index}.`);
45
+ throw new Error(`Invalid field-path "${fieldPath}" near index ${index}.`);
46
46
  }
47
- throw new Error(`Invalid jsonpath "${jsonpath}" near index ${index}.`);
47
+ throw new Error(`Invalid field-path "${fieldPath}" near index ${index}.`);
48
48
  }
49
49
  if (tokens.length === 0) {
50
- throw new Error(`Invalid jsonpath "${jsonpath}". Path must target a field.`);
50
+ throw new Error(`Invalid field-path "${fieldPath}". Path must target a field.`);
51
51
  }
52
52
  return tokens;
53
53
  }
54
- function setVersionAtJsonPath(document, jsonpath, version) {
55
- const tokens = parseJsonPath(jsonpath);
54
+ function setVersionAtJsonPath(document, fieldPath, version) {
55
+ const tokens = parseFieldPath(fieldPath);
56
56
  let cursor = document;
57
57
  for (let index = 0; index < tokens.length - 1; index += 1) {
58
58
  const token = tokens[index];
59
59
  if (typeof token === "number") {
60
60
  if (!Array.isArray(cursor) || token >= cursor.length) {
61
- throw new Error(`jsonpath "${jsonpath}" does not resolve to an existing field.`);
61
+ throw new Error(`field-path "${fieldPath}" does not resolve to an existing field.`);
62
62
  }
63
63
  cursor = cursor[token];
64
64
  continue;
65
65
  }
66
66
  if (!isRecord(cursor) || !(token in cursor)) {
67
- throw new Error(`jsonpath "${jsonpath}" does not resolve to an existing field.`);
67
+ throw new Error(`field-path "${fieldPath}" does not resolve to an existing field.`);
68
68
  }
69
69
  cursor = cursor[token];
70
70
  }
71
71
  const leaf = tokens.at(-1);
72
72
  if (typeof leaf === "number") {
73
73
  if (!Array.isArray(cursor) || leaf >= cursor.length) {
74
- throw new Error(`jsonpath "${jsonpath}" does not resolve to an existing field.`);
74
+ throw new Error(`field-path "${fieldPath}" does not resolve to an existing field.`);
75
75
  }
76
76
  const current = cursor[leaf];
77
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.`);
78
+ throw new Error(`field-path "${fieldPath}" must point to a string or number field for version updates.`);
79
79
  }
80
80
  cursor[leaf] = version;
81
81
  return;
82
82
  }
83
83
  if (!isRecord(cursor) || !(leaf in cursor)) {
84
- throw new Error(`jsonpath "${jsonpath}" does not resolve to an existing field.`);
84
+ throw new Error(`field-path "${fieldPath}" does not resolve to an existing field.`);
85
85
  }
86
86
  const current = cursor[leaf];
87
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.`);
88
+ throw new Error(`field-path "${fieldPath}" must point to a string or number field for version updates.`);
89
89
  }
90
90
  cursor[leaf] = version;
91
91
  }
92
+ function resolveFieldPath(rule) {
93
+ const fieldPath = rule["field-path"] ?? rule.jsonpath;
94
+ if (!fieldPath) {
95
+ throw new Error(`${rule.type} artifact rules require "field-path" (or deprecated "jsonpath").`);
96
+ }
97
+ return fieldPath;
98
+ }
92
99
  function parseRegexPattern(pattern) {
93
100
  const slashPattern = pattern.match(/^\/((?:\\\/|[^/])+)\/([a-z]*)$/u);
94
101
  if (slashPattern) {
@@ -116,22 +123,36 @@ function applyRegexRule(content, pattern, version) {
116
123
  const replacement = typeof groupOne === "string" ? full.replace(groupOne, version) : version;
117
124
  return `${content.slice(0, start)}${replacement}${content.slice(start + full.length)}`;
118
125
  }
126
+ function applyTomlRulePreservingFormatting(content, fieldPath, version) {
127
+ const simplePath = fieldPath.match(/^\$\.([A-Za-z0-9_-]+)$/u);
128
+ if (!simplePath) {
129
+ const parsed = toml_1.default.parse(content);
130
+ setVersionAtJsonPath(parsed, fieldPath, version);
131
+ return `${toml_1.default.stringify(parsed)}\n`;
132
+ }
133
+ const key = simplePath[1];
134
+ const linePattern = new RegExp(`^(\\s*${key}\\s*=\\s*)(["'])([^"']*)(\\2)(\\s*(?:#.*)?)$`, "mu");
135
+ const match = content.match(linePattern);
136
+ if (!match) {
137
+ throw new Error(`field-path "${fieldPath}" does not resolve to an existing field.`);
138
+ }
139
+ const [, prefix = "", quote = '"', , , suffix = ""] = match;
140
+ return content.replace(linePattern, `${prefix}${quote}${version}${quote}${suffix}`);
141
+ }
119
142
  function applyArtifactRuleToContent(content, rule, version) {
120
143
  if (rule.type === "regex") {
121
144
  return applyRegexRule(content, rule.pattern, version);
122
145
  }
123
146
  if (rule.type === "json") {
124
147
  const parsed = JSON.parse(content);
125
- setVersionAtJsonPath(parsed, rule.jsonpath, version);
148
+ setVersionAtJsonPath(parsed, resolveFieldPath(rule), version);
126
149
  return `${JSON.stringify(parsed, null, 2)}\n`;
127
150
  }
128
151
  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`;
152
+ return applyTomlRulePreservingFormatting(content, resolveFieldPath(rule), version);
132
153
  }
133
154
  const parsed = yaml_1.default.parse(content);
134
- setVersionAtJsonPath(parsed, rule.jsonpath, version);
155
+ setVersionAtJsonPath(parsed, resolveFieldPath(rule), version);
135
156
  return `${yaml_1.default.stringify(parsed)}`;
136
157
  }
137
158
  function normalizeRelative(base, target) {
@@ -10,7 +10,9 @@ exports.openOrUpdateSimpleReviewRequest = openOrUpdateSimpleReviewRequest;
10
10
  exports.pushReleaseBranch = pushReleaseBranch;
11
11
  exports.isReleaseCommitMessage = isReleaseCommitMessage;
12
12
  const node_child_process_1 = require("node:child_process");
13
+ const node_fs_1 = __importDefault(require("node:fs"));
13
14
  const node_path_1 = __importDefault(require("node:path"));
15
+ const toml_1 = __importDefault(require("@iarna/toml"));
14
16
  const load_config_js_1 = require("../../config/load-config.js");
15
17
  const changelog_js_1 = require("../../domain/release/changelog.js");
16
18
  const plan_js_1 = require("../../domain/release/plan.js");
@@ -68,6 +70,120 @@ function ensureCleanWorktree(cwd, logger) {
68
70
  logger?.warn(`Ignoring safe tracked changes before versionary pr:\n${ignored.join("\n")}`);
69
71
  }
70
72
  }
73
+ function ensureCargoLockUpToDate(cwd) {
74
+ const lockfilePath = node_path_1.default.join(cwd, "Cargo.lock");
75
+ if (!node_fs_1.default.existsSync(lockfilePath)) {
76
+ return [];
77
+ }
78
+ const before = node_fs_1.default.readFileSync(lockfilePath, "utf8");
79
+ try {
80
+ (0, node_child_process_1.execFileSync)("cargo", ["generate-lockfile"], {
81
+ cwd,
82
+ stdio: ["ignore", "pipe", "pipe"],
83
+ });
84
+ }
85
+ catch (error) {
86
+ const message = error instanceof Error ? error.message : String(error);
87
+ throw new Error(`Failed to refresh Cargo.lock via "cargo generate-lockfile". Ensure cargo is installed and available in PATH. Details: ${message}`);
88
+ }
89
+ const after = node_fs_1.default.readFileSync(lockfilePath, "utf8");
90
+ return after !== before ? ["Cargo.lock"] : [];
91
+ }
92
+ function normalizeReleaseNameForTag(releaseName) {
93
+ return releaseName
94
+ .trim()
95
+ .replace(/^@/u, "")
96
+ .replaceAll("/", "-")
97
+ .replace(/\s+/gu, "-");
98
+ }
99
+ function readNodePackageName(versionPath) {
100
+ const raw = JSON.parse(node_fs_1.default.readFileSync(versionPath, "utf8"));
101
+ const name = raw.name;
102
+ if (typeof name !== "string" || name.trim().length === 0) {
103
+ return null;
104
+ }
105
+ return name.trim();
106
+ }
107
+ function readRustPackageName(versionPath) {
108
+ const parsed = toml_1.default.parse(node_fs_1.default.readFileSync(versionPath, "utf8"));
109
+ const name = parsed.package?.name;
110
+ if (typeof name !== "string" || name.trim().length === 0) {
111
+ return null;
112
+ }
113
+ return name.trim();
114
+ }
115
+ function readRPackageName(versionPath) {
116
+ const content = node_fs_1.default.readFileSync(versionPath, "utf8");
117
+ const match = content.match(/^Package:\s*(.+)\s*$/mu);
118
+ if (!match?.[1]) {
119
+ return null;
120
+ }
121
+ const name = match[1].trim();
122
+ return name.length > 0 ? name : null;
123
+ }
124
+ function resolveReleaseName(cwd, packagePath, packageConfig, strategyName, versionFile) {
125
+ const configuredName = packageConfig["package-name"]?.trim();
126
+ if (configuredName) {
127
+ return configuredName;
128
+ }
129
+ const versionPath = node_path_1.default.join(cwd, versionFile);
130
+ if (strategyName === "node") {
131
+ return readNodePackageName(versionPath) ?? packagePath;
132
+ }
133
+ if (strategyName === "rust") {
134
+ return readRustPackageName(versionPath) ?? packagePath;
135
+ }
136
+ if (strategyName === "r") {
137
+ return readRPackageName(versionPath) ?? packagePath;
138
+ }
139
+ return packagePath;
140
+ }
141
+ function buildReleaseTargets(cwd, plan, loadedConfig) {
142
+ const releaseTargets = plan.packages
143
+ ? plan.packages
144
+ .filter((pkg) => pkg.nextVersion)
145
+ .map((pkg) => {
146
+ if (pkg.path === ".") {
147
+ return {
148
+ path: pkg.path,
149
+ version: pkg.nextVersion ?? "",
150
+ tag: `v${pkg.nextVersion ?? ""}`,
151
+ };
152
+ }
153
+ const packageConfig = loadedConfig.packages?.[pkg.path] ?? {};
154
+ const packageContext = (0, package_context_js_1.resolvePackageStrategyContext)(loadedConfig, pkg.path, packageConfig);
155
+ const releaseName = resolveReleaseName(cwd, pkg.path, packageConfig, packageContext.strategy.name, packageContext.versionFile);
156
+ const tagPrefix = normalizeReleaseNameForTag(releaseName);
157
+ return {
158
+ path: pkg.path,
159
+ version: pkg.nextVersion ?? "",
160
+ tag: `${tagPrefix}-v${pkg.nextVersion ?? ""}`,
161
+ };
162
+ })
163
+ : [
164
+ {
165
+ path: ".",
166
+ version: plan.nextVersion ?? "",
167
+ tag: `v${plan.nextVersion ?? ""}`,
168
+ },
169
+ ];
170
+ const seenTags = new Map();
171
+ for (const target of releaseTargets) {
172
+ const existingPath = seenTags.get(target.tag);
173
+ if (existingPath) {
174
+ throw new Error(`Duplicate release tag "${target.tag}" for packages "${existingPath}" and "${target.path}". Configure unique "package-name" values.`);
175
+ }
176
+ seenTags.set(target.tag, target.path);
177
+ }
178
+ return releaseTargets;
179
+ }
180
+ function formatReleaseCommitTitle(releaseTargets) {
181
+ if (releaseTargets.length === 0) {
182
+ return "chore(release): v0.0.0";
183
+ }
184
+ const tags = releaseTargets.map((target) => target.tag);
185
+ return `chore(release): ${tags.join(", ")}`;
186
+ }
71
187
  function prepareSimpleReleasePr(cwd = process.cwd(), options = {}) {
72
188
  const plan = (0, plan_js_1.createSimplePlan)(cwd);
73
189
  const loaded = (0, load_config_js_1.loadConfig)(cwd);
@@ -98,10 +214,12 @@ function prepareSimpleReleasePr(cwd = process.cwd(), options = {}) {
98
214
  updatedVersionFiles.push(...strategy.writeVersion(cwd, loaded.config, plan.nextVersion));
99
215
  }
100
216
  const updatedArtifactFiles = (0, artifact_rules_js_1.applyConfiguredArtifactRules)(cwd, loaded.config, plan);
217
+ const updatedRustLockFiles = ensureCargoLockUpToDate(cwd);
101
218
  const section = (0, changelog_js_1.renderSimpleChangelog)(plan);
102
219
  (0, changelog_js_1.prependChangelog)(cwd, plan.changelogFile, section);
220
+ const releaseTargets = buildReleaseTargets(cwd, plan, loaded.config);
103
221
  const branch = plan.releaseBranchPrefix;
104
- const title = `chore(release): v${plan.nextVersion}`;
222
+ const title = formatReleaseCommitTitle(releaseTargets);
105
223
  (0, node_child_process_1.execFileSync)("git", ["checkout", "-B", branch], {
106
224
  cwd,
107
225
  stdio: ["ignore", "pipe", "ignore"],
@@ -110,6 +228,7 @@ function prepareSimpleReleasePr(cwd = process.cwd(), options = {}) {
110
228
  ...new Set([
111
229
  ...updatedVersionFiles,
112
230
  ...updatedArtifactFiles,
231
+ ...updatedRustLockFiles,
113
232
  plan.changelogFile,
114
233
  ]),
115
234
  ];
@@ -121,23 +240,6 @@ function prepareSimpleReleasePr(cwd = process.cwd(), options = {}) {
121
240
  cwd,
122
241
  stdio: ["ignore", "pipe", "ignore"],
123
242
  });
124
- const releaseTargets = plan.packages
125
- ? plan.packages
126
- .filter((pkg) => pkg.nextVersion)
127
- .map((pkg) => ({
128
- path: pkg.path,
129
- version: pkg.nextVersion ?? "",
130
- tag: pkg.path === "."
131
- ? `v${pkg.nextVersion ?? ""}`
132
- : `${pkg.path.replaceAll("/", "-")}-v${pkg.nextVersion ?? ""}`,
133
- }))
134
- : [
135
- {
136
- path: ".",
137
- version: plan.nextVersion,
138
- tag: `v${plan.nextVersion}`,
139
- },
140
- ];
141
243
  (0, state_js_1.writeBaselineSha)(cwd, undefined, releaseTargets);
142
244
  (0, node_child_process_1.execFileSync)("git", ["add", (0, state_js_1.getBaselineStatePath)(cwd)], {
143
245
  cwd,
@@ -157,10 +259,13 @@ function prepareSimpleReleasePr(cwd = process.cwd(), options = {}) {
157
259
  };
158
260
  }
159
261
  function renderSimpleReviewRequestBody(version, previousVersion, commits, plan = null, cwd = process.cwd()) {
262
+ const rootPackageLabel = node_path_1.default.basename(cwd);
263
+ const formatPackageLabel = (packagePath) => packagePath === "." ? rootPackageLabel : packagePath;
160
264
  if (plan?.packages && plan.packages.length > 1) {
161
265
  const sections = plan.packages
162
266
  .filter((pkg) => pkg.nextVersion)
163
267
  .map((pkg) => {
268
+ const packageLabel = formatPackageLabel(pkg.path);
164
269
  const notes = (0, changelog_js_1.renderSimpleReleaseNotes)({
165
270
  currentVersion: pkg.currentVersion,
166
271
  nextVersion: pkg.nextVersion ?? "",
@@ -170,12 +275,12 @@ function renderSimpleReviewRequestBody(version, previousVersion, commits, plan =
170
275
  const linkedHeader = notes.match(/^##\s+\[([^\]]+)\]\(([^)]+)\)\s+\(([^)]+)\)/u);
171
276
  if (linkedHeader) {
172
277
  const [, , compareUrl, date] = linkedHeader;
173
- return notes.replace(/^##\s+\[[^\]]+\]\([^)]+\)\s+\([^)]+\)/u, `## [${pkg.path}: ${pkg.nextVersion ?? ""}](${compareUrl}) (${date})`);
278
+ return notes.replace(/^##\s+\[[^\]]+\]\([^)]+\)\s+\([^)]+\)/u, `## [${packageLabel}: ${pkg.nextVersion ?? ""}](${compareUrl}) (${date})`);
174
279
  }
175
280
  const plainHeader = notes.match(/^##\s+([^\s]+)\s+\(([^)]+)\)/u);
176
281
  if (plainHeader) {
177
282
  const [, , date] = plainHeader;
178
- return notes.replace(/^##\s+[^\s]+\s+\([^)]+\)/u, `## ${pkg.path}: ${pkg.nextVersion ?? ""} (${date})`);
283
+ return notes.replace(/^##\s+[^\s]+\s+\([^)]+\)/u, `## ${packageLabel}: ${pkg.nextVersion ?? ""} (${date})`);
179
284
  }
180
285
  return notes;
181
286
  })
@@ -223,5 +328,5 @@ function pushReleaseBranch(cwd, branch) {
223
328
  });
224
329
  }
225
330
  function isReleaseCommitMessage(subject) {
226
- return /^chore\(release\):\sv\d+\.\d+\.\d+/u.test(subject);
331
+ 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);
227
332
  }
@@ -11,26 +11,29 @@ const node_child_process_1 = require("node:child_process");
11
11
  const node_fs_1 = __importDefault(require("node:fs"));
12
12
  const node_path_1 = __importDefault(require("node:path"));
13
13
  const load_config_js_1 = require("../../config/load-config.js");
14
+ const MANIFEST_VERSION_KEY = "manifest-version";
15
+ const BASELINE_SHA_KEY = "baseline-sha";
16
+ const RELEASE_TARGETS_KEY = "release-targets";
14
17
  function parseStateFile(raw, filePath) {
15
18
  const parsed = JSON.parse(raw);
16
19
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
17
20
  throw new Error(`Invalid release manifest at ${filePath}: expected an object.`);
18
21
  }
19
22
  const manifest = parsed;
20
- if (manifest.manifestVersion !== undefined &&
21
- manifest.manifestVersion !== 1) {
22
- throw new Error(`Unsupported manifestVersion in ${filePath}: ${String(manifest.manifestVersion)}.`);
23
+ if (manifest[MANIFEST_VERSION_KEY] !== undefined &&
24
+ manifest[MANIFEST_VERSION_KEY] !== 1) {
25
+ throw new Error(`Unsupported ${MANIFEST_VERSION_KEY} in ${filePath}: ${String(manifest[MANIFEST_VERSION_KEY])}.`);
23
26
  }
24
- if (manifest.baselineSha !== undefined &&
25
- typeof manifest.baselineSha !== "string") {
26
- throw new Error(`Invalid release manifest at ${filePath}: baselineSha must be a string.`);
27
+ if (manifest[BASELINE_SHA_KEY] !== undefined &&
28
+ typeof manifest[BASELINE_SHA_KEY] !== "string") {
29
+ throw new Error(`Invalid release manifest at ${filePath}: ${BASELINE_SHA_KEY} must be a string.`);
27
30
  }
28
- if (manifest.releaseTargets !== undefined &&
29
- !Array.isArray(manifest.releaseTargets)) {
30
- throw new Error(`Invalid release manifest at ${filePath}: releaseTargets must be an array.`);
31
+ if (manifest[RELEASE_TARGETS_KEY] !== undefined &&
32
+ !Array.isArray(manifest[RELEASE_TARGETS_KEY])) {
33
+ throw new Error(`Invalid release manifest at ${filePath}: ${RELEASE_TARGETS_KEY} must be an array.`);
31
34
  }
32
- if (Array.isArray(manifest.releaseTargets)) {
33
- for (const target of manifest.releaseTargets) {
35
+ if (Array.isArray(manifest[RELEASE_TARGETS_KEY])) {
36
+ for (const target of manifest[RELEASE_TARGETS_KEY]) {
34
37
  if (!target || typeof target !== "object" || Array.isArray(target)) {
35
38
  throw new Error(`Invalid release manifest at ${filePath}: each release target must be an object.`);
36
39
  }
@@ -38,7 +41,7 @@ function parseStateFile(raw, filePath) {
38
41
  if (typeof record.path !== "string" ||
39
42
  typeof record.version !== "string" ||
40
43
  typeof record.tag !== "string") {
41
- throw new Error(`Invalid release manifest at ${filePath}: releaseTargets must contain string path, version, and tag.`);
44
+ throw new Error(`Invalid release manifest at ${filePath}: ${RELEASE_TARGETS_KEY} must contain string path, version, and tag.`);
42
45
  }
43
46
  }
44
47
  }
@@ -66,7 +69,7 @@ function readBaselineSha(cwd = process.cwd()) {
66
69
  return null;
67
70
  }
68
71
  const parsed = parseStateFile(node_fs_1.default.readFileSync(filePath, "utf8"), filePath);
69
- return parsed.baselineSha ?? null;
72
+ return parsed[BASELINE_SHA_KEY] ?? null;
70
73
  }
71
74
  function readReleaseTargets(cwd = process.cwd()) {
72
75
  const filePath = getBaselineStatePath(cwd);
@@ -74,10 +77,10 @@ function readReleaseTargets(cwd = process.cwd()) {
74
77
  return [];
75
78
  }
76
79
  const parsed = parseStateFile(node_fs_1.default.readFileSync(filePath, "utf8"), filePath);
77
- return parsed.releaseTargets ?? [];
80
+ return parsed[RELEASE_TARGETS_KEY] ?? [];
78
81
  }
79
82
  function writeBaselineSha(cwd = process.cwd(), sha, releaseTargets = []) {
80
- const baselineSha = sha ??
83
+ const baselineShaValue = sha ??
81
84
  (0, node_child_process_1.execFileSync)("git", ["rev-parse", "HEAD"], {
82
85
  cwd,
83
86
  encoding: "utf8",
@@ -85,9 +88,9 @@ function writeBaselineSha(cwd = process.cwd(), sha, releaseTargets = []) {
85
88
  }).trim();
86
89
  const filePath = getBaselineStatePath(cwd);
87
90
  const next = {
88
- manifestVersion: 1,
89
- baselineSha,
90
- releaseTargets,
91
+ [MANIFEST_VERSION_KEY]: 1,
92
+ [BASELINE_SHA_KEY]: baselineShaValue,
93
+ [RELEASE_TARGETS_KEY]: releaseTargets,
91
94
  };
92
95
  node_fs_1.default.writeFileSync(filePath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
93
96
  }
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  export declare const configSchema: z.ZodObject<{
3
+ $schema: z.ZodOptional<z.ZodString>;
3
4
  version: z.ZodLiteral<1>;
4
5
  "review-mode": z.ZodOptional<z.ZodEnum<{
5
6
  direct: "direct";
@@ -30,10 +31,11 @@ export declare const configSchema: z.ZodObject<{
30
31
  regex: "regex";
31
32
  }>;
32
33
  path: z.ZodString;
34
+ "field-path": z.ZodOptional<z.ZodString>;
33
35
  jsonpath: z.ZodOptional<z.ZodString>;
34
36
  pattern: z.ZodOptional<z.ZodString>;
35
37
  }, z.core.$strip>>>;
36
- }, z.core.$strip>>>;
38
+ }, z.core.$strict>>>;
37
39
  plugins: z.ZodOptional<z.ZodArray<z.ZodString>>;
38
- }, z.core.$strip>;
40
+ }, z.core.$strict>;
39
41
  export type ConfigSchema = z.infer<typeof configSchema>;
@@ -6,16 +6,18 @@ const artifactRuleSchema = zod_1.z
6
6
  .object({
7
7
  type: zod_1.z.enum(["json", "toml", "yaml", "regex"]),
8
8
  path: zod_1.z.string().min(1),
9
+ "field-path": zod_1.z.string().optional(),
9
10
  jsonpath: zod_1.z.string().optional(),
10
11
  pattern: zod_1.z.string().optional(),
11
12
  })
12
13
  .superRefine((value, ctx) => {
13
14
  const needsJsonPath = value.type === "json" || value.type === "toml" || value.type === "yaml";
14
- if (needsJsonPath && !value.jsonpath) {
15
+ const hasFieldPath = Boolean(value["field-path"] ?? value.jsonpath);
16
+ if (needsJsonPath && !hasFieldPath) {
15
17
  ctx.addIssue({
16
18
  code: zod_1.z.ZodIssueCode.custom,
17
- message: `${value.type} artifact rules require "jsonpath".`,
18
- path: ["jsonpath"],
19
+ message: `${value.type} artifact rules require "field-path" (or deprecated "jsonpath").`,
20
+ path: ["field-path"],
19
21
  });
20
22
  }
21
23
  if (needsJsonPath && value.pattern) {
@@ -25,6 +27,13 @@ const artifactRuleSchema = zod_1.z
25
27
  path: ["pattern"],
26
28
  });
27
29
  }
30
+ if (value["field-path"] && value.jsonpath) {
31
+ ctx.addIssue({
32
+ code: zod_1.z.ZodIssueCode.custom,
33
+ message: 'Specify only one of "field-path" or deprecated "jsonpath".',
34
+ path: ["field-path"],
35
+ });
36
+ }
28
37
  if (value.type === "regex" && !value.pattern) {
29
38
  ctx.addIssue({
30
39
  code: zod_1.z.ZodIssueCode.custom,
@@ -32,21 +41,25 @@ const artifactRuleSchema = zod_1.z
32
41
  path: ["pattern"],
33
42
  });
34
43
  }
35
- if (value.type === "regex" && value.jsonpath) {
44
+ if (value.type === "regex" && (value["field-path"] || value.jsonpath)) {
36
45
  ctx.addIssue({
37
46
  code: zod_1.z.ZodIssueCode.custom,
38
- message: 'regex artifact rules do not support "jsonpath".',
39
- path: ["jsonpath"],
47
+ message: 'regex artifact rules do not support "field-path" or deprecated "jsonpath".',
48
+ path: ["field-path"],
40
49
  });
41
50
  }
42
51
  });
43
- const packageSchema = zod_1.z.object({
52
+ const packageSchema = zod_1.z
53
+ .object({
44
54
  "release-type": zod_1.z.string().optional(),
45
55
  "package-name": zod_1.z.string().optional(),
46
56
  "exclude-paths": zod_1.z.array(zod_1.z.string()).optional(),
47
57
  "extra-files": zod_1.z.array(artifactRuleSchema).optional(),
48
- });
49
- exports.configSchema = zod_1.z.object({
58
+ })
59
+ .strict();
60
+ exports.configSchema = zod_1.z
61
+ .object({
62
+ $schema: zod_1.z.string().optional(),
50
63
  version: zod_1.z.literal(1),
51
64
  "review-mode": zod_1.z.enum(["direct", "review"]).optional(),
52
65
  "version-file": zod_1.z.string().optional(),
@@ -61,4 +74,5 @@ exports.configSchema = zod_1.z.object({
61
74
  "release-type": zod_1.z.string().optional(),
62
75
  packages: zod_1.z.record(zod_1.z.string().min(1), packageSchema).optional(),
63
76
  plugins: zod_1.z.array(zod_1.z.string().min(1)).optional(),
64
- });
77
+ })
78
+ .strict();
@@ -287,6 +287,14 @@ function parseDependencyName(line) {
287
287
  const match = line.match(/^\s*(?:"([^"]+)"|'([^']+)'|([A-Za-z0-9_-]+))\s*=/u);
288
288
  return match?.[1] ?? match?.[2] ?? match?.[3] ?? null;
289
289
  }
290
+ function rewriteCargoVersionRequirement(currentRequirement, nextVersion) {
291
+ const simpleRequirementMatch = currentRequirement.match(/^(\s*)(\^|~|>=|<=|>|<|=)?(\s*)([0-9A-Za-z.+-]+)(\s*)$/u);
292
+ if (!simpleRequirementMatch) {
293
+ return nextVersion;
294
+ }
295
+ const [, leadingWhitespace = "", operator = "", operatorWhitespace = "", , trailingWhitespace = "",] = simpleRequirementMatch;
296
+ return `${leadingWhitespace}${operator}${operatorWhitespace}${nextVersion}${trailingWhitespace}`;
297
+ }
290
298
  function writeInternalDependencyVersionInLine(line, dependencyName, versionByDependency) {
291
299
  const nextVersion = versionByDependency.get(dependencyName);
292
300
  if (!nextVersion) {
@@ -294,19 +302,24 @@ function writeInternalDependencyVersionInLine(line, dependencyName, versionByDep
294
302
  }
295
303
  const stringVersionMatch = line.match(/^(\s*(?:"[^"]+"|'[^']+'|[A-Za-z0-9_-]+)\s*=\s*)(["'])([^"']*)(\2)(\s*(?:#.*)?)?$/u);
296
304
  if (stringVersionMatch) {
297
- const [, prefix = "", quote = '"', , , suffix = ""] = stringVersionMatch;
298
- return `${prefix}${quote}${nextVersion}${quote}${suffix}`;
305
+ const [, prefix = "", quote = '"', current = "", , suffix = ""] = stringVersionMatch;
306
+ const rewrittenRequirement = rewriteCargoVersionRequirement(current, nextVersion);
307
+ return `${prefix}${quote}${rewrittenRequirement}${quote}${suffix}`;
299
308
  }
300
309
  const inlineTableMatch = line.match(/^(\s*(?:"[^"]+"|'[^']+'|[A-Za-z0-9_-]+)\s*=\s*\{)(.*)(\}\s*(?:#.*)?)$/u);
301
- if (!inlineTableMatch) {
302
- return line;
310
+ if (inlineTableMatch) {
311
+ const [, prefix = "", tableBody = "", suffix = ""] = inlineTableMatch;
312
+ const updatedTableBody = tableBody.replace(/(\bversion\s*=\s*)(["'])([^"']*)(\2)/u, (_match, versionPrefix, quote, currentVersion) => `${versionPrefix}${quote}${rewriteCargoVersionRequirement(String(currentVersion), nextVersion)}${quote}`);
313
+ if (updatedTableBody === tableBody) {
314
+ return line;
315
+ }
316
+ return `${prefix}${updatedTableBody}${suffix}`;
303
317
  }
304
- const [, prefix = "", tableBody = "", suffix = ""] = inlineTableMatch;
305
- const updatedTableBody = tableBody.replace(/(\bversion\s*=\s*)(["'])([^"']*)(\2)/u, `$1$2${nextVersion}$4`);
306
- if (updatedTableBody === tableBody) {
318
+ const updatedTableLine = line.replace(/(\bversion\s*=\s*)(["'])([^"']*)(\2)/u, (_match, versionPrefix, quote, currentVersion) => `${versionPrefix}${quote}${rewriteCargoVersionRequirement(String(currentVersion), nextVersion)}${quote}`);
319
+ if (updatedTableLine === line) {
307
320
  return line;
308
321
  }
309
- return `${prefix}${updatedTableBody}${suffix}`;
322
+ return updatedTableLine;
310
323
  }
311
324
  function writeInternalDependencyVersions(cargoTomlRaw, internalCrates, version) {
312
325
  if (internalCrates.size === 0) {
@@ -2,6 +2,7 @@ export type ConfigFileFormat = "jsonc" | "json" | "toml" | "js";
2
2
  export interface VersionaryArtifactRule {
3
3
  type: "json" | "toml" | "yaml" | "regex";
4
4
  path: string;
5
+ "field-path"?: string;
5
6
  jsonpath?: string;
6
7
  pattern?: string;
7
8
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "versionary",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Automatic release framework based on conventional commits and semantic versioning",
5
5
  "keywords": [
6
6
  "releasing",