versionary 0.24.0 → 0.26.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
@@ -144,6 +144,17 @@ For a quick trial, use:
144
144
  Python source file (e.g. `src/<pkg>/__init__.py`) to update a `__version__`
145
145
  assignment instead. Refreshes `poetry.lock`/`uv.lock`/`pdm.lock` at the
146
146
  package root if present
147
+ - `release-type` can also be an array of strategy names to compose them across
148
+ manifests, e.g. `["python", "rust"]` for a PyO3/maturin project: the first
149
+ entry is the *primary* (drives `readVersion`, `readPackageName`, and consumes
150
+ any `version-file` override); each *secondary* writes its default manifest
151
+ with the same target version. Common combinations:
152
+ - `["python", "rust"]` — PyO3/maturin (`pyproject.toml` + `Cargo.toml` +
153
+ `Cargo.lock`)
154
+ - `["node", "rust"]` — napi-rs (`package.json` + `Cargo.toml` + `Cargo.lock`)
155
+ - `["r", "rust"]` — R packages with embedded Rust crates (note: nested
156
+ `src/rust/Cargo.toml` layouts are not supported by the array form yet —
157
+ use a single strategy until per-strategy `version-file` overrides land)
147
158
  - simple/default strategy keeps `version.txt` as source of truth and does not
148
159
  update `package.json`
149
160
  - stable release branch (`release-branch`, default: `versionary/release`) so
@@ -30,9 +30,9 @@ export declare const configSchema: z.ZodObject<{
30
30
  "bump-minor-pre-major": z.ZodOptional<z.ZodBoolean>;
31
31
  "allow-stable-major": z.ZodOptional<z.ZodBoolean>;
32
32
  "include-commit-authors": z.ZodOptional<z.ZodBoolean>;
33
- "release-type": z.ZodOptional<z.ZodString>;
33
+ "release-type": z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
34
34
  packages: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
35
- "release-type": z.ZodOptional<z.ZodString>;
35
+ "release-type": z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
36
36
  "package-name": z.ZodOptional<z.ZodString>;
37
37
  "changelog-file": z.ZodOptional<z.ZodString>;
38
38
  "changelog-format": z.ZodOptional<z.ZodEnum<{
@@ -54,7 +54,9 @@ const artifactRuleSchema = zod_1.z
54
54
  });
55
55
  const packageSchema = zod_1.z
56
56
  .object({
57
- "release-type": zod_1.z.string().optional(),
57
+ "release-type": zod_1.z
58
+ .union([zod_1.z.string().min(1), zod_1.z.array(zod_1.z.string().min(1)).min(1)])
59
+ .optional(),
58
60
  "package-name": zod_1.z.string().optional(),
59
61
  "changelog-file": zod_1.z.string().optional(),
60
62
  "changelog-format": zod_1.z.enum(["markdown-changelog", "r-news"]).optional(),
@@ -83,7 +85,9 @@ exports.configSchema = zod_1.z
83
85
  "bump-minor-pre-major": zod_1.z.boolean().optional(),
84
86
  "allow-stable-major": zod_1.z.boolean().optional(),
85
87
  "include-commit-authors": zod_1.z.boolean().optional(),
86
- "release-type": zod_1.z.string().optional(),
88
+ "release-type": zod_1.z
89
+ .union([zod_1.z.string().min(1), zod_1.z.array(zod_1.z.string().min(1)).min(1)])
90
+ .optional(),
87
91
  packages: zod_1.z.record(zod_1.z.string().min(1), packageSchema).optional(),
88
92
  })
89
93
  .strict()
