versionary 1.0.1 → 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 +211 -24
- 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/cohorts.d.ts +6 -0
- package/dist/release/cohorts.js +125 -0
- package/dist/release/plan.js +49 -45
- package/dist/release/pr.d.ts +69 -0
- package/dist/release/pr.js +555 -22
- 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,9 +1,11 @@
|
|
|
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";
|
|
8
10
|
/**
|
|
9
11
|
* What a dry run would actually release. `plan.nextVersion` is the repository
|
|
@@ -53,6 +55,140 @@ function parseFlags(args) {
|
|
|
53
55
|
function emitJson(payload) {
|
|
54
56
|
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
55
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
|
+
}
|
|
56
192
|
async function main() {
|
|
57
193
|
const [, , command, ...args] = process.argv;
|
|
58
194
|
const flags = parseFlags(args);
|
|
@@ -62,51 +198,69 @@ async function main() {
|
|
|
62
198
|
encoding: "utf8",
|
|
63
199
|
stdio: ["ignore", "pipe", "ignore"],
|
|
64
200
|
}).trim();
|
|
65
|
-
if (isReleaseCommitMessage(commitMessage)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return 0;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
if (flags.json) {
|
|
77
|
-
const release = await runReleaseDetailed(process.cwd(), {
|
|
78
|
-
logger,
|
|
79
|
-
"dry-run": flags["dry-run"],
|
|
80
|
-
});
|
|
81
|
-
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) {
|
|
82
209
|
emitJson({
|
|
83
210
|
action: "release-skipped",
|
|
84
211
|
message: release.reason,
|
|
85
212
|
releaseCreated: false,
|
|
86
213
|
tagNames: [],
|
|
87
214
|
});
|
|
88
|
-
return 0;
|
|
89
215
|
}
|
|
90
|
-
|
|
216
|
+
else {
|
|
217
|
+
console.log(release.reason);
|
|
218
|
+
}
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
if (release.action === "release-dry-run") {
|
|
222
|
+
if (flags.json) {
|
|
91
223
|
emitJson({
|
|
92
224
|
action: "release-dry-run",
|
|
93
225
|
message: release.message,
|
|
94
226
|
releaseCreated: false,
|
|
95
227
|
tagNames: release.targets.map((target) => target.tag),
|
|
228
|
+
releaseTargets: release.releaseTargets,
|
|
96
229
|
targets: release.targets,
|
|
97
230
|
});
|
|
98
|
-
return 0;
|
|
99
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];
|
|
100
243
|
emitJson({
|
|
101
244
|
action: "release-published",
|
|
102
245
|
message: release.message,
|
|
103
246
|
releaseCreated: release.releases.length > 0,
|
|
104
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,
|
|
105
253
|
});
|
|
106
|
-
return 0;
|
|
107
254
|
}
|
|
108
|
-
|
|
109
|
-
|
|
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)) {
|
|
110
264
|
return 0;
|
|
111
265
|
}
|
|
112
266
|
const plan = createReleasePlan();
|
|
@@ -127,6 +281,26 @@ async function main() {
|
|
|
127
281
|
console.log(message);
|
|
128
282
|
return 0;
|
|
129
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
|
+
}
|
|
130
304
|
if (flags["dry-run"]) {
|
|
131
305
|
const dryRunMessage = `Dry run: would prepare release PR branch ${plan.releaseBranchPrefix} for ${formatDryRunReleaseSubject(plan)}`;
|
|
132
306
|
if (flags.json) {
|
|
@@ -221,6 +395,9 @@ async function main() {
|
|
|
221
395
|
return 0;
|
|
222
396
|
}
|
|
223
397
|
if (command === "pr") {
|
|
398
|
+
if (await recoverPendingReleaseIfNeeded({ ...flags, json: false }, console)) {
|
|
399
|
+
return 0;
|
|
400
|
+
}
|
|
224
401
|
const plan = createReleasePlan();
|
|
225
402
|
if (!plan.nextVersion) {
|
|
226
403
|
if (!flags["dry-run"]) {
|
|
@@ -231,6 +408,16 @@ async function main() {
|
|
|
231
408
|
console.log("No releasable commits found. Nothing to do.");
|
|
232
409
|
return 0;
|
|
233
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
|
+
}
|
|
234
421
|
if (flags["dry-run"]) {
|
|
235
422
|
console.log(`Dry run: would prepare release PR branch ${plan.releaseBranchPrefix} for ${formatDryRunReleaseSubject(plan)}`);
|
|
236
423
|
return 0;
|
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);
|
|
@@ -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;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
function normalizedCohorts(cohorts) {
|
|
3
|
+
return cohorts
|
|
4
|
+
.map((cohort) => [...new Set(cohort)].sort((a, b) => a.localeCompare(b)))
|
|
5
|
+
.filter((cohort) => cohort.length > 0)
|
|
6
|
+
.sort((a, b) => (a[0] ?? "").localeCompare(b[0] ?? ""));
|
|
7
|
+
}
|
|
8
|
+
export function buildInitialReleaseCohorts(plan) {
|
|
9
|
+
const releasing = (plan.packages ?? [])
|
|
10
|
+
.filter((pkg) => pkg.nextVersion)
|
|
11
|
+
.map((pkg) => pkg.path)
|
|
12
|
+
.sort((a, b) => a.localeCompare(b));
|
|
13
|
+
const releasingSet = new Set(releasing);
|
|
14
|
+
const parent = new Map(releasing.map((packagePath) => [packagePath, packagePath]));
|
|
15
|
+
const find = (packagePath) => {
|
|
16
|
+
const current = parent.get(packagePath) ?? packagePath;
|
|
17
|
+
if (current === packagePath) {
|
|
18
|
+
return current;
|
|
19
|
+
}
|
|
20
|
+
const root = find(current);
|
|
21
|
+
parent.set(packagePath, root);
|
|
22
|
+
return root;
|
|
23
|
+
};
|
|
24
|
+
const union = (left, right) => {
|
|
25
|
+
const leftRoot = find(left);
|
|
26
|
+
const rightRoot = find(right);
|
|
27
|
+
if (leftRoot === rightRoot) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const [first, second] = [leftRoot, rightRoot].sort((a, b) => a.localeCompare(b));
|
|
31
|
+
if (first && second) {
|
|
32
|
+
parent.set(second, first);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
for (const pkg of plan.packages ?? []) {
|
|
36
|
+
if (!pkg.nextVersion || !releasingSet.has(pkg.path)) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
for (const sourcePath of pkg.dependencySourcePaths ?? []) {
|
|
40
|
+
if (releasingSet.has(sourcePath)) {
|
|
41
|
+
union(pkg.path, sourcePath);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const grouped = new Map();
|
|
46
|
+
for (const packagePath of releasing) {
|
|
47
|
+
const root = find(packagePath);
|
|
48
|
+
grouped.set(root, [...(grouped.get(root) ?? []), packagePath]);
|
|
49
|
+
}
|
|
50
|
+
return normalizedCohorts([...grouped.values()]);
|
|
51
|
+
}
|
|
52
|
+
function overlaps(left, right) {
|
|
53
|
+
for (const value of left) {
|
|
54
|
+
if (right.has(value)) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
export function stabilizeReleaseCohorts(initial, changedFilesFor) {
|
|
61
|
+
let cohorts = normalizedCohorts(initial);
|
|
62
|
+
while (cohorts.length > 1) {
|
|
63
|
+
const footprints = cohorts.map((cohort) => new Set(changedFilesFor(cohort).map((file) => file.replaceAll("\\", "/"))));
|
|
64
|
+
const parent = cohorts.map((_, index) => index);
|
|
65
|
+
const find = (index) => {
|
|
66
|
+
const current = parent[index] ?? index;
|
|
67
|
+
if (current === index) {
|
|
68
|
+
return current;
|
|
69
|
+
}
|
|
70
|
+
const root = find(current);
|
|
71
|
+
parent[index] = root;
|
|
72
|
+
return root;
|
|
73
|
+
};
|
|
74
|
+
const union = (left, right) => {
|
|
75
|
+
const leftRoot = find(left);
|
|
76
|
+
const rightRoot = find(right);
|
|
77
|
+
if (leftRoot !== rightRoot) {
|
|
78
|
+
parent[Math.max(leftRoot, rightRoot)] = Math.min(leftRoot, rightRoot);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
for (let left = 0; left < cohorts.length; left += 1) {
|
|
82
|
+
for (let right = left + 1; right < cohorts.length; right += 1) {
|
|
83
|
+
if (overlaps(footprints[left] ?? new Set(), footprints[right] ?? new Set())) {
|
|
84
|
+
union(left, right);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const merged = new Map();
|
|
89
|
+
for (let index = 0; index < cohorts.length; index += 1) {
|
|
90
|
+
const root = find(index);
|
|
91
|
+
merged.set(root, [
|
|
92
|
+
...(merged.get(root) ?? []),
|
|
93
|
+
...(cohorts[index] ?? []),
|
|
94
|
+
]);
|
|
95
|
+
}
|
|
96
|
+
const next = normalizedCohorts([...merged.values()]);
|
|
97
|
+
if (next.length === cohorts.length) {
|
|
98
|
+
return cohorts;
|
|
99
|
+
}
|
|
100
|
+
cohorts = next;
|
|
101
|
+
}
|
|
102
|
+
return cohorts;
|
|
103
|
+
}
|
|
104
|
+
// The prefix shared by every separate release branch. It is the configured
|
|
105
|
+
// release branch itself, so listing on it also matches the legacy single
|
|
106
|
+
// release branch left behind by repos migrating from the combined PR flow.
|
|
107
|
+
export function resolveSeparateReleaseBranchPrefix(prefix) {
|
|
108
|
+
return prefix.replace(/\/+$/gu, "");
|
|
109
|
+
}
|
|
110
|
+
export function resolveSeparateReleaseBranch(prefix, releaseName, packagePath) {
|
|
111
|
+
const normalizedPrefix = resolveSeparateReleaseBranchPrefix(prefix);
|
|
112
|
+
const slug = releaseName
|
|
113
|
+
.trim()
|
|
114
|
+
.replace(/^@/u, "")
|
|
115
|
+
.replace(/[^A-Za-z0-9._-]+/gu, "-")
|
|
116
|
+
.replace(/^-+|-+$/gu, "") || "package";
|
|
117
|
+
const digest = createHash("sha256")
|
|
118
|
+
.update(packagePath)
|
|
119
|
+
.digest("hex")
|
|
120
|
+
.slice(0, 12);
|
|
121
|
+
// A sibling of the legacy release branch, not a child of it: git cannot hold
|
|
122
|
+
// both `refs/heads/<prefix>` and `refs/heads/<prefix>/<name>`, and repos
|
|
123
|
+
// migrating from the combined PR flow still have the former on the remote.
|
|
124
|
+
return `${normalizedPrefix}-${slug}-${digest}`;
|
|
125
|
+
}
|