versionary 0.8.1 → 0.9.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
@@ -101,6 +101,8 @@ For a quick trial, use:
101
101
  - `independent` computes package bumps per path
102
102
  - `fixed` computes one shared bump across configured package paths
103
103
  - per-package `package-name` can override release identity (labels + tag base)
104
+ - per-package `changelog-file` writes package release notes to
105
+ `<package-path>/<changelog-file>`
104
106
 
105
107
  Rust strategy examples:
106
108
 
@@ -123,6 +125,8 @@ Rust strategy examples:
123
125
  Current rust auto-update behavior (phase scope):
124
126
 
125
127
  - updates crate versions in each targeted crate `[package].version`
128
+ - supports targeted crates using `version.workspace = true` by updating
129
+ `[workspace.package].version` in the owning workspace manifest
126
130
  - updates internal workspace dependency versions when the dependency name
127
131
  matches another targeted crate name
128
132
  - refreshes `Cargo.lock` via `cargo generate-lockfile` when `Cargo.lock` exists
@@ -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,6 +214,22 @@ 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";
@@ -216,8 +268,33 @@ function prepareSimpleReleasePr(cwd = process.cwd(), options = {}) {
216
268
  }
217
269
  const updatedArtifactFiles = (0, artifact_rules_js_1.applyConfiguredArtifactRules)(cwd, loaded.config, plan);
218
270
  const updatedRustLockFiles = ensureCargoLockUpToDate(cwd);
271
+ const packageReleaseMetadata = buildPackageReleaseMetadata(cwd, plan, loaded.config);
219
272
  const section = (0, changelog_js_1.renderSimpleChangelog)(plan);
220
273
  (0, changelog_js_1.prependChangelog)(cwd, plan.changelogFile, section);
274
+ const updatedChangelogFiles = [plan.changelogFile];
275
+ for (const packagePlan of plan.packages ?? []) {
276
+ if (!packagePlan.nextVersion || packagePlan.path === ".") {
277
+ continue;
278
+ }
279
+ const packageConfig = loaded.config.packages?.[packagePlan.path] ?? {};
280
+ const packageChangelogFile = packageConfig["changelog-file"];
281
+ if (!packageChangelogFile) {
282
+ continue;
283
+ }
284
+ const packageMetadata = packageReleaseMetadata[packagePlan.path];
285
+ if (!packageMetadata) {
286
+ continue;
287
+ }
288
+ const packageSection = (0, changelog_js_1.renderPackageChangelogSection)({
289
+ currentVersion: packagePlan.currentVersion,
290
+ nextVersion: packagePlan.nextVersion,
291
+ commits: packagePlan.commits,
292
+ tagPrefix: packageMetadata.tagPrefix,
293
+ cwd,
294
+ });
295
+ (0, changelog_js_1.prependChangelog)(cwd, node_path_1.default.posix.join(packagePlan.path, packageChangelogFile), packageSection);
296
+ updatedChangelogFiles.push(node_path_1.default.posix.join(packagePlan.path, packageChangelogFile));
297
+ }
221
298
  const releaseTargets = buildReleaseTargets(cwd, plan, loaded.config);
222
299
  const branch = plan.releaseBranchPrefix;
223
300
  const title = formatReleaseCommitTitle(releaseTargets);
@@ -230,7 +307,7 @@ function prepareSimpleReleasePr(cwd = process.cwd(), options = {}) {
230
307
  ...updatedVersionFiles,
231
308
  ...updatedArtifactFiles,
232
309
  ...updatedRustLockFiles,
233
- plan.changelogFile,
310
+ ...updatedChangelogFiles,
234
311
  ]),
235
312
  ];
236
313
  (0, node_child_process_1.execFileSync)("git", ["add", ...filesToAdd], {
@@ -75,6 +75,7 @@ async function runSimpleReleaseDetailed(cwd = process.cwd(), options = {}) {
75
75
  if (!plugin?.createReleaseMetadata) {
76
76
  throw new Error(`Plugin "${plugin?.name ?? "unknown"}" does not implement createReleaseMetadata.`);
77
77
  }
78
+ const createReleaseMetadata = plugin.createReleaseMetadata;
78
79
  const releaseTargets = (0, state_js_1.readReleaseTargets)(cwd);
79
80
  const targets = releaseTargets.length > 0
80
81
  ? releaseTargets
@@ -92,7 +93,10 @@ async function runSimpleReleaseDetailed(cwd = process.cwd(), options = {}) {
92
93
  version: target.version,
93
94
  notes: readReleaseNotes(cwd, target.version, changelogFile),
94
95
  }, {
95
- createReleaseMetadata: (input) => plugin.createReleaseMetadata(input, { cwd, logger: options.logger }),
96
+ createReleaseMetadata: (input) => createReleaseMetadata(input, {
97
+ cwd,
98
+ logger: options.logger,
99
+ }),
96
100
  logger: options.logger,
97
101
  });
98
102
  releases.push({
package/dist/cli/index.js CHANGED
@@ -125,7 +125,7 @@ async function main() {
125
125
  : "";
126
126
  const heading = "# Changelog\n\n";
127
127
  const body = existing.replace(/^# Changelog\s*/u, "");
128
- node_fs_1.default.writeFileSync(changelogPath, `${heading}${section}\n${body}`.trimEnd() + "\n", "utf8");
128
+ node_fs_1.default.writeFileSync(changelogPath, `${`${heading}${section}\n${body}`.trimEnd()}\n`, "utf8");
129
129
  console.log(`Updated ${plan.changelogFile}`);
130
130
  return 0;
131
131
  }
@@ -22,6 +22,7 @@ export declare const configSchema: z.ZodObject<{
22
22
  packages: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
23
23
  "release-type": z.ZodOptional<z.ZodString>;
24
24
  "package-name": z.ZodOptional<z.ZodString>;
25
+ "changelog-file": z.ZodOptional<z.ZodString>;
25
26
  "exclude-paths": z.ZodOptional<z.ZodArray<z.ZodString>>;
26
27
  "extra-files": z.ZodOptional<z.ZodArray<z.ZodObject<{
27
28
  type: z.ZodEnum<{
@@ -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
  })
@@ -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)
@@ -22,8 +22,20 @@ function resolvePackageStrategyContext(rootConfig, packagePath, packageConfig) {
22
22
  : { ...rootConfig };
23
23
  const baseStrategy = (0, resolve_js_1.resolveVersionStrategy)(baseConfig);
24
24
  if (!packageReleaseType) {
25
- const versionFile = baseConfig["version-file"] ?? baseStrategy.getVersionFile(baseConfig);
26
- const config = withVersionFile(baseConfig, versionFile);
25
+ const versionFile = packagePath === "."
26
+ ? (baseConfig["version-file"] ??
27
+ baseStrategy.getVersionFile(baseConfig))
28
+ : baseStrategy.name === "simple"
29
+ ? (baseConfig["version-file"] ??
30
+ baseStrategy.getVersionFile(baseConfig))
31
+ : node_path_1.default.posix.join(packagePath, baseStrategy.getVersionFile({
32
+ ...baseConfig,
33
+ "version-file": undefined,
34
+ }));
35
+ const config = withVersionFile({
36
+ ...baseConfig,
37
+ packages: undefined,
38
+ }, versionFile);
27
39
  return {
28
40
  strategy: baseStrategy,
29
41
  config,
@@ -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.`);
@@ -9,6 +9,7 @@ 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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "versionary",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Automatic release framework based on conventional commits and semantic versioning",
5
5
  "keywords": [
6
6
  "releasing",