@@ -0,0 +1,2 @@
1
+ import type { VersionStrategy } from "./types.js";
2
+ export declare function compositeVersionStrategy(strategies: readonly VersionStrategy[]): VersionStrategy;
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.compositeVersionStrategy = compositeVersionStrategy;
4
+ function configForSecondary(config) {
5
+ if (config["version-file"] === undefined) {
6
+ return config;
7
+ }
8
+ const { "version-file": _omit, ...rest } = config;
9
+ return rest;
10
+ }
11
+ function dedupedSorted(values) {
12
+ return [...new Set(values)].sort((a, b) => a.localeCompare(b));
13
+ }
14
+ function compositeVersionStrategy(strategies) {
15
+ if (strategies.length === 0) {
16
+ throw new Error("compositeVersionStrategy requires at least one strategy.");
17
+ }
18
+ if (strategies.length === 1) {
19
+ const only = strategies[0];
20
+ if (!only) {
21
+ throw new Error("compositeVersionStrategy requires at least one strategy.");
22
+ }
23
+ return only;
24
+ }
25
+ const primary = strategies[0];
26
+ const secondaries = strategies.slice(1);
27
+ if (!primary) {
28
+ throw new Error("compositeVersionStrategy requires at least one strategy.");
29
+ }
30
+ const composite = {
31
+ name: strategies.map((strategy) => strategy.name).join("+"),
32
+ getVersionFile(config) {
33
+ return primary.getVersionFile(config);
34
+ },
35
+ readVersion(cwd, config) {
36
+ return primary.readVersion(cwd, config);
37
+ },
38
+ writeVersion(cwd, config, version) {
39
+ const updated = [];
40
+ updated.push(...primary.writeVersion(cwd, config, version));
41
+ const secondaryConfig = configForSecondary(config);
42
+ for (const secondary of secondaries) {
43
+ updated.push(...secondary.writeVersion(cwd, secondaryConfig, version));
44
+ }
45
+ return dedupedSorted(updated);
46
+ },
47
+ };
48
+ if (primary.getDefaultChangelogFormat ||
49
+ secondaries.some((strategy) => strategy.getDefaultChangelogFormat)) {
50
+ composite.getDefaultChangelogFormat = () => {
51
+ return primary.getDefaultChangelogFormat?.() ?? "markdown-changelog";
52
+ };
53
+ }
54
+ if (primary.validateProject ||
55
+ secondaries.some((strategy) => strategy.validateProject)) {
56
+ composite.validateProject = (cwd, config) => {
57
+ const messages = [];
58
+ const primaryError = primary.validateProject?.(cwd, config) ?? null;
59
+ if (primaryError) {
60
+ messages.push(primaryError);
61
+ }
62
+ const secondaryConfig = configForSecondary(config);
63
+ for (const secondary of secondaries) {
64
+ const error = secondary.validateProject?.(cwd, secondaryConfig) ?? null;
65
+ if (error) {
66
+ messages.push(error);
67
+ }
68
+ }
69
+ return messages.length === 0 ? null : messages.join("\n");
70
+ };
71
+ }
72
+ if (primary.readPackageName) {
73
+ composite.readPackageName = (cwd, config) => {
74
+ return primary.readPackageName?.(cwd, config) ?? null;
75
+ };
76
+ }
77
+ if (primary.propagateDependentPatchImpacts ||
78
+ secondaries.some((strategy) => strategy.propagateDependentPatchImpacts)) {
79
+ composite.propagateDependentPatchImpacts = (cwd, packages) => {
80
+ const impacted = [];
81
+ impacted.push(...(primary.propagateDependentPatchImpacts?.(cwd, packages) ?? []));
82
+ for (const secondary of secondaries) {
83
+ impacted.push(...(secondary.propagateDependentPatchImpacts?.(cwd, packages) ?? []));
84
+ }
85
+ return dedupedSorted(impacted);
86
+ };
87
+ }
88
+ if (primary.finalizeVersionWrites ||
89
+ secondaries.some((strategy) => strategy.finalizeVersionWrites)) {
90
+ composite.finalizeVersionWrites = (cwd, writes, context) => {
91
+ const updated = [];
92
+ updated.push(...(primary.finalizeVersionWrites?.(cwd, writes, context) ?? []));
93
+ for (const secondary of secondaries) {
94
+ updated.push(...(secondary.finalizeVersionWrites?.(cwd, writes, context) ?? []));
95
+ }
96
+ return dedupedSorted(updated);
97
+ };
98
+ }
99
+ return composite;
100
+ }
@@ -166,6 +166,67 @@ function readPyProjectFromCwd(cwd) {
166
166
  const content = node_fs_1.default.readFileSync(target, "utf8");
167
167
  return { parsed: parsePyProject(content, "pyproject.toml"), rawPath: target };
168
168
  }
