@reviewflow/sdk 0.0.0-stage → 6.22.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.
Files changed (50) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +86 -2
  3. package/build/files-node.mjs +70 -0
  4. package/build/git-node.mjs +1792 -0
  5. package/build/index-browser.es.js +3328 -0
  6. package/build/index-node.mjs +3328 -0
  7. package/package.json +135 -3
  8. package/src/api/pullRequest.ts +617 -0
  9. package/src/config/insertCheckoutEntry.ts +54 -0
  10. package/src/diff/diffPaths.ts +78 -0
  11. package/src/diff/impacts.ts +47 -0
  12. package/src/diff/readingOrder.ts +428 -0
  13. package/src/diff/themes.ts +105 -0
  14. package/src/diff/types.ts +141 -0
  15. package/src/files/editableFile.ts +163 -0
  16. package/src/files.ts +13 -0
  17. package/src/git/groupDiffPaths.ts +134 -0
  18. package/src/git/manifests.ts +96 -0
  19. package/src/git/moduleBindings.ts +571 -0
  20. package/src/git/pullRequestDiff.ts +159 -0
  21. package/src/git/readBranchDiff.ts +359 -0
  22. package/src/git/readImpacts.ts +797 -0
  23. package/src/git/readingOrder.ts +190 -0
  24. package/src/git/run.ts +213 -0
  25. package/src/git.ts +38 -0
  26. package/src/github/applySuggestions.ts +399 -0
  27. package/src/github/editPullRequest.ts +790 -0
  28. package/src/github/fetchCompareDiff.ts +21 -0
  29. package/src/github/imageProxies.ts +43 -0
  30. package/src/github/lineDiff.ts +145 -0
  31. package/src/github/prQuery.ts +298 -0
  32. package/src/github/pullRequestDetail.ts +1347 -0
  33. package/src/github/pullRequestHead.ts +65 -0
  34. package/src/github/pullRequestRevisions.ts +124 -0
  35. package/src/github/transport.ts +171 -0
  36. package/src/index.ts +35 -0
  37. package/src/reviewflow/board.ts +333 -0
  38. package/src/reviewflow/changedParts.ts +80 -0
  39. package/src/reviewflow/checksFromReviewflow.ts +75 -0
  40. package/src/reviewflow/client.ts +589 -0
  41. package/src/reviewflow/follow.ts +345 -0
  42. package/src/reviewflow/status.ts +17 -0
  43. package/src/reviewflow/stream.ts +199 -0
  44. package/src/types/entities.ts +8 -0
  45. package/src/types/ids.ts +1 -0
  46. package/src/types/reviewflow.ts +32 -0
  47. package/src/utils/pullRequestEdit.ts +108 -0
  48. package/src/utils/pullRequestPart.ts +12 -0
  49. package/src/utils/pullRequestReaction.ts +19 -0
  50. package/src/utils/pullRequestSuggestion.ts +148 -0
