versionary 0.14.0 → 0.15.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/dist/cli/index.js CHANGED
@@ -141,6 +141,23 @@ async function main() {
141
141
  return 0;
142
142
  }
143
143
  const pr = (0, pr_js_1.prepareReleasePr)(process.cwd(), { logger });
144
+ if (!pr.updated) {
145
+ const message = `Release PR branch ${pr.branch} is already up to date.`;
146
+ if (flags.json) {
147
+ emitJson({
148
+ action: "pr-up-to-date",
149
+ message,
150
+ releaseCreated: false,
151
+ tagNames: [],
152
+ branch: pr.branch,
153
+ title: pr.title,
154
+ });
155
+ return 0;
156
+ }
157
+ console.log(message);
158
+ console.log(`Title: ${pr.title}`);
159
+ return 0;
160
+ }
144
161
  (0, pr_js_1.pushReleaseBranch)(process.cwd(), pr.branch);
145
162
  const reviewResult = await (0, pr_js_1.openOrUpdateReviewRequest)(process.cwd(), pr.branch, pr.title, pr.version, pr.previousVersion, pr.commits, pr.plan, { logger });
146
163
  const message = `Prepared release PR branch ${pr.branch}`;
@@ -196,6 +213,11 @@ async function main() {
196
213
  return 0;
197
214
  }
198
215
  const pr = (0, pr_js_1.prepareReleasePr)(process.cwd(), { logger: console });
216
+ if (!pr.updated) {
217
+ console.log(`Release PR branch ${pr.branch} is already up to date.`);
218
+ console.log(`Title: ${pr.title}`);
219
+ return 0;
220
+ }
199
221
  (0, pr_js_1.pushReleaseBranch)(process.cwd(), pr.branch);
200
222
  const reviewResult = await (0, pr_js_1.openOrUpdateReviewRequest)(process.cwd(), pr.branch, pr.title, pr.version, pr.previousVersion, pr.commits, pr.plan);
201
223
  console.log(`Prepared release PR branch ${pr.branch}`);
@@ -222,6 +222,27 @@ function parseFooters(body) {
222
222
  });
223
223
  }
224
224
  };
