versionary 1.3.0 → 1.4.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.
@@ -7,6 +7,7 @@ const SUPPORTED_FILES = [
7
7
  { file: "versionary.jsonc", format: "jsonc" },
8
8
  { file: "versionary.json", format: "json" },
9
9
  ];
10
+ const warnedDeprecatedConfigPaths = new Set();
10
11
  function parseConfig(raw, format) {
11
12
  if (format === "json" || format === "jsonc") {
12
13
  return parseJsonc(raw);
@@ -16,6 +17,43 @@ function parseConfig(raw, format) {
16
17
  function isRecord(value) {
17
18
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
18
19
  }
20
+ function normalizeConfig(config) {
21
+ const { "bump-minor-pre-major": rootBumpMinorPreMajor, packages, ...rootConfig } = config;
22
+ let usedDeprecatedPreMajorKey = rootBumpMinorPreMajor !== undefined;
23
+ const normalizedPackages = packages
24
+ ? Object.fromEntries(Object.entries(packages).map(([packagePath, packageConfig]) => {
25
+ const { "bump-minor-pre-major": packageBumpMinorPreMajor, ...canonicalPackageConfig } = packageConfig;
26
+ usedDeprecatedPreMajorKey ||= packageBumpMinorPreMajor !== undefined;
27
+ return [
28
+ packagePath,
29
+ {
30
+ ...canonicalPackageConfig,
31
+ ...(packageBumpMinorPreMajor === undefined
32
+ ? {}
33
+ : { "allow-stable-major": !packageBumpMinorPreMajor }),
34
+ },
35
+ ];
36
+ }))
37
+ : undefined;
38
+ return {
39
+ config: {
40
+ ...rootConfig,
41
+ ...(rootBumpMinorPreMajor === undefined
42
+ ? {}
43
+ : { "allow-stable-major": !rootBumpMinorPreMajor }),
44
+ ...(normalizedPackages ? { packages: normalizedPackages } : {}),
45
+ },
46
+ usedDeprecatedPreMajorKey,
47
+ };
48
+ }
49
+ function warnForDeprecatedConfig(path) {
50
+ // Release commands can load the same configuration in several stages.
51
+ if (warnedDeprecatedConfigPaths.has(path)) {
52
+ return;
53
+ }
54
+ warnedDeprecatedConfigPaths.add(path);
55
+ console.warn('Warning: "bump-minor-pre-major" is deprecated; use "allow-stable-major" with the inverse value instead.');
56
+ }
19
57
  function validateReleaseTypes(config) {
20
58
  try {
21
59
  resolveVersionStrategy(config);
@@ -67,10 +105,14 @@ export function loadConfig(cwd = process.cwd()) {
67
105
  throw new Error('The "plugins" config key is no longer supported. Versionary uses built-in integrations only.');
68
106
  }
69
107
  const validated = configSchema.parse(parsed);
70
- validateReleaseTypes(validated);
108
+ const normalized = normalizeConfig(validated);
109
+ if (normalized.usedDeprecatedPreMajorKey) {
110
+ warnForDeprecatedConfig(found.path);
111
+ }
112
+ validateReleaseTypes(normalized.config);
71
113
  return {
72
114
  path: found.path,
73
115
  format: found.format,
74
- config: validated,
116
+ config: normalized.config,
75
117
  };
76
118
  }
@@ -39,6 +39,7 @@ export declare const configSchema: z.ZodObject<{
39
39
  "markdown-changelog": "markdown-changelog";
40
40
  "r-news": "r-news";
41
41
  }>>;
42
+ "release-draft": z.ZodOptional<z.ZodBoolean>;
42
43
  "bump-minor-pre-major": z.ZodOptional<z.ZodBoolean>;
43
44
  "allow-stable-major": z.ZodOptional<z.ZodBoolean>;
44
45
  "exclude-paths": z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -56,6 +56,10 @@ const artifactRuleSchema = z
56
56
  });
57
57
  }
58
58
  });
59
+ const deprecatedBumpMinorPreMajorSchema = z.boolean().optional().meta({
60
+ deprecated: true,
61
+ description: 'Deprecated. Use "allow-stable-major" with the inverse value instead.',
62
+ });
59
63
  const packageSchema = z
