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.
@@ -15,6 +15,12 @@ export type RunReleaseResult = {
15
15
  tag: string;
16
16
  version: string;
17
17
  }[];
18
+ releaseTargets: {
19
+ path: string;
20
+ tag: string;
21
+ version: string;
22
+ dependencies: string[];
23
+ }[];
18
24
  } | {
19
25
  action: "release-published";
20
26
  message: string;
@@ -24,6 +30,12 @@ export type RunReleaseResult = {
24
30
  tagStatus: "created" | "exists";
25
31
  metadataStatus: "created" | "exists";
26
32
  }[];
33
+ releaseTargets: {
34
+ path: string;
35
+ tag: string;
36
+ version: string;
37
+ dependencies: string[];
38
+ }[];
27
39
  };
28
40
  export interface RunReleaseOptions {
29
41
  logger?: VersionaryPluginContext["logger"];
@@ -8,7 +8,8 @@ import { resolveVersionStrategy } from "../strategy/resolve.js";
8
8
  import { getChangelogDefaults } from "./plan.js";
9
9
  import { isReleaseCommitMessage } from "./pr.js";
10
10
  import { executeIdempotentReleaseTarget } from "./recovery.js";
11
- import { readPendingReleaseTargets } from "./state.js";
11
+ import { hasReleaseStateChangeAtHead, readPendingReleaseTargets, readRecordedPendingReleaseTargets, } from "./state.js";
12
+ import { releaseTargetHandoff } from "./targets.js";
12
13
  function escapeRegExp(input) {
13
14
  return input.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
14
15
  }
@@ -132,7 +133,8 @@ export async function runRelease(cwd = process.cwd()) {
132
133
  }
133
134
  export async function runReleaseDetailed(cwd = process.cwd(), options = {}) {
134
135
  const commitMessage = getHeadCommitMessage(cwd);
135
- if (!isReleaseCommitMessage(commitMessage)) {
136
+ if (!isReleaseCommitMessage(commitMessage) &&
137
+ !hasReleaseStateChangeAtHead(cwd)) {
136
138
  return {
137
139
  action: "release-skipped",
138
140
  reason: "No release commit context detected; skipping release stage.",
@@ -145,18 +147,33 @@ export async function runReleaseDetailed(cwd = process.cwd(), options = {}) {
145
147
  defaultChangelogFormat: strategy.getDefaultChangelogFormat?.(),
146
148
  });
147
149
  const referenceCommentMode = loaded.config["release-reference-comments"] ?? "off";
148
- const version = strategy.readVersion(cwd, loaded.config);
149
- const defaultTag = `v${version}`;
150
150
  const releaseTargets = readPendingReleaseTargets(cwd);
151
- const targets = releaseTargets.length > 0
151
+ if (loaded.config["separate-release-prs"] && releaseTargets.length === 0) {
152
+ return {
153
+ action: "release-skipped",
154
+ reason: "No untagged package release targets found; skipping release stage.",
155
+ };
156
+ }
157
+ // Recorded-but-fully-tagged targets mean this release already published, so
158
+ // replaying them keeps a re-run idempotent (and still repairs missing release
159
+ // metadata). Only a repo that never recorded any target may fall back to
160
+ // tagging the root package.
161
+ const recordedTargets = releaseTargets.length > 0
152
162
  ? releaseTargets
163
+ : readRecordedPendingReleaseTargets(cwd);
164
+ const version = recordedTargets.length === 0
165
+ ? strategy.readVersion(cwd, loaded.config)
166
+ : undefined;
167
+ const targets = recordedTargets.length > 0
168
+ ? recordedTargets
153
169
  : [
154
170
  {
155
171
  path: ".",
156
- version,
157
- tag: defaultTag,
172
+ version: version,
173
+ tag: `v${version}`,
158
174
  },
159
175
  ];
176
+ const releaseTargetPlan = releaseTargetHandoff(targets, options.logger);
160
177
  if (options["dry-run"]) {
161
178
  const targetList = targets.map((target) => `${target.tag} (${target.version})`);
162
179
  return {
@@ -165,6 +182,7 @@ export async function runReleaseDetailed(cwd = process.cwd(), options = {}) {
165
182
  tag: target.tag,
166
183
  version: target.version,
167
184
  })),
185
+ releaseTargets: releaseTargetPlan,
168
186
  message: `Dry run: would publish releases ${targetList.join(", ")}`,
169
187
  };
170
188
  }
@@ -221,6 +239,7 @@ export async function runReleaseDetailed(cwd = process.cwd(), options = {}) {
221
239
  return {
222
240
  action: "release-published",
223
241
  releases,
242
+ releaseTargets: releaseTargetPlan,
224
243
  message: `Published releases ${published.join(", ")}`,
225
244
  };
226
245
  }
@@ -2,9 +2,24 @@ export interface ReleaseTargetState {
2
2
  path: string;
3
3
  version: string;
4
4
  tag: string;
5
+ dependencies?: string[];
6
+ }
7
+ export interface PendingReleaseCohort {
8
+ branch: string;
9
+ targets: ReleaseTargetState[];
5
10
  }
6
11
  export declare function getBaselineStatePath(cwd: string): string;
12
+ export declare function getPackageStateDirectory(cwd: string): string;
7
13
  export declare function readBaselineSha(cwd?: string): string | null;
8
14
  export declare function readReleaseTargets(cwd?: string): ReleaseTargetState[];
9
15
  export declare function readPendingReleaseTargets(cwd?: string): ReleaseTargetState[];
10
- export declare function writeBaselineSha(cwd?: string, sha?: string, releaseTargets?: ReleaseTargetState[]): void;
16
+ export declare function readRecordedPendingReleaseTargets(cwd?: string): ReleaseTargetState[];
17
+ export declare function readPendingReleaseCohorts(cwd?: string): PendingReleaseCohort[];
18
+ /**
19
+ * Pending targets remain in the manifest after publishing, so the tag set is
20
+ * the durable evidence that the pending release actually completed.
21
+ */
22
+ export declare function hasFullyUntaggedPendingRelease(cwd?: string): boolean;
23
+ export declare function writePackageReleaseState(cwd: string, baselineSha: string, releaseTargets: ReleaseTargetState[], branch: string): string[];
24
+ export declare function hasReleaseStateChangeAtHead(cwd?: string): boolean;
25
+ export declare function writeBaselineSha(cwd?: string, sha?: string, releaseTargets?: ReleaseTargetState[]): string[];
@@ -1,4 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
4
5
  import { loadConfig } from "../config/load-config.js";
@@ -41,6 +42,11 @@ function validateReleaseTargets(value, key, filePath) {
41
42
  typeof record.tag !== "string") {
42
43
  throw new Error(`Invalid release manifest at ${filePath}: ${key} must contain string path, version, and tag.`);
43
44
  }
45
+ if (record.dependencies !== undefined &&
46
+ (!Array.isArray(record.dependencies) ||
47
+ !record.dependencies.every((dependency) => typeof dependency === "string"))) {
48
+ throw new Error(`Invalid release manifest at ${filePath}: ${key} dependencies must be an array of package-path strings.`);
49
+ }
44
50
  }
45
51
  }
