versionary 0.2.0 → 0.3.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.
Files changed (63) hide show
  1. package/README.md +187 -11
  2. package/dist/{simple → app/release}/pr.d.ts +7 -4
  3. package/dist/{simple → app/release}/pr.js +89 -74
  4. package/dist/app/release/recovery.d.ts +17 -0
  5. package/dist/app/release/recovery.js +101 -0
  6. package/dist/{simple → app/release}/release.js +37 -24
  7. package/dist/app/release/state.d.ts +10 -0
  8. package/dist/{simple → app/release}/state.js +16 -4
  9. package/dist/app/release/verify.d.ts +2 -0
  10. package/dist/app/release/verify.js +5 -0
  11. package/dist/cli/index.js +12 -10
  12. package/dist/config/load-config.js +2 -12
  13. package/dist/config/schema.d.ts +1 -0
  14. package/dist/config/schema.js +1 -0
  15. package/dist/domain/release/changelog.d.ts +12 -0
  16. package/dist/domain/release/changelog.js +123 -0
  17. package/dist/{simple → domain/release}/plan.d.ts +4 -3
  18. package/dist/{simple → domain/release}/plan.js +32 -15
  19. package/dist/domain/release/semver.d.ts +15 -0
  20. package/dist/domain/release/semver.js +111 -0
  21. package/dist/domain/strategy/node.d.ts +2 -0
  22. package/dist/domain/strategy/node.js +37 -0
  23. package/dist/domain/strategy/resolve.d.ts +3 -0
  24. package/dist/domain/strategy/resolve.js +11 -0
  25. package/dist/domain/strategy/simple.d.ts +2 -0
  26. package/dist/domain/strategy/simple.js +28 -0
  27. package/dist/domain/strategy/types.d.ts +7 -0
  28. package/dist/domain/strategy/types.js +2 -0
  29. package/dist/index.d.ts +5 -3
  30. package/dist/index.js +5 -1
  31. package/dist/infra/git/commits.d.ts +65 -0
  32. package/dist/infra/git/commits.js +436 -0
  33. package/dist/infra/scm/github/plugin.d.ts +1 -0
  34. package/dist/infra/scm/github/plugin.js +5 -0
  35. package/dist/infra/scm/runtime.d.ts +2 -0
  36. package/dist/infra/scm/runtime.js +24 -0
  37. package/dist/infra/scm/types.d.ts +1 -0
  38. package/dist/infra/scm/types.js +2 -0
  39. package/dist/plugins/runtime.d.ts +1 -2
  40. package/dist/plugins/runtime.js +3 -22
  41. package/dist/scm/github-plugin.js +133 -40
  42. package/dist/strategies/node.d.ts +1 -0
  43. package/dist/strategies/node.js +5 -0
  44. package/dist/strategies/resolve.d.ts +1 -0
  45. package/dist/strategies/resolve.js +5 -0
  46. package/dist/strategies/simple.d.ts +1 -0
  47. package/dist/strategies/simple.js +5 -0
  48. package/dist/strategies/types.d.ts +1 -0
  49. package/dist/strategies/types.js +2 -0
  50. package/dist/types/config.d.ts +1 -0
  51. package/dist/types/plugins.d.ts +1 -0
  52. package/dist/verify/verify-project.js +3 -1
  53. package/package.json +1 -1
  54. package/dist/simple/changelog.d.ts +0 -3
  55. package/dist/simple/changelog.js +0 -46
  56. package/dist/simple/git.d.ts +0 -10
  57. package/dist/simple/git.js +0 -114
  58. package/dist/simple/semver.d.ts +0 -8
  59. package/dist/simple/semver.js +0 -25
  60. package/dist/simple/state.d.ts +0 -3
  61. /package/dist/{simple → app/release}/release.d.ts +0 -0
  62. /package/dist/{simple → infra/git}/repo-url.d.ts +0 -0
  63. /package/dist/{simple → infra/git}/repo-url.js +0 -0
@@ -4,13 +4,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runSimpleRelease = runSimpleRelease;
7
+ const node_child_process_1 = require("node:child_process");
7
8
  const node_fs_1 = __importDefault(require("node:fs"));
8
9
  const node_path_1 = __importDefault(require("node:path"));
9
- const node_child_process_1 = require("node:child_process");
10
- const load_config_js_1 = require("../config/load-config.js");
11
- const capabilities_js_1 = require("../plugins/capabilities.js");
12
- const runtime_js_1 = require("../plugins/runtime.js");
10
+ const load_config_js_1 = require("../../config/load-config.js");
11
+ const resolve_js_1 = require("../../domain/strategy/resolve.js");
12
+ const capabilities_js_1 = require("../../plugins/capabilities.js");
13
+ const runtime_js_1 = require("../../plugins/runtime.js");
13
14
  const pr_js_1 = require("./pr.js");