169
+ function packageNameFromParsed(parsed) {
170
+ const projectName = parsed.project?.name;
171
+ if (typeof projectName === "string" && projectName.trim().length > 0) {
172
+ return projectName.trim();
173
+ }
174
+ const poetryName = parsed.toolPoetry?.name;
175
+ if (typeof poetryName === "string" && poetryName.trim().length > 0) {
176
+ return poetryName.trim();
177
+ }
178
+ return null;
179
+ }
180
+ function normalizeModuleName(name) {
181
+ return name.replace(/[-.]+/gu, "_");
182
+ }
183
+ function findAuxiliaryInitPy(cwd, parsed) {
184
+ const packageName = packageNameFromParsed(parsed);
185
+ if (!packageName) {
186
+ return null;
187
+ }
188
+ const moduleName = normalizeModuleName(packageName);
189
+ const candidates = [
190
+ node_path_1.default.join("src", moduleName, "__init__.py"),
191
+ node_path_1.default.join(moduleName, "__init__.py"),
192
+ ];
193
+ for (const candidate of candidates) {
194
+ const absolute = node_path_1.default.join(cwd, candidate);
195
+ if (!node_fs_1.default.existsSync(absolute)) {
196
+ continue;
197
+ }
198
+ const content = node_fs_1.default.readFileSync(absolute, "utf8");
199
+ if (SOURCE_FILE_VERSION_PATTERN.test(content)) {
200
+ return candidate;
201
+ }
202
+ }
203
+ return null;
204
+ }
205
+ function tryUpdateInitPyVersion(cwd, relativePath, version) {
206
+ const absolute = node_path_1.default.join(cwd, relativePath);
207
+ const existing = node_fs_1.default.readFileSync(absolute, "utf8");
208
+ if (!SOURCE_FILE_VERSION_PATTERN.test(existing)) {
209
+ return false;
210
+ }
211
+ const updated = writeSourceFileVersion(existing, relativePath, version);
212
+ node_fs_1.default.writeFileSync(absolute, updated, "utf8");
213
+ return true;
214
+ }
215
+ function tryUpdatePyProjectVersion(cwd, version) {
216
+ const target = node_path_1.default.join(cwd, "pyproject.toml");
217
+ if (!node_fs_1.default.existsSync(target)) {
218
+ return null;
219
+ }
220
+ const content = node_fs_1.default.readFileSync(target, "utf8");
221
+ const parsed = parsePyProject(content, "pyproject.toml");
222
+ const { projectVersion, poetryVersion } = lookupPyProjectVersions(parsed);
223
+ if (!projectVersion && !poetryVersion) {
224
+ return null;
225
+ }
226
+ const updated = writePyProjectVersion(content, "pyproject.toml", version);
227
+ node_fs_1.default.writeFileSync(target, updated, "utf8");
228
+ return "pyproject.toml";
229
+ }
169
230
  exports.pythonVersionStrategy = {
170
231
  name: "python",
171
232
  getVersionFile(config) {
@@ -209,11 +270,27 @@ exports.pythonVersionStrategy = {
209
270
  throw new Error(`Versionary requires ${versionFile} to exist.`);
210
271
  }
211
272
  const existing = node_fs_1.default.readFileSync(versionPath, "utf8");
212
- const updated = isSourceFileMode(versionFile)
213
- ? writeSourceFileVersion(existing, versionFile, version)
214
- : writePyProjectVersion(existing, versionFile, version);
215
- node_fs_1.default.writeFileSync(versionPath, updated, "utf8");
216
- return [versionFile];
273
+ const written = [];
274
+ if (isSourceFileMode(versionFile)) {
275
+ const updated = writeSourceFileVersion(existing, versionFile, version);
276
+ node_fs_1.default.writeFileSync(versionPath, updated, "utf8");
277
+ written.push(versionFile);
278
+ const auxiliary = tryUpdatePyProjectVersion(cwd, version);
279
+ if (auxiliary && auxiliary !== versionFile) {
280
+ written.push(auxiliary);
281
+ }
282
+ }
283
+ else {
284
+ const updated = writePyProjectVersion(existing, versionFile, version);
285
+ node_fs_1.default.writeFileSync(versionPath, updated, "utf8");
286
+ written.push(versionFile);
287
+ const parsed = parsePyProject(updated, versionFile);
288
+ const initPy = findAuxiliaryInitPy(cwd, parsed);
289
+ if (initPy && tryUpdateInitPyVersion(cwd, initPy, version)) {
290
+ written.push(initPy);
291
+ }
292
+ }
293
+ return written;
217
294
  },
218
295
  readPackageName(cwd, _config) {
219
296
  const found = readPyProjectFromCwd(cwd);
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.listKnownReleaseTypes = listKnownReleaseTypes;
4
4
  exports.resolveVersionStrategy = resolveVersionStrategy;
5
+ const composite_js_1 = require("./composite.js");
5
6
  const latex_js_1 = require("./latex.js");
6
7
  const node_js_1 = require("./node.js");
7
8
  const python_js_1 = require("./python.js");
@@ -19,12 +20,30 @@ const strategyRegistry = {
19
20
  function listKnownReleaseTypes() {
20
21
  return Object.keys(strategyRegistry).sort((a, b) => a.localeCompare(b));
21
22
  }
22
- function resolveVersionStrategy(config) {
23
- const releaseType = config["release-type"] ?? "simple";
24
- const strategy = strategyRegistry[releaseType];
23
+ function resolveSingle(name) {
24
+ const strategy = strategyRegistry[name];
25
25
  if (!strategy) {
26
26
  const known = listKnownReleaseTypes().join(", ");
27
- throw new Error(`Unsupported release-type "${releaseType}". Supported release types: ${known}.`);
27
+ throw new Error(`Unsupported release-type "${name}". Supported release types: ${known}.`);
28
28
  }
29
29
  return strategy;
30
30
  }
31
+ function resolveVersionStrategy(config) {
32
+ const releaseType = config["release-type"] ?? "simple";
33
+ if (Array.isArray(releaseType)) {
34
+ if (releaseType.length === 0) {
35
+ throw new Error("release-type array must contain at least one strategy name.");
36
+ }
37
+ const seen = new Set();
38
+ const strategies = [];
39
+ for (const name of releaseType) {
40
+ if (seen.has(name)) {
41
+ throw new Error(`release-type array contains duplicate entry "${name}".`);
42
+ }
43
+ seen.add(name);
44
+ strategies.push(resolveSingle(name));
45
+ }
46
+ return (0, composite_js_1.compositeVersionStrategy)(strategies);
47
+ }
48
+ return resolveSingle(releaseType);
49
+ }
@@ -748,6 +748,9 @@ exports.rustVersionStrategy = {
748
748
  const manifestToPath = new Map();
749
749
  for (const pkg of packages) {
750
750
  const manifest = pkg.versionFile;
751
+ if (node_path_1.default.posix.basename(normalizeSlashPath(manifest)) !== "Cargo.toml") {
752
+ continue;
753
+ }
751
754
  candidateManifests.push(manifest);
752
755
  manifestToPath.set(manifest, pkg.packagePath);
753
756
  if (pkg.nextVersion) {
@@ -763,6 +766,10 @@ exports.rustVersionStrategy = {
763
766
  finalizeVersionWrites(cwd, writes, _context) {
764
767
  const manifestToVersion = {};
765
768
  for (const write of writes) {
769
+ if (node_path_1.default.posix.basename(normalizeSlashPath(write.versionFile)) !==
770
+ "Cargo.toml") {
771
+ continue;
772
+ }
766
773
  manifestToVersion[write.versionFile] = write.version;
767
774
  }
768
775
  return [
@@ -9,7 +9,7 @@ export interface VersionaryArtifactRule {
9
9
  pattern?: string;
10
10
  }
11
11
  export interface VersionaryPackage {
12
- "release-type"?: string;
12
+ "release-type"?: string | string[];
13
13
  "package-name"?: string;
14
14
  "changelog-file"?: string;
15
15
  "changelog-format"?: VersionaryChangelogFormat;
@@ -33,7 +33,7 @@ export interface VersionaryConfig {
33
33
  "bump-minor-pre-major"?: boolean;
34
34
  "allow-stable-major"?: boolean;
35
35
  "include-commit-authors"?: boolean;
36
- "release-type"?: string;
36
+ "release-type"?: string | string[];
37
37
  packages?: Record<string, VersionaryPackage>;
38
38
  }
39
39
  export interface LoadedConfig {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "versionary",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "description": "Automatic release framework based on conventional commits and semantic versioning",
5
5
  "keywords": [
6
6
  "releasing",