225
+ const extractInlineSentenceReferences = (text) => {
226
+ const inlineMatches = text.matchAll(/\b(close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\b\s+((?:(?<owner>[A-Za-z0-9_.-]+)\/(?<repo>[A-Za-z0-9_.-]+))?#(?<issue>\d+)|GH-(?<ghIssue>\d+))/giu);
227
+ for (const match of inlineMatches) {
228
+ const action = match[1] ?? null;
229
+ const raw = match[2] ?? "";
230
+ const owner = match.groups?.owner ?? null;
231
+ const repository = match.groups?.repo ?? null;
232
+ const issue = match.groups?.issue ?? match.groups?.ghIssue ?? null;
233
+ if (!issue || !/^\d+$/u.test(issue)) {
234
+ continue;
235
+ }
236
+ references.push({
237
+ action,
238
+ owner,
239
+ repository,
240
+ issue,
241
+ raw,
242
+ prefix: raw.startsWith("GH-") ? "GH-" : "#",
243
+ });
244
+ }
245
+ };
225
246
  for (const line of footerLines) {
226
247
  const footerMatch = parseFooterLine(line);
227
248
  if (footerMatch) {
@@ -248,12 +269,19 @@ function parseFooters(body) {
248
269
  }
249
270
  extractReferences(`${footer.token}: ${footer.value}`, footer.token);
250
271
  }
272
+ extractInlineSentenceReferences(body);
273
+ const dedupedReferences = [
274
+ ...new Map(references.map((reference) => [
275
+ `${reference.action ?? ""}:${reference.owner ?? ""}/${reference.repository ?? ""}:${reference.prefix}:${reference.issue ?? ""}`,
276
+ reference,
277
+ ])).values(),
278
+ ];
251
279
  return {
252
280
  bodyText: bodyLines.join("\n").trim() || "",
253
281
  footerText: footerLines.length > 0 ? footerLines.join("\n").trim() : null,
254
282
  footers,
255
283
  diagnostics,
256
- references,
284
+ references: dedupedReferences,
257
285
  notes,
258
286
  };
259
287
  }
@@ -51,6 +51,28 @@ function groupCommitLines(commits, repoUrl) {
51
51
  const features = [];
52
52
  const fixes = [];
53
53
  const reverts = [];
54
+ const getRevertedSubject = (commit) => {
55
+ const normalizedDescription = (commit.description ?? "")
56
+ .trim()
57
+ .replace(/^"(.*)"$/u, "$1");
58
+ if (normalizedDescription.length > 0) {
59
+ return normalizedDescription;
60
+ }
61
+ const quoted = commit.subject.match(/^revert:\s+"(.+)"$/iu);
62
+ if (quoted) {
63
+ return quoted[1].trim();
64
+ }
65
+ const plain = commit.subject.match(/^revert:\s+(.+)$/iu);
66
+ return plain ? plain[1].trim() : "";
67
+ };
68
+ const shouldIncludeRevert = (commit) => {
69
+ const revertedSubject = getRevertedSubject(commit);
70
+ if (revertedSubject.length === 0) {
71
+ return true;
72
+ }
73
+ const revertedCommit = (0, commits_js_1.parseConventionalCommitMessage)(revertedSubject);
74
+ return (0, commits_js_1.inferReleaseTypeFromParsedCommit)(revertedCommit) !== null;
75
+ };
54
76
  for (const commit of commits) {
55
77
  const type = (0, commits_js_1.inferReleaseTypeFromParsedCommit)(commit);
56
78
  if (!type) {
@@ -67,6 +89,9 @@ function groupCommitLines(commits, repoUrl) {
67
89
  const commitType = (commit.type ?? "").toLowerCase();
68
90
  const isBreaking = type === "major";
69
91
  if (commit.isRevert) {
92
+ if (!shouldIncludeRevert(commit)) {
93
+ continue;
94
+ }
70
95
  reverts.push(line);
71
96
  continue;
72
97
  }
@@ -28,6 +28,7 @@ export declare function getChangelogDefaults(config: {
28
28
  "release-type"?: VersionaryConfig["release-type"];
29
29
  "changelog-file"?: VersionaryConfig["changelog-file"];
30
30
  "changelog-format"?: VersionaryConfig["changelog-format"];
31
+ defaultChangelogFormat?: VersionaryChangelogFormat;
31
32
  }): {
32
33
  changelogFile: string;
33
34
  changelogFormat: VersionaryChangelogFormat;
@@ -19,61 +19,56 @@ function getMode(configMode) {
19
19
  }
20
20
  function getChangelogDefaults(config) {
21
21
  const changelogFormat = config["changelog-format"] ??
22
- (config["release-type"] === "r" ? "r-news" : "markdown-changelog");
22
+ config.defaultChangelogFormat ??
23
+ "markdown-changelog";
23
24
  const changelogFile = config["changelog-file"] ??
24
25
  (changelogFormat === "r-news" ? "NEWS.md" : "CHANGELOG.md");
25
26
  return { changelogFile, changelogFormat };
26
27
  }
28
+ function getNormalizedPackages(config) {
29
+ const configured = Object.entries(config.packages ?? {}).map(([packagePath, packageConfig]) => ({
30
+ path: packagePath,
31
+ config: packageConfig,
32
+ implicitRoot: false,
33
+ }));
34
+ if (configured.length === 0) {
35
+ return [{ path: ".", config: {}, implicitRoot: false }];
36
+ }
37
+ if (configured.some((pkg) => pkg.path === ".")) {
38
+ return configured;
39
+ }
40
+ return [{ path: ".", config: {}, implicitRoot: true }, ...configured];
41
+ }
27
42
  function createReleasePlan(cwd = process.cwd()) {
28
43
  const loaded = (0, load_config_js_1.loadConfig)(cwd);
29
44
  const strategy = (0, resolve_js_1.resolveVersionStrategy)(loaded.config);
45
+ const configuredPackageCount = Object.keys(loaded.config.packages ?? {}).length;
46
+ const hasPackages = configuredPackageCount > 0;
47
+ const hasExplicitRootPackage = Boolean(loaded.config.packages?.["."]);
48
+ const normalizedPackages = getNormalizedPackages(loaded.config);
30
49
  const versionFile = strategy.getVersionFile(loaded.config);
31
- const { changelogFile, changelogFormat } = getChangelogDefaults(loaded.config);
32
- const packageName = loaded.config["release-type"] === "r"
33
- ? (strategy.readPackageName?.(cwd, loaded.config) ?? node_path_1.default.basename(cwd))
34
- : node_path_1.default.basename(cwd);
50
+ const { changelogFile, changelogFormat } = getChangelogDefaults({
51
+ ...loaded.config,
52
+ defaultChangelogFormat: strategy.getDefaultChangelogFormat?.(),
53
+ });
54
+ const packageName = hasPackages
55
+ ? node_path_1.default.basename(cwd)
56
+ : (strategy.readPackageName?.(cwd, loaded.config) ?? node_path_1.default.basename(cwd));
35
57
  const releaseBranchPrefix = loaded.config["release-branch"] ?? "versionary/release";
36
58
  const baselineSha = (0, state_js_1.readBaselineSha)(cwd) ?? loaded.config["bootstrap-sha"] ?? null;
37
59
  const releaseTargetByPath = new Map((0, state_js_1.readReleaseTargets)(cwd).map((target) => [target.path, target]));
38
60
  const allowStableMajor = loaded.config["allow-stable-major"] ?? false;
39
- const configuredPackages = Object.entries(loaded.config.packages ?? {}).map(([pkgPath, cfg]) => ({
40
- path: pkgPath,
41
- ...cfg,
42
- }));
43
61
  const monorepoMode = getMode(loaded.config["monorepo-mode"]);
44
- const hasPackages = configuredPackages.length > 0;
45
- if (!hasPackages) {
46
- const versionPath = node_path_1.default.join(cwd, versionFile);
47
- if (!node_fs_1.default.existsSync(versionPath)) {
48
- throw new Error(`Versionary requires ${versionFile} to exist.`);
62
+ const buildPackagePlan = (pkg) => {
63
+ const packageContext = (0, package_context_js_1.resolvePackageStrategyContext)(loaded.config, pkg.path, pkg.config);
64
+ const currentVersionFile = node_path_1.default.join(cwd, packageContext.versionFile);
65
+ if (!node_fs_1.default.existsSync(currentVersionFile)) {
66
+ throw new Error(`Versionary requires ${packageContext.versionFile} to exist for package "${pkg.path}".`);
49
67
  }
50
- const currentVersion = strategy.readVersion(cwd, loaded.config);
51
- const parsedCommits = (0, commits_js_1.getParsedCommitsSinceLastTag)(cwd, baselineSha);
52
- const effectiveCommits = (0, commits_js_1.applyRevertSuppression)(parsedCommits);
53
- const commits = effectiveCommits;
54
- const releaseType = (0, commits_js_1.analyzeParsedCommits)(parsedCommits);
55
- const nextVersion = releaseType
56
- ? (0, semver_js_1.bumpVersion)(currentVersion, releaseType, { allowStableMajor })
57
- : null;
58
- return {
59
- mode: "simple",
60
- releaseType,
61
- currentVersion,
62
- nextVersion,
63
- packageName,
64
- versionFile,
65
- changelogFile,
66
- changelogFormat,
67
- releaseBranchPrefix,
68
- baselineSha,
69
- commits,
70
- };
71
- }
72
- const packagePlans = configuredPackages
73
- .map((pkg) => {
74
- const packageContext = (0, package_context_js_1.resolvePackageStrategyContext)(loaded.config, pkg.path, pkg);
75
68
  const packageCurrentVersion = packageContext.strategy.readVersion(cwd, packageContext.config);
76
- const parsedCommits = (0, commits_js_1.getParsedCommitsForPath)(cwd, releaseTargetByPath.get(pkg.path)?.tag ?? baselineSha, pkg.path, pkg["exclude-paths"] ?? []);
69
+ const parsedCommits = !hasPackages && pkg.path === "."
70
+ ? (0, commits_js_1.getParsedCommitsSinceLastTag)(cwd, baselineSha)
71
+ : (0, commits_js_1.getParsedCommitsForPath)(cwd, releaseTargetByPath.get(pkg.path)?.tag ?? baselineSha, pkg.path, pkg.config["exclude-paths"] ?? []);
77
72
  const effectiveCommits = (0, commits_js_1.applyRevertSuppression)(parsedCommits);
78
73
  const commits = effectiveCommits;
79
74
  const releaseType = (0, commits_js_1.analyzeParsedCommits)(parsedCommits);
@@ -82,15 +77,36 @@ function createReleasePlan(cwd = process.cwd()) {
82
77
  : null;
83
78
  return {
84
79
  path: pkg.path,
80
+ implicitRoot: pkg.implicitRoot,
85
81
  releaseType,
86
82
  currentVersion: packageCurrentVersion,
87
83
  nextVersion,
88
84
  bumpReason: nextVersion ? "direct" : undefined,
89
85
  commits,
90
86
  parsedCommits,
87
+ resolvedVersionFile: packageContext.versionFile,
91
88
  };
92
- })
93
- .sort((a, b) => a.path.localeCompare(b.path));
89
+ };
90
+ const explicitPackagePlans = normalizedPackages
91
+ .filter((pkg) => !pkg.implicitRoot)
92
+ .map((pkg) => buildPackagePlan(pkg));
93
+ const implicitRoot = normalizedPackages.find((pkg) => pkg.implicitRoot);
94
+ const implicitRootPlan = implicitRoot
95
+ ? {
96
+ path: ".",
97
+ implicitRoot: true,
98
+ releaseType: null,
99
+ currentVersion: explicitPackagePlans[0]?.currentVersion ?? "0.0.0",
100
+ nextVersion: null,
101
+ commits: [],
102
+ parsedCommits: [],
103
+ resolvedVersionFile: versionFile,
104
+ }
105
+ : null;
106
+ const packagePlans = [
107
+ ...explicitPackagePlans,
108
+ ...(implicitRootPlan ? [implicitRootPlan] : []),
109
+ ].sort((a, b) => a.path.localeCompare(b.path));
94
110
  const packageCurrentVersionByPath = {};
95
111
  const strategyPackagesByName = new Map();
96
112
  for (const packagePlan of packagePlans) {
@@ -100,7 +116,7 @@ function createReleasePlan(cwd = process.cwd()) {
100
116
  if (existingGroup) {
101
117
  existingGroup.packages.push({
102
118
  packagePath: packagePlan.path,
103
- versionFile: packageContext.versionFile,
119
+ versionFile: packagePlan.resolvedVersionFile,
104
120
  currentVersion: packagePlan.currentVersion,
105
121
  nextVersion: packagePlan.nextVersion,
106
122
  });
@@ -111,7 +127,7 @@ function createReleasePlan(cwd = process.cwd()) {
111
127
  packages: [
112
128
  {
113
129
  packagePath: packagePlan.path,
114
- versionFile: packageContext.versionFile,
130
+ versionFile: packagePlan.resolvedVersionFile,
115
131
  currentVersion: packagePlan.currentVersion,
116
132
  nextVersion: packagePlan.nextVersion,
117
133
  },
@@ -139,12 +155,29 @@ function createReleasePlan(cwd = process.cwd()) {
139
155
  bumpReason: "dependency-propagation",
140
156
  };
141
157
  });
158
+ const visiblePackages = adjustedPackages.filter((pkgPlan) => !pkgPlan.implicitRoot || hasExplicitRootPackage);
159
+ const rootPackagePlan = adjustedPackages.find((pkgPlan) => pkgPlan.path === ".");
160
+ if (!rootPackagePlan) {
161
+ throw new Error('Internal error: normalized package list must always include root path ".".');
162
+ }
163
+ if (!hasPackages) {
164
+ return {
165
+ mode: "simple",
166
+ releaseType: rootPackagePlan.releaseType,
167
+ currentVersion: rootPackagePlan.currentVersion,
168
+ nextVersion: rootPackagePlan.nextVersion,
169
+ packageName,
170
+ versionFile,
171
+ changelogFile,
172
+ changelogFormat,
173
+ releaseBranchPrefix,
174
+ baselineSha,
175
+ commits: rootPackagePlan.commits,
176
+ };
177
+ }
142
178
  if (monorepoMode === "fixed") {
143
179
  const fixedType = (0, commits_js_1.analyzeParsedCommits)(adjustedPackages.flatMap((pkgPlan) => pkgPlan.parsedCommits));
144
- const fixedBaseVersion = adjustedPackages.find((pkgPlan) => pkgPlan.path === ".")
145
- ?.currentVersion ??
146
- adjustedPackages[0]?.currentVersion ??
147
- "0.0.0";
180
+ const fixedBaseVersion = rootPackagePlan.currentVersion;
148
181
  const fixedNextVersion = fixedType
149
182
  ? (0, semver_js_1.bumpVersion)(fixedBaseVersion, fixedType, { allowStableMajor })
150
183
  : null;
@@ -165,13 +198,13 @@ function createReleasePlan(cwd = process.cwd()) {
165
198
  releaseBranchPrefix,
166
199
  baselineSha,
167
200
  commits: adjusted.flatMap((pkgPlan) => pkgPlan.commits),
168
- packages: adjusted,
201
+ packages: adjusted
202
+ .filter((pkgPlan) => !pkgPlan.implicitRoot || hasExplicitRootPackage)
203
+ .map(({ implicitRoot: _implicitRoot, ...pkgPlan }) => pkgPlan),
169
204
  };
170
205
  }
171
206
  const overallType = (0, commits_js_1.analyzeParsedCommits)(adjustedPackages.flatMap((pkgPlan) => pkgPlan.parsedCommits));
172
- const overallBaseVersion = adjustedPackages.find((pkgPlan) => pkgPlan.path === ".")?.currentVersion ??
173
- adjustedPackages[0]?.currentVersion ??
174
- "0.0.0";
207
+ const overallBaseVersion = rootPackagePlan.currentVersion;
175
208
  const overallNextVersion = overallType
176
209
  ? (0, semver_js_1.bumpVersion)(overallBaseVersion, overallType, { allowStableMajor })
177
210
  : null;
@@ -187,7 +220,7 @@ function createReleasePlan(cwd = process.cwd()) {
187
220
  releaseBranchPrefix,
188
221
  baselineSha,
189
222
  commits: adjustedPackages.flatMap((pkgPlan) => pkgPlan.commits),
190
- packages: adjustedPackages,
223
+ packages: visiblePackages.map(({ implicitRoot: _implicitRoot, ...pkgPlan }) => pkgPlan),
191
224
  };
192
225
  }
193
226
  /** @deprecated Use createReleasePlan. */
@@ -14,6 +14,7 @@ export declare function prepareReleasePr(cwd?: string, options?: {
14
14
  previousVersion: string;
15
15
  commits: ParsedCommit[];
16
16
  plan: ReleasePlan;
17
+ updated: boolean;
17
18
  };
18
19
  export declare function renderSimpleReviewRequestBody(version: string, previousVersion: string, commits: ParsedCommit[], plan?: SimplePlan | null, cwd?: string): string;
19
20
  export declare function openOrUpdateReviewRequest(cwd: string, branch: string, title: string, version: string, previousVersion: string, commits: ParsedCommit[], plan?: SimplePlan | null, options?: {
@@ -12,12 +12,10 @@ exports.openOrUpdateSimpleReviewRequest = openOrUpdateSimpleReviewRequest;
12
12
  exports.pushReleaseBranch = pushReleaseBranch;
13
13
  exports.isReleaseCommitMessage = isReleaseCommitMessage;
14
14
  const node_child_process_1 = require("node:child_process");
15
- const node_fs_1 = __importDefault(require("node:fs"));
16
15
  const node_path_1 = __importDefault(require("node:path"));
17
16
  const load_config_js_1 = require("../config/load-config.js");
18
17
  const client_js_1 = require("../scm/client.js");
19
18
  const package_context_js_1 = require("../strategy/package-context.js");
20
- const resolve_js_1 = require("../strategy/resolve.js");
21
19
  const artifact_rules_js_1 = require("./artifact-rules.js");
22
20
  const changelog_js_1 = require("./changelog.js");
23
21
  const plan_js_1 = require("./plan.js");
@@ -70,61 +68,6 @@ function ensureCleanWorktree(cwd, logger) {
70
68
  logger?.warn(`Ignoring safe tracked changes before versionary pr:\n${ignored.join("\n")}`);
71
69
  }
72
70
  }
73
- function normalizeSlashPath(input) {
74
- return input.replaceAll("\\", "/");
75
- }
76
- function listCargoLockFiles(cwd) {
77
- const lockfiles = [];
78
- const queue = [cwd];
79
- while (queue.length > 0) {
80
- const currentDir = queue.shift();
81
- if (!currentDir) {
82
- continue;
83
- }
84
- const entries = node_fs_1.default.readdirSync(currentDir, { withFileTypes: true });
85
- for (const entry of entries) {
86
- if (entry.name === ".git") {
87
- continue;
88
- }
89
- const fullPath = node_path_1.default.join(currentDir, entry.name);
90
- if (entry.isDirectory()) {
91
- queue.push(fullPath);
92
- continue;
93
- }
94
- if (!entry.isFile() || entry.name !== "Cargo.lock") {
95
- continue;
96
- }
97
- lockfiles.push(normalizeSlashPath(node_path_1.default.relative(cwd, fullPath)));
98
- }
99
- }
100
- return lockfiles.sort((a, b) => a.localeCompare(b));
101
- }
102
- function ensureCargoLockUpToDate(cwd) {
103
- const lockfiles = listCargoLockFiles(cwd);
104
- if (lockfiles.length === 0) {
105
- return [];
106
- }
107
- const updatedLockfiles = [];
108
- for (const lockfile of lockfiles) {
109
- const lockfilePath = node_path_1.default.join(cwd, lockfile);
110
- const before = node_fs_1.default.readFileSync(lockfilePath, "utf8");
111
- try {
112
- (0, node_child_process_1.execFileSync)("cargo", ["generate-lockfile"], {
113
- cwd: node_path_1.default.dirname(lockfilePath),
114
- stdio: ["ignore", "pipe", "pipe"],
115
- });
116
- }
117
- catch (error) {
118
- const message = error instanceof Error ? error.message : String(error);
119
- throw new Error(`Failed to refresh ${lockfile} via "cargo generate-lockfile". Ensure cargo is installed and available in PATH. Details: ${message}`);
120
- }
121
- const after = node_fs_1.default.readFileSync(lockfilePath, "utf8");
122
- if (after !== before) {
123
- updatedLockfiles.push(lockfile);
124
- }
125
- }
126
- return updatedLockfiles;
127
- }
128
71
  function normalizeReleaseNameForTag(releaseName) {
129
72
  return releaseName
130
73
  .trim()
@@ -132,6 +75,43 @@ function normalizeReleaseNameForTag(releaseName) {
132
75
  .replaceAll("/", "-")
133
76
  .replace(/\s+/gu, "-");
134
77
  }
78
+ function getCommitTreeSha(cwd, revision) {
79
+ return (0, node_child_process_1.execFileSync)("git", ["rev-parse", `${revision}^{tree}`], {
80
+ cwd,
81
+ encoding: "utf8",
82
+ stdio: ["ignore", "pipe", "ignore"],
83
+ }).trim();
84
+ }
85
+ function hasOriginRemote(cwd) {
86
+ const remotes = (0, node_child_process_1.execFileSync)("git", ["remote"], {
87
+ cwd,
88
+ encoding: "utf8",
89
+ stdio: ["ignore", "pipe", "ignore"],
90
+ })
91
+ .split("\n")
92
+ .map((remote) => remote.trim())
93
+ .filter((remote) => remote.length > 0);
94
+ return remotes.includes("origin");
95
+ }
96
+ function remoteReleaseBranchExists(cwd, branch) {
97
+ if (!hasOriginRemote(cwd)) {
98
+ return false;
99
+ }
100
+ const output = (0, node_child_process_1.execFileSync)("git", ["ls-remote", "--heads", "origin", branch], {
101
+ cwd,
102
+ encoding: "utf8",
103
+ stdio: ["ignore", "pipe", "ignore"],
104
+ }).trim();
105
+ return output.length > 0;
106
+ }
107
+ function fetchRemoteReleaseBranch(cwd, branch) {
108
+ const remoteRef = `refs/remotes/origin/${branch}`;
109
+ (0, node_child_process_1.execFileSync)("git", ["fetch", "--no-tags", "origin", `refs/heads/${branch}:${remoteRef}`], {
110
+ cwd,
111
+ stdio: ["ignore", "pipe", "ignore"],
112
+ });
113
+ return remoteRef;
114
+ }
135
115
  function resolveReleaseName(cwd, packagePath, packageConfig, strategy, strategyConfig) {
136
116
  const configuredName = packageConfig["package-name"]?.trim();
137
117
  if (configuredName) {
@@ -207,7 +187,6 @@ function formatReleaseCommitTitle(releaseTargets) {
207
187
  function prepareReleasePr(cwd = process.cwd(), options = {}) {
208
188
  const plan = (0, plan_js_1.createReleasePlan)(cwd);
209
189
  const loaded = (0, load_config_js_1.loadConfig)(cwd);
210
- const strategy = (0, resolve_js_1.resolveVersionStrategy)(loaded.config);
211
190
  if (!plan.nextVersion) {
212
191
  throw new Error("No releasable commits found. Nothing to open a release PR for.");
213
192
  }
@@ -225,35 +204,35 @@ function prepareReleasePr(cwd = process.cwd(), options = {}) {
225
204
  writes: [write],
226
205
  });
227
206
  };
228
- if (plan.packages && plan.packages.length > 0) {
229
- for (const packagePlan of plan.packages) {
230
- if (!packagePlan.nextVersion) {
231
- continue;
232
- }
233
- const packageConfig = loaded.config.packages?.[packagePlan.path] ?? {};
234
- const packageContext = (0, package_context_js_1.resolvePackageStrategyContext)(loaded.config, packagePlan.path, packageConfig);
235
- const packageUpdated = packageContext.strategy.writeVersion(cwd, packageContext.config, packagePlan.nextVersion);
236
- updatedVersionFiles.push(...packageUpdated);
237
- addStrategyWrite(packageContext.strategy, {
238
- packagePath: packagePlan.path,
239
- versionFile: packageContext.versionFile,
240
- version: packagePlan.nextVersion,
241
- });
207
+ const versionTargets = plan.packages && plan.packages.length > 0
208
+ ? plan.packages
209
+ : [
210
+ {
211
+ path: ".",
212
+ releaseType: plan.releaseType,
213
+ currentVersion: plan.currentVersion,
214
+ nextVersion: plan.nextVersion,
215
+ commits: plan.commits,
216
+ },
217
+ ];
218
+ for (const packagePlan of versionTargets) {
219
+ if (!packagePlan.nextVersion) {
220
+ continue;
242
221
  }
243
- }
244
- else {
245
- updatedVersionFiles.push(...strategy.writeVersion(cwd, loaded.config, plan.nextVersion));
246
- addStrategyWrite(strategy, {
247
- packagePath: ".",
248
- versionFile: strategy.getVersionFile(loaded.config),
249
- version: plan.nextVersion,
222
+ const packageConfig = loaded.config.packages?.[packagePlan.path] ?? {};
223
+ const packageContext = (0, package_context_js_1.resolvePackageStrategyContext)(loaded.config, packagePlan.path, packageConfig);
224
+ const packageUpdated = packageContext.strategy.writeVersion(cwd, packageContext.config, packagePlan.nextVersion);
225
+ updatedVersionFiles.push(...packageUpdated);
226
+ addStrategyWrite(packageContext.strategy, {
227
+ packagePath: packagePlan.path,
228
+ versionFile: packageContext.versionFile,
229
+ version: packagePlan.nextVersion,
250
230
  });
251
231
  }
252
232
  for (const strategyGroup of writesByStrategy.values()) {
253
233
  updatedVersionFiles.push(...(strategyGroup.strategy.finalizeVersionWrites?.(cwd, strategyGroup.writes) ?? []));
254
234
  }
255
235
  const updatedArtifactFiles = (0, artifact_rules_js_1.applyConfiguredArtifactRules)(cwd, loaded.config, plan);
256
- const updatedRustLockFiles = ensureCargoLockUpToDate(cwd);
257
236
  const packageReleaseMetadata = buildPackageReleaseMetadata(cwd, plan, loaded.config);
258
237
  const section = (0, changelog_js_1.renderReleasePlanChangelog)(plan);
259
238
  (0, changelog_js_1.prependChangelog)(cwd, plan.changelogFile, section, plan.changelogFormat);
@@ -263,10 +242,12 @@ function prepareReleasePr(cwd = process.cwd(), options = {}) {
263
242
  continue;
264
243
  }
265
244
  const packageConfig = loaded.config.packages?.[packagePlan.path] ?? {};
245
+ const packageContext = (0, package_context_js_1.resolvePackageStrategyContext)(loaded.config, packagePlan.path, packageConfig);
266
246
  const { changelogFile: packageChangelogFile } = (0, plan_js_1.getChangelogDefaults)({
267
247
  "release-type": packageConfig["release-type"] ?? loaded.config["release-type"],
268
248
  "changelog-file": packageConfig["changelog-file"] ?? loaded.config["changelog-file"],
269
249
  "changelog-format": packageConfig["changelog-format"] ?? loaded.config["changelog-format"],
250
+ defaultChangelogFormat: packageContext.strategy.getDefaultChangelogFormat?.(),
270
251
  });
271
252
  const packageMetadata = packageReleaseMetadata[packagePlan.path];
272
253
  if (!packageMetadata) {
@@ -285,6 +266,15 @@ function prepareReleasePr(cwd = process.cwd(), options = {}) {
285
266
  const releaseTargets = buildReleaseTargets(cwd, plan, loaded.config);
286
267
  const branch = plan.releaseBranchPrefix;
287
268
  const title = formatReleaseCommitTitle(releaseTargets);
269
+ const releaseBaselineSha = (0, node_child_process_1.execFileSync)("git", ["rev-parse", "HEAD"], {
270
+ cwd,
271
+ encoding: "utf8",
272
+ stdio: ["ignore", "pipe", "ignore"],
273
+ }).trim();
274
+ const hasRemoteReleaseBranch = remoteReleaseBranchExists(cwd, branch);
275
+ const remoteReleaseRef = hasRemoteReleaseBranch
276
+ ? fetchRemoteReleaseBranch(cwd, branch)
277
+ : null;
288
278
  (0, node_child_process_1.execFileSync)("git", ["checkout", "-B", branch], {
289
279
  cwd,
290
280
  stdio: ["ignore", "pipe", "ignore"],
@@ -293,7 +283,6 @@ function prepareReleasePr(cwd = process.cwd(), options = {}) {
293
283
  ...new Set([
294
284
  ...updatedVersionFiles,
295
285
  ...updatedArtifactFiles,
296
- ...updatedRustLockFiles,
297
286
  ...updatedChangelogFiles,
298
287
  ]),
299
288
  ];
@@ -305,7 +294,7 @@ function prepareReleasePr(cwd = process.cwd(), options = {}) {
305
294
  cwd,
306
295
  stdio: ["ignore", "pipe", "ignore"],
307
296
  });
308
- (0, state_js_1.writeBaselineSha)(cwd, undefined, releaseTargets);
297
+ (0, state_js_1.writeBaselineSha)(cwd, releaseBaselineSha, releaseTargets);
309
298
  (0, node_child_process_1.execFileSync)("git", ["add", (0, state_js_1.getBaselineStatePath)(cwd)], {
310
299
  cwd,
311
300
  stdio: ["ignore", "pipe", "ignore"],
@@ -314,6 +303,8 @@ function prepareReleasePr(cwd = process.cwd(), options = {}) {
314
303
  cwd,
315
304
  stdio: ["ignore", "pipe", "ignore"],
316
305
  });
306
+ const updated = !remoteReleaseRef ||
307
+ getCommitTreeSha(cwd, "HEAD") !== getCommitTreeSha(cwd, remoteReleaseRef);
317
308
  return {
318
309
  branch,
319
310
  title,
@@ -321,6 +312,7 @@ function prepareReleasePr(cwd = process.cwd(), options = {}) {
321
312
  previousVersion: plan.currentVersion,
322
313
  commits: plan.commits,
323
314
  plan,
315
+ updated,
324
316
  };
325
317
  }
326
318
  function renderSimpleReviewRequestBody(version, previousVersion, commits, plan = null, cwd = process.cwd()) {
@@ -99,19 +99,29 @@ function resolveTargetChangelogFile(config, rootChangelogFile, targetPath) {
99
99
  return rootChangelogFile;
100
100
  }
101
101
  const packageConfig = config.packages?.[targetPath];
102
+ const packageStrategy = (0, resolve_js_1.resolveVersionStrategy)({
103
+ ...config,
104
+ "release-type": packageConfig?.["release-type"] ?? config["release-type"],
105
+ });
102
106
  const { changelogFile: packageChangelogFile } = (0, plan_js_1.getChangelogDefaults)({
103
107
  "release-type": packageConfig?.["release-type"] ?? config["release-type"],
104
108
  "changelog-file": packageConfig?.["changelog-file"] ?? config["changelog-file"],
105
109
  "changelog-format": packageConfig?.["changelog-format"] ?? config["changelog-format"],
110
+ defaultChangelogFormat: packageStrategy.getDefaultChangelogFormat?.(),
106
111
  });
107
112
  return node_path_1.default.posix.join(targetPath, packageChangelogFile);
108
113
  }
109
114
  function resolveTargetChangelogFormat(config, targetPath) {
110
- const packageConfig = targetPath === "." ? undefined : config.packages?.[targetPath];
115
+ const packageConfig = config.packages?.[targetPath];
116
+ const packageStrategy = (0, resolve_js_1.resolveVersionStrategy)({
117
+ ...config,
118
+ "release-type": packageConfig?.["release-type"] ?? config["release-type"],
119
+ });
111
120
  const { changelogFormat } = (0, plan_js_1.getChangelogDefaults)({
112
121
  "release-type": packageConfig?.["release-type"] ?? config["release-type"],
113
122
  "changelog-file": packageConfig?.["changelog-file"] ?? config["changelog-file"],
114
123
  "changelog-format": packageConfig?.["changelog-format"] ?? config["changelog-format"],
124
+ defaultChangelogFormat: packageStrategy.getDefaultChangelogFormat?.(),
115
125
  });
116
126
  return changelogFormat;
117
127
  }
@@ -132,7 +142,10 @@ async function runReleaseDetailed(cwd = process.cwd(), options = {}) {
132
142
  }
133
143
  const loaded = (0, load_config_js_1.loadConfig)(cwd);
134
144
  const strategy = (0, resolve_js_1.resolveVersionStrategy)(loaded.config);
135
- const { changelogFile } = (0, plan_js_1.getChangelogDefaults)(loaded.config);
145
+ const { changelogFile } = (0, plan_js_1.getChangelogDefaults)({
146
+ ...loaded.config,
147
+ defaultChangelogFormat: strategy.getDefaultChangelogFormat?.(),
148
+ });
136
149
  const referenceCommentMode = loaded.config["release-reference-comments"] ?? "off";
137
150
  const version = strategy.readVersion(cwd, loaded.config);
138
151
  const defaultTag = `v${version}`;
@@ -30,6 +30,18 @@ function verifyProject(cwd = process.cwd()) {
30
30
  ? undefined
31
31
  : `Create ${versionFile} or set "version-file" to the correct path for your release strategy.`,
32
32
  });
33
+ if (exists) {
34
+ const validationError = strategy.validateProject?.(cwd, config.config);
35
+ checks.push({
36
+ name: `strategy-validate:${strategy.name}`,
37
+ ok: !validationError,
38
+ details: validationError ?? "Strategy-level validation passed",
39
+ category: "version-files",
40
+ remediation: validationError
41
+ ? `Fix strategy-specific version metadata in ${versionFile} for release-type "${strategy.name}".`
42
+ : undefined,
43
+ });
44
+ }
33
45
  if (config.config.packages) {
34
46
  for (const [pkgPathRaw, packageConfig] of Object.entries(config.config.packages)) {
35
47
  const pkgPath = node_path_1.default.join(cwd, pkgPathRaw);
@@ -58,6 +70,18 @@ function verifyProject(cwd = process.cwd()) {
58
70
  ? undefined
59
71
  : `Create ${packageVersionFile} or adjust package release settings ("release-type"/"version-file") for ${pkgPathRaw}.`,
60
72
  });
73
+ if (packageVersionExists) {
74
+ const packageValidationError = packageContext.strategy.validateProject?.(cwd, packageContext.config);
75
+ checks.push({
76
+ name: `strategy-validate:${packageContext.strategy.name}:${pkgPathRaw}`,
77
+ ok: !packageValidationError,
78
+ details: packageValidationError ?? "Strategy-level validation passed",
79
+ category: "version-files",
80
+ remediation: packageValidationError
81
+ ? `Fix strategy-specific version metadata in ${packageVersionFile} for package "${pkgPathRaw}".`
82
+ : undefined,
83
+ });
84
+ }
61
85
  }
62
86
  }
63
87
  }
@@ -21,9 +21,26 @@ function writeDescriptionVersion(content, versionFile, version) {
21
21
  }
22
22
  exports.rVersionStrategy = {
23
23
  name: "r",
24
+ getDefaultChangelogFormat() {
25
+ return "r-news";
26
+ },
24
27
  getVersionFile(config) {
25
28
  return config["version-file"] ?? "DESCRIPTION";
26
29
  },
30
+ validateProject(cwd, config) {
31
+ const versionFile = this.getVersionFile(config);
32
+ const versionPath = node_path_1.default.join(cwd, versionFile);
33
+ if (!node_fs_1.default.existsSync(versionPath)) {
34
+ return null;
35
+ }
36
+ try {
37
+ readDescriptionVersion(node_fs_1.default.readFileSync(versionPath, "utf8"), versionFile);
38
+ return null;
39
+ }
40
+ catch (error) {
41
+ return error instanceof Error ? error.message : String(error);
42
+ }
43
+ },
27
44
  readVersion(cwd, config) {
28
45
  const versionFile = this.getVersionFile(config);
29
46
  const versionPath = node_path_1.default.join(cwd, versionFile);
@@ -7,6 +7,7 @@ exports.rustVersionStrategy = void 0;
7
7
  exports.applyRustWorkspaceDependencyUpdates = applyRustWorkspaceDependencyUpdates;
8
8
  exports.detectRustDependencyImpact = detectRustDependencyImpact;
9
9
  exports.toCargoManifestPath = toCargoManifestPath;
10
+ const node_child_process_1 = require("node:child_process");
10
11
  const node_fs_1 = __importDefault(require("node:fs"));
11
12
  const node_path_1 = __importDefault(require("node:path"));
12
13
  const toml_1 = __importDefault(require("@iarna/toml"));
@@ -96,6 +97,58 @@ function collectAllCrateManifests(cwd) {
96
97
  }
97
98
  return manifests.sort((a, b) => a.localeCompare(b));
98
99
  }
100
+ function listCargoLockFiles(cwd) {
101
+ const lockfiles = [];
102
+ const queue = [cwd];
103
+ while (queue.length > 0) {
104
+ const currentDir = queue.shift();
105
+ if (!currentDir) {
106
+ continue;
107
+ }
108
+ const entries = node_fs_1.default.readdirSync(currentDir, { withFileTypes: true });
109
+ for (const entry of entries) {
110
+ if (entry.name === ".git") {
111
+ continue;
112
+ }
113
+ const fullPath = node_path_1.default.join(currentDir, entry.name);
114
+ if (entry.isDirectory()) {
115
+ queue.push(fullPath);
116
+ continue;
117
+ }
118
+ if (!entry.isFile() || entry.name !== "Cargo.lock") {
119
+ continue;
120
+ }
121
+ lockfiles.push(normalizeSlashPath(node_path_1.default.relative(cwd, fullPath)));
122
+ }
123
+ }
124
+ return lockfiles.sort((a, b) => a.localeCompare(b));
125
+ }
126
+ function ensureCargoLockUpToDate(cwd) {
127
+ const lockfiles = listCargoLockFiles(cwd);
128
+ if (lockfiles.length === 0) {
129
+ return [];
130
+ }
131
+ const updatedLockfiles = [];
132
+ for (const lockfile of lockfiles) {
133
+ const lockfilePath = node_path_1.default.join(cwd, lockfile);
134
+ const before = node_fs_1.default.readFileSync(lockfilePath, "utf8");
135
+ try {
136
+ (0, node_child_process_1.execFileSync)("cargo", ["generate-lockfile"], {
137
+ cwd: node_path_1.default.dirname(lockfilePath),
138
+ stdio: ["ignore", "pipe", "pipe"],
139
+ });
140
+ }
141
+ catch (error) {
142
+ const message = error instanceof Error ? error.message : String(error);
143
+ throw new Error(`Failed to refresh ${lockfile} via "cargo generate-lockfile". Ensure cargo is installed and available in PATH. Details: ${message}`);
144
+ }
145
+ const after = node_fs_1.default.readFileSync(lockfilePath, "utf8");
146
+ if (after !== before) {
147
+ updatedLockfiles.push(lockfile);
148
+ }
149
+ }
150
+ return updatedLockfiles;
151
+ }
99
152
  function parseCargoManifest(versionFile, cargoTomlRaw) {
100
153
  let parsed;
101
154
  try {
@@ -556,6 +609,26 @@ exports.rustVersionStrategy = {
556
609
  const cargoTomlRaw = node_fs_1.default.readFileSync(node_path_1.default.join(cwd, selectedManifest), "utf8");
557
610
  return readResolvedCargoVersion(cwd, selectedManifest, cargoTomlRaw);
558
611
  },
612
+ validateProject(cwd, config) {
613
+ try {
614
+ const versionFile = this.getVersionFile(config);
615
+ const manifests = collectRustTargetManifests(cwd, versionFile, !config.packages);
616
+ const selectedManifest = manifests[0];
617
+ if (!selectedManifest) {
618
+ return null;
619
+ }
620
+ const versionPath = node_path_1.default.join(cwd, selectedManifest);
621
+ if (!node_fs_1.default.existsSync(versionPath)) {
622
+ return null;
623
+ }
624
+ const cargoTomlRaw = node_fs_1.default.readFileSync(versionPath, "utf8");
625
+ readResolvedCargoVersion(cwd, selectedManifest, cargoTomlRaw);
626
+ return null;
627
+ }
628
+ catch (error) {
629
+ return error instanceof Error ? error.message : String(error);
630
+ }
631
+ },
559
632
  writeVersion(cwd, config, version) {
560
633
  const versionFile = this.getVersionFile(config);
561
634
  const manifests = collectRustTargetManifests(cwd, versionFile, !config.packages);
@@ -653,6 +726,9 @@ exports.rustVersionStrategy = {
653
726
  for (const write of writes) {
654
727
  manifestToVersion[write.versionFile] = write.version;
655
728
  }
656
- return applyRustWorkspaceDependencyUpdates(cwd, manifestToVersion);
729
+ return [
730
+ ...applyRustWorkspaceDependencyUpdates(cwd, manifestToVersion),
731
+ ...ensureCargoLockUpToDate(cwd),
732
+ ];
657
733
  },
658
734
  };
@@ -1,4 +1,4 @@
1
- import type { VersionaryConfig } from "../types/config.js";
1
+ import type { VersionaryChangelogFormat, VersionaryConfig } from "../types/config.js";
2
2
  export interface StrategyPackagePlanContext {
3
3
  packagePath: string;
4
4
  versionFile: string;
@@ -13,8 +13,10 @@ export interface StrategyVersionWriteContext {
13
13
  export interface VersionStrategy {
14
14
  name: string;
15
15
  getVersionFile(config: VersionaryConfig): string;
16
+ getDefaultChangelogFormat?(): VersionaryChangelogFormat;
16
17
  readVersion(cwd: string, config: VersionaryConfig): string;
17
18
  writeVersion(cwd: string, config: VersionaryConfig, version: string): string[];
19
+ validateProject?(cwd: string, config: VersionaryConfig): string | null;
18
20
  readPackageName?(cwd: string, config: VersionaryConfig): string | null;
19
21
  propagateDependentPatchImpacts?(cwd: string, packages: StrategyPackagePlanContext[]): string[];
20
22
  finalizeVersionWrites?(cwd: string, writes: StrategyVersionWriteContext[]): string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "versionary",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Automatic release framework based on conventional commits and semantic versioning",
5
5
  "keywords": [
6
6
  "releasing",