15
+ const recovery_js_1 = require("./recovery.js");
16
+ const state_js_1 = require("./state.js");
14
17
  function getHeadCommitSubject(cwd) {
15
18
  return (0, node_child_process_1.execFileSync)("git", ["log", "-1", "--pretty=%s"], {
16
19
  cwd,
@@ -18,18 +21,6 @@ function getHeadCommitSubject(cwd) {
18
21
  stdio: ["ignore", "pipe", "ignore"],
19
22
  }).trim();
20
23
  }
21
- function createTagIfMissing(cwd, tag) {
22
- const tagExists = (0, node_child_process_1.execFileSync)("git", ["tag", "--list", tag], {
23
- cwd,
24
- encoding: "utf8",
25
- stdio: ["ignore", "pipe", "ignore"],
26
- }).trim();
27
- if (tagExists.length > 0) {
28
- return;
29
- }
30
- (0, node_child_process_1.execFileSync)("git", ["tag", tag], { cwd, stdio: ["ignore", "pipe", "ignore"] });
31
- (0, node_child_process_1.execFileSync)("git", ["push", "origin", tag], { cwd, stdio: ["ignore", "pipe", "ignore"] });
32
- }
33
24
  function readReleaseNotes(cwd, version, changelogFile) {
34
25
  const changelogPath = node_path_1.default.join(cwd, changelogFile);
35
26
  if (!node_fs_1.default.existsSync(changelogPath)) {
@@ -48,7 +39,10 @@ function readReleaseNotes(cwd, version, changelogFile) {
48
39
  break;
49
40
  }
50
41
  }
51
- const notes = lines.slice(start + 1, end).join("\n").trim();
42
+ const notes = lines
43
+ .slice(start + 1, end)
44
+ .join("\n")
45
+ .trim();
52
46
  return notes.length > 0 ? notes : `Automated release for v${version}`;
53
47
  }
54
48
  async function runSimpleRelease(cwd = process.cwd()) {
@@ -57,11 +51,10 @@ async function runSimpleRelease(cwd = process.cwd()) {
57
51
  return "No release commit context detected; skipping release stage.";
58
52
  }
59
53
  const loaded = (0, load_config_js_1.loadConfig)(cwd);
60
- const versionFile = loaded.config["version-file"] ?? "version.txt";
54
+ const strategy = (0, resolve_js_1.resolveVersionStrategy)(loaded.config);
61
55
  const changelogFile = loaded.config["changelog-file"] ?? "CHANGELOG.md";
62
- const version = node_fs_1.default.readFileSync(node_path_1.default.join(cwd, versionFile), "utf8").trim();
63
- const tag = `v${version}`;
64
- createTagIfMissing(cwd, tag);
56
+ const version = strategy.readVersion(cwd, loaded.config);
57
+ const defaultTag = `v${version}`;
65
58
  const plugins = (0, runtime_js_1.loadRuntimePlugins)();
66
59
  const scmPlugins = (0, capabilities_js_1.findPluginsByCapability)(plugins, "scm.releaseMetadata");
67
60
  if (scmPlugins.length === 0) {
@@ -71,7 +64,27 @@ async function runSimpleRelease(cwd = process.cwd()) {
71
64
  if (!plugin?.createReleaseMetadata) {
72
65
  throw new Error(`Plugin "${plugin?.name ?? "unknown"}" does not implement createReleaseMetadata.`);
73
66
  }
74
- const notes = readReleaseNotes(cwd, version, changelogFile);
75
- const result = await plugin.createReleaseMetadata({ tag, version, notes }, { cwd, logger: console });
76
- return `Published release ${tag}: ${result.url}`;
67
+ const releaseTargets = (0, state_js_1.readReleaseTargets)(cwd);
68
+ const targets = releaseTargets.length > 0
69
+ ? releaseTargets
70
+ : [
71
+ {
72
+ path: ".",
73
+ version,
74
+ tag: defaultTag,
75
+ notes: readReleaseNotes(cwd, version, changelogFile),
76
+ },
77
+ ];
78
+ const published = [];
79
+ for (const target of targets) {
80
+ const outcome = await (0, recovery_js_1.executeIdempotentReleaseTarget)(cwd, {
81
+ tag: target.tag,
82
+ version: target.version,
83
+ notes: target.notes ?? readReleaseNotes(cwd, target.version, changelogFile),
84
+ }, {
85
+ createReleaseMetadata: (input) => plugin.createReleaseMetadata(input, { cwd, logger: console }),
86
+ });
87
+ published.push(`${outcome.tag}: ${outcome.url} (tag=${outcome.tagStatus}, metadata=${outcome.metadataStatus})`);
88
+ }
89
+ return `Published releases ${published.join(", ")}`;
77
90
  }
@@ -0,0 +1,10 @@
1
+ export interface ReleaseTargetState {
2
+ path: string;
3
+ version: string;
4
+ tag: string;
5
+ notes?: string;
6
+ }
7
+ export declare function getBaselineStatePath(cwd: string): string;
8
+ export declare function readBaselineSha(cwd?: string): string | null;
9
+ export declare function readReleaseTargets(cwd?: string): ReleaseTargetState[];
10
+ export declare function writeBaselineSha(cwd?: string, sha?: string, releaseTargets?: ReleaseTargetState[]): void;
@@ -5,11 +5,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getBaselineStatePath = getBaselineStatePath;
7
7
  exports.readBaselineSha = readBaselineSha;
8
+ exports.readReleaseTargets = readReleaseTargets;
8
9
  exports.writeBaselineSha = writeBaselineSha;
10
+ const node_child_process_1 = require("node:child_process");
9
11
  const node_fs_1 = __importDefault(require("node:fs"));
10
12
  const node_path_1 = __importDefault(require("node:path"));
11
- const node_child_process_1 = require("node:child_process");
12
- const load_config_js_1 = require("../config/load-config.js");
13
+ const load_config_js_1 = require("../../config/load-config.js");
13
14
  function getBaselineStatePath(cwd) {
14
15
  const loaded = (0, load_config_js_1.loadConfig)(cwd);
15
16
  const configured = loaded.config["baseline-file"];
@@ -34,7 +35,15 @@ function readBaselineSha(cwd = process.cwd()) {
34
35
  const parsed = JSON.parse(node_fs_1.default.readFileSync(filePath, "utf8"));
35
36
  return parsed.baselineSha ?? null;
36
37
  }
37
- function writeBaselineSha(cwd = process.cwd(), sha) {
38
+ function readReleaseTargets(cwd = process.cwd()) {
39
+ const filePath = getBaselineStatePath(cwd);
40
+ if (!node_fs_1.default.existsSync(filePath)) {
41
+ return [];
42
+ }
43
+ const parsed = JSON.parse(node_fs_1.default.readFileSync(filePath, "utf8"));
44
+ return parsed.releaseTargets ?? [];
45
+ }
46
+ function writeBaselineSha(cwd = process.cwd(), sha, releaseTargets = []) {
38
47
  const baselineSha = sha ??
39
48
  (0, node_child_process_1.execFileSync)("git", ["rev-parse", "HEAD"], {
40
49
  cwd,
@@ -42,6 +51,9 @@ function writeBaselineSha(cwd = process.cwd(), sha) {
42
51
  stdio: ["ignore", "pipe", "ignore"],
43
52
  }).trim();
44
53
  const filePath = getBaselineStatePath(cwd);
45
- const next = { baselineSha };
54
+ const next = {
55
+ baselineSha,
56
+ releaseTargets,
57
+ };
46
58
  node_fs_1.default.writeFileSync(filePath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
47
59
  }
@@ -0,0 +1,2 @@
1
+ export type { VerifyResult } from "../../verify/verify-project.js";
2
+ export { verifyProject } from "../../verify/verify-project.js";
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifyProject = void 0;
4
+ var verify_project_js_1 = require("../../verify/verify-project.js");
5
+ Object.defineProperty(exports, "verifyProject", { enumerable: true, get: function () { return verify_project_js_1.verifyProject; } });
package/dist/cli/index.js CHANGED
@@ -4,16 +4,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ const node_child_process_1 = require("node:child_process");
7
8
  const node_fs_1 = __importDefault(require("node:fs"));
8
9
  const node_path_1 = __importDefault(require("node:path"));
9
- const node_child_process_1 = require("node:child_process");
10
- const plan_js_1 = require("../simple/plan.js");
11
- const changelog_js_1 = require("../simple/changelog.js");
12
- const pr_js_1 = require("../simple/pr.js");
13
- const release_js_1 = require("../simple/release.js");
14
- const verify_project_js_1 = require("../verify/verify-project.js");
10
+ const pr_js_1 = require("../app/release/pr.js");
11
+ const release_js_1 = require("../app/release/release.js");
12
+ const verify_js_1 = require("../app/release/verify.js");
13
+ const changelog_js_1 = require("../domain/release/changelog.js");
14
+ const plan_js_1 = require("../domain/release/plan.js");
15
15
  function printVerifyResult() {
16
- const result = (0, verify_project_js_1.verifyProject)();
16
+ const result = (0, verify_js_1.verifyProject)();
17
17
  for (const check of result.checks) {
18
18
  const status = check.ok ? "OK" : "FAIL";
19
19
  console.log(`[${status}] ${check.name} - ${check.details}`);
@@ -39,7 +39,7 @@ async function main() {
39
39
  }
40
40
  const pr = (0, pr_js_1.prepareSimpleReleasePr)();
41
41
  (0, pr_js_1.pushReleaseBranch)(process.cwd(), pr.branch);
42
- const reviewResult = await (0, pr_js_1.openOrUpdateSimpleReviewRequest)(process.cwd(), pr.branch, pr.title, pr.version, pr.commits);
42
+ const reviewResult = await (0, pr_js_1.openOrUpdateSimpleReviewRequest)(process.cwd(), pr.branch, pr.title, pr.version, pr.previousVersion, pr.commits, pr.plan);
43
43
  console.log(`Prepared release PR branch ${pr.branch}`);
44
44
  console.log(`Title: ${pr.title}`);
45
45
  console.log(reviewResult);
@@ -66,7 +66,9 @@ async function main() {
66
66
  return 0;
67
67
  }
68
68
  const changelogPath = node_path_1.default.join(process.cwd(), plan.changelogFile);
69
- const existing = node_fs_1.default.existsSync(changelogPath) ? node_fs_1.default.readFileSync(changelogPath, "utf8") : "";
69
+ const existing = node_fs_1.default.existsSync(changelogPath)
70
+ ? node_fs_1.default.readFileSync(changelogPath, "utf8")
71
+ : "";
70
72
  const heading = "# Changelog\n\n";
71
73
  const body = existing.replace(/^# Changelog\s*/u, "");
72
74
  node_fs_1.default.writeFileSync(changelogPath, `${heading}${section}\n${body}`.trimEnd() + "\n", "utf8");
@@ -76,7 +78,7 @@ async function main() {
76
78
  if (command === "pr") {
77
79
  const pr = (0, pr_js_1.prepareSimpleReleasePr)();
78
80
  (0, pr_js_1.pushReleaseBranch)(process.cwd(), pr.branch);
79
- const reviewResult = await (0, pr_js_1.openOrUpdateSimpleReviewRequest)(process.cwd(), pr.branch, pr.title, pr.version, pr.commits);
81
+ const reviewResult = await (0, pr_js_1.openOrUpdateSimpleReviewRequest)(process.cwd(), pr.branch, pr.title, pr.version, pr.previousVersion, pr.commits, pr.plan);
80
82
  console.log(`Prepared release PR branch ${pr.branch}`);
81
83
  console.log(`Title: ${pr.title}`);
82
84
  console.log(reviewResult);
@@ -7,27 +7,17 @@ exports.findConfigFile = findConfigFile;
7
7
  exports.loadConfig = loadConfig;
8
8
  const node_fs_1 = __importDefault(require("node:fs"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
- const toml_1 = require("@iarna/toml");
11
10
  const jsonc_parser_1 = require("jsonc-parser");
12
11
  const schema_js_1 = require("./schema.js");
13
12
  const SUPPORTED_FILES = [
14
13
  { file: "versionary.jsonc", format: "jsonc" },
15
14
  { file: "versionary.json", format: "json" },
16
- { file: "versionary.toml", format: "toml" },
17
- { file: "versionary.js", format: "js" },
18
- { file: "versionary.config.jsonc", format: "jsonc" },
19
- { file: "versionary.config.json", format: "json" },
20
- { file: "versionary.config.toml", format: "toml" },
21
- { file: "versionary.config.js", format: "js" },
22
15
  ];
23
16
  function parseConfig(raw, format) {
24
17
  if (format === "json" || format === "jsonc") {
25
18
  return (0, jsonc_parser_1.parse)(raw);
26
19
  }
27
- if (format === "toml") {
28
- return (0, toml_1.parse)(raw);
29
- }
30
- throw new Error("JavaScript config loading is not implemented yet. Use JSONC/JSON/TOML for now.");
20
+ throw new Error("Unsupported config format. Use versionary.jsonc or versionary.json.");
31
21
  }
32
22
  function isRecord(value) {
33
23
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
@@ -44,7 +34,7 @@ function findConfigFile(cwd) {
44
34
  function loadConfig(cwd = process.cwd()) {
45
35
  const found = findConfigFile(cwd);
46
36
  if (!found) {
47
- throw new Error("No Versionary config found. Create versionary.jsonc (preferred), .json, or .toml.");
37
+ throw new Error("No Versionary config found. Create versionary.jsonc (preferred) or versionary.json.");
48
38
  }
49
39
  const raw = node_fs_1.default.readFileSync(found.path, "utf8");
50
40
  const parsed = parseConfig(raw, found.format);
@@ -15,6 +15,7 @@ export declare const configSchema: z.ZodObject<{
15
15
  fixed: "fixed";
16
16
  }>>;
17
17
  "bump-minor-pre-major": z.ZodOptional<z.ZodBoolean>;
18
+ "allow-stable-major": z.ZodOptional<z.ZodBoolean>;
18
19
  "include-commit-authors": z.ZodOptional<z.ZodBoolean>;
19
20
  "release-type": z.ZodOptional<z.ZodString>;
20
21
  packages: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
@@ -24,6 +24,7 @@ exports.configSchema = zod_1.z.object({
24
24
  "bootstrap-sha": zod_1.z.string().optional(),
25
25
  "monorepo-mode": zod_1.z.enum(["independent", "fixed"]).optional(),
26
26
  "bump-minor-pre-major": zod_1.z.boolean().optional(),
27
+ "allow-stable-major": zod_1.z.boolean().optional(),
27
28
  "include-commit-authors": zod_1.z.boolean().optional(),
28
29
  "release-type": zod_1.z.string().optional(),
29
30
  packages: zod_1.z.record(zod_1.z.string().min(1), packageSchema).optional(),
@@ -0,0 +1,12 @@
1
+ import { type ParsedCommit } from "../../infra/git/commits.js";
2
+ import type { SimplePlan } from "./plan.js";
3
+ export declare function renderSimpleReleaseNotes(input: {
4
+ currentVersion: string;
5
+ nextVersion: string;
6
+ commits: ParsedCommit[];
7
+ cwd?: string;
8
+ }, options?: {
9
+ includeFooter?: boolean;
10
+ }): string;
11
+ export declare function renderSimpleChangelog(plan: SimplePlan): string;
12
+ export declare function prependChangelog(cwd: string, changelogFile: string, section: string): void;
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.renderSimpleReleaseNotes = renderSimpleReleaseNotes;
7
+ exports.renderSimpleChangelog = renderSimpleChangelog;
8
+ exports.prependChangelog = prependChangelog;
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const commits_js_1 = require("../../infra/git/commits.js");
12
+ const repo_url_js_1 = require("../../infra/git/repo-url.js");
13
+ function formatDate() {
14
+ return new Date().toISOString().slice(0, 10);
15
+ }
16
+ function formatCommitMessage(subject) {
17
+ const conventional = subject.match(/^[a-z]+(?:\(([^)]+)\))?!?:\s+(.+)$/iu);
18
+ if (!conventional) {
19
+ return { label: "", message: subject };
20
+ }
21
+ const scope = conventional[1]?.trim();
22
+ const message = conventional[2]?.trim() ?? subject;
23
+ const label = scope ? `**${scope}:** ` : "";
24
+ return { label, message };
25
+ }
26
+ function formatCommitReferences(commit, repoBaseUrl) {
27
+ const references = commit.references ?? [];
28
+ if (references.length === 0) {
29
+ return "";
30
+ }
31
+ return references
32
+ .map((reference) => {
33
+ const issue = reference.issue;
34
+ if (!issue) {
35
+ return "";
36
+ }
37
+ const action = (reference.action?.trim() || "refs").toLowerCase();
38
+ if (!repoBaseUrl) {
39
+ return `${action} #${issue}`;
40
+ }
41
+ return `${action} [#${issue}](${repoBaseUrl}/issues/${issue})`;
42
+ })
43
+ .filter((entry) => entry.length > 0)
44
+ .join(", ");
45
+ }
46
+ function groupCommitLines(commits, repoUrl) {
47
+ const breaking = [];
48
+ const features = [];
49
+ const fixes = [];
50
+ for (const commit of commits) {
51
+ const type = (0, commits_js_1.inferReleaseTypeFromParsedCommit)(commit);
52
+ if (!type) {
53
+ continue;
54
+ }
55
+ const { label, message } = formatCommitMessage(commit.subject);
56
+ const short = commit.hash.slice(0, 7);
57
+ const hashLabel = repoUrl
58
+ ? `[\`${short}\`](${repoUrl}/commit/${commit.hash})`
59
+ : `\`${short}\``;
60
+ const references = formatCommitReferences(commit, repoUrl);
61
+ const referencesSuffix = references ? `, ${references}` : "";
62
+ const line = `- ${label}${message} (${hashLabel})${referencesSuffix}`;
63
+ const commitType = (commit.type ?? "").toLowerCase();
64
+ const isBreaking = type === "major";
65
+ if (isBreaking) {
66
+ breaking.push(line);
67
+ }
68
+ if (commitType === "feat" || (!isBreaking && type === "minor")) {
69
+ features.push(line);
70
+ continue;
71
+ }
72
+ if (commitType === "fix" ||
73
+ commitType === "perf" ||
74
+ (!isBreaking && type === "patch")) {
75
+ fixes.push(line);
76
+ }
77
+ }
78
+ return { breaking, features, fixes };
79
+ }
80
+ function renderSimpleReleaseNotes(input, options = {}) {
81
+ const repoUrl = (0, repo_url_js_1.resolveRepositoryWebBaseUrl)(input.cwd ?? process.cwd());
82
+ const header = repoUrl
83
+ ? `## [${input.nextVersion}](${repoUrl}/compare/v${input.currentVersion}...v${input.nextVersion}) (${formatDate()})`
84
+ : `## ${input.nextVersion} (${formatDate()})`;
85
+ const grouped = groupCommitLines(input.commits, repoUrl);
86
+ const sections = [];
87
+ if (grouped.breaking.length > 0) {
88
+ sections.push("### Breaking changes", ...grouped.breaking, "");
89
+ }
90
+ if (grouped.features.length > 0) {
91
+ sections.push("### Features", ...grouped.features, "");
92
+ }
93
+ if (grouped.fixes.length > 0) {
94
+ sections.push("### Bug Fixes", ...grouped.fixes, "");
95
+ }
96
+ const lines = [header, "", ...sections];
97
+ if (options.includeFooter) {
98
+ lines.push("This PR was generated by Versionary.");
99
+ }
100
+ return lines.join("\n");
101
+ }
102
+ function renderSimpleChangelog(plan) {
103
+ if (!plan.nextVersion) {
104
+ return "";
105
+ }
106
+ return renderSimpleReleaseNotes({
107
+ currentVersion: plan.currentVersion,
108
+ nextVersion: plan.nextVersion,
109
+ commits: plan.commits,
110
+ cwd: process.cwd(),
111
+ });
112
+ }
113
+ function prependChangelog(cwd, changelogFile, section) {
114
+ const changelogPath = node_path_1.default.join(cwd, changelogFile);
115
+ const existing = node_fs_1.default.existsSync(changelogPath)
116
+ ? node_fs_1.default.readFileSync(changelogPath, "utf8")
117
+ : "";
118
+ const heading = existing.startsWith("# Changelog") ? "" : "# Changelog\n\n";
119
+ const separator = existing.length > 0 ? "\n" : "";
120
+ const next = `${heading}${section}${separator}${existing.replace(/^# Changelog\s*/u, "")}`.trimEnd() +
121
+ "\n";
122
+ node_fs_1.default.writeFileSync(changelogPath, next, "utf8");
123
+ }
@@ -1,4 +1,4 @@
1
- import { type CommitInfo } from "./git.js";
1
+ import { type ParsedCommit } from "../../infra/git/commits.js";
2
2
  import { type ReleaseType } from "./semver.js";
3
3
  export interface SimplePlan {
4
4
  mode: "simple";
@@ -9,12 +9,13 @@ export interface SimplePlan {
9
9
  changelogFile: string;
10
10
  releaseBranchPrefix: string;
11
11
  baselineSha: string | null;
12
- commits: CommitInfo[];
12
+ commits: ParsedCommit[];
13
13
  packages?: Array<{
14
14
  path: string;
15
15
  releaseType: ReleaseType;
16
+ currentVersion: string;
16
17
  nextVersion: string | null;
17
- commits: CommitInfo[];
18
+ commits: ParsedCommit[];
18
19
  }>;
19
20
  }
20
21
  export declare function createSimplePlan(cwd?: string): SimplePlan;
@@ -6,16 +6,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createSimplePlan = createSimplePlan;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
- const load_config_js_1 = require("../config/load-config.js");
10
- const git_js_1 = require("./git.js");
9
+ const state_js_1 = require("../../app/release/state.js");
10
+ const load_config_js_1 = require("../../config/load-config.js");
11
+ const commits_js_1 = require("../../infra/git/commits.js");
12
+ const resolve_js_1 = require("../strategy/resolve.js");
11
13
  const semver_js_1 = require("./semver.js");
12
- const state_js_1 = require("./state.js");
13
14
  function getMode(configMode) {
14
15
  return configMode ?? "independent";
15
16
  }
16
17
  function createSimplePlan(cwd = process.cwd()) {
17
18
  const loaded = (0, load_config_js_1.loadConfig)(cwd);
18
- const versionFile = loaded.config["version-file"] ?? "version.txt";
19
+ const strategy = (0, resolve_js_1.resolveVersionStrategy)(loaded.config);
20
+ const versionFile = strategy.getVersionFile(loaded.config);
19
21
  const changelogFile = loaded.config["changelog-file"] ?? "CHANGELOG.md";
20
22
  const releaseBranchPrefix = loaded.config["release-branch"] ?? "versionary/release";
21
23
  const baselineSha = (0, state_js_1.readBaselineSha)(cwd) ?? loaded.config["bootstrap-sha"] ?? null;
@@ -23,7 +25,8 @@ function createSimplePlan(cwd = process.cwd()) {
23
25
  if (!node_fs_1.default.existsSync(versionPath)) {
24
26
  throw new Error(`Versionary requires ${versionFile} to exist.`);
25
27
  }
26
- const currentVersion = node_fs_1.default.readFileSync(versionPath, "utf8").trim();
28
+ const currentVersion = strategy.readVersion(cwd, loaded.config);
29
+ const allowStableMajor = loaded.config["allow-stable-major"] ?? false;
27
30
  const configuredPackages = Object.entries(loaded.config.packages ?? {}).map(([pkgPath, cfg]) => ({
28
31
  path: pkgPath,
29
32
  ...cfg,
@@ -31,9 +34,13 @@ function createSimplePlan(cwd = process.cwd()) {
31
34
  const monorepoMode = getMode(loaded.config["monorepo-mode"]);
32
35
  const hasPackages = configuredPackages.length > 0;
33
36
  if (!hasPackages) {
34
- const commits = (0, git_js_1.getCommitsSinceLastTag)(cwd, baselineSha);
35
- const releaseType = (0, git_js_1.analyzeCommits)(commits);
36
- const nextVersion = releaseType ? (0, semver_js_1.bumpVersion)(currentVersion, releaseType) : null;
37
+ const parsedCommits = (0, commits_js_1.getParsedCommitsSinceLastTag)(cwd, baselineSha);
38
+ const effectiveCommits = (0, commits_js_1.applyRevertSuppression)(parsedCommits);
39
+ const commits = effectiveCommits;
40
+ const releaseType = (0, commits_js_1.analyzeParsedCommits)(parsedCommits);
41
+ const nextVersion = releaseType
42
+ ? (0, semver_js_1.bumpVersion)(currentVersion, releaseType, { allowStableMajor })
43
+ : null;
37
44
  return {
38
45
  mode: "simple",
39
46
  releaseType,
@@ -48,20 +55,28 @@ function createSimplePlan(cwd = process.cwd()) {
48
55
  }
49
56
  const packagePlans = configuredPackages
50
57
  .map((pkg) => {
51
- const commits = (0, git_js_1.getCommitsForPath)(cwd, baselineSha, pkg.path, pkg["exclude-paths"] ?? []);
52
- const releaseType = (0, git_js_1.analyzeCommits)(commits);
53
- const nextVersion = releaseType ? (0, semver_js_1.bumpVersion)(currentVersion, releaseType) : null;
58
+ const parsedCommits = (0, commits_js_1.getParsedCommitsForPath)(cwd, baselineSha, pkg.path, pkg["exclude-paths"] ?? []);
59
+ const effectiveCommits = (0, commits_js_1.applyRevertSuppression)(parsedCommits);
60
+ const commits = effectiveCommits;
61
+ const releaseType = (0, commits_js_1.analyzeParsedCommits)(parsedCommits);
62
+ const nextVersion = releaseType
63
+ ? (0, semver_js_1.bumpVersion)(currentVersion, releaseType, { allowStableMajor })
64
+ : null;
54
65
  return {
55
66
  path: pkg.path,
56
67
  releaseType,
68
+ currentVersion,
57
69
  nextVersion,
58
70
  commits,
71
+ parsedCommits,
59
72
  };
60
73
  })
61
74
  .sort((a, b) => a.path.localeCompare(b.path));
62
75
  if (monorepoMode === "fixed") {
63
- const fixedType = (0, git_js_1.analyzeCommits)(packagePlans.flatMap((pkgPlan) => pkgPlan.commits));
64
- const fixedNextVersion = fixedType ? (0, semver_js_1.bumpVersion)(currentVersion, fixedType) : null;
76
+ const fixedType = (0, commits_js_1.analyzeParsedCommits)(packagePlans.flatMap((pkgPlan) => pkgPlan.parsedCommits));
77
+ const fixedNextVersion = fixedType
78
+ ? (0, semver_js_1.bumpVersion)(currentVersion, fixedType, { allowStableMajor })
79
+ : null;
65
80
  const adjusted = packagePlans.map((pkgPlan) => ({
66
81
  ...pkgPlan,
67
82
  releaseType: fixedType,
@@ -80,8 +95,10 @@ function createSimplePlan(cwd = process.cwd()) {
80
95
  packages: adjusted,
81
96
  };
82
97
  }
83
- const overallType = (0, git_js_1.analyzeCommits)(packagePlans.flatMap((pkgPlan) => pkgPlan.commits));
84
- const overallNextVersion = overallType ? (0, semver_js_1.bumpVersion)(currentVersion, overallType) : null;
98
+ const overallType = (0, commits_js_1.analyzeParsedCommits)(packagePlans.flatMap((pkgPlan) => pkgPlan.parsedCommits));
99
+ const overallNextVersion = overallType
100
+ ? (0, semver_js_1.bumpVersion)(currentVersion, overallType, { allowStableMajor })
101
+ : null;
85
102
  return {
86
103
  mode: "simple",
87
104
  releaseType: overallType,
@@ -0,0 +1,15 @@
1
+ export type ReleaseType = "major" | "minor" | "patch" | null;
2
+ export interface ParsedVersion {
3
+ major: number;
4
+ minor: number;
5
+ patch: number;
6
+ prerelease: string[];
7
+ build: string[];
8
+ }
9
+ export interface BumpVersionOptions {
10
+ allowStableMajor?: boolean;
11
+ }
12
+ export declare function parseVersion(version: string): ParsedVersion;
13
+ export declare function isValidVersion(version: string): boolean;
14
+ export declare function compareVersions(leftRaw: string, rightRaw: string): number;
15
+ export declare function bumpVersion(current: string, releaseType: Exclude<ReleaseType, null>, options?: BumpVersionOptions): string;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseVersion = parseVersion;
4
+ exports.isValidVersion = isValidVersion;
5
+ exports.compareVersions = compareVersions;
6
+ exports.bumpVersion = bumpVersion;
7
+ const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/u;
8
+ function parseVersion(version) {
9
+ const match = version.trim().match(SEMVER_PATTERN);
10
+ if (!match) {
11
+ throw new Error(`Invalid version in version file: "${version}". Expected SemVer 2.0.0 format.`);
12
+ }
13
+ return {
14
+ major: Number(match[1]),
15
+ minor: Number(match[2]),
16
+ patch: Number(match[3]),
17
+ prerelease: match[4] ? match[4].split(".") : [],
18
+ build: match[5] ? match[5].split(".") : [],
19
+ };
20
+ }
21
+ function isValidVersion(version) {
22
+ return SEMVER_PATTERN.test(version.trim());
23
+ }
24
+ function isNumericIdentifier(identifier) {
25
+ return /^(0|[1-9]\d*)$/u.test(identifier);
26
+ }
27
+ function comparePreReleaseIdentifiers(left, right) {
28
+ const leftNumeric = isNumericIdentifier(left);
29
+ const rightNumeric = isNumericIdentifier(right);
30
+ if (leftNumeric && rightNumeric) {
31
+ const leftNumber = Number(left);
32
+ const rightNumber = Number(right);
33
+ if (leftNumber < rightNumber) {
34
+ return -1;
35
+ }
36
+ if (leftNumber > rightNumber) {
37
+ return 1;
38
+ }
39
+ return 0;
40
+ }
41
+ if (leftNumeric && !rightNumeric) {
42
+ return -1;
43
+ }
44
+ if (!leftNumeric && rightNumeric) {
45
+ return 1;
46
+ }
47
+ if (left < right) {
48
+ return -1;
49
+ }
50
+ if (left > right) {
51
+ return 1;
52
+ }
53
+ return 0;
54
+ }
55
+ function compareVersions(leftRaw, rightRaw) {
56
+ const left = parseVersion(leftRaw);
57
+ const right = parseVersion(rightRaw);
58
+ if (left.major !== right.major) {
59
+ return left.major < right.major ? -1 : 1;
60
+ }
61
+ if (left.minor !== right.minor) {
62
+ return left.minor < right.minor ? -1 : 1;
63
+ }
64
+ if (left.patch !== right.patch) {
65
+ return left.patch < right.patch ? -1 : 1;
66
+ }
67
+ const leftPre = left.prerelease;
68
+ const rightPre = right.prerelease;
69
+ if (leftPre.length === 0 && rightPre.length === 0) {
70
+ return 0;
71
+ }
72
+ if (leftPre.length === 0) {
73
+ return 1;
74
+ }
75
+ if (rightPre.length === 0) {
76
+ return -1;
77
+ }
78
+ const maxLength = Math.max(leftPre.length, rightPre.length);
79
+ for (let index = 0; index < maxLength; index += 1) {
80
+ const leftIdentifier = leftPre[index];
81
+ const rightIdentifier = rightPre[index];
82
+ if (leftIdentifier === undefined) {
83
+ return -1;
84
+ }
85
+ if (rightIdentifier === undefined) {
86
+ return 1;
87
+ }
88
+ const comparison = comparePreReleaseIdentifiers(leftIdentifier, rightIdentifier);
89
+ if (comparison !== 0) {
90
+ return comparison;
91
+ }
92
+ }
93
+ return 0;
94
+ }
95
+ function bumpVersion(current, releaseType, options = {}) {
96
+ const parsed = parseVersion(current);
97
+ const allowStableMajor = options.allowStableMajor ?? false;
98
+ if (releaseType === "major") {
99
+ if (parsed.major === 0 && !allowStableMajor) {
100
+ return `0.${parsed.minor + 1}.0`;
101
+ }
102
+ if (parsed.major === 0 && allowStableMajor) {
103
+ return "1.0.0";
104
+ }
105
+ return `${parsed.major + 1}.0.0`;
106
+ }
107
+ if (releaseType === "minor") {
108
+ return `${parsed.major}.${parsed.minor + 1}.0`;
109
+ }
110
+ return `${parsed.major}.${parsed.minor}.${parsed.patch + 1}`;
111
+ }
@@ -0,0 +1,2 @@
1
+ import type { VersionStrategy } from "./types.js";
2
+ export declare const nodeVersionStrategy: VersionStrategy;