versionary 1.0.1 → 1.2.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 +45 -18
- 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/changelog.js +1 -2
- 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
|
@@ -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
|
+
}
|
package/dist/release/plan.js
CHANGED
|
@@ -259,6 +259,50 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
259
259
|
}
|
|
260
260
|
return reachable;
|
|
261
261
|
};
|
|
262
|
+
const withReleasingDependencies = (plans) => {
|
|
263
|
+
const releasingPaths = new Set(plans
|
|
264
|
+
.filter((pkgPlan) => pkgPlan.nextVersion)
|
|
265
|
+
.map((pkgPlan) => pkgPlan.path));
|
|
266
|
+
for (const pkgPlan of plans) {
|
|
267
|
+
const strategyContext = strategyContextByPath.get(pkgPlan.path);
|
|
268
|
+
if (strategyContext) {
|
|
269
|
+
strategyContext.nextVersion = pkgPlan.nextVersion;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const dependencySourcePathsByPackage = new Map();
|
|
273
|
+
for (const sourcePackage of plans) {
|
|
274
|
+
if (!sourcePackage.nextVersion) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
for (const impactedPath of findDependents(sourcePackage.path)) {
|
|
278
|
+
if (impactedPath === sourcePackage.path) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const existing = dependencySourcePathsByPackage.get(impactedPath);
|
|
282
|
+
if (existing) {
|
|
283
|
+
existing.add(sourcePackage.path);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
dependencySourcePathsByPackage.set(impactedPath, new Set([sourcePackage.path]));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return plans.map((pkgPlan) => {
|
|
291
|
+
const dependencySourcePaths = [
|
|
292
|
+
...(dependencySourcePathsByPackage.get(pkgPlan.path) ??
|
|
293
|
+
new Set()),
|
|
294
|
+
]
|
|
295
|
+
.filter((sourcePath) => releasingPaths.has(sourcePath))
|
|
296
|
+
.sort((a, b) => a.localeCompare(b));
|
|
297
|
+
if (dependencySourcePaths.length === 0) {
|
|
298
|
+
return pkgPlan;
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
...pkgPlan,
|
|
302
|
+
dependencySourcePaths,
|
|
303
|
+
};
|
|
304
|
+
});
|
|
305
|
+
};
|
|
262
306
|
// Both rules below can enable each other: forcing a bump creates a dependent
|
|
263
307
|
// whose requirement must be rewritten, and rewriting a dependent can in turn
|
|
264
308
|
// expose a stale dependency a further level up. Iterating to a fixpoint
|
|
@@ -302,51 +346,9 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
302
346
|
break;
|
|
303
347
|
}
|
|
304
348
|
}
|
|
305
|
-
const packageNextVersionByPath = {};
|
|
306
|
-
for (const pkgPlan of workingPlans) {
|
|
307
|
-
if (pkgPlan.nextVersion) {
|
|
308
|
-
packageNextVersionByPath[pkgPlan.path] = pkgPlan.nextVersion;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
const dependencySourcePathsByPackage = new Map();
|
|
312
|
-
const addDependencySourcePath = (targetPath, sourcePath) => {
|
|
313
|
-
if (targetPath === sourcePath) {
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
const existing = dependencySourcePathsByPackage.get(targetPath);
|
|
317
|
-
if (existing) {
|
|
318
|
-
existing.add(sourcePath);
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
dependencySourcePathsByPackage.set(targetPath, new Set([sourcePath]));
|
|
322
|
-
};
|
|
323
349
|
// Attribute each dependent's requirement rewrite to the specific sources
|
|
324
350
|
// driving it, using the settled version set so chained bumps are credited.
|
|
325
|
-
|
|
326
|
-
for (const sourcePackage of strategyGroup.packages) {
|
|
327
|
-
if (!sourcePackage.nextVersion) {
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
for (const impactedPath of findDependents(sourcePackage.packagePath)) {
|
|
331
|
-
addDependencySourcePath(impactedPath, sourcePackage.packagePath);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
const propagatedPackages = workingPlans.map((pkgPlan) => {
|
|
336
|
-
const dependencySourcePaths = [
|
|
337
|
-
...(dependencySourcePathsByPackage.get(pkgPlan.path) ??
|
|
338
|
-
new Set()),
|
|
339
|
-
]
|
|
340
|
-
.filter((sourcePath) => Boolean(packageNextVersionByPath[sourcePath]))
|
|
341
|
-
.sort((a, b) => a.localeCompare(b));
|
|
342
|
-
if (dependencySourcePaths.length === 0) {
|
|
343
|
-
return pkgPlan;
|
|
344
|
-
}
|
|
345
|
-
return {
|
|
346
|
-
...pkgPlan,
|
|
347
|
-
dependencySourcePaths,
|
|
348
|
-
};
|
|
349
|
-
});
|
|
351
|
+
const propagatedPackages = withReleasingDependencies(workingPlans);
|
|
350
352
|
const followsByPath = new Map();
|
|
351
353
|
for (const [packagePath, packageConfig] of Object.entries(loaded.config.packages ?? {})) {
|
|
352
354
|
const follows = packageConfig.follows ?? [];
|
|
@@ -429,11 +431,13 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
429
431
|
: analyzedFixedType
|
|
430
432
|
? bumpVersion(fixedBaseVersion, analyzedFixedType, { allowStableMajor })
|
|
431
433
|
: null;
|
|
432
|
-
|
|
434
|
+
// Fixed mode can promote unchanged dependencies into the release, so the
|
|
435
|
+
// final shared version set must drive dependency attribution.
|
|
436
|
+
const adjusted = withReleasingDependencies(adjustedPackages.map((pkgPlan) => ({
|
|
433
437
|
...pkgPlan,
|
|
434
438
|
releaseType: fixedType,
|
|
435
439
|
nextVersion: fixedNextVersion,
|
|
436
|
-
}));
|
|
440
|
+
})));
|
|
437
441
|
return {
|
|
438
442
|
mode: "simple",
|
|
439
443
|
releaseType: fixedType,
|
package/dist/release/pr.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ 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
4
|
import { type ReleasePlan } from "./plan.js";
|
|
5
|
+
import { type ReleaseTargetState } from "./state.js";
|
|
5
6
|
/**
|
|
6
7
|
* Read the manual-notes ("Unreleased") prose from the top of a changelog file.
|
|
7
8
|
* Returns an empty string when the file is absent or has no notes block.
|
|
@@ -20,6 +21,31 @@ export declare function splitSafeDirtyFiles(files: string[]): {
|
|
|
20
21
|
ignored: string[];
|
|
21
22
|
blocking: string[];
|
|
22
23
|
};
|
|
24
|
+
export interface PendingReleasePrResult {
|
|
25
|
+
branch: string;
|
|
26
|
+
title: string;
|
|
27
|
+
updated: boolean;
|
|
28
|
+
targets: ReleaseTargetState[];
|
|
29
|
+
body: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function renderPendingReleaseReviewRequestBody(targets: ReleaseTargetState[]): string;
|
|
32
|
+
/**
|
|
33
|
+
* Recreate the release marker on the corrected base without advancing an
|
|
34
|
+
* untagged version. The empty commit is intentional: the version and
|
|
35
|
+
* changelog changes were already reviewed in the original release PR.
|
|
36
|
+
*/
|
|
37
|
+
export declare function preparePendingReleasePr(cwd?: string, options?: {
|
|
38
|
+
logger?: VersionaryPluginContext["logger"];
|
|
39
|
+
}): PendingReleasePrResult;
|
|
40
|
+
/**
|
|
41
|
+
* Recreate each unpublished package cohort on its own corrected-base branch.
|
|
42
|
+
* A temporary worktree keeps the caller on the trunk commit that triggered
|
|
43
|
+
* recovery while still leaving local branch refs available for pushing.
|
|
44
|
+
*/
|
|
45
|
+
export declare function preparePendingSeparateReleasePrs(cwd?: string, options?: {
|
|
46
|
+
logger?: VersionaryPluginContext["logger"];
|
|
47
|
+
"dry-run"?: boolean;
|
|
48
|
+
}): SeparateReviewCandidate[];
|
|
23
49
|
export declare function prepareReleasePr(cwd?: string, options?: {
|
|
24
50
|
logger?: VersionaryPluginContext["logger"];
|
|
25
51
|
}): {
|
|
@@ -32,11 +58,54 @@ export declare function prepareReleasePr(cwd?: string, options?: {
|
|
|
32
58
|
updated: boolean;
|
|
33
59
|
highlights: string;
|
|
34
60
|
};
|
|
61
|
+
export interface PreparedSeparateReleasePr {
|
|
62
|
+
branch: string;
|
|
63
|
+
title: string;
|
|
64
|
+
version: string;
|
|
65
|
+
previousVersion: string;
|
|
66
|
+
commits: ParsedCommit[];
|
|
67
|
+
plan: ReleasePlan;
|
|
68
|
+
updated: boolean;
|
|
69
|
+
highlights: string;
|
|
70
|
+
packagePaths: string[];
|
|
71
|
+
targets: ReleaseTargetState[];
|
|
72
|
+
body: string;
|
|
73
|
+
}
|
|
74
|
+
export interface SeparateReviewCandidate {
|
|
75
|
+
branch: string;
|
|
76
|
+
title: string;
|
|
77
|
+
updated: boolean;
|
|
78
|
+
packagePaths: string[];
|
|
79
|
+
targets: ReleaseTargetState[];
|
|
80
|
+
body: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Build every independently mergeable release branch from the same immutable
|
|
84
|
+
* base without moving or modifying the caller's worktree.
|
|
85
|
+
*/
|
|
86
|
+
export declare function prepareSeparateReleasePrs(cwd?: string, options?: {
|
|
87
|
+
logger?: VersionaryPluginContext["logger"];
|
|
88
|
+
"dry-run"?: boolean;
|
|
89
|
+
}): PreparedSeparateReleasePr[];
|
|
35
90
|
export declare function renderSimpleReviewRequestBody(version: string, previousVersion: string, commits: ParsedCommit[], plan?: ReleasePlan | null, cwd?: string, highlights?: string, loadedConfig?: VersionaryConfig): string;
|
|
36
91
|
export declare function openOrUpdateReviewRequest(cwd: string, branch: string, title: string, version: string, previousVersion: string, commits: ParsedCommit[], plan?: ReleasePlan | null, options?: {
|
|
37
92
|
logger?: VersionaryPluginContext["logger"];
|
|
38
93
|
highlights?: string;
|
|
94
|
+
body?: string;
|
|
39
95
|
}): Promise<string>;
|
|
96
|
+
export interface ReviewRequestRunResult {
|
|
97
|
+
packagePaths: string[];
|
|
98
|
+
branch: string;
|
|
99
|
+
title: string;
|
|
100
|
+
reviewUrl?: string;
|
|
101
|
+
status: "prepared" | "up-to-date" | "dry-run" | "recovered";
|
|
102
|
+
targets: ReleaseTargetState[];
|
|
103
|
+
}
|
|
104
|
+
export declare function reconcileSeparateReviewRequests(cwd: string, prepared: SeparateReviewCandidate[], options?: {
|
|
105
|
+
logger?: VersionaryPluginContext["logger"];
|
|
106
|
+
"dry-run"?: boolean;
|
|
107
|
+
recovered?: boolean;
|
|
108
|
+
}): Promise<ReviewRequestRunResult[]>;
|
|
40
109
|
export declare function closeStaleReviewRequestIfExists(cwd?: string, options?: {
|
|
41
110
|
logger?: VersionaryPluginContext["logger"];
|
|
42
111
|
}): Promise<{
|