versionary 1.3.0 → 1.4.1
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/action/index.js +35 -20
- package/dist/cli/index.js +31 -0
- package/dist/config/load-config.js +44 -2
- package/dist/config/schema.d.ts +1 -0
- package/dist/config/schema.js +7 -2
- package/dist/release/direct.d.ts +5 -0
- package/dist/release/direct.js +80 -0
- package/dist/release/plan.js +11 -20
- package/dist/release/pr.d.ts +4 -0
- package/dist/release/pr.js +3 -3
- package/dist/release/release.d.ts +1 -0
- package/dist/release/release.js +6 -1
- package/dist/release/semver.d.ts +1 -1
- package/dist/release/semver.js +2 -2
- package/dist/release/verify-project.js +27 -22
- package/dist/strategy/python.js +46 -11
- package/dist/types/config.d.ts +3 -0
- package/package.json +2 -2
package/dist/action/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { appendFileSync } from "node:fs";
|
|
3
|
+
import { appendFileSync, readFileSync } from "node:fs";
|
|
4
4
|
function getInput(name) {
|
|
5
5
|
const canonical = `INPUT_${name.replace(/ /g, "_").toUpperCase()}`;
|
|
6
6
|
const underscoreAlias = canonical.replace(/-/g, "_");
|
|
@@ -74,9 +74,41 @@ function setOutput(name, value) {
|
|
|
74
74
|
const delimiter = `versionary-${randomUUID()}`;
|
|
75
75
|
appendFileSync(outputPath, `${name}<<${delimiter}\n${value}\n${delimiter}\n`, "utf8");
|
|
76
76
|
}
|
|
77
|
+
function isForkRepository() {
|
|
78
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
79
|
+
if (!eventPath) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const event = JSON.parse(readFileSync(eventPath, "utf8"));
|
|
84
|
+
return event?.repository?.fork === true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Only a confirmed fork may bypass the token requirement.
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function skipRun(action, message) {
|
|
92
|
+
const payload = { action, message, releaseCreated: false, tagNames: [] };
|
|
93
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
94
|
+
setOutput("action", action);
|
|
95
|
+
setOutput("message", message);
|
|
96
|
+
setOutput("release_created", "false");
|
|
97
|
+
setOutput("tag_name", "");
|
|
98
|
+
setOutput("tag_names", "[]");
|
|
99
|
+
setOutput("release_targets", "[]");
|
|
100
|
+
setOutput("review_url", "");
|
|
101
|
+
setOutput("review_requests", "[]");
|
|
102
|
+
setOutput("branch", "");
|
|
103
|
+
setOutput("title", "");
|
|
104
|
+
}
|
|
77
105
|
function main() {
|
|
78
106
|
const token = getInput("token");
|
|
79
107
|
if (!token) {
|
|
108
|
+
if (isForkRepository()) {
|
|
109
|
+
skipRun("fork-skipped", "Skipping release automation in a fork without a release token.");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
80
112
|
throw new Error("Input required and not supplied: token.");
|
|
81
113
|
}
|
|
82
114
|
const versionaryVersion = getInput("versionary-version") || "0.7.0";
|
|
@@ -120,25 +152,8 @@ function main() {
|
|
|
120
152
|
// recovery cycle.
|
|
121
153
|
const releaseCanPublish = hasVersionaryReleaseMarker(cwd, sha) && isAncestor(cwd, sha, remoteSha);
|
|
122
154
|
if (!releaseCanPublish) {
|
|
123
|
-
|
|
124
|
-
`${ref} now points to ${remoteSha.slice(0, 7)}
|
|
125
|
-
const stalePayload = {
|
|
126
|
-
action: "stale-run-skipped",
|
|
127
|
-
message: staleMessage,
|
|
128
|
-
releaseCreated: false,
|
|
129
|
-
tagNames: [],
|
|
130
|
-
};
|
|
131
|
-
process.stdout.write(`${JSON.stringify(stalePayload)}\n`);
|
|
132
|
-
setOutput("action", stalePayload.action);
|
|
133
|
-
setOutput("message", stalePayload.message);
|
|
134
|
-
setOutput("release_created", "false");
|
|
135
|
-
setOutput("tag_name", "");
|
|
136
|
-
setOutput("tag_names", "[]");
|
|
137
|
-
setOutput("release_targets", "[]");
|
|
138
|
-
setOutput("review_url", "");
|
|
139
|
-
setOutput("review_requests", "[]");
|
|
140
|
-
setOutput("branch", "");
|
|
141
|
-
setOutput("title", "");
|
|
155
|
+
skipRun("stale-run-skipped", `Skipping stale push run for ${sha.slice(0, 7)}; ` +
|
|
156
|
+
`${ref} now points to ${remoteSha.slice(0, 7)}.`);
|
|
142
157
|
return;
|
|
143
158
|
}
|
|
144
159
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { loadConfig } from "../config/load-config.js";
|
|
4
4
|
import { prependChangelog, renderReleasePlanChangelog, } from "../release/changelog.js";
|
|
5
|
+
import { runDirectRelease } from "../release/direct.js";
|
|
5
6
|
import { createReleasePlan } from "../release/plan.js";
|
|
6
7
|
import { closeStaleReviewRequestIfExists, isReleaseCommitMessage, openOrUpdateReviewRequest, preparePendingReleasePr, preparePendingSeparateReleasePrs, prepareReleasePr, prepareSeparateReleasePrs, pushReleaseBranch, reconcileSeparateReviewRequests, resolveReleaseHighlights, } from "../release/pr.js";
|
|
7
8
|
import { runRelease, runReleaseDetailed } from "../release/release.js";
|
|
@@ -194,6 +195,36 @@ async function main() {
|
|
|
194
195
|
const flags = parseFlags(args);
|
|
195
196
|
const logger = flags.json ? undefined : console;
|
|
196
197
|
if (!command || command === "run") {
|
|
198
|
+
if (loadConfig(process.cwd()).config["review-mode"] === "direct") {
|
|
199
|
+
const release = await runDirectRelease(process.cwd(), {
|
|
200
|
+
logger,
|
|
201
|
+
"dry-run": flags["dry-run"],
|
|
202
|
+
});
|
|
203
|
+
const message = release.action === "release-skipped" ? release.reason : release.message;
|
|
204
|
+
if (flags.json) {
|
|
205
|
+
emitJson({
|
|
206
|
+
action: release.action,
|
|
207
|
+
message,
|
|
208
|
+
releaseCreated: release.action === "release-published" &&
|
|
209
|
+
release.releases.length > 0,
|
|
210
|
+
tagNames: release.action === "release-published"
|
|
211
|
+
? release.releases.map((target) => target.tag)
|
|
212
|
+
: release.action === "release-dry-run"
|
|
213
|
+
? release.targets.map((target) => target.tag)
|
|
214
|
+
: [],
|
|
215
|
+
...("releaseTargets" in release
|
|
216
|
+
? { releaseTargets: release.releaseTargets }
|
|
217
|
+
: {}),
|
|
218
|
+
...(release.action === "release-dry-run"
|
|
219
|
+
? { targets: release.targets }
|
|
220
|
+
: {}),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
console.log(message);
|
|
225
|
+
}
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
197
228
|
const commitMessage = execFileSync("git", ["log", "-1", "--pretty=%B"], {
|
|
198
229
|
encoding: "utf8",
|
|
199
230
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -7,6 +7,7 @@ const SUPPORTED_FILES = [
|
|
|
7
7
|
{ file: "versionary.jsonc", format: "jsonc" },
|
|
8
8
|
{ file: "versionary.json", format: "json" },
|
|
9
9
|
];
|
|
10
|
+
const warnedDeprecatedConfigPaths = new Set();
|
|
10
11
|
function parseConfig(raw, format) {
|
|
11
12
|
if (format === "json" || format === "jsonc") {
|
|
12
13
|
return parseJsonc(raw);
|
|
@@ -16,6 +17,43 @@ function parseConfig(raw, format) {
|
|
|
16
17
|
function isRecord(value) {
|
|
17
18
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
18
19
|
}
|
|
20
|
+
function normalizeConfig(config) {
|
|
21
|
+
const { "bump-minor-pre-major": rootBumpMinorPreMajor, packages, ...rootConfig } = config;
|
|
22
|
+
let usedDeprecatedPreMajorKey = rootBumpMinorPreMajor !== undefined;
|
|
23
|
+
const normalizedPackages = packages
|
|
24
|
+
? Object.fromEntries(Object.entries(packages).map(([packagePath, packageConfig]) => {
|
|
25
|
+
const { "bump-minor-pre-major": packageBumpMinorPreMajor, ...canonicalPackageConfig } = packageConfig;
|
|
26
|
+
usedDeprecatedPreMajorKey ||= packageBumpMinorPreMajor !== undefined;
|
|
27
|
+
return [
|
|
28
|
+
packagePath,
|
|
29
|
+
{
|
|
30
|
+
...canonicalPackageConfig,
|
|
31
|
+
...(packageBumpMinorPreMajor === undefined
|
|
32
|
+
? {}
|
|
33
|
+
: { "allow-stable-major": !packageBumpMinorPreMajor }),
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
}))
|
|
37
|
+
: undefined;
|
|
38
|
+
return {
|
|
39
|
+
config: {
|
|
40
|
+
...rootConfig,
|
|
41
|
+
...(rootBumpMinorPreMajor === undefined
|
|
42
|
+
? {}
|
|
43
|
+
: { "allow-stable-major": !rootBumpMinorPreMajor }),
|
|
44
|
+
...(normalizedPackages ? { packages: normalizedPackages } : {}),
|
|
45
|
+
},
|
|
46
|
+
usedDeprecatedPreMajorKey,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function warnForDeprecatedConfig(path) {
|
|
50
|
+
// Release commands can load the same configuration in several stages.
|
|
51
|
+
if (warnedDeprecatedConfigPaths.has(path)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
warnedDeprecatedConfigPaths.add(path);
|
|
55
|
+
console.warn('Warning: "bump-minor-pre-major" is deprecated; use "allow-stable-major" with the inverse value instead.');
|
|
56
|
+
}
|
|
19
57
|
function validateReleaseTypes(config) {
|
|
20
58
|
try {
|
|
21
59
|
resolveVersionStrategy(config);
|
|
@@ -67,10 +105,14 @@ export function loadConfig(cwd = process.cwd()) {
|
|
|
67
105
|
throw new Error('The "plugins" config key is no longer supported. Versionary uses built-in integrations only.');
|
|
68
106
|
}
|
|
69
107
|
const validated = configSchema.parse(parsed);
|
|
70
|
-
|
|
108
|
+
const normalized = normalizeConfig(validated);
|
|
109
|
+
if (normalized.usedDeprecatedPreMajorKey) {
|
|
110
|
+
warnForDeprecatedConfig(found.path);
|
|
111
|
+
}
|
|
112
|
+
validateReleaseTypes(normalized.config);
|
|
71
113
|
return {
|
|
72
114
|
path: found.path,
|
|
73
115
|
format: found.format,
|
|
74
|
-
config:
|
|
116
|
+
config: normalized.config,
|
|
75
117
|
};
|
|
76
118
|
}
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ export declare const configSchema: z.ZodObject<{
|
|
|
39
39
|
"markdown-changelog": "markdown-changelog";
|
|
40
40
|
"r-news": "r-news";
|
|
41
41
|
}>>;
|
|
42
|
+
"release-draft": z.ZodOptional<z.ZodBoolean>;
|
|
42
43
|
"bump-minor-pre-major": z.ZodOptional<z.ZodBoolean>;
|
|
43
44
|
"allow-stable-major": z.ZodOptional<z.ZodBoolean>;
|
|
44
45
|
"exclude-paths": z.ZodOptional<z.ZodArray<z.ZodString>>;
|
package/dist/config/schema.js
CHANGED
|
@@ -56,6 +56,10 @@ const artifactRuleSchema = z
|
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
});
|
|
59
|
+
const deprecatedBumpMinorPreMajorSchema = z.boolean().optional().meta({
|
|
60
|
+
deprecated: true,
|
|
61
|
+
description: 'Deprecated. Use "allow-stable-major" with the inverse value instead.',
|
|
62
|
+
});
|
|
59
63
|
const packageSchema = z
|
|
60
64
|
.object({
|
|
61
65
|
"release-type": z
|
|
@@ -64,7 +68,8 @@ const packageSchema = z
|
|
|
64
68
|
"package-name": z.string().optional(),
|
|
65
69
|
"changelog-file": z.string().optional(),
|
|
66
70
|
"changelog-format": z.enum(["markdown-changelog", "r-news"]).optional(),
|
|
67
|
-
"
|
|
71
|
+
"release-draft": z.boolean().optional(),
|
|
72
|
+
"bump-minor-pre-major": deprecatedBumpMinorPreMajorSchema,
|
|
68
73
|
"allow-stable-major": z.boolean().optional(),
|
|
69
74
|
"exclude-paths": z.array(z.string()).optional(),
|
|
70
75
|
"extra-files": z.array(artifactRuleSchema).optional(),
|
|
@@ -98,7 +103,7 @@ export const configSchema = z
|
|
|
98
103
|
"baseline-file": z.string().optional(),
|
|
99
104
|
"bootstrap-sha": z.string().optional(),
|
|
100
105
|
"monorepo-mode": z.enum(["independent", "fixed"]).optional(),
|
|
101
|
-
"bump-minor-pre-major":
|
|
106
|
+
"bump-minor-pre-major": deprecatedBumpMinorPreMajorSchema,
|
|
102
107
|
"allow-stable-major": z.boolean().optional(),
|
|
103
108
|
"include-commit-authors": z.boolean().optional(),
|
|
104
109
|
"exclude-paths": z.array(z.string()).optional(),
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { loadConfig } from "../config/load-config.js";
|
|
3
|
+
import { createReleasePlan } from "./plan.js";
|
|
4
|
+
import { buildReleaseTargets, isReleaseCommitMessage, preparePendingReleasePr, prepareReleasePr, } from "./pr.js";
|
|
5
|
+
import { runReleaseDetailed, } from "./release.js";
|
|
6
|
+
import { hasFullyUntaggedPendingRelease, hasReleaseStateChangeAtHead, readPendingReleaseTargets, } from "./state.js";
|
|
7
|
+
import { releaseTargetHandoff } from "./targets.js";
|
|
8
|
+
function git(cwd, ...args) {
|
|
9
|
+
return execFileSync("git", args, {
|
|
10
|
+
cwd,
|
|
11
|
+
encoding: "utf8",
|
|
12
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
13
|
+
}).trim();
|
|
14
|
+
}
|
|
15
|
+
function resolveDirectBranch(cwd) {
|
|
16
|
+
const ref = process.env.GITHUB_REF ?? "";
|
|
17
|
+
const branch = process.env.VERSIONARY_BASE_BRANCH ||
|
|
18
|
+
(ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : "") ||
|
|
19
|
+
git(cwd, "branch", "--show-current");
|
|
20
|
+
if (!branch) {
|
|
21
|
+
throw new Error("Direct releases require a branch. Check out the release base branch or set VERSIONARY_BASE_BRANCH.");
|
|
22
|
+
}
|
|
23
|
+
git(cwd, "check-ref-format", `refs/heads/${branch}`);
|
|
24
|
+
return branch;
|
|
25
|
+
}
|
|
26
|
+
function pushDirectRelease(cwd, branch) {
|
|
27
|
+
git(cwd, "fetch", "origin", `refs/heads/${branch}`);
|
|
28
|
+
try {
|
|
29
|
+
// An earlier release may finish after a newer commit has reached trunk.
|
|
30
|
+
git(cwd, "merge-base", "--is-ancestor", "HEAD", "FETCH_HEAD");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// A normal push rejects concurrent divergent updates without rewriting trunk.
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
git(cwd, "push", "origin", `HEAD:refs/heads/${branch}`);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new Error(`Failed pushing direct release commit to ${branch}; no release was published. Check branch permissions and whether the branch advanced, then retry.`, { cause: error });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function runDirectRelease(cwd = process.cwd(), options = {}) {
|
|
44
|
+
const releaseContext = isReleaseCommitMessage(git(cwd, "log", "-1", "--pretty=%B")) ||
|
|
45
|
+
hasReleaseStateChangeAtHead(cwd);
|
|
46
|
+
if (!releaseContext) {
|
|
47
|
+
const pending = hasFullyUntaggedPendingRelease(cwd);
|
|
48
|
+
const plan = pending ? undefined : createReleasePlan(cwd);
|
|
49
|
+
if (plan && !plan.nextVersion) {
|
|
50
|
+
return {
|
|
51
|
+
action: "noop",
|
|
52
|
+
message: "No releasable commits found. Nothing to do.",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (options["dry-run"]) {
|
|
56
|
+
const targets = plan
|
|
57
|
+
? buildReleaseTargets(cwd, plan, loadConfig(cwd).config)
|
|
58
|
+
: readPendingReleaseTargets(cwd);
|
|
59
|
+
return {
|
|
60
|
+
action: "release-dry-run",
|
|
61
|
+
message: `Dry run: would ${pending ? "recover and publish" : "prepare and publish"} releases ${targets.map((target) => target.tag).join(", ")}`,
|
|
62
|
+
targets: targets.map(({ tag, version }) => ({ tag, version })),
|
|
63
|
+
releaseTargets: releaseTargetHandoff(targets, options.logger),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const branch = resolveDirectBranch(cwd);
|
|
67
|
+
if (pending) {
|
|
68
|
+
preparePendingReleasePr(cwd, { logger: options.logger, branch });
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
prepareReleasePr(cwd, { logger: options.logger, branch });
|
|
72
|
+
}
|
|
73
|
+
pushDirectRelease(cwd, branch);
|
|
74
|
+
}
|
|
75
|
+
else if (!options["dry-run"]) {
|
|
76
|
+
// A retry may start at a release commit whose previous branch push failed.
|
|
77
|
+
pushDirectRelease(cwd, resolveDirectBranch(cwd));
|
|
78
|
+
}
|
|
79
|
+
return runReleaseDetailed(cwd, options);
|
|
80
|
+
}
|
package/dist/release/plan.js
CHANGED
|
@@ -10,20 +10,11 @@ import { readBaselineSha, readReleaseTargets } from "./state.js";
|
|
|
10
10
|
function getMode(configMode) {
|
|
11
11
|
return configMode ?? "independent";
|
|
12
12
|
}
|
|
13
|
-
function
|
|
14
|
-
if (packageConfig?.["bump-minor-pre-major"] !== undefined) {
|
|
15
|
-
return packageConfig["bump-minor-pre-major"];
|
|
16
|
-
}
|
|
13
|
+
function resolveAllowStableMajor(config, packageConfig) {
|
|
17
14
|
if (packageConfig?.["allow-stable-major"] !== undefined) {
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
if (config["bump-minor-pre-major"] !== undefined) {
|
|
21
|
-
return config["bump-minor-pre-major"];
|
|
22
|
-
}
|
|
23
|
-
if (config["allow-stable-major"] !== undefined) {
|
|
24
|
-
return !config["allow-stable-major"];
|
|
15
|
+
return packageConfig["allow-stable-major"];
|
|
25
16
|
}
|
|
26
|
-
return
|
|
17
|
+
return config["allow-stable-major"] ?? false;
|
|
27
18
|
}
|
|
28
19
|
export function getChangelogDefaults(config) {
|
|
29
20
|
const changelogFormat = config["changelog-format"] ??
|
|
@@ -65,8 +56,8 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
65
56
|
const releaseBranchPrefix = loaded.config["release-branch"] ?? "versionary/release";
|
|
66
57
|
const baselineSha = readBaselineSha(cwd) ?? loaded.config["bootstrap-sha"] ?? null;
|
|
67
58
|
const releaseTargetByPath = new Map(readReleaseTargets(cwd).map((target) => [target.path, target]));
|
|
68
|
-
const
|
|
69
|
-
const
|
|
59
|
+
const allowStableMajor = resolveAllowStableMajor(loaded.config);
|
|
60
|
+
const allowStableMajorForPath = (packagePath) => resolveAllowStableMajor(loaded.config, loaded.config.packages?.[packagePath]);
|
|
70
61
|
const monorepoMode = getMode(loaded.config["monorepo-mode"]);
|
|
71
62
|
const buildPackagePlan = (pkg) => {
|
|
72
63
|
const packageContext = resolvePackageStrategyContext(loaded.config, pkg.path, pkg.config);
|
|
@@ -109,7 +100,7 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
109
100
|
releaseType = analyzedType;
|
|
110
101
|
nextVersion = releaseType
|
|
111
102
|
? bumpVersion(packageCurrentVersion, releaseType, {
|
|
112
|
-
|
|
103
|
+
allowStableMajor: allowStableMajorForPath(pkg.path),
|
|
113
104
|
})
|
|
114
105
|
: null;
|
|
115
106
|
bumpReason = nextVersion ? "direct" : undefined;
|
|
@@ -184,7 +175,7 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
184
175
|
const current = packageCurrentVersionByPath[target.path] ?? target.currentVersion;
|
|
185
176
|
target.releaseType = "patch";
|
|
186
177
|
target.nextVersion = bumpVersion(current, "patch", {
|
|
187
|
-
|
|
178
|
+
allowStableMajor: allowStableMajorForPath(target.path),
|
|
188
179
|
});
|
|
189
180
|
target.bumpReason = reason;
|
|
190
181
|
const strategyContext = strategyContextByPath.get(target.path);
|
|
@@ -217,7 +208,7 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
217
208
|
}
|
|
218
209
|
const hypotheticalVersion = strategyContext.nextVersion ??
|
|
219
210
|
bumpVersion(packageCurrentVersionByPath[sourcePath] ??
|
|
220
|
-
strategyContext.currentVersion, "patch", {
|
|
211
|
+
strategyContext.currentVersion, "patch", { allowStableMajor: allowStableMajorForPath(sourcePath) });
|
|
221
212
|
return strategyGroup.strategy.propagateDependentPatchImpacts(cwd, strategyGroup.packages.map((pkg) => ({
|
|
222
213
|
...pkg,
|
|
223
214
|
nextVersion: pkg.packagePath === sourcePath ? hypotheticalVersion : null,
|
|
@@ -401,7 +392,7 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
401
392
|
const baseVersion = packageCurrentVersionByPath[pkgPlan.path] ?? pkgPlan.currentVersion;
|
|
402
393
|
const nextVersion = combinedReleaseType
|
|
403
394
|
? bumpVersion(baseVersion, combinedReleaseType, {
|
|
404
|
-
|
|
395
|
+
allowStableMajor: allowStableMajorForPath(pkgPlan.path),
|
|
405
396
|
})
|
|
406
397
|
: null;
|
|
407
398
|
return {
|
|
@@ -444,7 +435,7 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
444
435
|
? rootPackagePlan.nextVersion
|
|
445
436
|
: analyzedFixedType
|
|
446
437
|
? bumpVersion(fixedBaseVersion, analyzedFixedType, {
|
|
447
|
-
|
|
438
|
+
allowStableMajor,
|
|
448
439
|
})
|
|
449
440
|
: null;
|
|
450
441
|
// Fixed mode can promote unchanged dependencies into the release, so the
|
|
@@ -482,7 +473,7 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
482
473
|
? rootPackagePlan.nextVersion
|
|
483
474
|
: analyzedOverallType
|
|
484
475
|
? bumpVersion(overallBaseVersion, analyzedOverallType, {
|
|
485
|
-
|
|
476
|
+
allowStableMajor,
|
|
486
477
|
})
|
|
487
478
|
: null;
|
|
488
479
|
return {
|
package/dist/release/pr.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadConfig } from "../config/load-config.js";
|
|
1
2
|
import type { ParsedCommit } from "../git/commits.js";
|
|
2
3
|
import type { VersionaryChangelogFormat, VersionaryConfig } from "../types/config.js";
|
|
3
4
|
import type { VersionaryPluginContext } from "../types/plugins.js";
|
|
@@ -21,6 +22,7 @@ export declare function splitSafeDirtyFiles(files: string[]): {
|
|
|
21
22
|
ignored: string[];
|
|
22
23
|
blocking: string[];
|
|
23
24
|
};
|
|
25
|
+
export declare function buildReleaseTargets(cwd: string, plan: ReleasePlan, loadedConfig: ReturnType<typeof loadConfig>["config"]): ReleaseTargetState[];
|
|
24
26
|
export interface PendingReleasePrResult {
|
|
25
27
|
branch: string;
|
|
26
28
|
title: string;
|
|
@@ -36,6 +38,7 @@ export declare function renderPendingReleaseReviewRequestBody(targets: ReleaseTa
|
|
|
36
38
|
*/
|
|
37
39
|
export declare function preparePendingReleasePr(cwd?: string, options?: {
|
|
38
40
|
logger?: VersionaryPluginContext["logger"];
|
|
41
|
+
branch?: string;
|
|
39
42
|
}): PendingReleasePrResult;
|
|
40
43
|
/**
|
|
41
44
|
* Recreate each unpublished package cohort on its own corrected-base branch.
|
|
@@ -48,6 +51,7 @@ export declare function preparePendingSeparateReleasePrs(cwd?: string, options?:
|
|
|
48
51
|
}): SeparateReviewCandidate[];
|
|
49
52
|
export declare function prepareReleasePr(cwd?: string, options?: {
|
|
50
53
|
logger?: VersionaryPluginContext["logger"];
|
|
54
|
+
branch?: string;
|
|
51
55
|
}): {
|
|
52
56
|
branch: string;
|
|
53
57
|
title: string;
|
package/dist/release/pr.js
CHANGED
|
@@ -133,7 +133,7 @@ function fetchRemoteReleaseBranch(cwd, branch) {
|
|
|
133
133
|
});
|
|
134
134
|
return remoteRef;
|
|
135
135
|
}
|
|
136
|
-
function buildReleaseTargets(cwd, plan, loadedConfig) {
|
|
136
|
+
export function buildReleaseTargets(cwd, plan, loadedConfig) {
|
|
137
137
|
const releasingPaths = new Set(plan.packages?.filter((pkg) => pkg.nextVersion).map((pkg) => pkg.path) ?? [
|
|
138
138
|
".",
|
|
139
139
|
]);
|
|
@@ -237,7 +237,7 @@ export function preparePendingReleasePr(cwd = process.cwd(), options = {}) {
|
|
|
237
237
|
}
|
|
238
238
|
ensureCleanWorktree(cwd, options.logger);
|
|
239
239
|
const loaded = loadConfig(cwd);
|
|
240
|
-
const branch = loaded.config["release-branch"] ?? "versionary/release";
|
|
240
|
+
const branch = options.branch ?? loaded.config["release-branch"] ?? "versionary/release";
|
|
241
241
|
const title = formatReleaseCommitTitle(targets);
|
|
242
242
|
const releaseBaselineSha = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
243
243
|
cwd,
|
|
@@ -466,7 +466,7 @@ export function prepareReleasePr(cwd = process.cwd(), options = {}) {
|
|
|
466
466
|
updatedChangelogFiles.push(packageChangelogPath);
|
|
467
467
|
}
|
|
468
468
|
const releaseTargets = buildReleaseTargets(cwd, plan, loaded.config);
|
|
469
|
-
const branch = plan.releaseBranchPrefix;
|
|
469
|
+
const branch = options.branch ?? plan.releaseBranchPrefix;
|
|
470
470
|
const title = formatReleaseCommitTitle(releaseTargets);
|
|
471
471
|
const hasRemoteReleaseBranch = remoteReleaseBranchExists(cwd, branch);
|
|
472
472
|
const remoteReleaseRef = hasRemoteReleaseBranch
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { VersionaryConfig } from "../types/config.js";
|
|
2
2
|
import type { VersionaryPluginContext } from "../types/plugins.js";
|
|
3
3
|
export declare function resolveTargetPackageName(cwd: string, config: VersionaryConfig, targetPath: string): string | undefined;
|
|
4
|
+
export declare function resolveTargetReleaseDraft(config: VersionaryConfig, targetPath: string): boolean;
|
|
4
5
|
export declare function extractReleaseNotes(content: string, version: string, changelogFormat: "markdown-changelog" | "r-news"): string;
|
|
5
6
|
export declare function extractClosingReferencesFromNotes(notes: string): number[];
|
|
6
7
|
export declare function resolveTargetChangelogFile(config: VersionaryConfig, rootChangelogFile: string, targetPath: string): string;
|
package/dist/release/release.js
CHANGED
|
@@ -22,6 +22,11 @@ export function resolveTargetPackageName(cwd, config, targetPath) {
|
|
|
22
22
|
}
|
|
23
23
|
return resolved;
|
|
24
24
|
}
|
|
25
|
+
export function resolveTargetReleaseDraft(config, targetPath) {
|
|
26
|
+
return (config.packages?.[targetPath]?.["release-draft"] ??
|
|
27
|
+
config["release-draft"] ??
|
|
28
|
+
false);
|
|
29
|
+
}
|
|
25
30
|
export function extractReleaseNotes(content, version, changelogFormat) {
|
|
26
31
|
const lines = content.split("\n");
|
|
27
32
|
const shortVersion = version.replace(/\.\d+$/u, "");
|
|
@@ -198,7 +203,7 @@ export async function runReleaseDetailed(cwd = process.cwd(), options = {}) {
|
|
|
198
203
|
tag: target.tag,
|
|
199
204
|
version: target.version,
|
|
200
205
|
notes: releaseNotes,
|
|
201
|
-
draft: loaded.config
|
|
206
|
+
draft: resolveTargetReleaseDraft(loaded.config, target.path),
|
|
202
207
|
makeLatest: target.path === "." ? "true" : "false",
|
|
203
208
|
}, {
|
|
204
209
|
createReleaseMetadata: (input) => scmClient.createReleaseMetadata(input, {
|
package/dist/release/semver.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export interface ParsedVersion {
|
|
|
8
8
|
build: string[];
|
|
9
9
|
}
|
|
10
10
|
export interface BumpVersionOptions {
|
|
11
|
-
|
|
11
|
+
allowStableMajor?: boolean;
|
|
12
12
|
}
|
|
13
13
|
export declare function parseVersion(version: string): ParsedVersion;
|
|
14
14
|
export declare function isValidVersion(version: string): boolean;
|
package/dist/release/semver.js
CHANGED
|
@@ -137,9 +137,9 @@ export function compareVersions(leftRaw, rightRaw) {
|
|
|
137
137
|
}
|
|
138
138
|
export function bumpVersion(current, releaseType, options = {}) {
|
|
139
139
|
const parsed = parseVersion(current);
|
|
140
|
-
const
|
|
140
|
+
const allowStableMajor = options.allowStableMajor ?? false;
|
|
141
141
|
if (releaseType === "major") {
|
|
142
|
-
if (parsed.major === 0 &&
|
|
142
|
+
if (parsed.major === 0 && !allowStableMajor) {
|
|
143
143
|
return `0.${parsed.minor + 1}.0`;
|
|
144
144
|
}
|
|
145
145
|
return `${parsed.major + 1}.0.0`;
|
|
@@ -12,32 +12,37 @@ export function verifyProject(cwd = process.cwd()) {
|
|
|
12
12
|
details: `Loaded ${path.basename(config.path)} (${config.format})`,
|
|
13
13
|
category: "config",
|
|
14
14
|
});
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
category: "version-files",
|
|
23
|
-
remediation: exists
|
|
24
|
-
? undefined
|
|
25
|
-
: `Create ${versionFile} or set "version-file" to the correct path for your release strategy.`,
|
|
26
|
-
});
|
|
27
|
-
if (exists) {
|
|
28
|
-
const validationError = strategy.validateProject?.(cwd, config.config);
|
|
15
|
+
const packages = Object.entries(config.config.packages ?? {});
|
|
16
|
+
const shouldValidateRoot = packages.length === 0 ||
|
|
17
|
+
packages.some(([packagePath]) => packagePath === ".");
|
|
18
|
+
if (shouldValidateRoot) {
|
|
19
|
+
const strategy = resolveVersionStrategy(config.config);
|
|
20
|
+
const versionFile = strategy.getVersionFile(config.config);
|
|
21
|
+
const exists = fs.existsSync(path.join(cwd, versionFile));
|
|
29
22
|
checks.push({
|
|
30
|
-
name: `
|
|
31
|
-
ok:
|
|
32
|
-
details:
|
|
23
|
+
name: `version-file:${versionFile}`,
|
|
24
|
+
ok: exists,
|
|
25
|
+
details: exists ? "Version file exists" : `Missing ${versionFile}`,
|
|
33
26
|
category: "version-files",
|
|
34
|
-
remediation:
|
|
35
|
-
?
|
|
36
|
-
:
|
|
27
|
+
remediation: exists
|
|
28
|
+
? undefined
|
|
29
|
+
: `Create ${versionFile} or set "version-file" to the correct path for your release strategy.`,
|
|
37
30
|
});
|
|
31
|
+
if (exists) {
|
|
32
|
+
const validationError = strategy.validateProject?.(cwd, config.config);
|
|
33
|
+
checks.push({
|
|
34
|
+
name: `strategy-validate:${strategy.name}`,
|
|
35
|
+
ok: !validationError,
|
|
36
|
+
details: validationError ?? "Strategy-level validation passed",
|
|
37
|
+
category: "version-files",
|
|
38
|
+
remediation: validationError
|
|
39
|
+
? `Fix strategy-specific version metadata in ${versionFile} for release-type "${strategy.name}".`
|
|
40
|
+
: undefined,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
38
43
|
}
|
|
39
|
-
if (
|
|
40
|
-
for (const [pkgPathRaw, packageConfig] of
|
|
44
|
+
if (packages.length > 0) {
|
|
45
|
+
for (const [pkgPathRaw, packageConfig] of packages) {
|
|
41
46
|
const pkgPath = path.join(cwd, pkgPathRaw);
|
|
42
47
|
const exists = fs.existsSync(pkgPath);
|
|
43
48
|
checks.push({
|
package/dist/strategy/python.js
CHANGED
|
@@ -140,7 +140,7 @@ const LOCKFILE_SPECS = [
|
|
|
140
140
|
installHint: "install PDM (https://pdm-project.org/)",
|
|
141
141
|
},
|
|
142
142
|
];
|
|
143
|
-
function refreshLockfile(cwd, spec) {
|
|
143
|
+
function refreshLockfile(cwd, spec, lockfilePath) {
|
|
144
144
|
try {
|
|
145
145
|
execFileSync(spec.command, [...spec.args], {
|
|
146
146
|
cwd,
|
|
@@ -149,7 +149,33 @@ function refreshLockfile(cwd, spec) {
|
|
|
149
149
|
}
|
|
150
150
|
catch (error) {
|
|
151
151
|
const message = error instanceof Error ? error.message : String(error);
|
|
152
|
-
throw new Error(`Failed to refresh ${
|
|
152
|
+
throw new Error(`Failed to refresh ${lockfilePath} via "${spec.command} ${spec.args.join(" ")}". Ensure ${spec.command} is on PATH (${spec.installHint}) or remove ${lockfilePath} from the working tree. Details: ${message}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function normalizeRelativePath(base, target) {
|
|
156
|
+
return path.relative(base, target).replaceAll("\\", "/");
|
|
157
|
+
}
|
|
158
|
+
function findNearestLockfileDirectory(cwd, packageDirectory, lockfile) {
|
|
159
|
+
const root = path.resolve(cwd);
|
|
160
|
+
let current = path.resolve(packageDirectory);
|
|
161
|
+
const relativePackageDirectory = path.relative(root, current);
|
|
162
|
+
if (relativePackageDirectory === ".." ||
|
|
163
|
+
relativePackageDirectory.startsWith(`..${path.sep}`) ||
|
|
164
|
+
path.isAbsolute(relativePackageDirectory)) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
while (true) {
|
|
168
|
+
if (fs.existsSync(path.join(current, lockfile))) {
|
|
169
|
+
return current;
|
|
170
|
+
}
|
|
171
|
+
if (current === root) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const parent = path.dirname(current);
|
|
175
|
+
if (parent === current) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
current = parent;
|
|
153
179
|
}
|
|
154
180
|
}
|
|
155
181
|
function readPyProjectFromCwd(cwd) {
|
|
@@ -301,16 +327,25 @@ export const pythonVersionStrategy = {
|
|
|
301
327
|
}
|
|
302
328
|
return null;
|
|
303
329
|
},
|
|
304
|
-
finalizeVersionWrites(cwd,
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
330
|
+
finalizeVersionWrites(cwd, writes, _context) {
|
|
331
|
+
const packageDirectories = [
|
|
332
|
+
...new Set(writes.map((write) => path.resolve(cwd, write.packagePath === "." ? "" : write.packagePath))),
|
|
333
|
+
].sort((a, b) => a.localeCompare(b));
|
|
334
|
+
const lockfiles = new Map();
|
|
335
|
+
for (const packageDirectory of packageDirectories) {
|
|
336
|
+
for (const spec of LOCKFILE_SPECS) {
|
|
337
|
+
const directory = findNearestLockfileDirectory(cwd, packageDirectory, spec.lockfile);
|
|
338
|
+
if (!directory) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const relativePath = normalizeRelativePath(cwd, path.join(directory, spec.lockfile));
|
|
342
|
+
lockfiles.set(relativePath, { directory, spec });
|
|
310
343
|
}
|
|
311
|
-
refreshLockfile(cwd, spec);
|
|
312
|
-
refreshed.push(spec.lockfile);
|
|
313
344
|
}
|
|
314
|
-
|
|
345
|
+
const refreshed = [...lockfiles.entries()].sort(([left], [right]) => left.localeCompare(right));
|
|
346
|
+
for (const [relativePath, { directory, spec }] of refreshed) {
|
|
347
|
+
refreshLockfile(directory, spec, relativePath);
|
|
348
|
+
}
|
|
349
|
+
return refreshed.map(([relativePath]) => relativePath);
|
|
315
350
|
},
|
|
316
351
|
};
|
package/dist/types/config.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface VersionaryPackage {
|
|
|
14
14
|
"package-name"?: string;
|
|
15
15
|
"changelog-file"?: string;
|
|
16
16
|
"changelog-format"?: VersionaryChangelogFormat;
|
|
17
|
+
"release-draft"?: boolean;
|
|
18
|
+
/** @deprecated Use `allow-stable-major` with the inverse value. */
|
|
17
19
|
"bump-minor-pre-major"?: boolean;
|
|
18
20
|
"allow-stable-major"?: boolean;
|
|
19
21
|
"exclude-paths"?: string[];
|
|
@@ -33,6 +35,7 @@ export interface VersionaryConfig {
|
|
|
33
35
|
"baseline-file"?: string;
|
|
34
36
|
"bootstrap-sha"?: string;
|
|
35
37
|
"monorepo-mode"?: "independent" | "fixed";
|
|
38
|
+
/** @deprecated Use `allow-stable-major` with the inverse value. */
|
|
36
39
|
"bump-minor-pre-major"?: boolean;
|
|
37
40
|
"allow-stable-major"?: boolean;
|
|
38
41
|
"include-commit-authors"?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "versionary",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "Automatic release framework based on conventional commits and semantic versioning",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"releasing",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"typescript": "^7.0.2",
|
|
40
40
|
"vite": "^8.0.0",
|
|
41
41
|
"vitepress": "^1.6.4",
|
|
42
|
-
"vitest": "^
|
|
42
|
+
"vitest": "^5.0.0"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "tsc -p tsconfig.json",
|