versionary 0.8.2 → 0.10.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 +9 -3
- package/dist/app/release/artifact-rules.js +28 -4
- package/dist/app/release/pr.js +100 -20
- package/dist/app/release/release.d.ts +8 -0
- package/dist/app/release/release.js +25 -10
- package/dist/app/release/state.js +14 -2
- package/dist/cli/index.js +99 -7
- package/dist/config/load-config.js +3 -0
- package/dist/config/schema.d.ts +2 -1
- package/dist/config/schema.js +2 -2
- package/dist/domain/release/changelog.d.ts +7 -0
- package/dist/domain/release/changelog.js +19 -0
- package/dist/domain/strategy/r.js +1 -1
- package/dist/domain/strategy/rust.js +138 -12
- package/dist/infra/scm/runtime.d.ts +1 -1
- package/dist/infra/scm/runtime.js +2 -19
- package/dist/types/config.d.ts +2 -2
- package/dist/verify/verify-project.d.ts +2 -0
- package/dist/verify/verify-project.js +13 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Versionary is being built to:
|
|
|
22
22
|
|
|
23
23
|
- support both direct releases and release-PR-gated releases
|
|
24
24
|
- work across repository types (Node, Rust, docs/LaTeX, etc.)
|
|
25
|
-
- stay SCM-agnostic at the core
|
|
25
|
+
- stay SCM-agnostic at the core with built-in integration adapters
|
|
26
26
|
(GitHub first; GitLab/Codeberg later)
|
|
27
27
|
- keep a small, stable core with explicit extension points
|
|
28
28
|
- handle trunk-based development and monorepo workflows cleanly
|
|
@@ -40,6 +40,7 @@ Out of scope (intentional):
|
|
|
40
40
|
|
|
41
41
|
- publishing artifacts to language registries
|
|
42
42
|
- replacing package-specific publish tooling
|
|
43
|
+
- external/user-provided plugin loading
|
|
43
44
|
|
|
44
45
|
Use your CI/CD platform for registry publishing, triggered from a created
|
|
45
46
|
release/tag.
|
|
@@ -95,12 +96,14 @@ For a quick trial, use:
|
|
|
95
96
|
- pre-1.0 policy defaults to conservative major handling: for `0.y.z`, breaking
|
|
96
97
|
changes bump to `0.(y+1).0`; set `allow-stable-major: true` to allow explicit
|
|
97
98
|
auto-transition to `1.0.0` on a breaking release
|
|
98
|
-
- review mode (`review-mode`): `
|
|
99
|
+
- review mode (`review-mode`): `pr` (PR/MR style) or `direct` (no review
|
|
99
100
|
request)
|
|
100
101
|
- optional monorepo planning with `monorepo-mode` and `packages`:
|
|
101
102
|
- `independent` computes package bumps per path
|
|
102
103
|
- `fixed` computes one shared bump across configured package paths
|
|
103
104
|
- per-package `package-name` can override release identity (labels + tag base)
|
|
105
|
+
- per-package `changelog-file` writes package release notes to
|
|
106
|
+
`<package-path>/<changelog-file>`
|
|
104
107
|
|
|
105
108
|
Rust strategy examples:
|
|
106
109
|
|
|
@@ -123,6 +126,8 @@ Rust strategy examples:
|
|
|
123
126
|
Current rust auto-update behavior (phase scope):
|
|
124
127
|
|
|
125
128
|
- updates crate versions in each targeted crate `[package].version`
|
|
129
|
+
- supports targeted crates using `version.workspace = true` by updating
|
|
130
|
+
`[workspace.package].version` in the owning workspace manifest
|
|
126
131
|
- updates internal workspace dependency versions when the dependency name
|
|
127
132
|
matches another targeted crate name
|
|
128
133
|
- refreshes `Cargo.lock` via `cargo generate-lockfile` when `Cargo.lock` exists
|
|
@@ -235,7 +240,8 @@ Minimum GitHub token/repo permissions for Versionary-managed metadata:
|
|
|
235
240
|
|
|
236
241
|
`review-mode` behavior:
|
|
237
242
|
|
|
238
|
-
- `
|
|
243
|
+
- `pr` (preferred; `review` is a backward-compatible alias): `pnpm run run`
|
|
244
|
+
prepares/updates the release branch and creates or
|
|
239
245
|
updates a release PR
|
|
240
246
|
- `direct`: `pnpm run run` prepares/updates the release branch and skips review
|
|
241
247
|
request creation
|
|
@@ -33,13 +33,17 @@ function parseFieldPath(fieldPath) {
|
|
|
33
33
|
const numberMatch = rest.match(/^(\d+)\]/u);
|
|
34
34
|
if (numberMatch) {
|
|
35
35
|
tokens.push(Number(numberMatch[1]));
|
|
36
|
-
index += 2 + numberMatch[1]
|
|
36
|
+
index += 2 + numberMatch[1]?.length;
|
|
37
37
|
continue;
|
|
38
38
|
}
|
|
39
39
|
const keyMatch = rest.match(/^"([^"]+)"\]/u);
|
|
40
40
|
if (keyMatch) {
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const key = keyMatch[1];
|
|
42
|
+
if (!key) {
|
|
43
|
+
throw new Error(`Invalid field-path "${fieldPath}" near index ${index}.`);
|
|
44
|
+
}
|
|
45
|
+
tokens.push(key);
|
|
46
|
+
index += 4 + keyMatch[1]?.length;
|
|
43
47
|
continue;
|
|
44
48
|
}
|
|
45
49
|
throw new Error(`Invalid field-path "${fieldPath}" near index ${index}.`);
|
|
@@ -56,6 +60,9 @@ function setVersionAtJsonPath(document, fieldPath, version) {
|
|
|
56
60
|
let cursor = document;
|
|
57
61
|
for (let index = 0; index < tokens.length - 1; index += 1) {
|
|
58
62
|
const token = tokens[index];
|
|
63
|
+
if (token === undefined) {
|
|
64
|
+
throw new Error(`field-path "${fieldPath}" does not resolve to an existing field.`);
|
|
65
|
+
}
|
|
59
66
|
if (typeof token === "number") {
|
|
60
67
|
if (!Array.isArray(cursor) || token >= cursor.length) {
|
|
61
68
|
throw new Error(`field-path "${fieldPath}" does not resolve to an existing field.`);
|
|
@@ -69,6 +76,9 @@ function setVersionAtJsonPath(document, fieldPath, version) {
|
|
|
69
76
|
cursor = cursor[token];
|
|
70
77
|
}
|
|
71
78
|
const leaf = tokens.at(-1);
|
|
79
|
+
if (leaf === undefined) {
|
|
80
|
+
throw new Error(`field-path "${fieldPath}" does not resolve to an existing field.`);
|
|
81
|
+
}
|
|
72
82
|
if (typeof leaf === "number") {
|
|
73
83
|
if (!Array.isArray(cursor) || leaf >= cursor.length) {
|
|
74
84
|
throw new Error(`field-path "${fieldPath}" does not resolve to an existing field.`);
|
|
@@ -99,7 +109,12 @@ function resolveFieldPath(rule) {
|
|
|
99
109
|
function parseRegexPattern(pattern) {
|
|
100
110
|
const slashPattern = pattern.match(/^\/((?:\\\/|[^/])+)\/([a-z]*)$/u);
|
|
101
111
|
if (slashPattern) {
|
|
102
|
-
|
|
112
|
+
const source = slashPattern[1];
|
|
113
|
+
const flags = slashPattern[2];
|
|
114
|
+
if (!source || flags === undefined) {
|
|
115
|
+
throw new Error(`Invalid regex pattern "${pattern}".`);
|
|
116
|
+
}
|
|
117
|
+
return new RegExp(source, flags);
|
|
103
118
|
}
|
|
104
119
|
return new RegExp(pattern, "m");
|
|
105
120
|
}
|
|
@@ -114,6 +129,9 @@ function applyRegexRule(content, pattern, version) {
|
|
|
114
129
|
throw new Error(`Regex pattern must match exactly one occurrence; matched ${matches.length}.`);
|
|
115
130
|
}
|
|
116
131
|
const match = matches[0];
|
|
132
|
+
if (!match) {
|
|
133
|
+
throw new Error("Regex match result missing.");
|
|
134
|
+
}
|
|
117
135
|
const start = match.index;
|
|
118
136
|
if (start === undefined) {
|
|
119
137
|
throw new Error("Regex match did not include an index.");
|
|
@@ -131,6 +149,9 @@ function applyTomlRulePreservingFormatting(content, fieldPath, version) {
|
|
|
131
149
|
return `${toml_1.default.stringify(parsed)}\n`;
|
|
132
150
|
}
|
|
133
151
|
const key = simplePath[1];
|
|
152
|
+
if (!key) {
|
|
153
|
+
throw new Error(`field-path "${fieldPath}" does not resolve to an existing field.`);
|
|
154
|
+
}
|
|
134
155
|
const linePattern = new RegExp(`^(\\s*${key}\\s*=\\s*)(["'])([^"']*)(\\2)(\\s*(?:#.*)?)$`, "mu");
|
|
135
156
|
const match = content.match(linePattern);
|
|
136
157
|
if (!match) {
|
|
@@ -141,6 +162,9 @@ function applyTomlRulePreservingFormatting(content, fieldPath, version) {
|
|
|
141
162
|
}
|
|
142
163
|
function applyArtifactRuleToContent(content, rule, version) {
|
|
143
164
|
if (rule.type === "regex") {
|
|
165
|
+
if (!rule.pattern) {
|
|
166
|
+
throw new Error('regex artifact rules require "pattern".');
|
|
167
|
+
}
|
|
144
168
|
return applyRegexRule(content, rule.pattern, version);
|
|
145
169
|
}
|
|
146
170
|
if (rule.type === "json") {
|
package/dist/app/release/pr.js
CHANGED
|
@@ -71,24 +71,60 @@ function ensureCleanWorktree(cwd, logger) {
|
|
|
71
71
|
logger?.warn(`Ignoring safe tracked changes before versionary pr:\n${ignored.join("\n")}`);
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
+
function normalizeSlashPath(input) {
|
|
75
|
+
return input.replaceAll("\\", "/");
|
|
76
|
+
}
|
|
77
|
+
function listCargoLockFiles(cwd) {
|
|
78
|
+
const lockfiles = [];
|
|
79
|
+
const queue = [cwd];
|
|
80
|
+
while (queue.length > 0) {
|
|
81
|
+
const currentDir = queue.shift();
|
|
82
|
+
if (!currentDir) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const entries = node_fs_1.default.readdirSync(currentDir, { withFileTypes: true });
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
if (entry.name === ".git") {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const fullPath = node_path_1.default.join(currentDir, entry.name);
|
|
91
|
+
if (entry.isDirectory()) {
|
|
92
|
+
queue.push(fullPath);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (!entry.isFile() || entry.name !== "Cargo.lock") {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
lockfiles.push(normalizeSlashPath(node_path_1.default.relative(cwd, fullPath)));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return lockfiles.sort((a, b) => a.localeCompare(b));
|
|
102
|
+
}
|
|
74
103
|
function ensureCargoLockUpToDate(cwd) {
|
|
75
|
-
const
|
|
76
|
-
if (
|
|
104
|
+
const lockfiles = listCargoLockFiles(cwd);
|
|
105
|
+
if (lockfiles.length === 0) {
|
|
77
106
|
return [];
|
|
78
107
|
}
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
108
|
+
const updatedLockfiles = [];
|
|
109
|
+
for (const lockfile of lockfiles) {
|
|
110
|
+
const lockfilePath = node_path_1.default.join(cwd, lockfile);
|
|
111
|
+
const before = node_fs_1.default.readFileSync(lockfilePath, "utf8");
|
|
112
|
+
try {
|
|
113
|
+
(0, node_child_process_1.execFileSync)("cargo", ["generate-lockfile"], {
|
|
114
|
+
cwd: node_path_1.default.dirname(lockfilePath),
|
|
115
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
120
|
+
throw new Error(`Failed to refresh ${lockfile} via "cargo generate-lockfile". Ensure cargo is installed and available in PATH. Details: ${message}`);
|
|
121
|
+
}
|
|
122
|
+
const after = node_fs_1.default.readFileSync(lockfilePath, "utf8");
|
|
123
|
+
if (after !== before) {
|
|
124
|
+
updatedLockfiles.push(lockfile);
|
|
125
|
+
}
|
|
89
126
|
}
|
|
90
|
-
|
|
91
|
-
return after !== before ? ["Cargo.lock"] : [];
|
|
127
|
+
return updatedLockfiles;
|
|
92
128
|
}
|
|
93
129
|
function normalizeReleaseNameForTag(releaseName) {
|
|
94
130
|
return releaseName
|
|
@@ -178,12 +214,31 @@ function buildReleaseTargets(cwd, plan, loadedConfig) {
|
|
|
178
214
|
}
|
|
179
215
|
return releaseTargets;
|
|
180
216
|
}
|
|
217
|
+
function buildPackageReleaseMetadata(cwd, plan, loadedConfig) {
|
|
218
|
+
const metadataByPath = {};
|
|
219
|
+
for (const pkg of plan.packages ?? []) {
|
|
220
|
+
if (!pkg.nextVersion || pkg.path === ".") {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const packageConfig = loadedConfig.packages?.[pkg.path] ?? {};
|
|
224
|
+
const packageContext = (0, package_context_js_1.resolvePackageStrategyContext)(loadedConfig, pkg.path, packageConfig);
|
|
225
|
+
const releaseName = resolveReleaseName(cwd, pkg.path, packageConfig, packageContext.strategy.name, packageContext.versionFile);
|
|
226
|
+
metadataByPath[pkg.path] = {
|
|
227
|
+
releaseName,
|
|
228
|
+
tagPrefix: normalizeReleaseNameForTag(releaseName),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
return metadataByPath;
|
|
232
|
+
}
|
|
181
233
|
function formatReleaseCommitTitle(releaseTargets) {
|
|
182
234
|
if (releaseTargets.length === 0) {
|
|
183
235
|
return "chore(release): v0.0.0";
|
|
184
236
|
}
|
|
185
237
|
const tags = releaseTargets.map((target) => target.tag);
|
|
186
|
-
|
|
238
|
+
if (tags.length === 1) {
|
|
239
|
+
return `chore(release): ${tags[0]}`;
|
|
240
|
+
}
|
|
241
|
+
return `chore(release): ${tags[0]} (+${tags.length - 1} more)`;
|
|
187
242
|
}
|
|
188
243
|
function prepareSimpleReleasePr(cwd = process.cwd(), options = {}) {
|
|
189
244
|
const plan = (0, plan_js_1.createSimplePlan)(cwd);
|
|
@@ -216,8 +271,33 @@ function prepareSimpleReleasePr(cwd = process.cwd(), options = {}) {
|
|
|
216
271
|
}
|
|
217
272
|
const updatedArtifactFiles = (0, artifact_rules_js_1.applyConfiguredArtifactRules)(cwd, loaded.config, plan);
|
|
218
273
|
const updatedRustLockFiles = ensureCargoLockUpToDate(cwd);
|
|
274
|
+
const packageReleaseMetadata = buildPackageReleaseMetadata(cwd, plan, loaded.config);
|
|
219
275
|
const section = (0, changelog_js_1.renderSimpleChangelog)(plan);
|
|
220
276
|
(0, changelog_js_1.prependChangelog)(cwd, plan.changelogFile, section);
|
|
277
|
+
const updatedChangelogFiles = [plan.changelogFile];
|
|
278
|
+
for (const packagePlan of plan.packages ?? []) {
|
|
279
|
+
if (!packagePlan.nextVersion || packagePlan.path === ".") {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const packageConfig = loaded.config.packages?.[packagePlan.path] ?? {};
|
|
283
|
+
const packageChangelogFile = packageConfig["changelog-file"];
|
|
284
|
+
if (!packageChangelogFile) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const packageMetadata = packageReleaseMetadata[packagePlan.path];
|
|
288
|
+
if (!packageMetadata) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const packageSection = (0, changelog_js_1.renderPackageChangelogSection)({
|
|
292
|
+
currentVersion: packagePlan.currentVersion,
|
|
293
|
+
nextVersion: packagePlan.nextVersion,
|
|
294
|
+
commits: packagePlan.commits,
|
|
295
|
+
tagPrefix: packageMetadata.tagPrefix,
|
|
296
|
+
cwd,
|
|
297
|
+
});
|
|
298
|
+
(0, changelog_js_1.prependChangelog)(cwd, node_path_1.default.posix.join(packagePlan.path, packageChangelogFile), packageSection);
|
|
299
|
+
updatedChangelogFiles.push(node_path_1.default.posix.join(packagePlan.path, packageChangelogFile));
|
|
300
|
+
}
|
|
221
301
|
const releaseTargets = buildReleaseTargets(cwd, plan, loaded.config);
|
|
222
302
|
const branch = plan.releaseBranchPrefix;
|
|
223
303
|
const title = formatReleaseCommitTitle(releaseTargets);
|
|
@@ -230,7 +310,7 @@ function prepareSimpleReleasePr(cwd = process.cwd(), options = {}) {
|
|
|
230
310
|
...updatedVersionFiles,
|
|
231
311
|
...updatedArtifactFiles,
|
|
232
312
|
...updatedRustLockFiles,
|
|
233
|
-
|
|
313
|
+
...updatedChangelogFiles,
|
|
234
314
|
]),
|
|
235
315
|
];
|
|
236
316
|
(0, node_child_process_1.execFileSync)("git", ["add", ...filesToAdd], {
|
|
@@ -297,14 +377,14 @@ function renderSimpleReviewRequestBody(version, previousVersion, commits, plan =
|
|
|
297
377
|
}
|
|
298
378
|
async function openOrUpdateSimpleReviewRequest(cwd, branch, title, version, previousVersion, commits, plan = null, options = {}) {
|
|
299
379
|
const loaded = (0, load_config_js_1.loadConfig)(cwd);
|
|
300
|
-
const releaseFlow = loaded.config["review-mode"] ?? "
|
|
301
|
-
if (releaseFlow
|
|
380
|
+
const releaseFlow = loaded.config["review-mode"] ?? "pr";
|
|
381
|
+
if (releaseFlow === "direct") {
|
|
302
382
|
return "Release flow mode is direct; skipping review request creation.";
|
|
303
383
|
}
|
|
304
384
|
const plugins = (0, runtime_js_1.loadRuntimePlugins)();
|
|
305
385
|
const scmPlugins = (0, capabilities_js_1.findPluginsByCapability)(plugins, "scm.reviewRequest");
|
|
306
386
|
if (scmPlugins.length === 0) {
|
|
307
|
-
throw new Error("review-mode is
|
|
387
|
+
throw new Error("review-mode is pr but no scm.reviewRequest plugin is available.");
|
|
308
388
|
}
|
|
309
389
|
const plugin = scmPlugins[0];
|
|
310
390
|
if (!plugin?.createOrUpdateReviewRequest) {
|
|
@@ -333,5 +413,5 @@ function isReleaseCommitMessage(commitMessage) {
|
|
|
333
413
|
return true;
|
|
334
414
|
}
|
|
335
415
|
const subject = commitMessage.split("\n")[0]?.trim() ?? "";
|
|
336
|
-
return /^chore\(release\):\s+(?:v\d+\.\d+\.\d+|\S+-v\d+\.\d+\.\d+)(?:,\s+(?:v\d+\.\d+\.\d+|\S+-v\d+\.\d+\.\d+))
|
|
416
|
+
return /^chore\(release\):\s+(?:(?:v\d+\.\d+\.\d+|\S+-v\d+\.\d+\.\d+)(?:,\s+(?:v\d+\.\d+\.\d+|\S+-v\d+\.\d+\.\d+))*|(?:v\d+\.\d+\.\d+|\S+-v\d+\.\d+\.\d+)\s+\(\+\d+\s+more\))(?:\s+\(#\d+\))?$/u.test(subject);
|
|
337
417
|
}
|
|
@@ -3,6 +3,13 @@ export declare function runSimpleRelease(cwd?: string): Promise<string>;
|
|
|
3
3
|
export type SimpleRunReleaseResult = {
|
|
4
4
|
action: "release-skipped";
|
|
5
5
|
reason: string;
|
|
6
|
+
} | {
|
|
7
|
+
action: "release-dry-run";
|
|
8
|
+
message: string;
|
|
9
|
+
targets: {
|
|
10
|
+
tag: string;
|
|
11
|
+
version: string;
|
|
12
|
+
}[];
|
|
6
13
|
} | {
|
|
7
14
|
action: "release-published";
|
|
8
15
|
message: string;
|
|
@@ -15,5 +22,6 @@ export type SimpleRunReleaseResult = {
|
|
|
15
22
|
};
|
|
16
23
|
export interface RunSimpleReleaseOptions {
|
|
17
24
|
logger?: VersionaryPluginContext["logger"];
|
|
25
|
+
"dry-run"?: boolean;
|
|
18
26
|
}
|
|
19
27
|
export declare function runSimpleReleaseDetailed(cwd?: string, options?: RunSimpleReleaseOptions): Promise<SimpleRunReleaseResult>;
|
|
@@ -66,15 +66,6 @@ async function runSimpleReleaseDetailed(cwd = process.cwd(), options = {}) {
|
|
|
66
66
|
const changelogFile = loaded.config["changelog-file"] ?? "CHANGELOG.md";
|
|
67
67
|
const version = strategy.readVersion(cwd, loaded.config);
|
|
68
68
|
const defaultTag = `v${version}`;
|
|
69
|
-
const plugins = (0, runtime_js_1.loadRuntimePlugins)();
|
|
70
|
-
const scmPlugins = (0, capabilities_js_1.findPluginsByCapability)(plugins, "scm.releaseMetadata");
|
|
71
|
-
if (scmPlugins.length === 0) {
|
|
72
|
-
throw new Error("No scm.releaseMetadata plugin is available.");
|
|
73
|
-
}
|
|
74
|
-
const plugin = scmPlugins[0];
|
|
75
|
-
if (!plugin?.createReleaseMetadata) {
|
|
76
|
-
throw new Error(`Plugin "${plugin?.name ?? "unknown"}" does not implement createReleaseMetadata.`);
|
|
77
|
-
}
|
|
78
69
|
const releaseTargets = (0, state_js_1.readReleaseTargets)(cwd);
|
|
79
70
|
const targets = releaseTargets.length > 0
|
|
80
71
|
? releaseTargets
|
|
@@ -85,6 +76,27 @@ async function runSimpleReleaseDetailed(cwd = process.cwd(), options = {}) {
|
|
|
85
76
|
tag: defaultTag,
|
|
86
77
|
},
|
|
87
78
|
];
|
|
79
|
+
if (options["dry-run"]) {
|
|
80
|
+
const targetList = targets.map((target) => `${target.tag} (${target.version})`);
|
|
81
|
+
return {
|
|
82
|
+
action: "release-dry-run",
|
|
83
|
+
targets: targets.map((target) => ({
|
|
84
|
+
tag: target.tag,
|
|
85
|
+
version: target.version,
|
|
86
|
+
})),
|
|
87
|
+
message: `Dry run: would publish releases ${targetList.join(", ")}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const plugins = (0, runtime_js_1.loadRuntimePlugins)();
|
|
91
|
+
const scmPlugins = (0, capabilities_js_1.findPluginsByCapability)(plugins, "scm.releaseMetadata");
|
|
92
|
+
if (scmPlugins.length === 0) {
|
|
93
|
+
throw new Error("No scm.releaseMetadata plugin is available.");
|
|
94
|
+
}
|
|
95
|
+
const plugin = scmPlugins[0];
|
|
96
|
+
if (!plugin?.createReleaseMetadata) {
|
|
97
|
+
throw new Error(`Plugin "${plugin?.name ?? "unknown"}" does not implement createReleaseMetadata.`);
|
|
98
|
+
}
|
|
99
|
+
const createReleaseMetadata = plugin.createReleaseMetadata;
|
|
88
100
|
const releases = [];
|
|
89
101
|
for (const target of targets) {
|
|
90
102
|
const outcome = await (0, recovery_js_1.executeIdempotentReleaseTarget)(cwd, {
|
|
@@ -92,7 +104,10 @@ async function runSimpleReleaseDetailed(cwd = process.cwd(), options = {}) {
|
|
|
92
104
|
version: target.version,
|
|
93
105
|
notes: readReleaseNotes(cwd, target.version, changelogFile),
|
|
94
106
|
}, {
|
|
95
|
-
createReleaseMetadata: (input) =>
|
|
107
|
+
createReleaseMetadata: (input) => createReleaseMetadata(input, {
|
|
108
|
+
cwd,
|
|
109
|
+
logger: options.logger,
|
|
110
|
+
}),
|
|
96
111
|
logger: options.logger,
|
|
97
112
|
});
|
|
98
113
|
releases.push({
|
|
@@ -79,7 +79,7 @@ function readReleaseTargets(cwd = process.cwd()) {
|
|
|
79
79
|
const parsed = parseStateFile(node_fs_1.default.readFileSync(filePath, "utf8"), filePath);
|
|
80
80
|
return parsed[RELEASE_TARGETS_KEY] ?? [];
|
|
81
81
|
}
|
|
82
|
-
function writeBaselineSha(cwd = process.cwd(), sha, releaseTargets
|
|
82
|
+
function writeBaselineSha(cwd = process.cwd(), sha, releaseTargets) {
|
|
83
83
|
const baselineShaValue = sha ??
|
|
84
84
|
(0, node_child_process_1.execFileSync)("git", ["rev-parse", "HEAD"], {
|
|
85
85
|
cwd,
|
|
@@ -87,10 +87,22 @@ function writeBaselineSha(cwd = process.cwd(), sha, releaseTargets = []) {
|
|
|
87
87
|
stdio: ["ignore", "pipe", "ignore"],
|
|
88
88
|
}).trim();
|
|
89
89
|
const filePath = getBaselineStatePath(cwd);
|
|
90
|
+
const existing = node_fs_1.default.existsSync(filePath)
|
|
91
|
+
? parseStateFile(node_fs_1.default.readFileSync(filePath, "utf8"), filePath)
|
|
92
|
+
: {};
|
|
93
|
+
const existingTargets = existing[RELEASE_TARGETS_KEY] ?? [];
|
|
94
|
+
const nextTargets = releaseTargets === undefined
|
|
95
|
+
? existingTargets
|
|
96
|
+
: [
|
|
97
|
+
...new Map([...existingTargets, ...releaseTargets].map((target) => [
|
|
98
|
+
target.path,
|
|
99
|
+
target,
|
|
100
|
+
])).values(),
|
|
101
|
+
].sort((a, b) => a.path.localeCompare(b.path));
|
|
90
102
|
const next = {
|
|
91
103
|
[MANIFEST_VERSION_KEY]: 1,
|
|
92
104
|
[BASELINE_SHA_KEY]: baselineShaValue,
|
|
93
|
-
[RELEASE_TARGETS_KEY]:
|
|
105
|
+
[RELEASE_TARGETS_KEY]: nextTargets,
|
|
94
106
|
};
|
|
95
107
|
node_fs_1.default.writeFileSync(filePath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
96
108
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -14,15 +14,33 @@ const changelog_js_1 = require("../domain/release/changelog.js");
|
|
|
14
14
|
const plan_js_1 = require("../domain/release/plan.js");
|
|
15
15
|
function printVerifyResult() {
|
|
16
16
|
const result = (0, verify_js_1.verifyProject)();
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
const categories = [
|
|
18
|
+
{ key: "config", title: "Config" },
|
|
19
|
+
{ key: "paths", title: "Paths" },
|
|
20
|
+
{ key: "version-files", title: "Version files" },
|
|
21
|
+
];
|
|
22
|
+
for (const category of categories) {
|
|
23
|
+
const checks = result.checks.filter((check) => check.category === category.key);
|
|
24
|
+
if (checks.length === 0) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
console.log(`${category.title}:`);
|
|
28
|
+
for (const check of checks) {
|
|
29
|
+
const status = check.ok ? "OK" : "FAIL";
|
|
30
|
+
console.log(` [${status}] ${check.name} - ${check.details}`);
|
|
31
|
+
if (!check.ok && check.remediation) {
|
|
32
|
+
console.log(` Fix: ${check.remediation}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
console.log("");
|
|
20
36
|
}
|
|
37
|
+
console.log(result.ok ? "Summary: all checks passed." : "Summary: checks failed.");
|
|
21
38
|
return result.ok ? 0 : 1;
|
|
22
39
|
}
|
|
23
40
|
function parseFlags(args) {
|
|
24
41
|
return {
|
|
25
42
|
json: args.includes("--json"),
|
|
43
|
+
"dry-run": args.includes("--dry-run"),
|
|
26
44
|
};
|
|
27
45
|
}
|
|
28
46
|
function emitJson(payload) {
|
|
@@ -38,9 +56,20 @@ async function main() {
|
|
|
38
56
|
stdio: ["ignore", "pipe", "ignore"],
|
|
39
57
|
}).trim();
|
|
40
58
|
if ((0, pr_js_1.isReleaseCommitMessage)(commitMessage)) {
|
|
59
|
+
if (flags["dry-run"] && !flags.json) {
|
|
60
|
+
const release = await (0, release_js_1.runSimpleReleaseDetailed)(process.cwd(), {
|
|
61
|
+
logger,
|
|
62
|
+
"dry-run": true,
|
|
63
|
+
});
|
|
64
|
+
if (release.action === "release-dry-run") {
|
|
65
|
+
console.log(release.message);
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
41
69
|
if (flags.json) {
|
|
42
70
|
const release = await (0, release_js_1.runSimpleReleaseDetailed)(process.cwd(), {
|
|
43
71
|
logger,
|
|
72
|
+
"dry-run": flags["dry-run"],
|
|
44
73
|
});
|
|
45
74
|
if (release.action === "release-skipped") {
|
|
46
75
|
emitJson({
|
|
@@ -51,6 +80,16 @@ async function main() {
|
|
|
51
80
|
});
|
|
52
81
|
return 0;
|
|
53
82
|
}
|
|
83
|
+
if (release.action === "release-dry-run") {
|
|
84
|
+
emitJson({
|
|
85
|
+
action: "release-dry-run",
|
|
86
|
+
message: release.message,
|
|
87
|
+
releaseCreated: false,
|
|
88
|
+
tagNames: release.targets.map((target) => target.tag),
|
|
89
|
+
targets: release.targets,
|
|
90
|
+
});
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
54
93
|
emitJson({
|
|
55
94
|
action: "release-published",
|
|
56
95
|
message: release.message,
|
|
@@ -78,6 +117,34 @@ async function main() {
|
|
|
78
117
|
console.log(message);
|
|
79
118
|
return 0;
|
|
80
119
|
}
|
|
120
|
+
if (flags["dry-run"]) {
|
|
121
|
+
const dryRunMessage = `Dry run: would prepare release PR branch ${plan.releaseBranchPrefix} for ${plan.nextVersion}`;
|
|
122
|
+
if (flags.json) {
|
|
123
|
+
emitJson({
|
|
124
|
+
action: "pr-dry-run",
|
|
125
|
+
message: dryRunMessage,
|
|
126
|
+
releaseCreated: false,
|
|
127
|
+
tagNames: [],
|
|
128
|
+
branch: plan.releaseBranchPrefix,
|
|
129
|
+
targets: plan.packages
|
|
130
|
+
?.filter((pkg) => pkg.nextVersion)
|
|
131
|
+
.map((pkg) => ({
|
|
132
|
+
tag: pkg.path === "."
|
|
133
|
+
? `v${pkg.nextVersion ?? ""}`
|
|
134
|
+
: `${pkg.path}-v${pkg.nextVersion ?? ""}`,
|
|
135
|
+
version: pkg.nextVersion ?? "",
|
|
136
|
+
})) ?? [
|
|
137
|
+
{
|
|
138
|
+
tag: `v${plan.nextVersion}`,
|
|
139
|
+
version: plan.nextVersion,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
});
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
console.log(dryRunMessage);
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
81
148
|
const pr = (0, pr_js_1.prepareSimpleReleasePr)(process.cwd(), { logger });
|
|
82
149
|
(0, pr_js_1.pushReleaseBranch)(process.cwd(), pr.branch);
|
|
83
150
|
const reviewResult = await (0, pr_js_1.openOrUpdateSimpleReviewRequest)(process.cwd(), pr.branch, pr.title, pr.version, pr.previousVersion, pr.commits, pr.plan, { logger });
|
|
@@ -125,11 +192,20 @@ async function main() {
|
|
|
125
192
|
: "";
|
|
126
193
|
const heading = "# Changelog\n\n";
|
|
127
194
|
const body = existing.replace(/^# Changelog\s*/u, "");
|
|
128
|
-
node_fs_1.default.writeFileSync(changelogPath, `${heading}${section}\n${body}`.trimEnd()
|
|
195
|
+
node_fs_1.default.writeFileSync(changelogPath, `${`${heading}${section}\n${body}`.trimEnd()}\n`, "utf8");
|
|
129
196
|
console.log(`Updated ${plan.changelogFile}`);
|
|
130
197
|
return 0;
|
|
131
198
|
}
|
|
132
199
|
if (command === "pr") {
|
|
200
|
+
if (flags["dry-run"]) {
|
|
201
|
+
const plan = (0, plan_js_1.createSimplePlan)();
|
|
202
|
+
if (!plan.nextVersion) {
|
|
203
|
+
console.log("No releasable commits found. Nothing to do.");
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
console.log(`Dry run: would prepare release PR branch ${plan.releaseBranchPrefix} for ${plan.nextVersion}`);
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
133
209
|
const pr = (0, pr_js_1.prepareSimpleReleasePr)(process.cwd(), { logger: console });
|
|
134
210
|
(0, pr_js_1.pushReleaseBranch)(process.cwd(), pr.branch);
|
|
135
211
|
const reviewResult = await (0, pr_js_1.openOrUpdateSimpleReviewRequest)(process.cwd(), pr.branch, pr.title, pr.version, pr.previousVersion, pr.commits, pr.plan);
|
|
@@ -139,18 +215,34 @@ async function main() {
|
|
|
139
215
|
return 0;
|
|
140
216
|
}
|
|
141
217
|
if (command === "release") {
|
|
218
|
+
if (flags["dry-run"]) {
|
|
219
|
+
const result = await (0, release_js_1.runSimpleReleaseDetailed)(process.cwd(), {
|
|
220
|
+
logger,
|
|
221
|
+
"dry-run": true,
|
|
222
|
+
});
|
|
223
|
+
if (result.action === "release-dry-run") {
|
|
224
|
+
console.log(result.message);
|
|
225
|
+
}
|
|
226
|
+
else if (result.action === "release-skipped") {
|
|
227
|
+
console.log(result.reason);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.log(result.message);
|
|
231
|
+
}
|
|
232
|
+
return 0;
|
|
233
|
+
}
|
|
142
234
|
const message = await (0, release_js_1.runSimpleRelease)(process.cwd());
|
|
143
235
|
console.log(message);
|
|
144
236
|
return 0;
|
|
145
237
|
}
|
|
146
238
|
console.log("Usage: versionary <command>");
|
|
147
239
|
console.log("Commands:");
|
|
148
|
-
console.log(" run [--json] Auto-dispatch release PR/update or release publish by context");
|
|
240
|
+
console.log(" run [--json] [--dry-run] Auto-dispatch release PR/update or release publish by context");
|
|
149
241
|
console.log(" verify Validate config and basic repository shape");
|
|
150
242
|
console.log(" plan Print release plan (simple mode)");
|
|
151
243
|
console.log(" changelog [--write] Print or write changelog section");
|
|
152
|
-
console.log(" pr Prepare release PR commit and branch");
|
|
153
|
-
console.log(" release Publish release metadata for release commit context");
|
|
244
|
+
console.log(" pr [--dry-run] Prepare release PR commit and branch");
|
|
245
|
+
console.log(" release [--dry-run] Publish release metadata for release commit context");
|
|
154
246
|
return 1;
|
|
155
247
|
}
|
|
156
248
|
main()
|
|
@@ -41,6 +41,9 @@ function loadConfig(cwd = process.cwd()) {
|
|
|
41
41
|
if (!isRecord(parsed)) {
|
|
42
42
|
throw new Error("Invalid config: expected an object at the root.");
|
|
43
43
|
}
|
|
44
|
+
if (Object.hasOwn(parsed, "plugins")) {
|
|
45
|
+
throw new Error('The "plugins" config key is no longer supported. Versionary uses built-in integrations only.');
|
|
46
|
+
}
|
|
44
47
|
const validated = schema_js_1.configSchema.parse(parsed);
|
|
45
48
|
return {
|
|
46
49
|
path: found.path,
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export declare const configSchema: z.ZodObject<{
|
|
|
4
4
|
version: z.ZodLiteral<1>;
|
|
5
5
|
"review-mode": z.ZodOptional<z.ZodEnum<{
|
|
6
6
|
direct: "direct";
|
|
7
|
+
pr: "pr";
|
|
7
8
|
review: "review";
|
|
8
9
|
}>>;
|
|
9
10
|
"version-file": z.ZodOptional<z.ZodString>;
|
|
@@ -22,6 +23,7 @@ export declare const configSchema: z.ZodObject<{
|
|
|
22
23
|
packages: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
23
24
|
"release-type": z.ZodOptional<z.ZodString>;
|
|
24
25
|
"package-name": z.ZodOptional<z.ZodString>;
|
|
26
|
+
"changelog-file": z.ZodOptional<z.ZodString>;
|
|
25
27
|
"exclude-paths": z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
26
28
|
"extra-files": z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
27
29
|
type: z.ZodEnum<{
|
|
@@ -36,6 +38,5 @@ export declare const configSchema: z.ZodObject<{
|
|
|
36
38
|
pattern: z.ZodOptional<z.ZodString>;
|
|
37
39
|
}, z.core.$strip>>>;
|
|
38
40
|
}, z.core.$strict>>>;
|
|
39
|
-
plugins: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
40
41
|
}, z.core.$strict>;
|
|
41
42
|
export type ConfigSchema = z.infer<typeof configSchema>;
|
package/dist/config/schema.js
CHANGED
|
@@ -53,6 +53,7 @@ const packageSchema = zod_1.z
|
|
|
53
53
|
.object({
|
|
54
54
|
"release-type": zod_1.z.string().optional(),
|
|
55
55
|
"package-name": zod_1.z.string().optional(),
|
|
56
|
+
"changelog-file": zod_1.z.string().optional(),
|
|
56
57
|
"exclude-paths": zod_1.z.array(zod_1.z.string()).optional(),
|
|
57
58
|
"extra-files": zod_1.z.array(artifactRuleSchema).optional(),
|
|
58
59
|
})
|
|
@@ -61,7 +62,7 @@ exports.configSchema = zod_1.z
|
|
|
61
62
|
.object({
|
|
62
63
|
$schema: zod_1.z.string().optional(),
|
|
63
64
|
version: zod_1.z.literal(1),
|
|
64
|
-
"review-mode": zod_1.z.enum(["direct", "review"]).optional(),
|
|
65
|
+
"review-mode": zod_1.z.enum(["direct", "pr", "review"]).optional(),
|
|
65
66
|
"version-file": zod_1.z.string().optional(),
|
|
66
67
|
"changelog-file": zod_1.z.string().optional(),
|
|
67
68
|
"release-branch": zod_1.z.string().optional(),
|
|
@@ -73,6 +74,5 @@ exports.configSchema = zod_1.z
|
|
|
73
74
|
"include-commit-authors": zod_1.z.boolean().optional(),
|
|
74
75
|
"release-type": zod_1.z.string().optional(),
|
|
75
76
|
packages: zod_1.z.record(zod_1.z.string().min(1), packageSchema).optional(),
|
|
76
|
-
plugins: zod_1.z.array(zod_1.z.string().min(1)).optional(),
|
|
77
77
|
})
|
|
78
78
|
.strict();
|
|
@@ -9,4 +9,11 @@ export declare function renderSimpleReleaseNotes(input: {
|
|
|
9
9
|
includeFooter?: boolean;
|
|
10
10
|
}): string;
|
|
11
11
|
export declare function renderSimpleChangelog(plan: SimplePlan): string;
|
|
12
|
+
export declare function renderPackageChangelogSection(input: {
|
|
13
|
+
currentVersion: string;
|
|
14
|
+
nextVersion: string;
|
|
15
|
+
commits: ParsedCommit[];
|
|
16
|
+
tagPrefix: string;
|
|
17
|
+
cwd?: string;
|
|
18
|
+
}): string;
|
|
12
19
|
export declare function prependChangelog(cwd: string, changelogFile: string, section: string): void;
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.renderSimpleReleaseNotes = renderSimpleReleaseNotes;
|
|
7
7
|
exports.renderSimpleChangelog = renderSimpleChangelog;
|
|
8
|
+
exports.renderPackageChangelogSection = renderPackageChangelogSection;
|
|
8
9
|
exports.prependChangelog = prependChangelog;
|
|
9
10
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
11
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -110,6 +111,24 @@ function renderSimpleChangelog(plan) {
|
|
|
110
111
|
cwd: process.cwd(),
|
|
111
112
|
});
|
|
112
113
|
}
|
|
114
|
+
function renderPackageChangelogSection(input) {
|
|
115
|
+
const repoUrl = (0, repo_url_js_1.resolveRepositoryWebBaseUrl)(input.cwd ?? process.cwd());
|
|
116
|
+
const header = repoUrl
|
|
117
|
+
? `## [${input.nextVersion}](${repoUrl}/compare/${input.tagPrefix}-v${input.currentVersion}...${input.tagPrefix}-v${input.nextVersion}) (${formatDate()})`
|
|
118
|
+
: `## ${input.nextVersion} (${formatDate()})`;
|
|
119
|
+
const grouped = groupCommitLines(input.commits, repoUrl);
|
|
120
|
+
const sections = [];
|
|
121
|
+
if (grouped.breaking.length > 0) {
|
|
122
|
+
sections.push("### Breaking changes", ...grouped.breaking, "");
|
|
123
|
+
}
|
|
124
|
+
if (grouped.features.length > 0) {
|
|
125
|
+
sections.push("### Features", ...grouped.features, "");
|
|
126
|
+
}
|
|
127
|
+
if (grouped.fixes.length > 0) {
|
|
128
|
+
sections.push("### Bug Fixes", ...grouped.fixes, "");
|
|
129
|
+
}
|
|
130
|
+
return [header, "", ...sections].join("\n");
|
|
131
|
+
}
|
|
113
132
|
function prependChangelog(cwd, changelogFile, section) {
|
|
114
133
|
const changelogPath = node_path_1.default.join(cwd, changelogFile);
|
|
115
134
|
const existing = node_fs_1.default.existsSync(changelogPath)
|
|
@@ -8,7 +8,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
function readDescriptionVersion(content, versionFile) {
|
|
10
10
|
const match = content.match(/^Version:\s*(.+)\s*$/mu);
|
|
11
|
-
if (!match
|
|
11
|
+
if (!match?.[1]) {
|
|
12
12
|
throw new Error(`${versionFile} is missing a valid "Version:" field required by release-type "r".`);
|
|
13
13
|
}
|
|
14
14
|
return match[1].trim();
|
|
@@ -202,19 +202,73 @@ function collectRustTargetManifests(cwd, versionFile, includeWorkspaceMembers) {
|
|
|
202
202
|
}
|
|
203
203
|
throw new Error(`Configured rust target "${versionFile}" is not a Rust crate manifest. Expected [package].version or [workspace].members with crate Cargo.toml files.`);
|
|
204
204
|
}
|
|
205
|
-
function
|
|
206
|
-
|
|
205
|
+
function isWorkspaceInheritedVersion(rawVersion) {
|
|
206
|
+
if (!rawVersion || typeof rawVersion !== "object") {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
const versionRecord = rawVersion;
|
|
210
|
+
return versionRecord.workspace === true;
|
|
211
|
+
}
|
|
212
|
+
function readWorkspacePackageVersion(cargoTomlRaw, versionFile) {
|
|
213
|
+
const { workspaceTable } = parseCargoManifest(versionFile, cargoTomlRaw);
|
|
214
|
+
if (!workspaceTable || typeof workspaceTable !== "object") {
|
|
215
|
+
throw new Error(`${versionFile} is missing [workspace.package].version required by members using version.workspace = true.`);
|
|
216
|
+
}
|
|
217
|
+
const workspacePackage = workspaceTable.package && typeof workspaceTable.package === "object"
|
|
218
|
+
? workspaceTable.package
|
|
219
|
+
: null;
|
|
220
|
+
const rawVersion = workspacePackage?.version;
|
|
221
|
+
if (typeof rawVersion !== "string" || rawVersion.trim().length === 0) {
|
|
222
|
+
throw new Error(`${versionFile} is missing [workspace.package].version required by members using version.workspace = true.`);
|
|
223
|
+
}
|
|
224
|
+
return rawVersion.trim();
|
|
225
|
+
}
|
|
226
|
+
function findWorkspaceManifestForMember(cwd, memberManifest) {
|
|
227
|
+
const cwdAbs = node_path_1.default.resolve(cwd);
|
|
228
|
+
let currentDir = node_path_1.default.resolve(cwd, node_path_1.default.dirname(memberManifest));
|
|
229
|
+
while (true) {
|
|
230
|
+
const relativeDir = node_path_1.default.relative(cwdAbs, currentDir);
|
|
231
|
+
if (relativeDir.startsWith("..")) {
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
const candidatePath = node_path_1.default.join(currentDir, "Cargo.toml");
|
|
235
|
+
if (node_fs_1.default.existsSync(candidatePath)) {
|
|
236
|
+
const relativeManifest = normalizeSlashPath(node_path_1.default.relative(cwdAbs, candidatePath));
|
|
237
|
+
const cargoTomlRaw = node_fs_1.default.readFileSync(candidatePath, "utf8");
|
|
238
|
+
const parsed = parseCargoManifest(relativeManifest, cargoTomlRaw);
|
|
239
|
+
if (parsed.workspaceTable) {
|
|
240
|
+
return relativeManifest;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (currentDir === cwdAbs) {
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
const parentDir = node_path_1.default.dirname(currentDir);
|
|
247
|
+
if (parentDir === currentDir) {
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
currentDir = parentDir;
|
|
251
|
+
}
|
|
252
|
+
throw new Error(`${memberManifest} uses version.workspace = true, but no workspace Cargo.toml with [workspace.package].version was found between that crate and repository root.`);
|
|
253
|
+
}
|
|
254
|
+
function readResolvedCargoVersion(cwd, manifest, cargoTomlRaw) {
|
|
255
|
+
const { packageTable } = parseCargoManifest(manifest, cargoTomlRaw);
|
|
207
256
|
if (!packageTable || typeof packageTable !== "object") {
|
|
208
|
-
throw new Error(`${
|
|
257
|
+
throw new Error(`${manifest} is missing [package].version. Add [package] with a SemVer version.`);
|
|
209
258
|
}
|
|
210
259
|
const rawVersion = packageTable.version;
|
|
211
260
|
if (rawVersion === undefined) {
|
|
212
|
-
throw new Error(`${
|
|
261
|
+
throw new Error(`${manifest} is missing [package].version. Add [package] with a SemVer version.`);
|
|
213
262
|
}
|
|
214
|
-
if (typeof rawVersion
|
|
215
|
-
|
|
263
|
+
if (typeof rawVersion === "string" && rawVersion.trim().length > 0) {
|
|
264
|
+
return rawVersion.trim();
|
|
216
265
|
}
|
|
217
|
-
|
|
266
|
+
if (!isWorkspaceInheritedVersion(rawVersion)) {
|
|
267
|
+
throw new Error(`${manifest} has invalid [package].version. Expected a non-empty SemVer string or version.workspace = true.`);
|
|
268
|
+
}
|
|
269
|
+
const workspaceManifest = findWorkspaceManifestForMember(cwd, manifest);
|
|
270
|
+
const workspaceRaw = node_fs_1.default.readFileSync(node_path_1.default.join(cwd, workspaceManifest), "utf8");
|
|
271
|
+
return readWorkspacePackageVersion(workspaceRaw, workspaceManifest);
|
|
218
272
|
}
|
|
219
273
|
function readCargoPackageName(cargoTomlRaw, versionFile) {
|
|
220
274
|
const { packageTable } = parseCargoManifest(versionFile, cargoTomlRaw);
|
|
@@ -272,6 +326,56 @@ function writeCargoVersion(cargoTomlRaw, versionFile, version) {
|
|
|
272
326
|
}
|
|
273
327
|
return updated;
|
|
274
328
|
}
|
|
329
|
+
function writeWorkspacePackageVersion(cargoTomlRaw, versionFile, version) {
|
|
330
|
+
const lineEnding = cargoTomlRaw.includes("\r\n") ? "\r\n" : "\n";
|
|
331
|
+
const hasFinalLineEnding = cargoTomlRaw.endsWith("\n") || cargoTomlRaw.endsWith("\r\n");
|
|
332
|
+
const lines = cargoTomlRaw.split(/\r?\n/u);
|
|
333
|
+
let inWorkspacePackageSection = false;
|
|
334
|
+
let foundWorkspacePackageSection = false;
|
|
335
|
+
let replacedVersion = false;
|
|
336
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
337
|
+
const line = lines[index] ?? "";
|
|
338
|
+
const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/u);
|
|
339
|
+
if (sectionMatch) {
|
|
340
|
+
const section = sectionMatch[1]?.trim();
|
|
341
|
+
inWorkspacePackageSection = section === "workspace.package";
|
|
342
|
+
if (inWorkspacePackageSection) {
|
|
343
|
+
foundWorkspacePackageSection = true;
|
|
344
|
+
}
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (!inWorkspacePackageSection) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const versionMatch = line.match(/^(\s*version\s*=\s*)(["'])([^"']*)(\2)(\s*(?:#.*)?)?$/u);
|
|
351
|
+
if (!versionMatch) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
const [, prefix = "", quote = '"', , , suffix = ""] = versionMatch;
|
|
355
|
+
lines[index] = `${prefix}${quote}${version}${quote}${suffix}`;
|
|
356
|
+
replacedVersion = true;
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
if (!foundWorkspacePackageSection || !replacedVersion) {
|
|
360
|
+
throw new Error(`${versionFile} is missing [workspace.package].version required by members using version.workspace = true.`);
|
|
361
|
+
}
|
|
362
|
+
let updated = lines.join(lineEnding);
|
|
363
|
+
if (hasFinalLineEnding && !updated.endsWith(lineEnding)) {
|
|
364
|
+
updated += lineEnding;
|
|
365
|
+
}
|
|
366
|
+
if (!hasFinalLineEnding && updated.endsWith(lineEnding)) {
|
|
367
|
+
updated = updated.slice(0, -lineEnding.length);
|
|
368
|
+
}
|
|
369
|
+
return updated;
|
|
370
|
+
}
|
|
371
|
+
function usesWorkspaceInheritedVersion(cargoTomlRaw, versionFile) {
|
|
372
|
+
const { packageTable } = parseCargoManifest(versionFile, cargoTomlRaw);
|
|
373
|
+
if (!packageTable || typeof packageTable !== "object") {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
const rawVersion = packageTable.version;
|
|
377
|
+
return isWorkspaceInheritedVersion(rawVersion);
|
|
378
|
+
}
|
|
275
379
|
function isDependencySection(section) {
|
|
276
380
|
if (ROOT_DEPENDENCY_SECTIONS.has(section)) {
|
|
277
381
|
return true;
|
|
@@ -450,13 +554,14 @@ exports.rustVersionStrategy = {
|
|
|
450
554
|
throw new Error(`Configured rust target "${versionFile}" did not resolve to a Rust crate manifest.`);
|
|
451
555
|
}
|
|
452
556
|
const cargoTomlRaw = node_fs_1.default.readFileSync(node_path_1.default.join(cwd, selectedManifest), "utf8");
|
|
453
|
-
return
|
|
557
|
+
return readResolvedCargoVersion(cwd, selectedManifest, cargoTomlRaw);
|
|
454
558
|
},
|
|
455
559
|
writeVersion(cwd, config, version) {
|
|
456
560
|
const versionFile = this.getVersionFile(config);
|
|
457
561
|
const manifests = collectRustTargetManifests(cwd, versionFile, !config.packages);
|
|
458
562
|
const updatedFiles = [];
|
|
459
563
|
const internalCrates = new Set();
|
|
564
|
+
const workspaceManifestsToUpdate = new Set();
|
|
460
565
|
for (const manifest of manifests) {
|
|
461
566
|
const versionPath = node_path_1.default.join(cwd, manifest);
|
|
462
567
|
if (!node_fs_1.default.existsSync(versionPath)) {
|
|
@@ -467,6 +572,7 @@ exports.rustVersionStrategy = {
|
|
|
467
572
|
continue;
|
|
468
573
|
}
|
|
469
574
|
internalCrates.add(readCargoPackageName(cargoTomlRaw, manifest));
|
|
575
|
+
readResolvedCargoVersion(cwd, manifest, cargoTomlRaw);
|
|
470
576
|
}
|
|
471
577
|
for (const manifest of manifests) {
|
|
472
578
|
const versionPath = node_path_1.default.join(cwd, manifest);
|
|
@@ -477,10 +583,30 @@ exports.rustVersionStrategy = {
|
|
|
477
583
|
if (!isCrateManifest(manifest, cargoTomlRaw)) {
|
|
478
584
|
continue;
|
|
479
585
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
586
|
+
let updatedCargoToml = cargoTomlRaw;
|
|
587
|
+
if (usesWorkspaceInheritedVersion(cargoTomlRaw, manifest)) {
|
|
588
|
+
workspaceManifestsToUpdate.add(findWorkspaceManifestForMember(cwd, manifest));
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
updatedCargoToml = writeCargoVersion(updatedCargoToml, manifest, version);
|
|
592
|
+
}
|
|
593
|
+
updatedCargoToml = writeInternalDependencyVersions(updatedCargoToml, internalCrates, version);
|
|
594
|
+
if (updatedCargoToml !== cargoTomlRaw) {
|
|
595
|
+
node_fs_1.default.writeFileSync(versionPath, updatedCargoToml, "utf8");
|
|
596
|
+
updatedFiles.push(manifest);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
for (const workspaceManifest of workspaceManifestsToUpdate) {
|
|
600
|
+
const workspacePath = node_path_1.default.join(cwd, workspaceManifest);
|
|
601
|
+
if (!node_fs_1.default.existsSync(workspacePath)) {
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
const workspaceRaw = node_fs_1.default.readFileSync(workspacePath, "utf8");
|
|
605
|
+
const updatedWorkspaceToml = writeInternalDependencyVersions(writeWorkspacePackageVersion(workspaceRaw, workspaceManifest, version), internalCrates, version);
|
|
606
|
+
if (updatedWorkspaceToml !== workspaceRaw) {
|
|
607
|
+
node_fs_1.default.writeFileSync(workspacePath, updatedWorkspaceToml, "utf8");
|
|
608
|
+
updatedFiles.push(workspaceManifest);
|
|
609
|
+
}
|
|
484
610
|
}
|
|
485
611
|
if (updatedFiles.length === 0) {
|
|
486
612
|
throw new Error(`Configured rust target "${versionFile}" did not resolve to a Rust crate manifest to update.`);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { VersionaryPluginRuntime } from "../../types/plugins.js";
|
|
2
|
-
export declare function loadRuntimePlugins(
|
|
2
|
+
export declare function loadRuntimePlugins(): VersionaryPluginRuntime[];
|
|
@@ -1,24 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.loadRuntimePlugins = loadRuntimePlugins;
|
|
4
|
-
const load_config_js_1 = require("../../config/load-config.js");
|
|
5
4
|
const plugin_js_1 = require("./github/plugin.js");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
};
|
|
9
|
-
function loadRuntimePlugins(cwd = process.cwd()) {
|
|
10
|
-
const loaded = (0, load_config_js_1.loadConfig)(cwd);
|
|
11
|
-
const configured = loaded.config.plugins ?? [];
|
|
12
|
-
const plugins = [(0, plugin_js_1.createGitHubPlugin)()];
|
|
13
|
-
const seen = new Set(plugins.map((plugin) => plugin.name));
|
|
14
|
-
for (const name of configured) {
|
|
15
|
-
const factory = BUILTIN_PLUGIN_FACTORIES[name];
|
|
16
|
-
if (!factory || seen.has(name)) {
|
|
17
|
-
continue;
|
|
18
|
-
}
|
|
19
|
-
const plugin = factory();
|
|
20
|
-
plugins.push(plugin);
|
|
21
|
-
seen.add(plugin.name);
|
|
22
|
-
}
|
|
23
|
-
return plugins;
|
|
5
|
+
function loadRuntimePlugins() {
|
|
6
|
+
return [(0, plugin_js_1.createGitHubPlugin)()];
|
|
24
7
|
}
|
package/dist/types/config.d.ts
CHANGED
|
@@ -9,12 +9,13 @@ export interface VersionaryArtifactRule {
|
|
|
9
9
|
export interface VersionaryPackage {
|
|
10
10
|
"release-type"?: string;
|
|
11
11
|
"package-name"?: string;
|
|
12
|
+
"changelog-file"?: string;
|
|
12
13
|
"exclude-paths"?: string[];
|
|
13
14
|
"extra-files"?: VersionaryArtifactRule[];
|
|
14
15
|
}
|
|
15
16
|
export interface VersionaryConfig {
|
|
16
17
|
version: 1;
|
|
17
|
-
"review-mode"?: "direct" | "review";
|
|
18
|
+
"review-mode"?: "direct" | "pr" | "review";
|
|
18
19
|
"version-file"?: string;
|
|
19
20
|
"changelog-file"?: string;
|
|
20
21
|
"release-branch"?: string;
|
|
@@ -26,7 +27,6 @@ export interface VersionaryConfig {
|
|
|
26
27
|
"include-commit-authors"?: boolean;
|
|
27
28
|
"release-type"?: string;
|
|
28
29
|
packages?: Record<string, VersionaryPackage>;
|
|
29
|
-
plugins?: string[];
|
|
30
30
|
}
|
|
31
31
|
export interface LoadedConfig {
|
|
32
32
|
path: string;
|
|
@@ -16,6 +16,7 @@ function verifyProject(cwd = process.cwd()) {
|
|
|
16
16
|
name: "config-load",
|
|
17
17
|
ok: true,
|
|
18
18
|
details: `Loaded ${node_path_1.default.basename(config.path)} (${config.format})`,
|
|
19
|
+
category: "config",
|
|
19
20
|
});
|
|
20
21
|
const strategy = (0, resolve_js_1.resolveVersionStrategy)(config.config);
|
|
21
22
|
const versionFile = strategy.getVersionFile(config.config);
|
|
@@ -24,6 +25,10 @@ function verifyProject(cwd = process.cwd()) {
|
|
|
24
25
|
name: `version-file:${versionFile}`,
|
|
25
26
|
ok: exists,
|
|
26
27
|
details: exists ? "Version file exists" : `Missing ${versionFile}`,
|
|
28
|
+
category: "version-files",
|
|
29
|
+
remediation: exists
|
|
30
|
+
? undefined
|
|
31
|
+
: `Create ${versionFile} or set "version-file" to the correct path for your release strategy.`,
|
|
27
32
|
});
|
|
28
33
|
if (config.config.packages) {
|
|
29
34
|
for (const [pkgPathRaw, packageConfig] of Object.entries(config.config.packages)) {
|
|
@@ -33,6 +38,10 @@ function verifyProject(cwd = process.cwd()) {
|
|
|
33
38
|
name: `package-path:${pkgPathRaw}`,
|
|
34
39
|
ok: exists,
|
|
35
40
|
details: exists ? "Path exists" : `Missing path: ${pkgPathRaw}`,
|
|
41
|
+
category: "paths",
|
|
42
|
+
remediation: exists
|
|
43
|
+
? undefined
|
|
44
|
+
: `Create ${pkgPathRaw} or remove/rename this entry under "packages" in versionary config.`,
|
|
36
45
|
});
|
|
37
46
|
if (exists) {
|
|
38
47
|
const packageContext = (0, package_context_js_1.resolvePackageStrategyContext)(config.config, pkgPathRaw, packageConfig);
|
|
@@ -44,6 +53,10 @@ function verifyProject(cwd = process.cwd()) {
|
|
|
44
53
|
details: packageVersionExists
|
|
45
54
|
? "Version file exists"
|
|
46
55
|
: `Missing ${packageVersionFile}`,
|
|
56
|
+
category: "version-files",
|
|
57
|
+
remediation: packageVersionExists
|
|
58
|
+
? undefined
|
|
59
|
+
: `Create ${packageVersionFile} or adjust package release settings ("release-type"/"version-file") for ${pkgPathRaw}.`,
|
|
47
60
|
});
|
|
48
61
|
}
|
|
49
62
|
}
|