versionary 1.0.0 → 1.1.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 +4 -0
- package/dist/cli/index.js +227 -26
- package/dist/config/schema.d.ts +2 -0
- package/dist/config/schema.js +32 -0
- package/dist/index.d.ts +2 -2
- package/dist/release/artifact-rules.js +27 -28
- package/dist/release/changelog.js +9 -6
- package/dist/release/cohorts.d.ts +6 -0
- package/dist/release/cohorts.js +125 -0
- package/dist/release/plan.d.ts +22 -0
- package/dist/release/plan.js +81 -45
- package/dist/release/pr.d.ts +69 -0
- package/dist/release/pr.js +563 -24
- package/dist/release/release.d.ts +12 -0
- package/dist/release/release.js +26 -7
- package/dist/release/state.d.ts +16 -1
- package/dist/release/state.js +182 -9
- package/dist/release/targets.d.ts +21 -0
- package/dist/release/targets.js +85 -0
- package/dist/scm/github-plugin.js +44 -0
- package/dist/scm/types.d.ts +11 -0
- package/dist/strategy/cmake.d.ts +2 -0
- package/dist/strategy/cmake.js +283 -0
- package/dist/strategy/resolve.js +2 -0
- package/dist/types/config.d.ts +2 -0
- package/dist/types/plugins.d.ts +3 -1
- package/package.json +3 -1
package/dist/action/index.js
CHANGED
|
@@ -112,7 +112,9 @@ function main() {
|
|
|
112
112
|
setOutput("release_created", "false");
|
|
113
113
|
setOutput("tag_name", "");
|
|
114
114
|
setOutput("tag_names", "[]");
|
|
115
|
+
setOutput("release_targets", "[]");
|
|
115
116
|
setOutput("review_url", "");
|
|
117
|
+
setOutput("review_requests", "[]");
|
|
116
118
|
setOutput("branch", "");
|
|
117
119
|
setOutput("title", "");
|
|
118
120
|
return;
|
|
@@ -148,7 +150,9 @@ function main() {
|
|
|
148
150
|
setOutput("release_created", releaseCreated);
|
|
149
151
|
setOutput("tag_name", firstTag);
|
|
150
152
|
setOutput("tag_names", JSON.stringify(tagNames));
|
|
153
|
+
setOutput("release_targets", JSON.stringify(Array.isArray(payload.releaseTargets) ? payload.releaseTargets : []));
|
|
151
154
|
setOutput("review_url", payload.reviewUrl ?? "");
|
|
155
|
+
setOutput("review_requests", JSON.stringify(Array.isArray(payload.reviewRequests) ? payload.reviewRequests : []));
|
|
152
156
|
setOutput("branch", payload.branch ?? "");
|
|
153
157
|
setOutput("title", payload.title ?? "");
|
|
154
158
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { loadConfig } from "../config/load-config.js";
|
|
3
4
|
import { prependChangelog, renderReleasePlanChangelog, } from "../release/changelog.js";
|
|
4
5
|
import { createReleasePlan } from "../release/plan.js";
|
|
5
|
-
import { closeStaleReviewRequestIfExists, isReleaseCommitMessage, openOrUpdateReviewRequest, prepareReleasePr, pushReleaseBranch, resolveReleaseHighlights, } from "../release/pr.js";
|
|
6
|
+
import { closeStaleReviewRequestIfExists, isReleaseCommitMessage, openOrUpdateReviewRequest, preparePendingReleasePr, preparePendingSeparateReleasePrs, prepareReleasePr, prepareSeparateReleasePrs, pushReleaseBranch, reconcileSeparateReviewRequests, resolveReleaseHighlights, } from "../release/pr.js";
|
|
6
7
|
import { runRelease, runReleaseDetailed } from "../release/release.js";
|
|
8
|
+
import { hasFullyUntaggedPendingRelease, hasReleaseStateChangeAtHead, readPendingReleaseTargets, } from "../release/state.js";
|
|
7
9
|
import { verifyProject } from "../release/verify-project.js";
|
|
10
|
+
/**
|
|
11
|
+
* What a dry run would actually release. `plan.nextVersion` is the repository
|
|
12
|
+
* aggregate, which in a monorepo names no real release, so prefer the
|
|
13
|
+
* per-package versions whenever the plan has them.
|
|
14
|
+
*/
|
|
15
|
+
function formatDryRunReleaseSubject(plan) {
|
|
16
|
+
const releasing = (plan.packages ?? []).filter((pkg) => pkg.nextVersion);
|
|
17
|
+
if (releasing.length === 0) {
|
|
18
|
+
return plan.nextVersion ?? "";
|
|
19
|
+
}
|
|
20
|
+
return releasing
|
|
21
|
+
.map((pkg) => `${pkg.path === "." ? plan.packageName : pkg.path} ${pkg.nextVersion}`)
|
|
22
|
+
.join(", ");
|
|
23
|
+
}
|
|
8
24
|
function printVerifyResult() {
|
|
9
25
|
const result = verifyProject();
|
|
10
26
|
const categories = [
|
|
@@ -39,6 +55,140 @@ function parseFlags(args) {
|
|
|
39
55
|
function emitJson(payload) {
|
|
40
56
|
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
41
57
|
}
|
|
58
|
+
function reviewResultPayload(action, message, reviewRequests) {
|
|
59
|
+
const primary = reviewRequests[0];
|
|
60
|
+
return {
|
|
61
|
+
action,
|
|
62
|
+
message,
|
|
63
|
+
releaseCreated: false,
|
|
64
|
+
tagNames: [],
|
|
65
|
+
reviewUrl: primary?.reviewUrl,
|
|
66
|
+
branch: primary?.branch,
|
|
67
|
+
title: primary?.title,
|
|
68
|
+
reviewRequests,
|
|
69
|
+
targets: reviewRequests.flatMap((request) => request.targets.map((target) => ({
|
|
70
|
+
tag: target.tag,
|
|
71
|
+
version: target.version,
|
|
72
|
+
}))),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function printReviewResults(message, reviewRequests) {
|
|
76
|
+
console.log(message);
|
|
77
|
+
for (const request of reviewRequests) {
|
|
78
|
+
console.log(`${request.branch}: ${request.title}`);
|
|
79
|
+
if (request.reviewUrl) {
|
|
80
|
+
console.log(request.reviewUrl);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function prepareCurrentSeparateReviewRequests(flags, logger) {
|
|
85
|
+
const cwd = process.cwd();
|
|
86
|
+
const prepared = prepareSeparateReleasePrs(cwd, {
|
|
87
|
+
logger,
|
|
88
|
+
"dry-run": flags["dry-run"],
|
|
89
|
+
});
|
|
90
|
+
const reviewRequests = await reconcileSeparateReviewRequests(cwd, prepared, {
|
|
91
|
+
logger,
|
|
92
|
+
"dry-run": flags["dry-run"],
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
reviewRequests,
|
|
96
|
+
updated: reviewRequests.some((request) => request.status === "prepared"),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async function recoverPendingReleaseIfNeeded(flags, logger) {
|
|
100
|
+
const cwd = process.cwd();
|
|
101
|
+
if (!hasFullyUntaggedPendingRelease(cwd)) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
const pendingTargets = readPendingReleaseTargets(cwd);
|
|
105
|
+
const loaded = loadConfig(cwd);
|
|
106
|
+
const releaseBranch = loaded.config["release-branch"] ?? "versionary/release";
|
|
107
|
+
if (loaded.config["separate-release-prs"]) {
|
|
108
|
+
const prepared = preparePendingSeparateReleasePrs(cwd, {
|
|
109
|
+
logger,
|
|
110
|
+
"dry-run": flags["dry-run"],
|
|
111
|
+
});
|
|
112
|
+
const reviewRequests = await reconcileSeparateReviewRequests(cwd, prepared, {
|
|
113
|
+
logger,
|
|
114
|
+
"dry-run": flags["dry-run"],
|
|
115
|
+
recovered: true,
|
|
116
|
+
});
|
|
117
|
+
const action = flags["dry-run"]
|
|
118
|
+
? "pr-dry-run"
|
|
119
|
+
: reviewRequests.some((request) => request.status === "recovered")
|
|
120
|
+
? "pr-prepared"
|
|
121
|
+
: "pr-up-to-date";
|
|
122
|
+
const message = flags["dry-run"]
|
|
123
|
+
? `Dry run: would recover ${prepared.length} pending package release PR${prepared.length === 1 ? "" : "s"}.`
|
|
124
|
+
: action === "pr-prepared"
|
|
125
|
+
? `Prepared ${prepared.length} pending package release recovery PR${prepared.length === 1 ? "" : "s"}.`
|
|
126
|
+
: "Pending package release recovery PRs are already up to date.";
|
|
127
|
+
if (flags.json) {
|
|
128
|
+
emitJson(reviewResultPayload(action, message, reviewRequests));
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
printReviewResults(message, reviewRequests);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
if (flags["dry-run"]) {
|
|
135
|
+
const message = `Dry run: would recover pending releases ${pendingTargets.map((target) => target.tag).join(", ")} on branch ${releaseBranch}`;
|
|
136
|
+
if (flags.json) {
|
|
137
|
+
emitJson({
|
|
138
|
+
action: "pr-dry-run",
|
|
139
|
+
message,
|
|
140
|
+
releaseCreated: false,
|
|
141
|
+
tagNames: [],
|
|
142
|
+
branch: releaseBranch,
|
|
143
|
+
targets: pendingTargets.map((target) => ({
|
|
144
|
+
tag: target.tag,
|
|
145
|
+
version: target.version,
|
|
146
|
+
})),
|
|
147
|
+
});
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
console.log(message);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
const recovery = preparePendingReleasePr(cwd, { logger });
|
|
154
|
+
if (!recovery.updated) {
|
|
155
|
+
const message = `Pending release recovery branch ${recovery.branch} is already up to date.`;
|
|
156
|
+
if (flags.json) {
|
|
157
|
+
emitJson({
|
|
158
|
+
action: "pr-up-to-date",
|
|
159
|
+
message,
|
|
160
|
+
releaseCreated: false,
|
|
161
|
+
tagNames: [],
|
|
162
|
+
branch: recovery.branch,
|
|
163
|
+
title: recovery.title,
|
|
164
|
+
});
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
console.log(message);
|
|
168
|
+
console.log(`Title: ${recovery.title}`);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
pushReleaseBranch(cwd, recovery.branch);
|
|
172
|
+
const primaryVersion = recovery.targets[0]?.version ?? "";
|
|
173
|
+
const reviewResult = await openOrUpdateReviewRequest(cwd, recovery.branch, recovery.title, primaryVersion, primaryVersion, [], null, { logger, body: recovery.body });
|
|
174
|
+
const message = `Prepared pending release recovery PR branch ${recovery.branch}`;
|
|
175
|
+
if (flags.json) {
|
|
176
|
+
emitJson({
|
|
177
|
+
action: "pr-prepared",
|
|
178
|
+
message,
|
|
179
|
+
releaseCreated: false,
|
|
180
|
+
tagNames: [],
|
|
181
|
+
reviewUrl: reviewResult,
|
|
182
|
+
branch: recovery.branch,
|
|
183
|
+
title: recovery.title,
|
|
184
|
+
});
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
console.log(message);
|
|
188
|
+
console.log(`Title: ${recovery.title}`);
|
|
189
|
+
console.log(reviewResult);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
42
192
|
async function main() {
|
|
43
193
|
const [, , command, ...args] = process.argv;
|
|
44
194
|
const flags = parseFlags(args);
|
|
@@ -48,51 +198,69 @@ async function main() {
|
|
|
48
198
|
encoding: "utf8",
|
|
49
199
|
stdio: ["ignore", "pipe", "ignore"],
|
|
50
200
|
}).trim();
|
|
51
|
-
if (isReleaseCommitMessage(commitMessage)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return 0;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (flags.json) {
|
|
63
|
-
const release = await runReleaseDetailed(process.cwd(), {
|
|
64
|
-
logger,
|
|
65
|
-
"dry-run": flags["dry-run"],
|
|
66
|
-
});
|
|
67
|
-
if (release.action === "release-skipped") {
|
|
201
|
+
if (isReleaseCommitMessage(commitMessage) ||
|
|
202
|
+
hasReleaseStateChangeAtHead(process.cwd())) {
|
|
203
|
+
const release = await runReleaseDetailed(process.cwd(), {
|
|
204
|
+
logger,
|
|
205
|
+
"dry-run": flags["dry-run"],
|
|
206
|
+
});
|
|
207
|
+
if (release.action === "release-skipped") {
|
|
208
|
+
if (flags.json) {
|
|
68
209
|
emitJson({
|
|
69
210
|
action: "release-skipped",
|
|
70
211
|
message: release.reason,
|
|
71
212
|
releaseCreated: false,
|
|
72
213
|
tagNames: [],
|
|
73
214
|
});
|
|
74
|
-
return 0;
|
|
75
215
|
}
|
|
76
|
-
|
|
216
|
+
else {
|
|
217
|
+
console.log(release.reason);
|
|
218
|
+
}
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
if (release.action === "release-dry-run") {
|
|
222
|
+
if (flags.json) {
|
|
77
223
|
emitJson({
|
|
78
224
|
action: "release-dry-run",
|
|
79
225
|
message: release.message,
|
|
80
226
|
releaseCreated: false,
|
|
81
227
|
tagNames: release.targets.map((target) => target.tag),
|
|
228
|
+
releaseTargets: release.releaseTargets,
|
|
82
229
|
targets: release.targets,
|
|
83
230
|
});
|
|
84
|
-
return 0;
|
|
85
231
|
}
|
|
232
|
+
else {
|
|
233
|
+
console.log(release.message);
|
|
234
|
+
}
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
237
|
+
let reviewRequests = [];
|
|
238
|
+
if (loadConfig(process.cwd()).config["separate-release-prs"]) {
|
|
239
|
+
reviewRequests = (await prepareCurrentSeparateReviewRequests({ ...flags, "dry-run": false }, logger)).reviewRequests;
|
|
240
|
+
}
|
|
241
|
+
if (flags.json) {
|
|
242
|
+
const primary = reviewRequests[0];
|
|
86
243
|
emitJson({
|
|
87
244
|
action: "release-published",
|
|
88
245
|
message: release.message,
|
|
89
246
|
releaseCreated: release.releases.length > 0,
|
|
90
247
|
tagNames: release.releases.map((target) => target.tag),
|
|
248
|
+
releaseTargets: release.releaseTargets,
|
|
249
|
+
reviewUrl: primary?.reviewUrl,
|
|
250
|
+
branch: primary?.branch,
|
|
251
|
+
title: primary?.title,
|
|
252
|
+
reviewRequests,
|
|
91
253
|
});
|
|
92
|
-
return 0;
|
|
93
254
|
}
|
|
94
|
-
|
|
95
|
-
|
|
255
|
+
else {
|
|
256
|
+
console.log(release.message);
|
|
257
|
+
if (reviewRequests.length > 0) {
|
|
258
|
+
printReviewResults(`Reconciled ${reviewRequests.length} remaining package release PR${reviewRequests.length === 1 ? "" : "s"}.`, reviewRequests);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return 0;
|
|
262
|
+
}
|
|
263
|
+
if (await recoverPendingReleaseIfNeeded(flags, logger)) {
|
|
96
264
|
return 0;
|
|
97
265
|
}
|
|
98
266
|
const plan = createReleasePlan();
|
|
@@ -113,8 +281,28 @@ async function main() {
|
|
|
113
281
|
console.log(message);
|
|
114
282
|
return 0;
|
|
115
283
|
}
|
|
284
|
+
if (loadConfig(process.cwd()).config["separate-release-prs"]) {
|
|
285
|
+
const { reviewRequests, updated } = await prepareCurrentSeparateReviewRequests(flags, logger);
|
|
286
|
+
const action = flags["dry-run"]
|
|
287
|
+
? "pr-dry-run"
|
|
288
|
+
: updated
|
|
289
|
+
? "pr-prepared"
|
|
290
|
+
: "pr-up-to-date";
|
|
291
|
+
const message = flags["dry-run"]
|
|
292
|
+
? `Dry run: would prepare ${reviewRequests.length} package release PR${reviewRequests.length === 1 ? "" : "s"}.`
|
|
293
|
+
: updated
|
|
294
|
+
? `Prepared ${reviewRequests.length} package release PR${reviewRequests.length === 1 ? "" : "s"}.`
|
|
295
|
+
: "Package release PRs are already up to date.";
|
|
296
|
+
if (flags.json) {
|
|
297
|
+
emitJson(reviewResultPayload(action, message, reviewRequests));
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
printReviewResults(message, reviewRequests);
|
|
301
|
+
}
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
|
116
304
|
if (flags["dry-run"]) {
|
|
117
|
-
const dryRunMessage = `Dry run: would prepare release PR branch ${plan.releaseBranchPrefix} for ${plan
|
|
305
|
+
const dryRunMessage = `Dry run: would prepare release PR branch ${plan.releaseBranchPrefix} for ${formatDryRunReleaseSubject(plan)}`;
|
|
118
306
|
if (flags.json) {
|
|
119
307
|
emitJson({
|
|
120
308
|
action: "pr-dry-run",
|
|
@@ -207,6 +395,9 @@ async function main() {
|
|
|
207
395
|
return 0;
|
|
208
396
|
}
|
|
209
397
|
if (command === "pr") {
|
|
398
|
+
if (await recoverPendingReleaseIfNeeded({ ...flags, json: false }, console)) {
|
|
399
|
+
return 0;
|
|
400
|
+
}
|
|
210
401
|
const plan = createReleasePlan();
|
|
211
402
|
if (!plan.nextVersion) {
|
|
212
403
|
if (!flags["dry-run"]) {
|
|
@@ -217,8 +408,18 @@ async function main() {
|
|
|
217
408
|
console.log("No releasable commits found. Nothing to do.");
|
|
218
409
|
return 0;
|
|
219
410
|
}
|
|
411
|
+
if (loadConfig(process.cwd()).config["separate-release-prs"]) {
|
|
412
|
+
const { reviewRequests, updated } = await prepareCurrentSeparateReviewRequests(flags, console);
|
|
413
|
+
const message = flags["dry-run"]
|
|
414
|
+
? `Dry run: would prepare ${reviewRequests.length} package release PR${reviewRequests.length === 1 ? "" : "s"}.`
|
|
415
|
+
: updated
|
|
416
|
+
? `Prepared ${reviewRequests.length} package release PR${reviewRequests.length === 1 ? "" : "s"}.`
|
|
417
|
+
: "Package release PRs are already up to date.";
|
|
418
|
+
printReviewResults(message, reviewRequests);
|
|
419
|
+
return 0;
|
|
420
|
+
}
|
|
220
421
|
if (flags["dry-run"]) {
|
|
221
|
-
console.log(`Dry run: would prepare release PR branch ${plan.releaseBranchPrefix} for ${plan
|
|
422
|
+
console.log(`Dry run: would prepare release PR branch ${plan.releaseBranchPrefix} for ${formatDryRunReleaseSubject(plan)}`);
|
|
222
423
|
return 0;
|
|
223
424
|
}
|
|
224
425
|
const pr = prepareReleasePr(process.cwd(), { logger: console });
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export declare const configSchema: z.ZodObject<{
|
|
|
19
19
|
strict: "strict";
|
|
20
20
|
}>>;
|
|
21
21
|
"release-branch": z.ZodOptional<z.ZodString>;
|
|
22
|
+
"separate-release-prs": z.ZodOptional<z.ZodBoolean>;
|
|
22
23
|
"baseline-file": z.ZodOptional<z.ZodString>;
|
|
23
24
|
"bootstrap-sha": z.ZodOptional<z.ZodString>;
|
|
24
25
|
"monorepo-mode": z.ZodOptional<z.ZodEnum<{
|
|
@@ -52,6 +53,7 @@ export declare const configSchema: z.ZodObject<{
|
|
|
52
53
|
"field-path": z.ZodOptional<z.ZodString>;
|
|
53
54
|
pattern: z.ZodOptional<z.ZodString>;
|
|
54
55
|
replacement: z.ZodOptional<z.ZodString>;
|
|
56
|
+
"expected-matches": z.ZodOptional<z.ZodNumber>;
|
|
55
57
|
}, z.core.$strip>>>;
|
|
56
58
|
follows: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
57
59
|
}, z.core.$strict>>>;
|
package/dist/config/schema.js
CHANGED
|
@@ -6,6 +6,7 @@ const artifactRuleSchema = z
|
|
|
6
6
|
"field-path": z.string().optional(),
|
|
7
7
|
pattern: z.string().optional(),
|
|
8
8
|
replacement: z.string().optional(),
|
|
9
|
+
"expected-matches": z.number().int().positive().optional(),
|
|
9
10
|
})
|
|
10
11
|
.superRefine((value, ctx) => {
|
|
11
12
|
const needsFieldPath = value.type === "json" ||
|
|
@@ -33,6 +34,13 @@ const artifactRuleSchema = z
|
|
|
33
34
|
path: ["replacement"],
|
|
34
35
|
});
|
|
35
36
|
}
|
|
37
|
+
if (needsFieldPath && value["expected-matches"] !== undefined) {
|
|
38
|
+
ctx.addIssue({
|
|
39
|
+
code: z.ZodIssueCode.custom,
|
|
40
|
+
message: `${value.type} artifact rules do not support "expected-matches".`,
|
|
41
|
+
path: ["expected-matches"],
|
|
42
|
+
});
|
|
43
|
+
}
|
|
36
44
|
if (value.type === "regex" && !value.pattern) {
|
|
37
45
|
ctx.addIssue({
|
|
38
46
|
code: z.ZodIssueCode.custom,
|
|
@@ -75,6 +83,7 @@ export const configSchema = z
|
|
|
75
83
|
.enum(["off", "best-effort", "strict"])
|
|
76
84
|
.optional(),
|
|
77
85
|
"release-branch": z.string().optional(),
|
|
86
|
+
"separate-release-prs": z.boolean().optional(),
|
|
78
87
|
"baseline-file": z.string().optional(),
|
|
79
88
|
"bootstrap-sha": z.string().optional(),
|
|
80
89
|
"monorepo-mode": z.enum(["independent", "fixed"]).optional(),
|
|
@@ -90,6 +99,29 @@ export const configSchema = z
|
|
|
90
99
|
.strict()
|
|
91
100
|
.superRefine((value, ctx) => {
|
|
92
101
|
const packages = value.packages;
|
|
102
|
+
if (value["separate-release-prs"]) {
|
|
103
|
+
if (!packages || Object.keys(packages).length === 0) {
|
|
104
|
+
ctx.addIssue({
|
|
105
|
+
code: z.ZodIssueCode.custom,
|
|
106
|
+
message: '"separate-release-prs" requires a non-empty packages map.',
|
|
107
|
+
path: ["separate-release-prs"],
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (value["monorepo-mode"] === "fixed") {
|
|
111
|
+
ctx.addIssue({
|
|
112
|
+
code: z.ZodIssueCode.custom,
|
|
113
|
+
message: '"separate-release-prs" cannot be combined with monorepo-mode "fixed".',
|
|
114
|
+
path: ["separate-release-prs"],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (value["review-mode"] === "direct") {
|
|
118
|
+
ctx.addIssue({
|
|
119
|
+
code: z.ZodIssueCode.custom,
|
|
120
|
+
message: '"separate-release-prs" cannot be combined with review-mode "direct".',
|
|
121
|
+
path: ["separate-release-prs"],
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
93
125
|
if (!packages) {
|
|
94
126
|
return;
|
|
95
127
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { verifyProject } from "./release/verify-project.js";
|
|
|
3
3
|
export { findPluginsByCapability, pluginHasCapability, } from "./scm/capabilities.js";
|
|
4
4
|
export { getScmClient } from "./scm/client.js";
|
|
5
5
|
export { createGitHubPlugin } from "./scm/github-plugin.js";
|
|
6
|
-
export type { ScmClient, ScmClientContext, ScmProvider, ScmReleaseMetadataInput, ScmReleaseMetadataResult, ScmReleaseReferenceCommentsInput, ScmReleaseReferenceCommentsResult, ScmReviewRequestInput, ScmReviewRequestResult, } from "./scm/types.js";
|
|
6
|
+
export type { ScmClient, ScmClientContext, ScmListReviewRequestsInput, ScmProvider, ScmReleaseMetadataInput, ScmReleaseMetadataResult, ScmReleaseReferenceCommentsInput, ScmReleaseReferenceCommentsResult, ScmReviewRequestInput, ScmReviewRequestResult, ScmReviewRequestSummary, } from "./scm/types.js";
|
|
7
7
|
export { resolveVersionStrategy } from "./strategy/resolve.js";
|
|
8
8
|
export type { VersionaryArtifactRule, VersionaryConfig, VersionaryPackage, } from "./types/config.js";
|
|
9
|
-
export type { VersionaryPluginCapability, VersionaryPluginContext, VersionaryPluginRuntime, VersionaryScmReleaseMetadataInput, VersionaryScmReleaseMetadataResult, VersionaryScmReleaseReferenceCommentsInput, VersionaryScmReleaseReferenceCommentsResult, VersionaryScmReviewRequestInput, VersionaryScmReviewRequestResult, } from "./types/plugins.js";
|
|
9
|
+
export type { VersionaryPluginCapability, VersionaryPluginContext, VersionaryPluginRuntime, VersionaryScmListReviewRequestsInput, VersionaryScmReleaseMetadataInput, VersionaryScmReleaseMetadataResult, VersionaryScmReleaseReferenceCommentsInput, VersionaryScmReleaseReferenceCommentsResult, VersionaryScmReviewRequestInput, VersionaryScmReviewRequestResult, VersionaryScmReviewRequestSummary, } from "./types/plugins.js";
|
|
@@ -183,40 +183,39 @@ function renderReplacementTemplate(template, version) {
|
|
|
183
183
|
return value;
|
|
184
184
|
});
|
|
185
185
|
}
|
|
186
|
-
function applyRegexRule(content, pattern, version, replacementTemplate) {
|
|
186
|
+
function applyRegexRule(content, pattern, version, expectedMatches, replacementTemplate) {
|
|
187
187
|
const regex = parseRegexPattern(pattern);
|
|
188
188
|
const matchFlags = regex.flags.includes("g")
|
|
189
189
|
? regex.flags
|
|
190
190
|
: `${regex.flags}g`;
|
|
191
191
|
const globalRegex = new RegExp(regex.source, matchFlags.includes("d") ? matchFlags : `${matchFlags}d`);
|
|
192
192
|
const matches = [...content.matchAll(globalRegex)];
|
|
193
|
-
if (matches.length !==
|
|
194
|
-
throw new Error(`Regex pattern
|
|
193
|
+
if (matches.length !== expectedMatches) {
|
|
194
|
+
throw new Error(`Regex pattern expected ${expectedMatches} ${expectedMatches === 1 ? "match" : "matches"}; matched ${matches.length}.`);
|
|
195
|
+
}
|
|
196
|
+
const renderedReplacement = replacementTemplate === undefined
|
|
197
|
+
? undefined
|
|
198
|
+
: renderReplacementTemplate(replacementTemplate, version);
|
|
199
|
+
let updated = content;
|
|
200
|
+
for (let index = matches.length - 1; index >= 0; index -= 1) {
|
|
201
|
+
const match = matches[index];
|
|
202
|
+
if (!match || match.index === undefined) {
|
|
203
|
+
throw new Error("Regex match did not include an index.");
|
|
204
|
+
}
|
|
205
|
+
// Apply edits from right to left so every match keeps its original indices.
|
|
206
|
+
if (renderedReplacement !== undefined) {
|
|
207
|
+
updated = `${updated.slice(0, match.index)}${renderedReplacement}${updated.slice(match.index + match[0].length)}`;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
// Splice by group indices so literal `$` sequences and repeated group content
|
|
211
|
+
// are handled correctly.
|
|
212
|
+
const [start, end] = match.indices?.[1] ?? [
|
|
213
|
+
match.index,
|
|
214
|
+
match.index + match[0].length,
|
|
215
|
+
];
|
|
216
|
+
updated = `${updated.slice(0, start)}${version}${updated.slice(end)}`;
|
|
195
217
|
}
|
|
196
|
-
|
|
197
|
-
if (!match) {
|
|
198
|
-
throw new Error("Regex match result missing.");
|
|
199
|
-
}
|
|
200
|
-
const start = match.index;
|
|
201
|
-
if (start === undefined) {
|
|
202
|
-
throw new Error("Regex match did not include an index.");
|
|
203
|
-
}
|
|
204
|
-
const full = match[0];
|
|
205
|
-
// With a replacement template, render it and replace the entire match.
|
|
206
|
-
if (replacementTemplate !== undefined) {
|
|
207
|
-
const rendered = renderReplacementTemplate(replacementTemplate, version);
|
|
208
|
-
return `${content.slice(0, start)}${rendered}${content.slice(start + full.length)}`;
|
|
209
|
-
}
|
|
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
|
-
// `String.replace` so literal `$` sequences and repeated group content are
|
|
213
|
-
// handled correctly.
|
|
214
|
-
const groupIndices = match.indices?.[1];
|
|
215
|
-
if (!groupIndices) {
|
|
216
|
-
return `${content.slice(0, start)}${version}${content.slice(start + full.length)}`;
|
|
217
|
-
}
|
|
218
|
-
const [groupStart, groupEnd] = groupIndices;
|
|
219
|
-
return `${content.slice(0, groupStart)}${version}${content.slice(groupEnd)}`;
|
|
218
|
+
return updated;
|
|
220
219
|
}
|
|
221
220
|
function applyTomlRulePreservingFormatting(content, fieldPath, version) {
|
|
222
221
|
const simplePath = fieldPath.match(/^\$\.([A-Za-z0-9_-]+)$/u);
|
|
@@ -403,7 +402,7 @@ function applyArtifactRuleToContent(content, rule, version) {
|
|
|
403
402
|
if (!rule.pattern) {
|
|
404
403
|
throw new Error('regex artifact rules require "pattern".');
|
|
405
404
|
}
|
|
406
|
-
return applyRegexRule(content, rule.pattern, version, rule.replacement);
|
|
405
|
+
return applyRegexRule(content, rule.pattern, version, rule["expected-matches"] ?? 1, rule.replacement);
|
|
407
406
|
}
|
|
408
407
|
if (rule.type === "json") {
|
|
409
408
|
const parsed = JSON.parse(content);
|
|
@@ -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 } from "./plan.js";
|
|
5
|
+
import { resolvePackageDependencies, resolveRootReleaseView, } 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() {
|
|
@@ -192,24 +192,27 @@ export function renderReviewRequestFooter() {
|
|
|
192
192
|
return REVIEW_REQUEST_FOOTER;
|
|
193
193
|
}
|
|
194
194
|
export function renderReleasePlanChangelog(plan, options = {}) {
|
|
195
|
-
|
|
195
|
+
// Root's changelog describes root's own release, so it must be driven by the
|
|
196
|
+
// root package's version and commit set rather than the plan-level aggregate.
|
|
197
|
+
const root = resolveRootReleaseView(plan);
|
|
198
|
+
if (!root.nextVersion) {
|
|
196
199
|
return "";
|
|
197
200
|
}
|
|
198
201
|
const dedupedCommits = [
|
|
199
|
-
...new Map(
|
|
202
|
+
...new Map(root.commits.map((commit) => [commit.hash, commit])).values(),
|
|
200
203
|
];
|
|
201
204
|
if (plan.changelogFormat === "r-news") {
|
|
202
205
|
return renderRNewsReleaseNotes({
|
|
203
206
|
packageName: plan.packageName,
|
|
204
|
-
nextVersion:
|
|
207
|
+
nextVersion: root.nextVersion,
|
|
205
208
|
commits: dedupedCommits,
|
|
206
209
|
cwd: process.cwd(),
|
|
207
210
|
highlights: options.highlights,
|
|
208
211
|
});
|
|
209
212
|
}
|
|
210
213
|
return renderReleaseNotesSection({
|
|
211
|
-
currentVersion:
|
|
212
|
-
nextVersion:
|
|
214
|
+
currentVersion: root.currentVersion,
|
|
215
|
+
nextVersion: root.nextVersion,
|
|
213
216
|
commits: dedupedCommits,
|
|
214
217
|
cwd: options.cwd ?? process.cwd(),
|
|
215
218
|
dependencies: resolvePackageDependencies(plan, "."),
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ReleasePlan } from "./plan.js";
|
|
2
|
+
export type ReleaseCohort = string[];
|
|
3
|
+
export declare function buildInitialReleaseCohorts(plan: ReleasePlan): ReleaseCohort[];
|
|
4
|
+
export declare function stabilizeReleaseCohorts(initial: readonly ReleaseCohort[], changedFilesFor: (packagePaths: ReleaseCohort) => readonly string[]): ReleaseCohort[];
|
|
5
|
+
export declare function resolveSeparateReleaseBranchPrefix(prefix: string): string;
|
|
6
|
+
export declare function resolveSeparateReleaseBranch(prefix: string, releaseName: string, packagePath: string): string;
|