46
52
  export function getBaselineStatePath(cwd) {
@@ -51,6 +57,57 @@ export function getBaselineStatePath(cwd) {
51
57
  }
52
58
  return path.join(cwd, ".versionary-manifest.json");
53
59
  }
60
+ export function getPackageStateDirectory(cwd) {
61
+ return `${getBaselineStatePath(cwd)}.d`;
62
+ }
63
+ function packageStateFileName(packagePath) {
64
+ const slug = packagePath === "."
65
+ ? "root"
66
+ : packagePath
67
+ .replaceAll("\\", "/")
68
+ .replace(/[^A-Za-z0-9._-]+/gu, "-")
69
+ .replace(/^-+|-+$/gu, "") || "package";
70
+ const digest = createHash("sha256")
71
+ .update(packagePath)
72
+ .digest("hex")
73
+ .slice(0, 12);
74
+ return `${slug}-${digest}.json`;
75
+ }
76
+ function parsePackageStateFile(raw, filePath) {
77
+ const parsed = JSON.parse(raw);
78
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
79
+ throw new Error(`Invalid package release state at ${filePath}: expected an object.`);
80
+ }
81
+ const state = parsed;
82
+ if (state[MANIFEST_VERSION_KEY] !== 1) {
83
+ throw new Error(`Unsupported ${MANIFEST_VERSION_KEY} in ${filePath}: ${String(state[MANIFEST_VERSION_KEY])}.`);
84
+ }
85
+ if (typeof state.path !== "string" ||
86
+ typeof state[BASELINE_SHA_KEY] !== "string" ||
87
+ typeof state["release-branch"] !== "string") {
88
+ throw new Error(`Invalid package release state at ${filePath}: path, baseline-sha, and release-branch must be strings.`);
89
+ }
90
+ validateReleaseTargets([state["release-target"]], "release-target", filePath);
91
+ const target = state["release-target"];
92
+ if (target.path !== state.path) {
93
+ throw new Error(`Invalid package release state at ${filePath}: release target path must match state path.`);
94
+ }
95
+ return state;
96
+ }
97
+ function readPackageStates(cwd) {
98
+ const directory = getPackageStateDirectory(cwd);
99
+ if (!fs.existsSync(directory)) {
100
+ return [];
101
+ }
102
+ return fs
103
+ .readdirSync(directory, { withFileTypes: true })
104
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
105
+ .map((entry) => {
106
+ const filePath = path.join(directory, entry.name);
107
+ return parsePackageStateFile(fs.readFileSync(filePath, "utf8"), filePath);
108
+ })
109
+ .sort((a, b) => a.path.localeCompare(b.path));
110
+ }
54
111
  export function readBaselineSha(cwd = process.cwd()) {
55
112
  const filePath = getBaselineStatePath(cwd);
56
113
  if (!fs.existsSync(filePath)) {
@@ -64,22 +121,132 @@ export function readBaselineSha(cwd = process.cwd()) {
64
121
  // so this list must persist entries across releases rather than be replaced.
65
122
  export function readReleaseTargets(cwd = process.cwd()) {
66
123
  const filePath = getBaselineStatePath(cwd);
67
- if (!fs.existsSync(filePath)) {
68
- return [];
69
- }
70
- const parsed = parseStateFile(fs.readFileSync(filePath, "utf8"), filePath);
71
- return parsed[RELEASE_TARGETS_KEY] ?? [];
124
+ const legacyTargets = fs.existsSync(filePath)
125
+ ? (parseStateFile(fs.readFileSync(filePath, "utf8"), filePath)[RELEASE_TARGETS_KEY] ?? [])
126
+ : [];
127
+ const packageTargets = readPackageStates(cwd).map((state) => state["release-target"]);
128
+ return [
129
+ ...new Map([...legacyTargets, ...packageTargets].map((target) => [
130
+ target.path,
131
+ target,
132
+ ])).values(),
133
+ ].sort((a, b) => a.path.localeCompare(b.path));
72
134
  }
73
135
  // The publish set introduced by the current release PR. `release` consumes this
74
136
  // so it only publishes/announces what this PR bumped, not every package in the
75
137
  // accumulated baseline.
76
138
  export function readPendingReleaseTargets(cwd = process.cwd()) {
139
+ return collectPendingReleaseCohorts(cwd, {
140
+ allowPartiallyTagged: true,
141
+ }).flatMap((cohort) => cohort.targets);
142
+ }
143
+ // Every target the release state records, including cohorts whose tags already
144
+ // exist. `release` needs these to tell "this repo never recorded a release"
145
+ // apart from "this release was already published"; only the former may fall
146
+ // back to tagging the root package.
147
+ export function readRecordedPendingReleaseTargets(cwd = process.cwd()) {
148
+ return collectPendingReleaseCohorts(cwd, {
149
+ allowPartiallyTagged: true,
150
+ includeFullyTagged: true,
151
+ }).flatMap((cohort) => cohort.targets);
152
+ }
153
+ export function readPendingReleaseCohorts(cwd = process.cwd()) {
154
+ return collectPendingReleaseCohorts(cwd, {});
155
+ }
156
+ function collectPendingReleaseCohorts(cwd, options) {
77
157
  const filePath = getBaselineStatePath(cwd);
78
- if (!fs.existsSync(filePath)) {
79
- return [];
158
+ const packageStates = readPackageStates(cwd);
159
+ const sidecarPaths = new Set(packageStates.map((state) => state.path));
160
+ const byBranch = new Map();
161
+ for (const state of packageStates) {
162
+ const targets = byBranch.get(state["release-branch"]) ?? [];
163
+ targets.push(state["release-target"]);
164
+ byBranch.set(state["release-branch"], targets);
165
+ }
166
+ if (fs.existsSync(filePath)) {
167
+ const parsed = parseStateFile(fs.readFileSync(filePath, "utf8"), filePath);
168
+ const legacyPending = (parsed[PENDING_RELEASE_TARGETS_KEY] ?? []).filter((target) => !sidecarPaths.has(target.path));
169
+ if (legacyPending.length > 0) {
170
+ const branch = loadConfig(cwd).config["release-branch"] ?? "versionary/release";
171
+ byBranch.set(branch, [...(byBranch.get(branch) ?? []), ...legacyPending]);
172
+ }
173
+ }
174
+ const pending = [];
175
+ for (const [branch, unsortedTargets] of byBranch) {
176
+ const targets = [...unsortedTargets].sort((a, b) => a.path.localeCompare(b.path));
177
+ const tagged = targets.filter((target) => hasLocalTag(cwd, target.tag));
178
+ if (tagged.length === targets.length && !options.includeFullyTagged) {
179
+ continue;
180
+ }
181
+ if (tagged.length > 0 &&
182
+ tagged.length < targets.length &&
183
+ !options.allowPartiallyTagged) {
184
+ const existingTags = tagged.map((target) => target.tag).join(", ");
185
+ const missingTags = targets
186
+ .filter((target) => !tagged.includes(target))
187
+ .map((target) => target.tag)
188
+ .join(", ");
189
+ throw new Error(`Pending release cohort on ${branch} is partially tagged and cannot move safely. Existing tags: ${existingTags}. Missing tags: ${missingTags}. Rerun the original release workflow to complete it.`);
190
+ }
191
+ pending.push({ branch, targets });
192
+ }
193
+ return pending.sort((a, b) => a.branch.localeCompare(b.branch));
194
+ }
195
+ function hasLocalTag(cwd, tag) {
196
+ try {
197
+ execFileSync("git", ["show-ref", "--verify", "--quiet", `refs/tags/${tag}`], {
198
+ cwd,
199
+ stdio: "ignore",
200
+ });
201
+ return true;
202
+ }
203
+ catch {
204
+ return false;
205
+ }
206
+ }
207
+ /**
208
+ * Pending targets remain in the manifest after publishing, so the tag set is
209
+ * the durable evidence that the pending release actually completed.
210
+ */
211
+ export function hasFullyUntaggedPendingRelease(cwd = process.cwd()) {
212
+ return readPendingReleaseCohorts(cwd).length > 0;
213
+ }
214
+ export function writePackageReleaseState(cwd, baselineSha, releaseTargets, branch) {
215
+ const directory = getPackageStateDirectory(cwd);
216
+ fs.mkdirSync(directory, { recursive: true });
217
+ const written = [];
218
+ for (const target of releaseTargets) {
219
+ const filePath = path.join(directory, packageStateFileName(target.path));
220
+ const state = {
221
+ [MANIFEST_VERSION_KEY]: 1,
222
+ path: target.path,
223
+ [BASELINE_SHA_KEY]: baselineSha,
224
+ "release-target": target,
225
+ "release-branch": branch,
226
+ };
227
+ fs.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
228
+ written.push(path.relative(cwd, filePath));
229
+ }
230
+ return written.sort((a, b) => a.localeCompare(b));
231
+ }
232
+ export function hasReleaseStateChangeAtHead(cwd = process.cwd()) {
233
+ const relativeDirectory = path.relative(cwd, getPackageStateDirectory(cwd));
234
+ try {
235
+ const changed = execFileSync("git", ["diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD^", "HEAD"], {
236
+ cwd,
237
+ encoding: "utf8",
238
+ stdio: ["ignore", "pipe", "ignore"],
239
+ })
240
+ .split("\n")
241
+ .map((entry) => entry.trim())
242
+ .filter(Boolean);
243
+ return changed.some((entry) => entry === relativeDirectory ||
244
+ entry.startsWith(`${relativeDirectory}${path.sep}`) ||
245
+ entry.startsWith(`${relativeDirectory}/`));
246
+ }
247
+ catch {
248
+ return false;
80
249
  }
81
- const parsed = parseStateFile(fs.readFileSync(filePath, "utf8"), filePath);
82
- return parsed[PENDING_RELEASE_TARGETS_KEY] ?? [];
83
250
  }
84
251
  export function writeBaselineSha(cwd = process.cwd(), sha, releaseTargets) {
85
252
  const baselineShaValue = sha ??
@@ -114,4 +281,10 @@ export function writeBaselineSha(cwd = process.cwd(), sha, releaseTargets) {
114
281
  [PENDING_RELEASE_TARGETS_KEY]: nextPending,
115
282
  };
116
283
  fs.writeFileSync(filePath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
284
+ const written = [path.relative(cwd, filePath)];
285
+ if (releaseTargets !== undefined &&
286
+ fs.existsSync(getPackageStateDirectory(cwd))) {
287
+ written.push(...writePackageReleaseState(cwd, baselineShaValue, releaseTargets, loadConfig(cwd).config["release-branch"] ?? "versionary/release"));
288
+ }
289
+ return written.sort((a, b) => a.localeCompare(b));
117
290
  }
@@ -0,0 +1,21 @@
1
+ import type { ReleaseTargetState } from "./state.js";
2
+ export interface ReleaseTargetOrder {
3
+ ordered: ReleaseTargetState[];
4
+ cyclicPaths: string[];
5
+ }
6
+ /**
7
+ * Return a deterministic dependency-first handoff for downstream publishers.
8
+ * Dependencies outside this release set are already published and do not
9
+ * constrain the current targets.
10
+ *
11
+ * A cycle cannot be ordered dependency-first, so the remaining targets are
12
+ * emitted in package-path order instead. The ordering is advisory metadata, so
13
+ * degrading it must not block tagging; callers surface `cyclicPaths` so the
14
+ * cycle still gets reported.
15
+ */
16
+ export declare function orderReleaseTargets(targets: readonly ReleaseTargetState[]): ReleaseTargetOrder;
17
+ export declare function releaseTargetHandoff(targets: readonly ReleaseTargetState[], logger?: {
18
+ warn: (message: string) => void;
19
+ }): Array<ReleaseTargetState & {
20
+ dependencies: string[];
21
+ }>;
@@ -0,0 +1,85 @@
1
+ function comparePaths(left, right) {
2
+ return left.localeCompare(right);
3
+ }
4
+ function buildDependencyGraph(targets) {
5
+ const releasingPaths = new Set(targets.map((target) => target.path));
6
+ return new Map(targets.map((target) => [
7
+ target.path,
8
+ new Set((target.dependencies ?? []).filter((dependencyPath) => dependencyPath !== target.path &&
9
+ releasingPaths.has(dependencyPath))),
10
+ ]));
11
+ }
12
+ /**
13
+ * Return the paths that actually sit on a dependency cycle, meaning those that
14
+ * are reachable from themselves. Packages that merely depend on a cycle are
15
+ * blocked by it but are not part of it, so naming them would send maintainers
16
+ * to inspect manifests that are fine.
17
+ */
18
+ function findCyclicPaths(graph) {
19
+ const cyclic = [];
20
+ for (const path of graph.keys()) {
21
+ const seen = new Set();
22
+ const stack = [...(graph.get(path) ?? [])];
23
+ let onCycle = false;
24
+ while (stack.length > 0) {
25
+ const current = stack.pop();
26
+ if (current === path) {
27
+ onCycle = true;
28
+ break;
29
+ }
30
+ if (seen.has(current)) {
31
+ continue;
32
+ }
33
+ seen.add(current);
34
+ stack.push(...(graph.get(current) ?? []));
35
+ }
36
+ if (onCycle) {
37
+ cyclic.push(path);
38
+ }
39
+ }
40
+ return cyclic.sort(comparePaths);
41
+ }
42
+ /**
43
+ * Return a deterministic dependency-first handoff for downstream publishers.
44
+ * Dependencies outside this release set are already published and do not
45
+ * constrain the current targets.
46
+ *
47
+ * A cycle cannot be ordered dependency-first, so the remaining targets are
48
+ * emitted in package-path order instead. The ordering is advisory metadata, so
49
+ * degrading it must not block tagging; callers surface `cyclicPaths` so the
50
+ * cycle still gets reported.
51
+ */
52
+ export function orderReleaseTargets(targets) {
53
+ const graph = buildDependencyGraph(targets);
54
+ const remaining = [...targets].sort((left, right) => comparePaths(left.path, right.path));
55
+ const emitted = new Set();
56
+ const ordered = [];
57
+ while (remaining.length > 0) {
58
+ const readyIndex = remaining.findIndex((target) => [...(graph.get(target.path) ?? [])].every((dependencyPath) => emitted.has(dependencyPath)));
59
+ // Nothing is ready only when every remaining target waits on a cycle;
60
+ // break the deadlock with the first target in package-path order.
61
+ const [target] = remaining.splice(readyIndex === -1 ? 0 : readyIndex, 1);
62
+ if (!target) {
63
+ continue;
64
+ }
65
+ ordered.push(target);
66
+ emitted.add(target.path);
67
+ }
68
+ return { ordered, cyclicPaths: findCyclicPaths(graph) };
69
+ }
70
+ export function releaseTargetHandoff(targets, logger) {
71
+ const { ordered, cyclicPaths } = orderReleaseTargets(targets);
72
+ if (cyclicPaths.length > 0) {
73
+ const quoted = cyclicPaths.map((path) => `"${path}"`).join(", ");
74
+ logger?.warn(`Release dependencies contain a cycle: ${quoted}. Publishing order for those packages falls back to package-path order.`);
75
+ }
76
+ const releasingPaths = new Set(targets.map((target) => target.path));
77
+ return ordered.map((target) => ({
78
+ ...target,
79
+ // Dependencies outside the release set are already published, and the
80
+ // documented contract is that every entry names a target in this handoff.
81
+ dependencies: [...new Set(target.dependencies ?? [])]
82
+ .filter((dependencyPath) => dependencyPath !== target.path && releasingPaths.has(dependencyPath))
83
+ .sort(comparePaths),
84
+ }));
85
+ }
@@ -223,6 +223,50 @@ export function createGitHubPlugin() {
223
223
  state: toReviewRequestState(created, `pull request #${created.number} in ${repoRef(repo)}`),
224
224
  };
225
225
  },
226
+ async listOpenReviewRequests(input, _context) {
227
+ const repo = getRepoFromEnv();
228
+ const octokit = new Octokit({ auth: getGitHubToken() });
229
+ const pulls = [];
230
+ try {
231
+ for (let page = 1;; page += 1) {
232
+ const response = await octokit.pulls.list({
233
+ owner: repo.owner,
234
+ repo: repo.repo,
235
+ state: "open",
236
+ base: input.baseBranch,
237
+ per_page: 100,
238
+ page,
239
+ });
240
+ pulls.push(...response.data);
241
+ if (response.data.length < 100) {
242
+ break;
243
+ }
244
+ }
245
+ }
246
+ catch (error) {
247
+ const { message } = parseGitHubError(error);
248
+ throw new Error(`Failed listing open pull requests into "${input.baseBranch}": [${repoRef(repo)}] ${message}`);
249
+ }
250
+ const requiredLabels = new Set(input.labels ?? []);
251
+ return pulls
252
+ .filter((pull) => pull.head.ref.startsWith(input.headBranchPrefix))
253
+ .filter((pull) => {
254
+ const labels = new Set(pull.labels
255
+ .map((label) => typeof label === "string" ? label : (label.name ?? ""))
256
+ .filter(Boolean));
257
+ return [...requiredLabels].every((label) => labels.has(label));
258
+ })
259
+ .map((pull) => ({
260
+ id: String(pull.id),
261
+ number: pull.number,
262
+ url: pull.html_url,
263
+ state: toReviewRequestState(pull, `pull request #${pull.number} in ${repoRef(repo)}`),
264
+ baseBranch: pull.base.ref,
265
+ headBranch: pull.head.ref,
266
+ title: pull.title,
267
+ }))
268
+ .sort((a, b) => a.headBranch.localeCompare(b.headBranch));
269
+ },
226
270
  async closeReviewRequestIfExists(input, _context) {
227
271
  const repo = getRepoFromEnv();
228
272
  const octokit = new Octokit({ auth: getGitHubToken() });
@@ -20,6 +20,16 @@ export interface ScmReviewRequestResult {
20
20
  url: string;
21
21
  state: "open" | "closed" | "merged";
22
22
  }
23
+ export interface ScmListReviewRequestsInput {
24
+ baseBranch: string;
25
+ headBranchPrefix: string;
26
+ labels?: string[];
27
+ }
28
+ export interface ScmReviewRequestSummary extends ScmReviewRequestResult {
29
+ baseBranch: string;
30
+ headBranch: string;
31
+ title: string;
32
+ }
23
33
  export interface ScmCloseReviewRequestInput {
24
34
  baseBranch: string;
25
35
  headBranch: string;
@@ -58,6 +68,7 @@ export interface ScmReleaseReferenceCommentsResult {
58
68
  export interface ScmClient {
59
69
  provider: ScmProvider;
60
70
  createOrUpdateReviewRequest: (input: ScmReviewRequestInput, context: ScmClientContext) => Promise<ScmReviewRequestResult>;
71
+ listOpenReviewRequests: (input: ScmListReviewRequestsInput, context: ScmClientContext) => Promise<ScmReviewRequestSummary[]>;
61
72
  closeReviewRequestIfExists: (input: ScmCloseReviewRequestInput, context: ScmClientContext) => Promise<ScmCloseReviewRequestResult>;
62
73
  createReleaseMetadata: (input: ScmReleaseMetadataInput, context: ScmClientContext) => Promise<ScmReleaseMetadataResult>;
63
74
  createReleaseReferenceComments?: (input: ScmReleaseReferenceCommentsInput, context: ScmClientContext) => Promise<ScmReleaseReferenceCommentsResult>;
@@ -0,0 +1,2 @@
1
+ import type { VersionStrategy } from "./types.js";
2
+ export declare const cmakeVersionStrategy: VersionStrategy;