60
64
  .object({
61
65
  "release-type": z
@@ -64,7 +68,8 @@ const packageSchema = z
64
68
  "package-name": z.string().optional(),
65
69
  "changelog-file": z.string().optional(),
66
70
  "changelog-format": z.enum(["markdown-changelog", "r-news"]).optional(),
67
- "bump-minor-pre-major": z.boolean().optional(),
71
+ "release-draft": z.boolean().optional(),
72
+ "bump-minor-pre-major": deprecatedBumpMinorPreMajorSchema,
68
73
  "allow-stable-major": z.boolean().optional(),
69
74
  "exclude-paths": z.array(z.string()).optional(),
70
75
  "extra-files": z.array(artifactRuleSchema).optional(),
@@ -98,7 +103,7 @@ export const configSchema = z
98
103
  "baseline-file": z.string().optional(),
99
104
  "bootstrap-sha": z.string().optional(),
100
105
  "monorepo-mode": z.enum(["independent", "fixed"]).optional(),
101
- "bump-minor-pre-major": z.boolean().optional(),
106
+ "bump-minor-pre-major": deprecatedBumpMinorPreMajorSchema,
102
107
  "allow-stable-major": z.boolean().optional(),
103
108
  "include-commit-authors": z.boolean().optional(),
104
109
  "exclude-paths": z.array(z.string()).optional(),
@@ -10,20 +10,11 @@ import { readBaselineSha, readReleaseTargets } from "./state.js";
10
10
  function getMode(configMode) {
11
11
  return configMode ?? "independent";
12
12
  }
13
- function resolveBumpMinorPreMajor(config, packageConfig) {
14
- if (packageConfig?.["bump-minor-pre-major"] !== undefined) {
15
- return packageConfig["bump-minor-pre-major"];
16
- }
13
+ function resolveAllowStableMajor(config, packageConfig) {
17
14
  if (packageConfig?.["allow-stable-major"] !== undefined) {
18
- return !packageConfig["allow-stable-major"];
19
- }
20
- if (config["bump-minor-pre-major"] !== undefined) {
21
- return config["bump-minor-pre-major"];
22
- }
23
- if (config["allow-stable-major"] !== undefined) {
24
- return !config["allow-stable-major"];
15
+ return packageConfig["allow-stable-major"];
25
16
  }
26
- return true;
17
+ return config["allow-stable-major"] ?? false;
27
18
  }
28
19
  export function getChangelogDefaults(config) {
29
20
  const changelogFormat = config["changelog-format"] ??
@@ -65,8 +56,8 @@ export function createReleasePlan(cwd = process.cwd()) {
65
56
  const releaseBranchPrefix = loaded.config["release-branch"] ?? "versionary/release";
66
57
  const baselineSha = readBaselineSha(cwd) ?? loaded.config["bootstrap-sha"] ?? null;
67
58
  const releaseTargetByPath = new Map(readReleaseTargets(cwd).map((target) => [target.path, target]));
68
- const bumpMinorPreMajor = resolveBumpMinorPreMajor(loaded.config);
69
- const bumpMinorPreMajorForPath = (packagePath) => resolveBumpMinorPreMajor(loaded.config, loaded.config.packages?.[packagePath]);
59
+ const allowStableMajor = resolveAllowStableMajor(loaded.config);
60
+ const allowStableMajorForPath = (packagePath) => resolveAllowStableMajor(loaded.config, loaded.config.packages?.[packagePath]);
70
61
  const monorepoMode = getMode(loaded.config["monorepo-mode"]);
71
62
  const buildPackagePlan = (pkg) => {
72
63
  const packageContext = resolvePackageStrategyContext(loaded.config, pkg.path, pkg.config);
@@ -109,7 +100,7 @@ export function createReleasePlan(cwd = process.cwd()) {
109
100
  releaseType = analyzedType;
110
101
  nextVersion = releaseType
111
102
  ? bumpVersion(packageCurrentVersion, releaseType, {
112
- bumpMinorPreMajor: bumpMinorPreMajorForPath(pkg.path),
103
+ allowStableMajor: allowStableMajorForPath(pkg.path),
113
104
  })
114
105
  : null;
115
106
  bumpReason = nextVersion ? "direct" : undefined;
@@ -184,7 +175,7 @@ export function createReleasePlan(cwd = process.cwd()) {
184
175
  const current = packageCurrentVersionByPath[target.path] ?? target.currentVersion;
185
176
  target.releaseType = "patch";
186
177
  target.nextVersion = bumpVersion(current, "patch", {
187
- bumpMinorPreMajor: bumpMinorPreMajorForPath(target.path),
178
+ allowStableMajor: allowStableMajorForPath(target.path),
188
179
  });
189
180
  target.bumpReason = reason;
190
181
  const strategyContext = strategyContextByPath.get(target.path);
@@ -217,7 +208,7 @@ export function createReleasePlan(cwd = process.cwd()) {
217
208
  }
218
209
  const hypotheticalVersion = strategyContext.nextVersion ??
219
210
  bumpVersion(packageCurrentVersionByPath[sourcePath] ??
220
- strategyContext.currentVersion, "patch", { bumpMinorPreMajor: bumpMinorPreMajorForPath(sourcePath) });
211
+ strategyContext.currentVersion, "patch", { allowStableMajor: allowStableMajorForPath(sourcePath) });
221
212
  return strategyGroup.strategy.propagateDependentPatchImpacts(cwd, strategyGroup.packages.map((pkg) => ({
222
213
  ...pkg,
223
214
  nextVersion: pkg.packagePath === sourcePath ? hypotheticalVersion : null,
@@ -401,7 +392,7 @@ export function createReleasePlan(cwd = process.cwd()) {
401
392
  const baseVersion = packageCurrentVersionByPath[pkgPlan.path] ?? pkgPlan.currentVersion;
402
393
  const nextVersion = combinedReleaseType
403
394
  ? bumpVersion(baseVersion, combinedReleaseType, {
404
- bumpMinorPreMajor: bumpMinorPreMajorForPath(pkgPlan.path),
395
+ allowStableMajor: allowStableMajorForPath(pkgPlan.path),
405
396
  })
406
397
  : null;
407
398
  return {
@@ -444,7 +435,7 @@ export function createReleasePlan(cwd = process.cwd()) {
444
435
  ? rootPackagePlan.nextVersion
445
436
  : analyzedFixedType
446
437
  ? bumpVersion(fixedBaseVersion, analyzedFixedType, {
447
- bumpMinorPreMajor,
438
+ allowStableMajor,
448
439
  })
449
440
  : null;
450
441
  // Fixed mode can promote unchanged dependencies into the release, so the
@@ -482,7 +473,7 @@ export function createReleasePlan(cwd = process.cwd()) {
482
473
  ? rootPackagePlan.nextVersion
483
474
  : analyzedOverallType
484
475
  ? bumpVersion(overallBaseVersion, analyzedOverallType, {
485
- bumpMinorPreMajor,
476
+ allowStableMajor,
486
477
  })
487
478
  : null;
488
479
  return {
@@ -1,6 +1,7 @@
1
1
  import type { VersionaryConfig } from "../types/config.js";
2
2
  import type { VersionaryPluginContext } from "../types/plugins.js";
3
3
  export declare function resolveTargetPackageName(cwd: string, config: VersionaryConfig, targetPath: string): string | undefined;
4
+ export declare function resolveTargetReleaseDraft(config: VersionaryConfig, targetPath: string): boolean;
4
5
  export declare function extractReleaseNotes(content: string, version: string, changelogFormat: "markdown-changelog" | "r-news"): string;
5
6
  export declare function extractClosingReferencesFromNotes(notes: string): number[];
6
7
  export declare function resolveTargetChangelogFile(config: VersionaryConfig, rootChangelogFile: string, targetPath: string): string;
@@ -22,6 +22,11 @@ export function resolveTargetPackageName(cwd, config, targetPath) {
22
22
  }
23
23
  return resolved;
24
24
  }
25
+ export function resolveTargetReleaseDraft(config, targetPath) {
26
+ return (config.packages?.[targetPath]?.["release-draft"] ??
27
+ config["release-draft"] ??
28
+ false);
29
+ }
25
30
  export function extractReleaseNotes(content, version, changelogFormat) {
26
31
  const lines = content.split("\n");
27
32
  const shortVersion = version.replace(/\.\d+$/u, "");
@@ -198,7 +203,7 @@ export async function runReleaseDetailed(cwd = process.cwd(), options = {}) {
198
203
  tag: target.tag,
199
204
  version: target.version,
200
205
  notes: releaseNotes,
201
- draft: loaded.config["release-draft"] ?? false,
206
+ draft: resolveTargetReleaseDraft(loaded.config, target.path),
202
207
  makeLatest: target.path === "." ? "true" : "false",
203
208
  }, {
204
209
  createReleaseMetadata: (input) => scmClient.createReleaseMetadata(input, {
@@ -8,7 +8,7 @@ export interface ParsedVersion {
8
8
  build: string[];
9
9
  }
10
10
  export interface BumpVersionOptions {
11
- bumpMinorPreMajor?: boolean;
11
+ allowStableMajor?: boolean;
12
12
  }
13
13
  export declare function parseVersion(version: string): ParsedVersion;
14
14
  export declare function isValidVersion(version: string): boolean;
@@ -137,9 +137,9 @@ export function compareVersions(leftRaw, rightRaw) {
137
137
  }
138
138
  export function bumpVersion(current, releaseType, options = {}) {
139
139
  const parsed = parseVersion(current);
140
- const bumpMinorPreMajor = options.bumpMinorPreMajor ?? true;
140
+ const allowStableMajor = options.allowStableMajor ?? false;
141
141
  if (releaseType === "major") {
142
- if (parsed.major === 0 && bumpMinorPreMajor) {
142
+ if (parsed.major === 0 && !allowStableMajor) {
143
143
  return `0.${parsed.minor + 1}.0`;
144
144
  }
145
145
  return `${parsed.major + 1}.0.0`;
@@ -12,32 +12,37 @@ export function verifyProject(cwd = process.cwd()) {
12
12
  details: `Loaded ${path.basename(config.path)} (${config.format})`,
13
13
  category: "config",
14
14
  });
15
- const strategy = resolveVersionStrategy(config.config);
16
- const versionFile = strategy.getVersionFile(config.config);
17
- const exists = fs.existsSync(path.join(cwd, versionFile));
18
- checks.push({
19
- name: `version-file:${versionFile}`,
20
- ok: exists,
21
- details: exists ? "Version file exists" : `Missing ${versionFile}`,
22
- category: "version-files",
23
- remediation: exists
24
- ? undefined
25
- : `Create ${versionFile} or set "version-file" to the correct path for your release strategy.`,
26
- });
27
- if (exists) {
28
- const validationError = strategy.validateProject?.(cwd, config.config);
15
+ const packages = Object.entries(config.config.packages ?? {});
16
+ const shouldValidateRoot = packages.length === 0 ||
17
+ packages.some(([packagePath]) => packagePath === ".");
18
+ if (shouldValidateRoot) {
19
+ const strategy = resolveVersionStrategy(config.config);
20
+ const versionFile = strategy.getVersionFile(config.config);
21
+ const exists = fs.existsSync(path.join(cwd, versionFile));
29
22
  checks.push({
30
- name: `strategy-validate:${strategy.name}`,
31
- ok: !validationError,
32
- details: validationError ?? "Strategy-level validation passed",
23
+ name: `version-file:${versionFile}`,
24
+ ok: exists,
25
+ details: exists ? "Version file exists" : `Missing ${versionFile}`,
33
26
  category: "version-files",
34
- remediation: validationError
35
- ? `Fix strategy-specific version metadata in ${versionFile} for release-type "${strategy.name}".`
36
- : undefined,
27
+ remediation: exists
28
+ ? undefined
29
+ : `Create ${versionFile} or set "version-file" to the correct path for your release strategy.`,
37
30
  });
31
+ if (exists) {
32
+ const validationError = strategy.validateProject?.(cwd, config.config);
33
+ checks.push({
34
+ name: `strategy-validate:${strategy.name}`,
35
+ ok: !validationError,
36
+ details: validationError ?? "Strategy-level validation passed",
37
+ category: "version-files",
38
+ remediation: validationError
39
+ ? `Fix strategy-specific version metadata in ${versionFile} for release-type "${strategy.name}".`
40
+ : undefined,
41
+ });
42
+ }
38
43
  }
39
- if (config.config.packages) {
40
- for (const [pkgPathRaw, packageConfig] of Object.entries(config.config.packages)) {
44
+ if (packages.length > 0) {
45
+ for (const [pkgPathRaw, packageConfig] of packages) {
41
46
  const pkgPath = path.join(cwd, pkgPathRaw);
42
47
  const exists = fs.existsSync(pkgPath);
43
48
  checks.push({
@@ -140,7 +140,7 @@ const LOCKFILE_SPECS = [
140
140
  installHint: "install PDM (https://pdm-project.org/)",
141
141
  },
142
142
  ];
143
- function refreshLockfile(cwd, spec) {
143
+ function refreshLockfile(cwd, spec, lockfilePath) {
144
144
  try {
145
145
  execFileSync(spec.command, [...spec.args], {
146
146
  cwd,
@@ -149,7 +149,33 @@ function refreshLockfile(cwd, spec) {
149
149
  }
150
150
  catch (error) {
151
151
  const message = error instanceof Error ? error.message : String(error);
152
- throw new Error(`Failed to refresh ${spec.lockfile} via "${spec.command} ${spec.args.join(" ")}". Ensure ${spec.command} is on PATH (${spec.installHint}) or remove ${spec.lockfile} from the working tree. Details: ${message}`);
152
+ throw new Error(`Failed to refresh ${lockfilePath} via "${spec.command} ${spec.args.join(" ")}". Ensure ${spec.command} is on PATH (${spec.installHint}) or remove ${lockfilePath} from the working tree. Details: ${message}`);
153
+ }
154
+ }
155
+ function normalizeRelativePath(base, target) {
156
+ return path.relative(base, target).replaceAll("\\", "/");
157
+ }
158
+ function findNearestLockfileDirectory(cwd, packageDirectory, lockfile) {
159
+ const root = path.resolve(cwd);
160
+ let current = path.resolve(packageDirectory);
161
+ const relativePackageDirectory = path.relative(root, current);
162
+ if (relativePackageDirectory === ".." ||
163
+ relativePackageDirectory.startsWith(`..${path.sep}`) ||
164
+ path.isAbsolute(relativePackageDirectory)) {
165
+ return null;
166
+ }
167
+ while (true) {
168
+ if (fs.existsSync(path.join(current, lockfile))) {
169
+ return current;
170
+ }
171
+ if (current === root) {
172
+ return null;
173
+ }
174
+ const parent = path.dirname(current);
175
+ if (parent === current) {
176
+ return null;
177
+ }
178
+ current = parent;
153
179
  }
154
180
  }
155
181
  function readPyProjectFromCwd(cwd) {
@@ -301,16 +327,25 @@ export const pythonVersionStrategy = {
301
327
  }
302
328
  return null;
303
329
  },
304
- finalizeVersionWrites(cwd, _writes, _context) {
305
- const refreshed = [];
306
- for (const spec of LOCKFILE_SPECS) {
307
- const lockPath = path.join(cwd, spec.lockfile);
308
- if (!fs.existsSync(lockPath)) {
309
- continue;
330
+ finalizeVersionWrites(cwd, writes, _context) {
331
+ const packageDirectories = [
332
+ ...new Set(writes.map((write) => path.resolve(cwd, write.packagePath === "." ? "" : write.packagePath))),
333
+ ].sort((a, b) => a.localeCompare(b));
334
+ const lockfiles = new Map();
335
+ for (const packageDirectory of packageDirectories) {
336
+ for (const spec of LOCKFILE_SPECS) {
337
+ const directory = findNearestLockfileDirectory(cwd, packageDirectory, spec.lockfile);
338
+ if (!directory) {
339
+ continue;
340
+ }
341
+ const relativePath = normalizeRelativePath(cwd, path.join(directory, spec.lockfile));
342
+ lockfiles.set(relativePath, { directory, spec });
310
343
  }
311
- refreshLockfile(cwd, spec);
312
- refreshed.push(spec.lockfile);
313
344
  }
314
- return refreshed.sort((a, b) => a.localeCompare(b));
345
+ const refreshed = [...lockfiles.entries()].sort(([left], [right]) => left.localeCompare(right));
346
+ for (const [relativePath, { directory, spec }] of refreshed) {
347
+ refreshLockfile(directory, spec, relativePath);
348
+ }
349
+ return refreshed.map(([relativePath]) => relativePath);
315
350
  },
316
351
  };
@@ -14,6 +14,8 @@ export interface VersionaryPackage {
14
14
  "package-name"?: string;
15
15
  "changelog-file"?: string;
16
16
  "changelog-format"?: VersionaryChangelogFormat;
17
+ "release-draft"?: boolean;
18
+ /** @deprecated Use `allow-stable-major` with the inverse value. */
17
19
  "bump-minor-pre-major"?: boolean;
18
20
  "allow-stable-major"?: boolean;
19
21
  "exclude-paths"?: string[];
@@ -33,6 +35,7 @@ export interface VersionaryConfig {
33
35
  "baseline-file"?: string;
34
36
  "bootstrap-sha"?: string;
35
37
  "monorepo-mode"?: "independent" | "fixed";
38
+ /** @deprecated Use `allow-stable-major` with the inverse value. */
36
39
  "bump-minor-pre-major"?: boolean;
37
40
  "allow-stable-major"?: boolean;
38
41
  "include-commit-authors"?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "versionary",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Automatic release framework based on conventional commits and semantic versioning",
5
5
  "keywords": [
6
6
  "releasing",