@@ -0,0 +1,345 @@
1
+ import type { PullRequestChange } from "../api/pullRequest.ts";
2
+ import { accountOfOwnerRepo } from "./board.ts";
3
+ import { changedPullRequestParts } from "./changedParts.ts";
4
+ import type {
5
+ ReviewflowClient,
6
+ ReviewflowGroup,
7
+ ReviewflowPrDetail,
8
+ } from "./client.ts";
9
+ import { isReviewflowFailure } from "./client.ts";
10
+ import type { ReviewflowStreamEvent } from "./stream.ts";
11
+
12
+ /**
13
+ * reviewflow's stream, followed while someone looks: open while at least one
14
+ * watcher is connected, closed `lingerMs` after the last one leaves. A stream
15
+ * nobody watches holds one of the live connections reviewflow allows per user
16
+ * and in total.
17
+ *
18
+ * Storage is the caller's: the follower reports what it learned through
19
+ * callbacks, and all it keeps is what the lifecycle needs.
20
+ */
21
+
22
+ export const lingerMs = 5 * 60_000;
23
+ export const boardReadDelayMs = 3000;
24
+ export const reconnectDelayMs = 1000;
25
+ const minRetryMs = 2000;
26
+ export const maxRetryMs = 5 * 60_000;
27
+
28
+ export type ReviewflowStreamState = "connecting" | "error" | "live" | "off";
29
+
30
+ export interface ReviewflowConnection {
31
+ client: Pick<ReviewflowClient, "board">;
32
+ events: (signal: AbortSignal) => AsyncIterable<ReviewflowStreamEvent>;
33
+ }
34
+
35
+ export interface ReviewflowFollower {
36
+ /** whether anyone is watching */
37
+ setWatched: (watched: boolean) => void;
38
+ /** the token changed: the open stream is the old token's */
39
+ restart: () => void;
40
+ state: () => ReviewflowStreamState;
41
+ close: () => void;
42
+ }
43
+
44
+ export interface ReviewflowFollowerLog {
45
+ level: "error" | "info" | "warn";
46
+ message: string;
47
+ /** the thrown value itself, for a caller that serializes its stack */
48
+ error?: unknown;
49
+ /** what the message is about, without the error */
50
+ fields?: Record<string, unknown>;
51
+ }
52
+
53
+ export interface CreateReviewflowFollowerParams {
54
+ /** null without a token; read again at each connection */
55
+ connect: () => ReviewflowConnection | null;
56
+ /** the accounts whose board is read, as the caller spells them */
57
+ accounts: () => string[];
58
+ /** the scope the stream sent before `ready`, then each later `pr` */
59
+ onPr: (pr: ReviewflowPrDetail, phase: "change" | "scope") => void;
60
+ onPrRemoved: (id: string) => void;
61
+ /** an account's board was read again */
62
+ onAccountBoard: (account: string, groups: ReviewflowGroup[]) => void;
63
+ onBoardError?: (account: string, message: string) => void;
64
+ /** parts of a pull request's screen went stale */
65
+ onPullRequestChanged?: (change: PullRequestChange) => void;
66
+ onStateChanged?: (state: ReviewflowStreamState, error: string | null) => void;
67
+ /** the stream connected, failed or ended; for a log */
68
+ onLog?: (entry: ReviewflowFollowerLog) => void;
69
+ }
70
+
71
+ const messageOf = (error: unknown): string =>
72
+ error instanceof Error ? error.message : String(error);
73
+
74
+ const retryDelayMs = (error: unknown, failures: number): number => {
75
+ if (isReviewflowFailure(error)) {
76
+ if (error.retryAfterMs !== undefined) return error.retryAfterMs;
77
+ // a revoked token stays revoked until another is stored: `restart`
78
+ if (error.status === 401) return maxRetryMs;
79
+ }
80
+ return Math.min(minRetryMs * 2 ** (failures - 1), maxRetryMs);
81
+ };
82
+
83
+ const wait = async (ms: number, signal: AbortSignal): Promise<void> =>
84
+ new Promise((resolve) => {
85
+ const done = (): void => {
86
+ // oxlint-disable-next-line no-use-before-define
87
+ clearTimeout(timer);
88
+ signal.removeEventListener("abort", done);
89
+ resolve();
90
+ };
91
+ const timer = setTimeout(done, ms);
92
+ signal.addEventListener("abort", done, { once: true });
93
+ });
94
+
95
+ interface BoardRead {
96
+ timer?: ReturnType<typeof setTimeout>;
97
+ inFlight: boolean;
98
+ again: boolean;
99
+ }
100
+
101
+ export const createReviewflowFollower = ({
102
+ connect,
103
+ accounts,
104
+ onPr,
105
+ onPrRemoved,
106
+ onAccountBoard,
107
+ onBoardError,
108
+ onPullRequestChanged,
109
+ onStateChanged,
110
+ onLog,
111
+ }: CreateReviewflowFollowerParams): ReviewflowFollower => {
112
+ let watched = false;
113
+ let state: ReviewflowStreamState = "off";
114
+ let controller: AbortController | null = null;
115
+ let linger: ReturnType<typeof setTimeout> | undefined;
116
+ const boardReads = new Map<string, BoardRead>();
117
+ /** which account a pull request is from, for a `prRemoved` that only names it */
118
+ const accountOfPr = new Map<string, string>();
119
+ /** kept across connections: a scope sent anew reports what changed meanwhile */
120
+ const lastSeen = new Map<string, ReviewflowPrDetail>();
121
+
122
+ const setState = (next: ReviewflowStreamState, error?: string): void => {
123
+ if (state === next && error === undefined) return;
124
+ state = next;
125
+ onStateChanged?.(next, error ?? null);
126
+ };
127
+
128
+ const comparePr = (pr: ReviewflowPrDetail): void => {
129
+ const previous = lastSeen.get(pr.id);
130
+ lastSeen.set(pr.id, pr);
131
+ if (previous === undefined || onPullRequestChanged === undefined) return;
132
+ const parts = changedPullRequestParts(previous, pr);
133
+ if (parts.length === 0) return;
134
+ onPullRequestChanged({ ownerRepo: pr.ownerRepo, number: pr.number, parts });
135
+ };
136
+
137
+ const linked = (account: string): string | undefined =>
138
+ accounts().find((owner) => owner.toLowerCase() === account.toLowerCase());
139
+
140
+ /** true when an event arrived during the read, whose answer may predate it */
141
+ const readBoard = async (
142
+ connection: ReviewflowConnection,
143
+ account: string,
144
+ signal: AbortSignal,
145
+ ): Promise<boolean> => {
146
+ const read = boardReads.get(account);
147
+ if (read === undefined || signal.aborted) return false;
148
+ read.timer = undefined;
149
+ read.inFlight = true;
150
+ read.again = false;
151
+ try {
152
+ const groups = await connection.client.board(account);
153
+ if (signal.aborted) return false;
154
+ onAccountBoard(account, groups);
155
+ } catch (error) {
156
+ if (signal.aborted) return false;
157
+ onBoardError?.(account, messageOf(error));
158
+ } finally {
159
+ read.inFlight = false;
160
+ }
161
+ return read.again;
162
+ };
163
+
164
+ const scheduleBoard = (
165
+ connection: ReviewflowConnection,
166
+ account: string,
167
+ signal: AbortSignal,
168
+ ): void => {
169
+ const owner = linked(account);
170
+ if (owner === undefined) return;
171
+ const read = boardReads.get(owner) ?? { inFlight: false, again: false };
172
+ boardReads.set(owner, read);
173
+ if (read.inFlight) {
174
+ read.again = true;
175
+ return;
176
+ }
177
+ read.timer ??= setTimeout(() => {
178
+ readBoard(connection, owner, signal)
179
+ .then((again) => {
180
+ if (again) scheduleBoard(connection, owner, signal);
181
+ })
182
+ .catch((error: unknown) => {
183
+ onLog?.({
184
+ level: "warn",
185
+ message: "reviewflow board read failed",
186
+ error,
187
+ });
188
+ });
189
+ }, boardReadDelayMs);
190
+ };
191
+
192
+ const scheduleEveryBoard = (
193
+ connection: ReviewflowConnection,
194
+ signal: AbortSignal,
195
+ ): void => {
196
+ for (const owner of accounts()) scheduleBoard(connection, owner, signal);
197
+ };
198
+
199
+ /**
200
+ * Returns when the server ends the stream, telling whether it got as far as
201
+ * `ready`; throws when it fails.
202
+ */
203
+ const follow = async (
204
+ connection: ReviewflowConnection,
205
+ signal: AbortSignal,
206
+ onReady: () => void,
207
+ ): Promise<boolean> => {
208
+ const scope: ReviewflowPrDetail[] = [];
209
+ let ready = false;
210
+ for await (const event of connection.events(signal)) {
211
+ if (event.type === "end") {
212
+ onLog?.({
213
+ level: "info",
214
+ message: "reviewflow ended the stream",
215
+ fields: { reason: event.reason },
216
+ });
217
+ // a reset or a scope change: what the board holds may be stale
218
+ scheduleEveryBoard(connection, signal);
219
+ return ready;
220
+ }
221
+ if (event.type === "ready") {
222
+ ready = true;
223
+ onReady();
224
+ onLog?.({
225
+ level: "info",
226
+ message: "reviewflow stream ready",
227
+ fields: { prs: scope.length },
228
+ });
229
+ for (const pr of scope.splice(0)) onPr(pr, "scope");
230
+ setState("live");
231
+ scheduleEveryBoard(connection, signal);
232
+ } else if (event.type === "pr") {
233
+ accountOfPr.set(event.pr.id, accountOfOwnerRepo(event.pr.ownerRepo));
234
+ comparePr(event.pr);
235
+ if (ready) {
236
+ onPr(event.pr, "change");
237
+ scheduleBoard(
238
+ connection,
239
+ accountOfOwnerRepo(event.pr.ownerRepo),
240
+ signal,
241
+ );
242
+ } else {
243
+ scope.push(event.pr);
244
+ }
245
+ } else if (ready) {
246
+ const account = accountOfPr.get(event.id);
247
+ accountOfPr.delete(event.id);
248
+ lastSeen.delete(event.id);
249
+ onPrRemoved(event.id);
250
+ if (account === undefined) scheduleEveryBoard(connection, signal);
251
+ else scheduleBoard(connection, account, signal);
252
+ }
253
+ }
254
+ return ready;
255
+ };
256
+
257
+ const run = async (signal: AbortSignal): Promise<void> => {
258
+ let failures = 0;
259
+ const resetFailures = (): void => {
260
+ failures = 0;
261
+ };
262
+ while (!signal.aborted) {
263
+ const connection = connect();
264
+ if (connection === null) {
265
+ setState("off");
266
+ return;
267
+ }
268
+ let delayMs = reconnectDelayMs;
269
+ setState("connecting");
270
+ try {
271
+ const reachedReady = await follow(connection, signal, resetFailures);
272
+ if (!reachedReady && !signal.aborted) {
273
+ failures++;
274
+ delayMs = retryDelayMs(undefined, failures);
275
+ onLog?.({
276
+ level: "warn",
277
+ message: "reviewflow ended the stream before it was ready",
278
+ fields: { retryInMs: delayMs },
279
+ });
280
+ }
281
+ } catch (error) {
282
+ if (signal.aborted) return;
283
+ failures++;
284
+ delayMs = retryDelayMs(error, failures);
285
+ onLog?.({
286
+ level: "warn",
287
+ message: "reviewflow stream failed",
288
+ error,
289
+ fields: { retryInMs: delayMs },
290
+ });
291
+ setState("error", messageOf(error));
292
+ }
293
+ if (state === "live") setState("connecting");
294
+ await wait(delayMs, signal);
295
+ }
296
+ };
297
+
298
+ const stop = (): void => {
299
+ controller?.abort();
300
+ controller = null;
301
+ for (const read of boardReads.values()) clearTimeout(read.timer);
302
+ boardReads.clear();
303
+ accountOfPr.clear();
304
+ setState("off");
305
+ };
306
+
307
+ const start = (): void => {
308
+ if (controller !== null) return;
309
+ const current = new AbortController();
310
+ controller = current;
311
+ run(current.signal)
312
+ .catch((error: unknown) => {
313
+ onLog?.({
314
+ level: "error",
315
+ message: "reviewflow stream stopped",
316
+ error,
317
+ });
318
+ })
319
+ .finally(() => {
320
+ if (controller === current) controller = null;
321
+ });
322
+ };
323
+
324
+ return {
325
+ setWatched: (value) => {
326
+ watched = value;
327
+ clearTimeout(linger);
328
+ linger = undefined;
329
+ if (value) start();
330
+ else if (controller !== null) linger = setTimeout(stop, lingerMs);
331
+ },
332
+ restart: () => {
333
+ const wasOpen = controller !== null;
334
+ stop();
335
+ lastSeen.clear();
336
+ if (watched || wasOpen) start();
337
+ },
338
+ state: () => state,
339
+ close: () => {
340
+ watched = false;
341
+ clearTimeout(linger);
342
+ stop();
343
+ },
344
+ };
345
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * How a client of the reviewflow API stands: whether a token is held, whose it
3
+ * is, and whether the live stream is followed. Carries no token material by
4
+ * construction, so it can be sent to a page and logged.
5
+ */
6
+ export interface ReviewflowConnectionStatus {
7
+ configured: boolean;
8
+ /** the reviewflow server, e.g. `https://reviewflow.example.com` */
9
+ url: string | null;
10
+ /** the GitHub login the token belongs to, as of the last verification */
11
+ login: string | null;
12
+ /** the accounts the token reads, as of the last verification */
13
+ accounts: string[];
14
+ stream: "connecting" | "error" | "live" | "off";
15
+ /** why the credentials are unreadable or the stream failed; never the token */
16
+ error: string | null;
17
+ }
@@ -0,0 +1,199 @@
1
+ import type { ReviewflowPrDetail } from "./client.ts";
2
+ import {
3
+ fail,
4
+ isReviewflowFailure,
5
+ readPrDetail,
6
+ responseFailure,
7
+ } from "./client.ts";
8
+
9
+ /**
10
+ * `GET /api/stream/v1` (`reviewflow/docs/api.md`): every pull request in the
11
+ * reader's scope as `pr` events, then `ready`, then each change as the whole
12
+ * pull request again. There is no resume: a reconnection sends the scope anew.
13
+ */
14
+
15
+ /** reviewflow heartbeats every 25s: two missed is a dead connection, a laptop that slept */
16
+ export const streamIdleTimeoutMs = 60_000;
17
+ /**
18
+ * Pull requests closed while no stream was open still reach the tracked
19
+ * branches at the next connection, as long as it is within this many days.
20
+ */
21
+ export const streamClosedWithinDays = 7;
22
+
23
+ export type ReviewflowStreamEvent =
24
+ | { type: "end"; reason: string }
25
+ | { type: "pr"; pr: ReviewflowPrDetail }
26
+ | { type: "prRemoved"; id: string }
27
+ | { type: "ready" };
28
+
29
+ /** Optional sink for what the stream drops; nothing is logged without one. */
30
+ export interface ReviewflowStreamLogger {
31
+ /** `error` is the thrown value itself, for a caller that serializes its stack */
32
+ warn: (message: string, context: { error: unknown }) => void;
33
+ }
34
+
35
+ interface Frame {
36
+ event: string;
37
+ data: string;
38
+ }
39
+
40
+ const parseFrame = (block: string): Frame => {
41
+ let event = "message";
42
+ const data: string[] = [];
43
+ for (const line of block.split("\n")) {
44
+ if (line.startsWith(":")) continue;
45
+ const separator = line.indexOf(":");
46
+ const field = separator === -1 ? line : line.slice(0, separator);
47
+ const value =
48
+ separator === -1 ? "" : line.slice(separator + 1).replace(/^ /, "");
49
+ if (field === "event") event = value;
50
+ else if (field === "data") data.push(value);
51
+ }
52
+ return { event, data: data.join("\n") };
53
+ };
54
+
55
+ const parseData = (frame: Frame): unknown => {
56
+ try {
57
+ return JSON.parse(frame.data) as unknown;
58
+ } catch {
59
+ return null;
60
+ }
61
+ };
62
+
63
+ /** null for a frame that changes nothing: a heartbeat, an event added later */
64
+ const toStreamEvent = (
65
+ frame: Frame,
66
+ logger: ReviewflowStreamLogger | undefined,
67
+ ): ReviewflowStreamEvent | null => {
68
+ switch (frame.event) {
69
+ case "pr": {
70
+ try {
71
+ return { type: "pr", pr: readPrDetail(parseData(frame)) };
72
+ } catch (error) {
73
+ // one unreadable pull request must not end the stream for the others
74
+ logger?.warn(
75
+ "reviewflow streamed a pull request the client cannot read",
76
+ { error },
77
+ );
78
+ return null;
79
+ }
80
+ }
81
+ case "prRemoved": {
82
+ const data = parseData(frame) as { _id?: unknown } | null;
83
+ return typeof data?._id === "string"
84
+ ? { type: "prRemoved", id: data._id }
85
+ : null;
86
+ }
87
+ case "ready":
88
+ return { type: "ready" };
89
+ case "scopeChanged":
90
+ return { type: "end", reason: "scopeChanged" };
91
+ case "reset": {
92
+ const data = parseData(frame) as { reason?: unknown } | null;
93
+ return {
94
+ type: "end",
95
+ reason: `reset: ${typeof data?.reason === "string" ? data.reason : "unknown"}`,
96
+ };
97
+ }
98
+ default:
99
+ return null;
100
+ }
101
+ };
102
+
103
+ interface ReadReviewflowStreamParams {
104
+ url: string;
105
+ token: string;
106
+ signal: AbortSignal;
107
+ /** injected by tests: nothing in the suite reaches the network */
108
+ fetch?: typeof globalThis.fetch;
109
+ idleTimeoutMs?: number;
110
+ logger?: ReviewflowStreamLogger;
111
+ }
112
+
113
+ /**
114
+ * Yields until the server ends the stream (`end`, or the body closing) and
115
+ * throws on a refusal or a connection gone silent, so the caller decides when
116
+ * to connect again. Aborting `signal` ends it quietly.
117
+ */
118
+ export async function* readReviewflowStream({
119
+ url,
120
+ token,
121
+ signal,
122
+ fetch = globalThis.fetch,
123
+ idleTimeoutMs = streamIdleTimeoutMs,
124
+ logger,
125
+ }: ReadReviewflowStreamParams): AsyncGenerator<ReviewflowStreamEvent> {
126
+ const controller = new AbortController();
127
+ let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
128
+ // the body is cancelled too: a dead socket does not always reject a read
129
+ const cancel = (): void => {
130
+ controller.abort();
131
+ reader?.cancel().catch(() => undefined);
132
+ };
133
+ signal.addEventListener("abort", cancel, { once: true });
134
+ let silent = false;
135
+ let idle: ReturnType<typeof setTimeout> | undefined;
136
+ const armIdle = (): void => {
137
+ clearTimeout(idle);
138
+ idle = setTimeout(() => {
139
+ silent = true;
140
+ cancel();
141
+ }, idleTimeoutMs);
142
+ };
143
+ const silence = (): Error =>
144
+ fail(
145
+ `reviewflow's stream went silent for ${String(idleTimeoutMs / 1000)}s`,
146
+ );
147
+
148
+ try {
149
+ armIdle();
150
+ const query = new URLSearchParams({
151
+ closedWithinDays: String(streamClosedWithinDays),
152
+ });
153
+ const response = await fetch(`${url}/api/stream/v1?${query.toString()}`, {
154
+ headers: {
155
+ authorization: `Bearer ${token}`,
156
+ accept: "text/event-stream",
157
+ },
158
+ signal: controller.signal,
159
+ });
160
+ if (!response.ok) throw await responseFailure(response);
161
+ if (response.body === null) {
162
+ throw fail("reviewflow answered the stream with no body");
163
+ }
164
+
165
+ reader = response.body.getReader();
166
+ const decoder = new TextDecoder();
167
+ let buffer = "";
168
+ for (;;) {
169
+ const { value, done } = await reader.read();
170
+ if (done) break;
171
+ armIdle();
172
+ buffer = (buffer + decoder.decode(value, { stream: true })).replaceAll(
173
+ "\r\n",
174
+ "\n",
175
+ );
176
+ let end = buffer.indexOf("\n\n");
177
+ while (end !== -1) {
178
+ const event = toStreamEvent(parseFrame(buffer.slice(0, end)), logger);
179
+ buffer = buffer.slice(end + 2);
180
+ if (event !== null) {
181
+ yield event;
182
+ if (event.type === "end") return;
183
+ }
184
+ end = buffer.indexOf("\n\n");
185
+ }
186
+ }
187
+ if (silent) throw silence();
188
+ } catch (error) {
189
+ if (signal.aborted) return;
190
+ if (silent) throw silence();
191
+ if (isReviewflowFailure(error)) throw error;
192
+ const cause = error instanceof Error ? error.message : String(error);
193
+ throw fail(`reviewflow's stream could not be read at ${url}: ${cause}`);
194
+ } finally {
195
+ clearTimeout(idle);
196
+ signal.removeEventListener("abort", cancel);
197
+ cancel();
198
+ }
199
+ }
@@ -0,0 +1,8 @@
1
+ export type PrState = "closed" | "draft" | "merged" | "open";
2
+ export type PrReviewDecision =
3
+ | "approved"
4
+ | "changesRequested"
5
+ | "reviewRequired"
6
+ | null;
7
+ /** `unknown` while GitHub has not computed it yet, which it does lazily */
8
+ export type PrMergeable = "conflicting" | "mergeable" | "unknown";
@@ -0,0 +1 @@
1
+ export type IsoDateTime = string;
@@ -0,0 +1,32 @@
1
+ export type ReviewflowReviewState =
2
+ | "approved"
3
+ | "changesRequested"
4
+ | "none"
5
+ | "requested";
6
+
7
+ export type ReviewflowMergeable = "conflicting" | "mergeable" | "unknown";
8
+
9
+ /** `unknown` is a pull request with no check at all */
10
+ export type ReviewflowChecksConclusion =
11
+ | "failed"
12
+ | "inProgress"
13
+ | "passed"
14
+ | "unknown";
15
+
16
+ /** a link reviewflow resolved on the pull request, a ticket key among them */
17
+ export interface ReviewflowStatusLink {
18
+ name: string;
19
+ label: string;
20
+ url: string;
21
+ }
22
+
23
+ export interface ReviewflowPrChanges {
24
+ changedFiles: number;
25
+ additions: number;
26
+ deletions: number;
27
+ }
28
+
29
+ export interface ReviewflowAccount {
30
+ login: string;
31
+ type: "Organization" | "User";
32
+ }