versionary 0.32.0 → 1.0.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/action/index.js +2 -2
- package/dist/cli/index.js +2 -7
- package/dist/config/schema.d.ts +4 -7
- package/dist/config/schema.js +8 -18
- package/dist/release/artifact-rules.d.ts +2 -2
- package/dist/release/artifact-rules.js +4 -4
- package/dist/release/changelog.d.ts +1 -3
- package/dist/release/changelog.js +29 -9
- package/dist/release/plan.d.ts +3 -5
- package/dist/release/plan.js +221 -65
- package/dist/release/pr.d.ts +7 -26
- package/dist/release/pr.js +4 -64
- package/dist/release/release-as.d.ts +29 -0
- package/dist/release/release-as.js +70 -0
- package/dist/release/release.d.ts +0 -8
- package/dist/release/release.js +0 -8
- package/dist/release/state.js +3 -12
- package/dist/strategy/composite.js +11 -0
- package/dist/strategy/node.js +15 -0
- package/dist/strategy/rust.js +50 -3
- package/dist/strategy/types.d.ts +11 -0
- package/dist/types/config.d.ts +1 -3
- package/package.json +2 -2
package/dist/action/index.js
CHANGED
|
@@ -57,9 +57,9 @@ function setOutput(name, value) {
|
|
|
57
57
|
appendFileSync(outputPath, `${name}<<${delimiter}\n${value}\n${delimiter}\n`, "utf8");
|
|
58
58
|
}
|
|
59
59
|
function main() {
|
|
60
|
-
const token = getInput("token")
|
|
60
|
+
const token = getInput("token");
|
|
61
61
|
if (!token) {
|
|
62
|
-
throw new Error("Input required and not supplied: token
|
|
62
|
+
throw new Error("Input required and not supplied: token.");
|
|
63
63
|
}
|
|
64
64
|
const versionaryVersion = getInput("versionary-version") || "0.7.0";
|
|
65
65
|
const cwd = getInput("working-directory") || ".";
|
package/dist/cli/index.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { loadConfig } from "../config/load-config.js";
|
|
4
3
|
import { prependChangelog, renderReleasePlanChangelog, } from "../release/changelog.js";
|
|
5
4
|
import { createReleasePlan } from "../release/plan.js";
|
|
6
|
-
import { closeStaleReviewRequestIfExists,
|
|
5
|
+
import { closeStaleReviewRequestIfExists, isReleaseCommitMessage, openOrUpdateReviewRequest, prepareReleasePr, pushReleaseBranch, resolveReleaseHighlights, } from "../release/pr.js";
|
|
7
6
|
import { runRelease, runReleaseDetailed } from "../release/release.js";
|
|
8
7
|
import { verifyProject } from "../release/verify-project.js";
|
|
9
8
|
function printVerifyResult() {
|
|
@@ -195,8 +194,7 @@ async function main() {
|
|
|
195
194
|
console.log("No releasable commits found.");
|
|
196
195
|
return 0;
|
|
197
196
|
}
|
|
198
|
-
const
|
|
199
|
-
const highlightsResult = resolveReleaseHighlights(process.cwd(), loaded.config, plan.changelogFile, plan.changelogFormat, logger);
|
|
197
|
+
const highlightsResult = resolveReleaseHighlights(process.cwd(), plan.changelogFile, plan.changelogFormat);
|
|
200
198
|
const section = renderReleasePlanChangelog(plan, {
|
|
201
199
|
highlights: highlightsResult.highlights,
|
|
202
200
|
});
|
|
@@ -205,9 +203,6 @@ async function main() {
|
|
|
205
203
|
return 0;
|
|
206
204
|
}
|
|
207
205
|
prependChangelog(process.cwd(), plan.changelogFile, section, plan.changelogFormat);
|
|
208
|
-
if (highlightsResult.source === "file" && highlightsResult.filePath) {
|
|
209
|
-
consumeNextReleaseFile(process.cwd(), highlightsResult.filePath);
|
|
210
|
-
}
|
|
211
206
|
console.log(`Updated ${plan.changelogFile}`);
|
|
212
207
|
return 0;
|
|
213
208
|
}
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -5,7 +5,6 @@ export declare const configSchema: z.ZodObject<{
|
|
|
5
5
|
"review-mode": z.ZodOptional<z.ZodEnum<{
|
|
6
6
|
direct: "direct";
|
|
7
7
|
pr: "pr";
|
|
8
|
-
review: "review";
|
|
9
8
|
}>>;
|
|
10
9
|
"version-file": z.ZodOptional<z.ZodString>;
|
|
11
10
|
"changelog-file": z.ZodOptional<z.ZodString>;
|
|
@@ -15,17 +14,16 @@ export declare const configSchema: z.ZodObject<{
|
|
|
15
14
|
}>>;
|
|
16
15
|
"release-draft": z.ZodOptional<z.ZodBoolean>;
|
|
17
16
|
"release-reference-comments": z.ZodOptional<z.ZodEnum<{
|
|
18
|
-
off: "off";
|
|
19
17
|
"best-effort": "best-effort";
|
|
18
|
+
off: "off";
|
|
20
19
|
strict: "strict";
|
|
21
20
|
}>>;
|
|
22
21
|
"release-branch": z.ZodOptional<z.ZodString>;
|
|
23
22
|
"baseline-file": z.ZodOptional<z.ZodString>;
|
|
24
|
-
"next-release-file": z.ZodOptional<z.ZodString>;
|
|
25
23
|
"bootstrap-sha": z.ZodOptional<z.ZodString>;
|
|
26
24
|
"monorepo-mode": z.ZodOptional<z.ZodEnum<{
|
|
27
|
-
independent: "independent";
|
|
28
25
|
fixed: "fixed";
|
|
26
|
+
independent: "independent";
|
|
29
27
|
}>>;
|
|
30
28
|
"bump-minor-pre-major": z.ZodOptional<z.ZodBoolean>;
|
|
31
29
|
"allow-stable-major": z.ZodOptional<z.ZodBoolean>;
|
|
@@ -45,14 +43,13 @@ export declare const configSchema: z.ZodObject<{
|
|
|
45
43
|
"extra-files": z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
46
44
|
type: z.ZodEnum<{
|
|
47
45
|
json: "json";
|
|
48
|
-
toml: "toml";
|
|
49
|
-
yaml: "yaml";
|
|
50
46
|
nix: "nix";
|
|
51
47
|
regex: "regex";
|
|
48
|
+
toml: "toml";
|
|
49
|
+
yaml: "yaml";
|
|
52
50
|
}>;
|
|
53
51
|
path: z.ZodString;
|
|
54
52
|
"field-path": z.ZodOptional<z.ZodString>;
|
|
55
|
-
jsonpath: z.ZodOptional<z.ZodString>;
|
|
56
53
|
pattern: z.ZodOptional<z.ZodString>;
|
|
57
54
|
replacement: z.ZodOptional<z.ZodString>;
|
|
58
55
|
}, z.core.$strip>>>;
|
package/dist/config/schema.js
CHANGED
|
@@ -4,44 +4,35 @@ const artifactRuleSchema = z
|
|
|
4
4
|
type: z.enum(["json", "toml", "yaml", "nix", "regex"]),
|
|
5
5
|
path: z.string().min(1),
|
|
6
6
|
"field-path": z.string().optional(),
|
|
7
|
-
jsonpath: z.string().optional(),
|
|
8
7
|
pattern: z.string().optional(),
|
|
9
8
|
replacement: z.string().optional(),
|
|
10
9
|
})
|
|
11
10
|
.superRefine((value, ctx) => {
|
|
12
|
-
const
|
|
11
|
+
const needsFieldPath = value.type === "json" ||
|
|
13
12
|
value.type === "toml" ||
|
|
14
13
|
value.type === "yaml" ||
|
|
15
14
|
value.type === "nix";
|
|
16
|
-
|
|
17
|
-
if (needsJsonPath && !hasFieldPath) {
|
|
15
|
+
if (needsFieldPath && !value["field-path"]) {
|
|
18
16
|
ctx.addIssue({
|
|
19
17
|
code: z.ZodIssueCode.custom,
|
|
20
|
-
message: `${value.type} artifact rules require "field-path"
|
|
18
|
+
message: `${value.type} artifact rules require "field-path".`,
|
|
21
19
|
path: ["field-path"],
|
|
22
20
|
});
|
|
23
21
|
}
|
|
24
|
-
if (
|
|
22
|
+
if (needsFieldPath && value.pattern) {
|
|
25
23
|
ctx.addIssue({
|
|
26
24
|
code: z.ZodIssueCode.custom,
|
|
27
25
|
message: `${value.type} artifact rules do not support "pattern".`,
|
|
28
26
|
path: ["pattern"],
|
|
29
27
|
});
|
|
30
28
|
}
|
|
31
|
-
if (
|
|
29
|
+
if (needsFieldPath && value.replacement) {
|
|
32
30
|
ctx.addIssue({
|
|
33
31
|
code: z.ZodIssueCode.custom,
|
|
34
32
|
message: `${value.type} artifact rules do not support "replacement".`,
|
|
35
33
|
path: ["replacement"],
|
|
36
34
|
});
|
|
37
35
|
}
|
|
38
|
-
if (value["field-path"] && value.jsonpath) {
|
|
39
|
-
ctx.addIssue({
|
|
40
|
-
code: z.ZodIssueCode.custom,
|
|
41
|
-
message: 'Specify only one of "field-path" or deprecated "jsonpath".',
|
|
42
|
-
path: ["field-path"],
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
36
|
if (value.type === "regex" && !value.pattern) {
|
|
46
37
|
ctx.addIssue({
|
|
47
38
|
code: z.ZodIssueCode.custom,
|
|
@@ -49,10 +40,10 @@ const artifactRuleSchema = z
|
|
|
49
40
|
path: ["pattern"],
|
|
50
41
|
});
|
|
51
42
|
}
|
|
52
|
-
if (value.type === "regex" &&
|
|
43
|
+
if (value.type === "regex" && value["field-path"]) {
|
|
53
44
|
ctx.addIssue({
|
|
54
45
|
code: z.ZodIssueCode.custom,
|
|
55
|
-
message: 'regex artifact rules do not support "field-path"
|
|
46
|
+
message: 'regex artifact rules do not support "field-path".',
|
|
56
47
|
path: ["field-path"],
|
|
57
48
|
});
|
|
58
49
|
}
|
|
@@ -75,7 +66,7 @@ export const configSchema = z
|
|
|
75
66
|
.object({
|
|
76
67
|
$schema: z.string().optional(),
|
|
77
68
|
version: z.literal(1),
|
|
78
|
-
"review-mode": z.enum(["direct", "pr"
|
|
69
|
+
"review-mode": z.enum(["direct", "pr"]).optional(),
|
|
79
70
|
"version-file": z.string().optional(),
|
|
80
71
|
"changelog-file": z.string().optional(),
|
|
81
72
|
"changelog-format": z.enum(["markdown-changelog", "r-news"]).optional(),
|
|
@@ -85,7 +76,6 @@ export const configSchema = z
|
|
|
85
76
|
.optional(),
|
|
86
77
|
"release-branch": z.string().optional(),
|
|
87
78
|
"baseline-file": z.string().optional(),
|
|
88
|
-
"next-release-file": z.string().optional(),
|
|
89
79
|
"bootstrap-sha": z.string().optional(),
|
|
90
80
|
"monorepo-mode": z.enum(["independent", "fixed"]).optional(),
|
|
91
81
|
"bump-minor-pre-major": z.boolean().optional(),
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { VersionaryConfig } from "../types/config.js";
|
|
2
|
-
import type {
|
|
3
|
-
export declare function applyConfiguredArtifactRules(cwd: string, config: VersionaryConfig, plan:
|
|
2
|
+
import type { ReleasePlan } from "./plan.js";
|
|
3
|
+
export declare function applyConfiguredArtifactRules(cwd: string, config: VersionaryConfig, plan: ReleasePlan): string[];
|
|
@@ -143,9 +143,9 @@ function setVersionAtJsonPath(document, fieldPath, version) {
|
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
function resolveFieldPath(rule) {
|
|
146
|
-
const fieldPath = rule["field-path"]
|
|
146
|
+
const fieldPath = rule["field-path"];
|
|
147
147
|
if (!fieldPath) {
|
|
148
|
-
throw new Error(`${rule.type} artifact rules require "field-path"
|
|
148
|
+
throw new Error(`${rule.type} artifact rules require "field-path".`);
|
|
149
149
|
}
|
|
150
150
|
return fieldPath;
|
|
151
151
|
}
|
|
@@ -207,8 +207,8 @@ function applyRegexRule(content, pattern, version, replacementTemplate) {
|
|
|
207
207
|
const rendered = renderReplacementTemplate(replacementTemplate, version);
|
|
208
208
|
return `${content.slice(0, start)}${rendered}${content.slice(start + full.length)}`;
|
|
209
209
|
}
|
|
210
|
-
//
|
|
211
|
-
// leaving the rest of the match intact. Splice by group indices rather than
|
|
210
|
+
// Without a template: substitute the full version into the first capture
|
|
211
|
+
// group, leaving the rest of the match intact. Splice by group indices rather than
|
|
212
212
|
// `String.replace` so literal `$` sequences and repeated group content are
|
|
213
213
|
// handled correctly.
|
|
214
214
|
const groupIndices = match.indices?.[1];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ParsedCommit } from "../git/commits.js";
|
|
2
2
|
import type { VersionaryChangelogFormat } from "../types/config.js";
|
|
3
|
-
import { type ReleasePlan
|
|
3
|
+
import { type ReleasePlan } from "./plan.js";
|
|
4
4
|
export declare function renderReleaseNotesSection(input: {
|
|
5
5
|
currentVersion: string;
|
|
6
6
|
nextVersion: string;
|
|
@@ -23,8 +23,6 @@ export declare function renderReleasePlanChangelog(plan: ReleasePlan, options?:
|
|
|
23
23
|
cwd?: string;
|
|
24
24
|
highlights?: string;
|
|
25
25
|
}): string;
|
|
26
|
-
/** @deprecated Use renderReleasePlanChangelog. */
|
|
27
|
-
export declare function renderSimpleChangelog(plan: SimplePlan): string;
|
|
28
26
|
/**
|
|
29
27
|
* Locate a manual-notes block at the top of an existing changelog and split it
|
|
30
28
|
* out. The notes block is the first release-level heading whose text is not a
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { applyRevertSuppression, inferReleaseTypeFromParsedCommit, parseConventionalCommitMessage, } from "../git/commits.js";
|
|
4
4
|
import { resolveRepositoryWebBaseUrl } from "../git/repo-url.js";
|
|
5
|
-
import { resolvePackageDependencies
|
|
5
|
+
import { resolvePackageDependencies } from "./plan.js";
|
|
6
6
|
import { isVersionHeading } from "./semver.js";
|
|
7
7
|
const REVIEW_REQUEST_FOOTER = "---\n\nThis PR was generated by [Versionary](https://github.com/jolars/versionary).";
|
|
8
8
|
function formatDate() {
|
|
@@ -68,6 +68,7 @@ function groupCommitLines(commits, repoUrl) {
|
|
|
68
68
|
const fixes = [];
|
|
69
69
|
const performance = [];
|
|
70
70
|
const reverts = [];
|
|
71
|
+
const other = [];
|
|
71
72
|
const effectiveCommits = applyRevertSuppression(commits);
|
|
72
73
|
const getRevertedSubject = (commit) => {
|
|
73
74
|
const normalizedDescription = (commit.description ?? "")
|
|
@@ -93,9 +94,6 @@ function groupCommitLines(commits, repoUrl) {
|
|
|
93
94
|
};
|
|
94
95
|
for (const commit of effectiveCommits) {
|
|
95
96
|
const type = inferReleaseTypeFromParsedCommit(commit);
|
|
96
|
-
if (!type) {
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
97
|
const { label, message } = formatCommitMessage(commit.subject);
|
|
100
98
|
const short = commit.hash.slice(0, 7);
|
|
101
99
|
const hashLabel = repoUrl
|
|
@@ -106,6 +104,10 @@ function groupCommitLines(commits, repoUrl) {
|
|
|
106
104
|
const line = `- ${label}${message} (${hashLabel})${referencesSuffix}`;
|
|
107
105
|
const commitType = (commit.type ?? "").toLowerCase();
|
|
108
106
|
const isBreaking = type === "major";
|
|
107
|
+
if (!type) {
|
|
108
|
+
other.push(line);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
109
111
|
if (commit.isRevert) {
|
|
110
112
|
if (!shouldIncludeRevert(commit)) {
|
|
111
113
|
continue;
|
|
@@ -128,7 +130,23 @@ function groupCommitLines(commits, repoUrl) {
|
|
|
128
130
|
fixes.push(line);
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
|
-
return { breaking, features, fixes, performance, reverts };
|
|
133
|
+
return { breaking, features, fixes, performance, reverts, other };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* A forced bump — a stale dependency, or a propagated requirement rewrite —
|
|
137
|
+
* can carry no release-worthy commits at all. Rather than publish a bare
|
|
138
|
+
* version heading, such a release falls back to listing whatever else is
|
|
139
|
+
* shipping under it.
|
|
140
|
+
*/
|
|
141
|
+
function needsOtherChangesFallback(grouped, hasHighlights, dependencyCount) {
|
|
142
|
+
if (hasHighlights || dependencyCount > 0 || grouped.other.length === 0) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return (grouped.breaking.length === 0 &&
|
|
146
|
+
grouped.features.length === 0 &&
|
|
147
|
+
grouped.fixes.length === 0 &&
|
|
148
|
+
grouped.performance.length === 0 &&
|
|
149
|
+
grouped.reverts.length === 0);
|
|
132
150
|
}
|
|
133
151
|
export function renderReleaseNotesSection(input, options = {}) {
|
|
134
152
|
const repoUrl = resolveRepositoryWebBaseUrl(input.cwd ?? process.cwd());
|
|
@@ -161,6 +179,9 @@ export function renderReleaseNotesSection(input, options = {}) {
|
|
|
161
179
|
if (input.dependencies && input.dependencies.length > 0) {
|
|
162
180
|
sections.push("### Dependencies", ...input.dependencies.map((dependency) => `- updated ${dependency.name} to v${dependency.version}`), "");
|
|
163
181
|
}
|
|
182
|
+
if (needsOtherChangesFallback(grouped, trimmedHighlights.length > 0, input.dependencies?.length ?? 0)) {
|
|
183
|
+
sections.push("### Other changes", ...grouped.other, "");
|
|
184
|
+
}
|
|
164
185
|
const body = [header, "", ...sections].join("\n").trimEnd();
|
|
165
186
|
if (options.includeFooter) {
|
|
166
187
|
return `${body}\n\n${renderReviewRequestFooter()}`;
|
|
@@ -198,10 +219,6 @@ export function renderReleasePlanChangelog(plan, options = {}) {
|
|
|
198
219
|
includeFooter: options.includeFooter,
|
|
199
220
|
});
|
|
200
221
|
}
|
|
201
|
-
/** @deprecated Use renderReleasePlanChangelog. */
|
|
202
|
-
export function renderSimpleChangelog(plan) {
|
|
203
|
-
return renderReleasePlanChangelog(plan);
|
|
204
|
-
}
|
|
205
222
|
/**
|
|
206
223
|
* Locate a manual-notes block at the top of an existing changelog and split it
|
|
207
224
|
* out. The notes block is the first release-level heading whose text is not a
|
|
@@ -292,6 +309,9 @@ export function renderRNewsReleaseNotes(input) {
|
|
|
292
309
|
if (grouped.reverts.length > 0) {
|
|
293
310
|
sections.push("## Reverts", "", ...grouped.reverts, "");
|
|
294
311
|
}
|
|
312
|
+
if (needsOtherChangesFallback(grouped, trimmedHighlights.length > 0, 0)) {
|
|
313
|
+
sections.push("## Other changes", "", ...grouped.other, "");
|
|
314
|
+
}
|
|
295
315
|
return [`# ${input.packageName} ${normalizedVersion}`, "", ...sections]
|
|
296
316
|
.join("\n")
|
|
297
317
|
.trimEnd();
|
package/dist/release/plan.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ParsedCommit } from "../git/commits.js";
|
|
2
2
|
import type { VersionaryChangelogFormat, VersionaryConfig } from "../types/config.js";
|
|
3
3
|
import { type ReleaseType } from "./semver.js";
|
|
4
|
+
type BumpReason = "direct" | "dependency-propagation" | "stale-dependency" | "follows" | "release-as";
|
|
4
5
|
export interface ReleasePlan {
|
|
5
6
|
mode: "simple";
|
|
6
7
|
releaseType: ReleaseType;
|
|
@@ -18,13 +19,11 @@ export interface ReleasePlan {
|
|
|
18
19
|
releaseType: ReleaseType;
|
|
19
20
|
currentVersion: string;
|
|
20
21
|
nextVersion: string | null;
|
|
21
|
-
bumpReason?:
|
|
22
|
+
bumpReason?: BumpReason;
|
|
22
23
|
dependencySourcePaths?: string[];
|
|
23
24
|
commits: ParsedCommit[];
|
|
24
25
|
}>;
|
|
25
26
|
}
|
|
26
|
-
/** @deprecated Use ReleasePlan. */
|
|
27
|
-
export type SimplePlan = ReleasePlan;
|
|
28
27
|
export declare function getChangelogDefaults(config: {
|
|
29
28
|
"release-type"?: VersionaryConfig["release-type"];
|
|
30
29
|
"changelog-file"?: VersionaryConfig["changelog-file"];
|
|
@@ -35,9 +34,8 @@ export declare function getChangelogDefaults(config: {
|
|
|
35
34
|
changelogFormat: VersionaryChangelogFormat;
|
|
36
35
|
};
|
|
37
36
|
export declare function createReleasePlan(cwd?: string): ReleasePlan;
|
|
38
|
-
/** @deprecated Use createReleasePlan. */
|
|
39
|
-
export declare function createSimplePlan(cwd?: string): ReleasePlan;
|
|
40
37
|
export declare function resolvePackageDependencies(plan: ReleasePlan, packagePath: string): Array<{
|
|
41
38
|
name: string;
|
|
42
39
|
version: string;
|
|
43
40
|
}>;
|
|
41
|
+
export {};
|
package/dist/release/plan.js
CHANGED
|
@@ -4,6 +4,7 @@ import { loadConfig } from "../config/load-config.js";
|
|
|
4
4
|
import { analyzeParsedCommits, applyRevertSuppression, getParsedCommitsForPath, getParsedCommitsSinceLastTag, } from "../git/commits.js";
|
|
5
5
|
import { resolvePackageStrategyContext } from "../strategy/package-context.js";
|
|
6
6
|
import { resolveVersionStrategy } from "../strategy/resolve.js";
|
|
7
|
+
import { releaseTypeBetween, resolveReleaseAsOverride } from "./release-as.js";
|
|
7
8
|
import { bumpVersion, maxReleaseType } from "./semver.js";
|
|
8
9
|
import { readBaselineSha, readReleaseTargets } from "./state.js";
|
|
9
10
|
function getMode(configMode) {
|
|
@@ -78,19 +79,34 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
78
79
|
}
|
|
79
80
|
const effectiveCommits = applyRevertSuppression(parsedCommits);
|
|
80
81
|
const commits = effectiveCommits;
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
const analyzedType = analyzeParsedCommits(parsedCommits);
|
|
83
|
+
const override = resolveReleaseAsOverride(commits, packageCurrentVersion);
|
|
84
|
+
let releaseType;
|
|
85
|
+
let nextVersion;
|
|
86
|
+
let bumpReason;
|
|
87
|
+
if (override) {
|
|
88
|
+
// An explicit `Release-As:` footer forces a release with the requested
|
|
89
|
+
// version, even when the conventional-commit analysis produces no bump.
|
|
90
|
+
nextVersion = override.version;
|
|
91
|
+
releaseType = releaseTypeBetween(packageCurrentVersion, override.version);
|
|
92
|
+
bumpReason = "release-as";
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
releaseType = analyzedType;
|
|
96
|
+
nextVersion = releaseType
|
|
97
|
+
? bumpVersion(packageCurrentVersion, releaseType, {
|
|
98
|
+
allowStableMajor: allowStableMajorForPath(pkg.path),
|
|
99
|
+
})
|
|
100
|
+
: null;
|
|
101
|
+
bumpReason = nextVersion ? "direct" : undefined;
|
|
102
|
+
}
|
|
87
103
|
return {
|
|
88
104
|
path: pkg.path,
|
|
89
105
|
implicitRoot: pkg.implicitRoot,
|
|
90
106
|
releaseType,
|
|
91
107
|
currentVersion: packageCurrentVersion,
|
|
92
108
|
nextVersion,
|
|
93
|
-
bumpReason
|
|
109
|
+
bumpReason,
|
|
94
110
|
commits,
|
|
95
111
|
parsedCommits,
|
|
96
112
|
resolvedVersionFile: packageContext.versionFile,
|
|
@@ -119,39 +135,179 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
119
135
|
...(implicitRootPlan ? [implicitRootPlan] : []),
|
|
120
136
|
].sort((a, b) => a.path.localeCompare(b.path));
|
|
121
137
|
const packageCurrentVersionByPath = {};
|
|
122
|
-
const packageNextVersionByPath = {};
|
|
123
138
|
const strategyPackagesByName = new Map();
|
|
139
|
+
// Strategy contexts are shared by reference with their group so that forcing
|
|
140
|
+
// a bump during the fixpoint below is immediately visible to the next
|
|
141
|
+
// `propagateDependentPatchImpacts` query.
|
|
142
|
+
const strategyContextByPath = new Map();
|
|
143
|
+
const strategyByPath = new Map();
|
|
124
144
|
for (const packagePlan of packagePlans) {
|
|
125
145
|
const packageConfig = loaded.config.packages?.[packagePlan.path] ?? {};
|
|
126
146
|
const packageContext = resolvePackageStrategyContext(loaded.config, packagePlan.path, packageConfig);
|
|
147
|
+
const strategyContext = {
|
|
148
|
+
packagePath: packagePlan.path,
|
|
149
|
+
versionFile: packagePlan.resolvedVersionFile,
|
|
150
|
+
currentVersion: packagePlan.currentVersion,
|
|
151
|
+
nextVersion: packagePlan.nextVersion,
|
|
152
|
+
};
|
|
153
|
+
strategyContextByPath.set(packagePlan.path, strategyContext);
|
|
154
|
+
strategyByPath.set(packagePlan.path, packageContext.strategy);
|
|
127
155
|
const existingGroup = strategyPackagesByName.get(packageContext.strategy.name);
|
|
128
156
|
if (existingGroup) {
|
|
129
|
-
existingGroup.packages.push(
|
|
130
|
-
packagePath: packagePlan.path,
|
|
131
|
-
versionFile: packagePlan.resolvedVersionFile,
|
|
132
|
-
currentVersion: packagePlan.currentVersion,
|
|
133
|
-
nextVersion: packagePlan.nextVersion,
|
|
134
|
-
});
|
|
157
|
+
existingGroup.packages.push(strategyContext);
|
|
135
158
|
}
|
|
136
159
|
else {
|
|
137
160
|
strategyPackagesByName.set(packageContext.strategy.name, {
|
|
138
161
|
strategy: packageContext.strategy,
|
|
139
|
-
packages: [
|
|
140
|
-
{
|
|
141
|
-
packagePath: packagePlan.path,
|
|
142
|
-
versionFile: packagePlan.resolvedVersionFile,
|
|
143
|
-
currentVersion: packagePlan.currentVersion,
|
|
144
|
-
nextVersion: packagePlan.nextVersion,
|
|
145
|
-
},
|
|
146
|
-
],
|
|
162
|
+
packages: [strategyContext],
|
|
147
163
|
});
|
|
148
164
|
}
|
|
149
165
|
packageCurrentVersionByPath[packagePlan.path] = packagePlan.currentVersion;
|
|
150
|
-
|
|
151
|
-
|
|
166
|
+
}
|
|
167
|
+
const workingPlans = packagePlans.map((pkgPlan) => ({ ...pkgPlan }));
|
|
168
|
+
const workingPlanByPath = new Map(workingPlans.map((pkgPlan) => [pkgPlan.path, pkgPlan]));
|
|
169
|
+
const forcePatchBump = (target, reason) => {
|
|
170
|
+
const current = packageCurrentVersionByPath[target.path] ?? target.currentVersion;
|
|
171
|
+
target.releaseType = "patch";
|
|
172
|
+
target.nextVersion = bumpVersion(current, "patch", {
|
|
173
|
+
allowStableMajor: allowStableMajorForPath(target.path),
|
|
174
|
+
});
|
|
175
|
+
target.bumpReason = reason;
|
|
176
|
+
const strategyContext = strategyContextByPath.get(target.path);
|
|
177
|
+
if (strategyContext) {
|
|
178
|
+
strategyContext.nextVersion = target.nextVersion;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Packages that record a version requirement on `sourcePath`, found by
|
|
183
|
+
* asking the strategy who would need a requirement rewrite if `sourcePath`
|
|
184
|
+
* alone released. Works whether or not `sourcePath` is currently bumping,
|
|
185
|
+
* so it doubles as a reverse-edge lookup for a package that has not entered
|
|
186
|
+
* the plan yet.
|
|
187
|
+
*/
|
|
188
|
+
const findDependents = (sourcePath) => {
|
|
189
|
+
const strategyContext = strategyContextByPath.get(sourcePath);
|
|
190
|
+
// The implicit root can name a version file that does not exist. Nothing
|
|
191
|
+
// can record a requirement on a package that has no manifest, and asking
|
|
192
|
+
// the strategy would make it read one.
|
|
193
|
+
if (!strategyContext ||
|
|
194
|
+
!fs.existsSync(path.join(cwd, strategyContext.versionFile))) {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
const strategy = strategyByPath.get(sourcePath);
|
|
198
|
+
const strategyGroup = strategy
|
|
199
|
+
? strategyPackagesByName.get(strategy.name)
|
|
200
|
+
: undefined;
|
|
201
|
+
if (!strategyGroup?.strategy.propagateDependentPatchImpacts) {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
const hypotheticalVersion = strategyContext.nextVersion ??
|
|
205
|
+
bumpVersion(packageCurrentVersionByPath[sourcePath] ??
|
|
206
|
+
strategyContext.currentVersion, "patch", { allowStableMajor: allowStableMajorForPath(sourcePath) });
|
|
207
|
+
return strategyGroup.strategy.propagateDependentPatchImpacts(cwd, strategyGroup.packages.map((pkg) => ({
|
|
208
|
+
...pkg,
|
|
209
|
+
nextVersion: pkg.packagePath === sourcePath ? hypotheticalVersion : null,
|
|
210
|
+
})));
|
|
211
|
+
};
|
|
212
|
+
const isPublishable = (packagePath) => {
|
|
213
|
+
const strategy = strategyByPath.get(packagePath);
|
|
214
|
+
const strategyContext = strategyContextByPath.get(packagePath);
|
|
215
|
+
if (!strategy?.isPublishable || !strategyContext) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
// Abstaining counts as publishable: the enforcement below should only be
|
|
219
|
+
// skipped on a positive signal that nothing reaches a registry.
|
|
220
|
+
return strategy.isPublishable(cwd, strategyContext) !== false;
|
|
221
|
+
};
|
|
222
|
+
// Forward dependency edges, inverted from the reverse-edge lookup above.
|
|
223
|
+
// Whether one package records a requirement on another is a property of the
|
|
224
|
+
// manifests, not of the versions in flight, so this is computed once.
|
|
225
|
+
const dependenciesByPath = new Map();
|
|
226
|
+
for (const pkgPlan of workingPlans) {
|
|
227
|
+
for (const dependentPath of findDependents(pkgPlan.path)) {
|
|
228
|
+
const existing = dependenciesByPath.get(dependentPath);
|
|
229
|
+
if (existing) {
|
|
230
|
+
existing.add(pkgPlan.path);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
dependenciesByPath.set(dependentPath, new Set([pkgPlan.path]));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Every package reachable by following dependency edges down from a
|
|
238
|
+
* publishable package that is currently releasing. Staleness matters
|
|
239
|
+
* transitively: a release that pulls in a stale grandparent resolves against
|
|
240
|
+
* the stale published copy just as readily as a direct dependency does.
|
|
241
|
+
*/
|
|
242
|
+
const collectReleasingDependencyClosure = () => {
|
|
243
|
+
const reachable = new Set();
|
|
244
|
+
const queue = workingPlans
|
|
245
|
+
.filter((pkgPlan) => pkgPlan.nextVersion && isPublishable(pkgPlan.path))
|
|
246
|
+
.map((pkgPlan) => pkgPlan.path);
|
|
247
|
+
while (queue.length > 0) {
|
|
248
|
+
const currentPath = queue.shift();
|
|
249
|
+
if (currentPath === undefined) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
for (const dependencyPath of dependenciesByPath.get(currentPath) ?? []) {
|
|
253
|
+
if (reachable.has(dependencyPath)) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
reachable.add(dependencyPath);
|
|
257
|
+
queue.push(dependencyPath);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return reachable;
|
|
261
|
+
};
|
|
262
|
+
// Both rules below can enable each other: forcing a bump creates a dependent
|
|
263
|
+
// whose requirement must be rewritten, and rewriting a dependent can in turn
|
|
264
|
+
// expose a stale dependency a further level up. Iterating to a fixpoint
|
|
265
|
+
// avoids having to order them, and terminates because a package can only
|
|
266
|
+
// ever move from not-releasing to releasing.
|
|
267
|
+
for (let iteration = 0; iteration <= workingPlans.length; iteration += 1) {
|
|
268
|
+
let changed = false;
|
|
269
|
+
// Rule 1: a package whose recorded requirement on a releasing sibling
|
|
270
|
+
// would change must release too, or the rewritten requirement ships in the
|
|
271
|
+
// release commit without a version to publish it under.
|
|
272
|
+
for (const strategyGroup of strategyPackagesByName.values()) {
|
|
273
|
+
const impacted = strategyGroup.strategy.propagateDependentPatchImpacts?.(cwd, strategyGroup.packages) ?? [];
|
|
274
|
+
for (const impactedPath of impacted) {
|
|
275
|
+
const target = workingPlanByPath.get(impactedPath);
|
|
276
|
+
if (!target || target.nextVersion) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
forcePatchBump(target, "dependency-propagation");
|
|
280
|
+
changed = true;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Rule 2: a package that changed since its own last release but produced
|
|
284
|
+
// no bump would leave a releasing dependent pointing at a stale published
|
|
285
|
+
// copy. Commit type is deliberately not consulted — the exposure comes
|
|
286
|
+
// from the change existing, not from how it was labeled.
|
|
287
|
+
const reachableDependencies = collectReleasingDependencyClosure();
|
|
288
|
+
for (const candidate of workingPlans) {
|
|
289
|
+
if (candidate.nextVersion || candidate.commits.length === 0) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (!reachableDependencies.has(candidate.path)) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (!isPublishable(candidate.path)) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
forcePatchBump(candidate, "stale-dependency");
|
|
299
|
+
changed = true;
|
|
300
|
+
}
|
|
301
|
+
if (!changed) {
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const packageNextVersionByPath = {};
|
|
306
|
+
for (const pkgPlan of workingPlans) {
|
|
307
|
+
if (pkgPlan.nextVersion) {
|
|
308
|
+
packageNextVersionByPath[pkgPlan.path] = pkgPlan.nextVersion;
|
|
152
309
|
}
|
|
153
310
|
}
|
|
154
|
-
const impactedPaths = new Set();
|
|
155
311
|
const dependencySourcePathsByPackage = new Map();
|
|
156
312
|
const addDependencySourcePath = (targetPath, sourcePath) => {
|
|
157
313
|
if (targetPath === sourcePath) {
|
|
@@ -164,48 +320,30 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
164
320
|
}
|
|
165
321
|
dependencySourcePathsByPackage.set(targetPath, new Set([sourcePath]));
|
|
166
322
|
};
|
|
323
|
+
// Attribute each dependent's requirement rewrite to the specific sources
|
|
324
|
+
// driving it, using the settled version set so chained bumps are credited.
|
|
167
325
|
for (const strategyGroup of strategyPackagesByName.values()) {
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
for (const sourcePackage of sourcePackages) {
|
|
174
|
-
const scopedImpacts = strategyGroup.strategy.propagateDependentPatchImpacts?.(cwd, strategyGroup.packages.map((pkg) => ({
|
|
175
|
-
...pkg,
|
|
176
|
-
nextVersion: pkg.packagePath === sourcePackage.packagePath
|
|
177
|
-
? sourcePackage.nextVersion
|
|
178
|
-
: null,
|
|
179
|
-
}))) ?? [];
|
|
180
|
-
for (const impactedPath of scopedImpacts) {
|
|
326
|
+
for (const sourcePackage of strategyGroup.packages) {
|
|
327
|
+
if (!sourcePackage.nextVersion) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
for (const impactedPath of findDependents(sourcePackage.packagePath)) {
|
|
181
331
|
addDependencySourcePath(impactedPath, sourcePackage.packagePath);
|
|
182
332
|
}
|
|
183
333
|
}
|
|
184
334
|
}
|
|
185
|
-
const propagatedPackages =
|
|
335
|
+
const propagatedPackages = workingPlans.map((pkgPlan) => {
|
|
186
336
|
const dependencySourcePaths = [
|
|
187
337
|
...(dependencySourcePathsByPackage.get(pkgPlan.path) ??
|
|
188
338
|
new Set()),
|
|
189
339
|
]
|
|
190
340
|
.filter((sourcePath) => Boolean(packageNextVersionByPath[sourcePath]))
|
|
191
341
|
.sort((a, b) => a.localeCompare(b));
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
return pkgPlan;
|
|
195
|
-
}
|
|
196
|
-
return {
|
|
197
|
-
...pkgPlan,
|
|
198
|
-
dependencySourcePaths,
|
|
199
|
-
};
|
|
342
|
+
if (dependencySourcePaths.length === 0) {
|
|
343
|
+
return pkgPlan;
|
|
200
344
|
}
|
|
201
|
-
const current = packageCurrentVersionByPath[pkgPlan.path] ?? pkgPlan.currentVersion;
|
|
202
345
|
return {
|
|
203
346
|
...pkgPlan,
|
|
204
|
-
releaseType: "patch",
|
|
205
|
-
nextVersion: bumpVersion(current, "patch", {
|
|
206
|
-
allowStableMajor: allowStableMajorForPath(pkgPlan.path),
|
|
207
|
-
}),
|
|
208
|
-
bumpReason: "dependency-propagation",
|
|
209
347
|
dependencySourcePaths,
|
|
210
348
|
};
|
|
211
349
|
});
|
|
@@ -217,6 +355,11 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
217
355
|
}
|
|
218
356
|
}
|
|
219
357
|
const adjustedPackages = propagatedPackages.map((pkgPlan) => {
|
|
358
|
+
if (pkgPlan.bumpReason === "release-as") {
|
|
359
|
+
// An explicit override pins this package's version; do not let a
|
|
360
|
+
// followed source recompute it out from under the requested version.
|
|
361
|
+
return pkgPlan;
|
|
362
|
+
}
|
|
220
363
|
const followsSources = followsByPath.get(pkgPlan.path) ?? [];
|
|
221
364
|
const bumpingSources = followsSources
|
|
222
365
|
.map((sourcePath) => propagatedPackages.find((pkg) => pkg.path === sourcePath))
|
|
@@ -237,6 +380,7 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
237
380
|
].sort((a, b) => a.localeCompare(b));
|
|
238
381
|
const sourceDrove = combinedReleaseType !== ownReleaseType ||
|
|
239
382
|
pkgPlan.bumpReason === "dependency-propagation" ||
|
|
383
|
+
pkgPlan.bumpReason === "stale-dependency" ||
|
|
240
384
|
pkgPlan.bumpReason === undefined;
|
|
241
385
|
const baseVersion = packageCurrentVersionByPath[pkgPlan.path] ?? pkgPlan.currentVersion;
|
|
242
386
|
const nextVersion = combinedReleaseType
|
|
@@ -272,12 +416,19 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
272
416
|
commits: rootPackagePlan.commits,
|
|
273
417
|
};
|
|
274
418
|
}
|
|
419
|
+
const rootOverridden = rootPackagePlan.bumpReason === "release-as";
|
|
275
420
|
if (monorepoMode === "fixed") {
|
|
276
|
-
const
|
|
421
|
+
const analyzedFixedType = analyzeParsedCommits(adjustedPackages.flatMap((pkgPlan) => pkgPlan.parsedCommits));
|
|
277
422
|
const fixedBaseVersion = rootPackagePlan.currentVersion;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
423
|
+
// A `Release-As:` footer on the shared (root) version pins every package.
|
|
424
|
+
const fixedType = rootOverridden
|
|
425
|
+
? rootPackagePlan.releaseType
|
|
426
|
+
: analyzedFixedType;
|
|
427
|
+
const fixedNextVersion = rootOverridden
|
|
428
|
+
? rootPackagePlan.nextVersion
|
|
429
|
+
: analyzedFixedType
|
|
430
|
+
? bumpVersion(fixedBaseVersion, analyzedFixedType, { allowStableMajor })
|
|
431
|
+
: null;
|
|
281
432
|
const adjusted = adjustedPackages.map((pkgPlan) => ({
|
|
282
433
|
...pkgPlan,
|
|
283
434
|
releaseType: fixedType,
|
|
@@ -300,11 +451,20 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
300
451
|
.map(({ implicitRoot: _implicitRoot, ...pkgPlan }) => pkgPlan),
|
|
301
452
|
};
|
|
302
453
|
}
|
|
303
|
-
const
|
|
454
|
+
const analyzedOverallType = analyzeParsedCommits(adjustedPackages.flatMap((pkgPlan) => pkgPlan.parsedCommits));
|
|
304
455
|
const overallBaseVersion = rootPackagePlan.currentVersion;
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
456
|
+
// A root-level `Release-As:` footer drives the aggregate version too, keeping
|
|
457
|
+
// the top-level plan consistent with the pinned root package.
|
|
458
|
+
const overallType = rootOverridden
|
|
459
|
+
? rootPackagePlan.releaseType
|
|
460
|
+
: analyzedOverallType;
|
|
461
|
+
const overallNextVersion = rootOverridden
|
|
462
|
+
? rootPackagePlan.nextVersion
|
|
463
|
+
: analyzedOverallType
|
|
464
|
+
? bumpVersion(overallBaseVersion, analyzedOverallType, {
|
|
465
|
+
allowStableMajor,
|
|
466
|
+
})
|
|
467
|
+
: null;
|
|
308
468
|
return {
|
|
309
469
|
mode: "simple",
|
|
310
470
|
releaseType: overallType,
|
|
@@ -320,10 +480,6 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
320
480
|
packages: visiblePackages.map(({ implicitRoot: _implicitRoot, ...pkgPlan }) => pkgPlan),
|
|
321
481
|
};
|
|
322
482
|
}
|
|
323
|
-
/** @deprecated Use createReleasePlan. */
|
|
324
|
-
export function createSimplePlan(cwd = process.cwd()) {
|
|
325
|
-
return createReleasePlan(cwd);
|
|
326
|
-
}
|
|
327
483
|
export function resolvePackageDependencies(plan, packagePath) {
|
|
328
484
|
const target = plan.packages?.find((pkg) => pkg.path === packagePath);
|
|
329
485
|
if (!target) {
|
package/dist/release/pr.d.ts
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
import type { ParsedCommit } from "../git/commits.js";
|
|
2
2
|
import type { VersionaryChangelogFormat, VersionaryConfig } from "../types/config.js";
|
|
3
3
|
import type { VersionaryPluginContext } from "../types/plugins.js";
|
|
4
|
-
import { type ReleasePlan
|
|
5
|
-
export declare function getNextReleaseFile(config: VersionaryConfig): string;
|
|
6
|
-
export declare function readNextReleaseHighlights(cwd: string, config: VersionaryConfig): {
|
|
7
|
-
content: string;
|
|
8
|
-
filePath: string;
|
|
9
|
-
} | null;
|
|
10
|
-
export declare function consumeNextReleaseFile(cwd: string, filePath: string): {
|
|
11
|
-
tracked: boolean;
|
|
12
|
-
};
|
|
4
|
+
import { type ReleasePlan } from "./plan.js";
|
|
13
5
|
/**
|
|
14
6
|
* Read the manual-notes ("Unreleased") prose from the top of a changelog file.
|
|
15
7
|
* Returns an empty string when the file is absent or has no notes block.
|
|
@@ -17,15 +9,13 @@ export declare function consumeNextReleaseFile(cwd: string, filePath: string): {
|
|
|
17
9
|
export declare function readChangelogHighlights(cwd: string, changelogFile: string, format: VersionaryChangelogFormat): string;
|
|
18
10
|
export interface ResolvedReleaseHighlights {
|
|
19
11
|
highlights: string;
|
|
20
|
-
source: "changelog" | "
|
|
21
|
-
filePath?: string;
|
|
12
|
+
source: "changelog" | "none";
|
|
22
13
|
}
|
|
23
14
|
/**
|
|
24
|
-
* Resolve release highlights
|
|
25
|
-
*
|
|
26
|
-
* (emitting a warning) so existing setups keep working.
|
|
15
|
+
* Resolve release highlights from the editable "Unreleased" section at the top
|
|
16
|
+
* of the changelog.
|
|
27
17
|
*/
|
|
28
|
-
export declare function resolveReleaseHighlights(cwd: string,
|
|
18
|
+
export declare function resolveReleaseHighlights(cwd: string, changelogFile: string, format: VersionaryChangelogFormat): ResolvedReleaseHighlights;
|
|
29
19
|
export declare function splitSafeDirtyFiles(files: string[]): {
|
|
30
20
|
ignored: string[];
|
|
31
21
|
blocking: string[];
|
|
@@ -42,8 +32,8 @@ export declare function prepareReleasePr(cwd?: string, options?: {
|
|
|
42
32
|
updated: boolean;
|
|
43
33
|
highlights: string;
|
|
44
34
|
};
|
|
45
|
-
export declare function renderSimpleReviewRequestBody(version: string, previousVersion: string, commits: ParsedCommit[], plan?:
|
|
46
|
-
export declare function openOrUpdateReviewRequest(cwd: string, branch: string, title: string, version: string, previousVersion: string, commits: ParsedCommit[], plan?:
|
|
35
|
+
export declare function renderSimpleReviewRequestBody(version: string, previousVersion: string, commits: ParsedCommit[], plan?: ReleasePlan | null, cwd?: string, highlights?: string, loadedConfig?: VersionaryConfig): string;
|
|
36
|
+
export declare function openOrUpdateReviewRequest(cwd: string, branch: string, title: string, version: string, previousVersion: string, commits: ParsedCommit[], plan?: ReleasePlan | null, options?: {
|
|
47
37
|
logger?: VersionaryPluginContext["logger"];
|
|
48
38
|
highlights?: string;
|
|
49
39
|
}): Promise<string>;
|
|
@@ -54,14 +44,5 @@ export declare function closeStaleReviewRequestIfExists(cwd?: string, options?:
|
|
|
54
44
|
url?: string;
|
|
55
45
|
number?: number;
|
|
56
46
|
}>;
|
|
57
|
-
/** @deprecated Use prepareReleasePr. */
|
|
58
|
-
export declare function prepareSimpleReleasePr(cwd?: string, options?: {
|
|
59
|
-
logger?: VersionaryPluginContext["logger"];
|
|
60
|
-
}): ReturnType<typeof prepareReleasePr>;
|
|
61
|
-
/** @deprecated Use openOrUpdateReviewRequest. */
|
|
62
|
-
export declare function openOrUpdateSimpleReviewRequest(cwd: string, branch: string, title: string, version: string, previousVersion: string, commits: ParsedCommit[], plan?: SimplePlan | null, options?: {
|
|
63
|
-
logger?: VersionaryPluginContext["logger"];
|
|
64
|
-
highlights?: string;
|
|
65
|
-
}): Promise<string>;
|
|
66
47
|
export declare function pushReleaseBranch(cwd: string, branch: string): void;
|
|
67
48
|
export declare function isReleaseCommitMessage(commitMessage: string): boolean;
|
package/dist/release/pr.js
CHANGED
|
@@ -17,38 +17,6 @@ const SAFE_DIRTY_FILES = new Set([
|
|
|
17
17
|
"npm-shrinkwrap.json",
|
|
18
18
|
]);
|
|
19
19
|
const VERSIONARY_RELEASE_TRAILER = "Versionary-Release: true";
|
|
20
|
-
export function getNextReleaseFile(config) {
|
|
21
|
-
return config["next-release-file"] ?? "NEXT_RELEASE.md";
|
|
22
|
-
}
|
|
23
|
-
export function readNextReleaseHighlights(cwd, config) {
|
|
24
|
-
const filePath = getNextReleaseFile(config);
|
|
25
|
-
const fullPath = path.join(cwd, filePath);
|
|
26
|
-
if (!fs.existsSync(fullPath)) {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
const content = fs.readFileSync(fullPath, "utf8").trim();
|
|
30
|
-
return { content, filePath };
|
|
31
|
-
}
|
|
32
|
-
function isTrackedFile(cwd, filePath) {
|
|
33
|
-
try {
|
|
34
|
-
execFileSync("git", ["ls-files", "--error-unmatch", "--", filePath], {
|
|
35
|
-
cwd,
|
|
36
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
37
|
-
});
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
export function consumeNextReleaseFile(cwd, filePath) {
|
|
45
|
-
const tracked = isTrackedFile(cwd, filePath);
|
|
46
|
-
const fullPath = path.join(cwd, filePath);
|
|
47
|
-
if (fs.existsSync(fullPath)) {
|
|
48
|
-
fs.rmSync(fullPath);
|
|
49
|
-
}
|
|
50
|
-
return { tracked };
|
|
51
|
-
}
|
|
52
20
|
/**
|
|
53
21
|
* Read the manual-notes ("Unreleased") prose from the top of a changelog file.
|
|
54
22
|
* Returns an empty string when the file is absent or has no notes block.
|
|
@@ -62,24 +30,14 @@ export function readChangelogHighlights(cwd, changelogFile, format) {
|
|
|
62
30
|
return extractUnreleasedNotes(existing, format).highlights;
|
|
63
31
|
}
|
|
64
32
|
/**
|
|
65
|
-
* Resolve release highlights
|
|
66
|
-
*
|
|
67
|
-
* (emitting a warning) so existing setups keep working.
|
|
33
|
+
* Resolve release highlights from the editable "Unreleased" section at the top
|
|
34
|
+
* of the changelog.
|
|
68
35
|
*/
|
|
69
|
-
export function resolveReleaseHighlights(cwd,
|
|
36
|
+
export function resolveReleaseHighlights(cwd, changelogFile, format) {
|
|
70
37
|
const fromChangelog = readChangelogHighlights(cwd, changelogFile, format);
|
|
71
38
|
if (fromChangelog.length > 0) {
|
|
72
39
|
return { highlights: fromChangelog, source: "changelog" };
|
|
73
40
|
}
|
|
74
|
-
const fromFile = readNextReleaseHighlights(cwd, config);
|
|
75
|
-
if (fromFile) {
|
|
76
|
-
logger?.warn(`${fromFile.filePath} is deprecated; add release notes under an "Unreleased" heading at the top of ${changelogFile} instead.`);
|
|
77
|
-
return {
|
|
78
|
-
highlights: fromFile.content,
|
|
79
|
-
source: "file",
|
|
80
|
-
filePath: fromFile.filePath,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
41
|
return { highlights: "", source: "none" };
|
|
84
42
|
}
|
|
85
43
|
function listTrackedDirtyFiles(cwd) {
|
|
@@ -297,20 +255,11 @@ export function prepareReleasePr(cwd = process.cwd(), options = {}) {
|
|
|
297
255
|
}
|
|
298
256
|
const updatedArtifactFiles = applyConfiguredArtifactRules(cwd, loaded.config, plan);
|
|
299
257
|
const packageReleaseMetadata = buildPackageReleaseMetadata(cwd, plan, loaded.config);
|
|
300
|
-
const highlightsResult = resolveReleaseHighlights(cwd,
|
|
258
|
+
const highlightsResult = resolveReleaseHighlights(cwd, plan.changelogFile, plan.changelogFormat);
|
|
301
259
|
const highlights = highlightsResult.highlights;
|
|
302
260
|
const section = renderReleasePlanChangelog(plan, { highlights, cwd });
|
|
303
261
|
prependChangelog(cwd, plan.changelogFile, section, plan.changelogFormat);
|
|
304
262
|
const updatedChangelogFiles = [plan.changelogFile];
|
|
305
|
-
let consumedHighlightsPath = null;
|
|
306
|
-
// The changelog "Unreleased" block is stripped in-place by prependChangelog;
|
|
307
|
-
// only the legacy side file needs explicit consumption and staging.
|
|
308
|
-
if (highlightsResult.source === "file" && highlightsResult.filePath) {
|
|
309
|
-
const { tracked } = consumeNextReleaseFile(cwd, highlightsResult.filePath);
|
|
310
|
-
if (tracked) {
|
|
311
|
-
consumedHighlightsPath = highlightsResult.filePath;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
263
|
for (const packagePlan of plan.packages ?? []) {
|
|
315
264
|
if (!packagePlan.nextVersion || packagePlan.path === ".") {
|
|
316
265
|
continue;
|
|
@@ -357,7 +306,6 @@ export function prepareReleasePr(cwd = process.cwd(), options = {}) {
|
|
|
357
306
|
...updatedVersionFiles,
|
|
358
307
|
...updatedArtifactFiles,
|
|
359
308
|
...updatedChangelogFiles,
|
|
360
|
-
...(consumedHighlightsPath ? [consumedHighlightsPath] : []),
|
|
361
309
|
]),
|
|
362
310
|
];
|
|
363
311
|
execFileSync("git", ["add", ...filesToAdd], {
|
|
@@ -497,14 +445,6 @@ export async function closeStaleReviewRequestIfExists(cwd = process.cwd(), optio
|
|
|
497
445
|
}
|
|
498
446
|
return result;
|
|
499
447
|
}
|
|
500
|
-
/** @deprecated Use prepareReleasePr. */
|
|
501
|
-
export function prepareSimpleReleasePr(cwd = process.cwd(), options = {}) {
|
|
502
|
-
return prepareReleasePr(cwd, options);
|
|
503
|
-
}
|
|
504
|
-
/** @deprecated Use openOrUpdateReviewRequest. */
|
|
505
|
-
export async function openOrUpdateSimpleReviewRequest(cwd, branch, title, version, previousVersion, commits, plan = null, options = {}) {
|
|
506
|
-
return openOrUpdateReviewRequest(cwd, branch, title, version, previousVersion, commits, plan, options);
|
|
507
|
-
}
|
|
508
448
|
export function pushReleaseBranch(cwd, branch) {
|
|
509
449
|
execFileSync("git", ["push", "--force-with-lease", "origin", branch], {
|
|
510
450
|
cwd,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ParsedCommit } from "../git/commits.js";
|
|
2
|
+
import { type ReleaseType } from "./semver.js";
|
|
3
|
+
/**
|
|
4
|
+
* Footer token (case-insensitive) that requests an explicit next version, e.g.
|
|
5
|
+
*
|
|
6
|
+
* chore: graduate to stable release
|
|
7
|
+
*
|
|
8
|
+
* Release-As: 1.0.0
|
|
9
|
+
*
|
|
10
|
+
* The override forces a release with the requested version even when the
|
|
11
|
+
* conventional-commit analysis alone would produce no bump.
|
|
12
|
+
*/
|
|
13
|
+
export declare const RELEASE_AS_FOOTER_TOKEN = "release-as";
|
|
14
|
+
export interface ReleaseAsOverride {
|
|
15
|
+
/** The requested target version (leading `v` stripped, validated SemVer). */
|
|
16
|
+
version: string;
|
|
17
|
+
/** Hash of the commit carrying the winning footer. */
|
|
18
|
+
sourceHash: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Scan a release window for a `Release-As:` footer override.
|
|
22
|
+
*
|
|
23
|
+
* Returns the explicit target version, or `null` when no footer is present.
|
|
24
|
+
* Throws on an invalid version, a downgrade or no-op relative to
|
|
25
|
+
* `currentVersion`, or conflicting override versions within the window.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveReleaseAsOverride(commits: ParsedCommit[], currentVersion: string): ReleaseAsOverride | null;
|
|
28
|
+
/** Derive the semantic release level implied by moving `from` -> `to`. */
|
|
29
|
+
export declare function releaseTypeBetween(from: string, to: string): ReleaseType;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { compareVersions, isValidVersion, parseVersion, } from "./semver.js";
|
|
2
|
+
/**
|
|
3
|
+
* Footer token (case-insensitive) that requests an explicit next version, e.g.
|
|
4
|
+
*
|
|
5
|
+
* chore: graduate to stable release
|
|
6
|
+
*
|
|
7
|
+
* Release-As: 1.0.0
|
|
8
|
+
*
|
|
9
|
+
* The override forces a release with the requested version even when the
|
|
10
|
+
* conventional-commit analysis alone would produce no bump.
|
|
11
|
+
*/
|
|
12
|
+
export const RELEASE_AS_FOOTER_TOKEN = "release-as";
|
|
13
|
+
function normalizeToken(token) {
|
|
14
|
+
return token.trim().toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Scan a release window for a `Release-As:` footer override.
|
|
18
|
+
*
|
|
19
|
+
* Returns the explicit target version, or `null` when no footer is present.
|
|
20
|
+
* Throws on an invalid version, a downgrade or no-op relative to
|
|
21
|
+
* `currentVersion`, or conflicting override versions within the window.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveReleaseAsOverride(commits, currentVersion) {
|
|
24
|
+
const overrides = [];
|
|
25
|
+
for (const commit of commits) {
|
|
26
|
+
for (const footer of commit.footers) {
|
|
27
|
+
if (normalizeToken(footer.token) !== RELEASE_AS_FOOTER_TOKEN) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const raw = footer.value.trim().replace(/^v/u, "");
|
|
31
|
+
if (!isValidVersion(raw)) {
|
|
32
|
+
throw new Error(`Invalid \`Release-As\` footer in commit ${commit.hash}: ` +
|
|
33
|
+
`"${footer.value}" is not a valid SemVer version.`);
|
|
34
|
+
}
|
|
35
|
+
overrides.push({ version: raw, sourceHash: commit.hash });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (overrides.length === 0) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const distinct = [...new Set(overrides.map((override) => override.version))];
|
|
42
|
+
if (distinct.length > 1) {
|
|
43
|
+
throw new Error(`Conflicting \`Release-As\` footers in release window: ${distinct
|
|
44
|
+
.map((version) => `"${version}"`)
|
|
45
|
+
.join(", ")}. Only one target version may be requested.`);
|
|
46
|
+
}
|
|
47
|
+
// All requested versions are identical here; the last footer wins.
|
|
48
|
+
const override = overrides[overrides.length - 1];
|
|
49
|
+
if (compareVersions(override.version, currentVersion) <= 0) {
|
|
50
|
+
throw new Error(`\`Release-As\` footer requests ${override.version}, which is not ` +
|
|
51
|
+
`greater than the current version ${currentVersion}. Downgrades and ` +
|
|
52
|
+
`no-op releases are not allowed.`);
|
|
53
|
+
}
|
|
54
|
+
return override;
|
|
55
|
+
}
|
|
56
|
+
/** Derive the semantic release level implied by moving `from` -> `to`. */
|
|
57
|
+
export function releaseTypeBetween(from, to) {
|
|
58
|
+
const before = parseVersion(from);
|
|
59
|
+
const after = parseVersion(to);
|
|
60
|
+
if (after.major !== before.major) {
|
|
61
|
+
return "major";
|
|
62
|
+
}
|
|
63
|
+
if (after.minor !== before.minor) {
|
|
64
|
+
return "minor";
|
|
65
|
+
}
|
|
66
|
+
if (after.patch !== before.patch) {
|
|
67
|
+
return "patch";
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
@@ -30,11 +30,3 @@ export interface RunReleaseOptions {
|
|
|
30
30
|
"dry-run"?: boolean;
|
|
31
31
|
}
|
|
32
32
|
export declare function runReleaseDetailed(cwd?: string, options?: RunReleaseOptions): Promise<RunReleaseResult>;
|
|
33
|
-
/** @deprecated Use RunReleaseResult. */
|
|
34
|
-
export type SimpleRunReleaseResult = RunReleaseResult;
|
|
35
|
-
/** @deprecated Use RunReleaseOptions. */
|
|
36
|
-
export type RunSimpleReleaseOptions = RunReleaseOptions;
|
|
37
|
-
/** @deprecated Use runRelease. */
|
|
38
|
-
export declare function runSimpleRelease(cwd?: string): Promise<string>;
|
|
39
|
-
/** @deprecated Use runReleaseDetailed. */
|
|
40
|
-
export declare function runSimpleReleaseDetailed(cwd?: string, options?: RunSimpleReleaseOptions): Promise<SimpleRunReleaseResult>;
|
package/dist/release/release.js
CHANGED
|
@@ -224,11 +224,3 @@ export async function runReleaseDetailed(cwd = process.cwd(), options = {}) {
|
|
|
224
224
|
message: `Published releases ${published.join(", ")}`,
|
|
225
225
|
};
|
|
226
226
|
}
|
|
227
|
-
/** @deprecated Use runRelease. */
|
|
228
|
-
export async function runSimpleRelease(cwd = process.cwd()) {
|
|
229
|
-
return runRelease(cwd);
|
|
230
|
-
}
|
|
231
|
-
/** @deprecated Use runReleaseDetailed. */
|
|
232
|
-
export async function runSimpleReleaseDetailed(cwd = process.cwd(), options = {}) {
|
|
233
|
-
return runReleaseDetailed(cwd, options);
|
|
234
|
-
}
|
package/dist/release/state.js
CHANGED
|
@@ -49,15 +49,7 @@ export function getBaselineStatePath(cwd) {
|
|
|
49
49
|
if (configured) {
|
|
50
50
|
return path.join(cwd, configured);
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
if (fs.existsSync(preferred)) {
|
|
54
|
-
return preferred;
|
|
55
|
-
}
|
|
56
|
-
const legacy = path.join(cwd, "versionary.versions.json");
|
|
57
|
-
if (fs.existsSync(legacy)) {
|
|
58
|
-
return legacy;
|
|
59
|
-
}
|
|
60
|
-
return preferred;
|
|
52
|
+
return path.join(cwd, ".versionary-manifest.json");
|
|
61
53
|
}
|
|
62
54
|
export function readBaselineSha(cwd = process.cwd()) {
|
|
63
55
|
const filePath = getBaselineStatePath(cwd);
|
|
@@ -80,15 +72,14 @@ export function readReleaseTargets(cwd = process.cwd()) {
|
|
|
80
72
|
}
|
|
81
73
|
// The publish set introduced by the current release PR. `release` consumes this
|
|
82
74
|
// so it only publishes/announces what this PR bumped, not every package in the
|
|
83
|
-
// accumulated baseline.
|
|
84
|
-
// written before this key existed (legacy compatibility).
|
|
75
|
+
// accumulated baseline.
|
|
85
76
|
export function readPendingReleaseTargets(cwd = process.cwd()) {
|
|
86
77
|
const filePath = getBaselineStatePath(cwd);
|
|
87
78
|
if (!fs.existsSync(filePath)) {
|
|
88
79
|
return [];
|
|
89
80
|
}
|
|
90
81
|
const parsed = parseStateFile(fs.readFileSync(filePath, "utf8"), filePath);
|
|
91
|
-
return
|
|
82
|
+
return parsed[PENDING_RELEASE_TARGETS_KEY] ?? [];
|
|
92
83
|
}
|
|
93
84
|
export function writeBaselineSha(cwd = process.cwd(), sha, releaseTargets) {
|
|
94
85
|
const baselineShaValue = sha ??
|
|
@@ -71,6 +71,17 @@ export function compositeVersionStrategy(strategies) {
|
|
|
71
71
|
return primary.readPackageName?.(cwd, config) ?? null;
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
+
if (primary.isPublishable || secondaries.some((s) => s.isPublishable)) {
|
|
75
|
+
composite.isPublishable = (cwd, pkg) => {
|
|
76
|
+
// Only strategies that recognize this package's version file have an
|
|
77
|
+
// opinion; among those, one publishing facet is enough to expose the
|
|
78
|
+
// package on a registry.
|
|
79
|
+
const opinions = [primary, ...secondaries]
|
|
80
|
+
.map((strategy) => strategy.isPublishable?.(cwd, pkg))
|
|
81
|
+
.filter((opinion) => typeof opinion === "boolean");
|
|
82
|
+
return opinions.length === 0 ? undefined : opinions.some(Boolean);
|
|
83
|
+
};
|
|
84
|
+
}
|
|
74
85
|
if (primary.propagateDependentPatchImpacts ||
|
|
75
86
|
secondaries.some((strategy) => strategy.propagateDependentPatchImpacts)) {
|
|
76
87
|
composite.propagateDependentPatchImpacts = (cwd, packages) => {
|
package/dist/strategy/node.js
CHANGED
|
@@ -71,4 +71,19 @@ export const nodeVersionStrategy = {
|
|
|
71
71
|
}
|
|
72
72
|
return name.trim();
|
|
73
73
|
},
|
|
74
|
+
isPublishable(cwd, pkg) {
|
|
75
|
+
if (path.basename(pkg.versionFile) !== "package.json") {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
const versionPath = path.join(cwd, pkg.versionFile);
|
|
79
|
+
if (!fs.existsSync(versionPath)) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
return readJsonFile(versionPath).private !== true;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
74
89
|
};
|
package/dist/strategy/rust.js
CHANGED
|
@@ -253,13 +253,53 @@ function collectRustTargetManifests(cwd, versionFile, includeWorkspaceMembers) {
|
|
|
253
253
|
: `"${versionFile}" has neither [package] nor [workspace].`;
|
|
254
254
|
throw new Error(`Configured rust target "${versionFile}" is not a Rust crate manifest. ${detail} Either remove the "packages" config so the workspace is auto-discovered, or point a package at a member crate path (e.g. "packages": { "crates/foo": {} }).`);
|
|
255
255
|
}
|
|
256
|
-
function
|
|
256
|
+
function isWorkspaceInheritedValue(rawVersion) {
|
|
257
257
|
if (!rawVersion || typeof rawVersion !== "object") {
|
|
258
258
|
return false;
|
|
259
259
|
}
|
|
260
260
|
const versionRecord = rawVersion;
|
|
261
261
|
return versionRecord.workspace === true;
|
|
262
262
|
}
|
|
263
|
+
function isPublishValueUnpublishable(rawPublish) {
|
|
264
|
+
// Cargo treats both `publish = false` and an empty registry list as
|
|
265
|
+
// "never publish"; a non-empty list still reaches a registry.
|
|
266
|
+
return (rawPublish === false ||
|
|
267
|
+
(Array.isArray(rawPublish) && rawPublish.length === 0));
|
|
268
|
+
}
|
|
269
|
+
function readManifestPublishable(cwd, manifest) {
|
|
270
|
+
const manifestPath = path.join(cwd, manifest);
|
|
271
|
+
if (!fs.existsSync(manifestPath)) {
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
const cargoTomlRaw = fs.readFileSync(manifestPath, "utf8");
|
|
275
|
+
let packageTable;
|
|
276
|
+
try {
|
|
277
|
+
({ packageTable } = parseCargoManifest(manifest, cargoTomlRaw));
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
if (!packageTable) {
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
const rawPublish = packageTable.publish;
|
|
286
|
+
if (!isWorkspaceInheritedValue(rawPublish)) {
|
|
287
|
+
return !isPublishValueUnpublishable(rawPublish);
|
|
288
|
+
}
|
|
289
|
+
let workspaceManifest;
|
|
290
|
+
try {
|
|
291
|
+
workspaceManifest = findWorkspaceManifestForMember(cwd, manifest);
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
const workspaceRaw = fs.readFileSync(path.join(cwd, workspaceManifest), "utf8");
|
|
297
|
+
const { workspaceTable } = parseCargoManifest(workspaceManifest, workspaceRaw);
|
|
298
|
+
const workspacePackage = workspaceTable?.package && typeof workspaceTable.package === "object"
|
|
299
|
+
? workspaceTable.package
|
|
300
|
+
: null;
|
|
301
|
+
return !isPublishValueUnpublishable(workspacePackage?.publish);
|
|
302
|
+
}
|
|
263
303
|
function readWorkspacePackageVersion(cargoTomlRaw, versionFile) {
|
|
264
304
|
const { workspaceTable } = parseCargoManifest(versionFile, cargoTomlRaw);
|
|
265
305
|
if (!workspaceTable || typeof workspaceTable !== "object") {
|
|
@@ -314,7 +354,7 @@ function readResolvedCargoVersion(cwd, manifest, cargoTomlRaw) {
|
|
|
314
354
|
if (typeof rawVersion === "string" && rawVersion.trim().length > 0) {
|
|
315
355
|
return rawVersion.trim();
|
|
316
356
|
}
|
|
317
|
-
if (!
|
|
357
|
+
if (!isWorkspaceInheritedValue(rawVersion)) {
|
|
318
358
|
throw new Error(`${manifest} has invalid [package].version. Expected a non-empty SemVer string or version.workspace = true.`);
|
|
319
359
|
}
|
|
320
360
|
const workspaceManifest = findWorkspaceManifestForMember(cwd, manifest);
|
|
@@ -425,7 +465,7 @@ function usesWorkspaceInheritedVersion(cargoTomlRaw, versionFile) {
|
|
|
425
465
|
return false;
|
|
426
466
|
}
|
|
427
467
|
const rawVersion = packageTable.version;
|
|
428
|
-
return
|
|
468
|
+
return isWorkspaceInheritedValue(rawVersion);
|
|
429
469
|
}
|
|
430
470
|
function isDependencySection(section) {
|
|
431
471
|
if (ROOT_DEPENDENCY_SECTIONS.has(section)) {
|
|
@@ -733,6 +773,13 @@ export const rustVersionStrategy = {
|
|
|
733
773
|
}
|
|
734
774
|
return readCargoPackageName(cargoTomlRaw, selectedManifest);
|
|
735
775
|
},
|
|
776
|
+
isPublishable(cwd, pkg) {
|
|
777
|
+
const manifest = normalizeSlashPath(pkg.versionFile);
|
|
778
|
+
if (path.posix.basename(manifest) !== "Cargo.toml") {
|
|
779
|
+
return undefined;
|
|
780
|
+
}
|
|
781
|
+
return readManifestPublishable(cwd, manifest);
|
|
782
|
+
},
|
|
736
783
|
propagateDependentPatchImpacts(cwd, packages) {
|
|
737
784
|
const manifestToVersion = {};
|
|
738
785
|
const candidateManifests = [];
|
package/dist/strategy/types.d.ts
CHANGED
|
@@ -22,6 +22,17 @@ export interface VersionStrategy {
|
|
|
22
22
|
writeVersion(cwd: string, config: VersionaryConfig, version: string): string[];
|
|
23
23
|
validateProject?(cwd: string, config: VersionaryConfig): string | null;
|
|
24
24
|
readPackageName?(cwd: string, config: VersionaryConfig): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* Whether this package is published to a registry. A package that never
|
|
27
|
+
* reaches a registry cannot leave a stale published copy behind, so release
|
|
28
|
+
* planning exempts it from published-dependency freshness enforcement.
|
|
29
|
+
*
|
|
30
|
+
* Returns `undefined` to abstain, which a strategy must do for a version
|
|
31
|
+
* file it does not recognize — otherwise, composed with another strategy, it
|
|
32
|
+
* would outvote the one that actually owns the manifest. Strategies that
|
|
33
|
+
* omit this hook entirely are assumed to publish everything.
|
|
34
|
+
*/
|
|
35
|
+
isPublishable?(cwd: string, pkg: StrategyPackagePlanContext): boolean | undefined;
|
|
25
36
|
propagateDependentPatchImpacts?(cwd: string, packages: StrategyPackagePlanContext[]): string[];
|
|
26
37
|
finalizeVersionWrites?(cwd: string, writes: StrategyVersionWriteContext[], context: StrategyFinalizeContext): string[];
|
|
27
38
|
}
|
package/dist/types/config.d.ts
CHANGED
|
@@ -5,7 +5,6 @@ export interface VersionaryArtifactRule {
|
|
|
5
5
|
type: "json" | "toml" | "yaml" | "nix" | "regex";
|
|
6
6
|
path: string;
|
|
7
7
|
"field-path"?: string;
|
|
8
|
-
jsonpath?: string;
|
|
9
8
|
pattern?: string;
|
|
10
9
|
replacement?: string;
|
|
11
10
|
}
|
|
@@ -21,7 +20,7 @@ export interface VersionaryPackage {
|
|
|
21
20
|
}
|
|
22
21
|
export interface VersionaryConfig {
|
|
23
22
|
version: 1;
|
|
24
|
-
"review-mode"?: "direct" | "pr"
|
|
23
|
+
"review-mode"?: "direct" | "pr";
|
|
25
24
|
"version-file"?: string;
|
|
26
25
|
"changelog-file"?: string;
|
|
27
26
|
"changelog-format"?: VersionaryChangelogFormat;
|
|
@@ -29,7 +28,6 @@ export interface VersionaryConfig {
|
|
|
29
28
|
"release-reference-comments"?: ReleaseReferenceCommentsMode;
|
|
30
29
|
"release-branch"?: string;
|
|
31
30
|
"baseline-file"?: string;
|
|
32
|
-
"next-release-file"?: string;
|
|
33
31
|
"bootstrap-sha"?: string;
|
|
34
32
|
"monorepo-mode"?: "independent" | "fixed";
|
|
35
33
|
"bump-minor-pre-major"?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "versionary",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Automatic release framework based on conventional commits and semantic versioning",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"releasing",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^26.0.1",
|
|
38
38
|
"tsx": "^4.20.6",
|
|
39
|
-
"typescript": "^
|
|
39
|
+
"typescript": "^7.0.2",
|
|
40
40
|
"vite": "^8.0.0",
|
|
41
41
|
"vitepress": "^1.6.4",
|
|
42
42
|
"vitest": "^4.1.4"
|