@blogic-cz/agent-tools 1.16.1 → 1.17.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/README.md +25 -0
- package/dist/gh-tool/issue/commands.d.ts +5 -0
- package/dist/gh-tool/issue/commands.d.ts.map +1 -1
- package/dist/gh-tool/issue/core.d.ts +19 -0
- package/dist/gh-tool/issue/core.d.ts.map +1 -1
- package/dist/gh-tool/issue/index.d.ts +1 -1
- package/dist/gh-tool/issue/index.d.ts.map +1 -1
- package/dist/gh-tool/issue/triage.d.ts +1 -1
- package/dist/gh-tool/issue/triage.d.ts.map +1 -1
- package/dist/gh-tool/main.d.ts.map +1 -1
- package/dist/gh-tool/pr/commands.d.ts +7 -7
- package/dist/gh-tool/pr/commands.d.ts.map +1 -1
- package/dist/gh-tool/pr/core.d.ts +9 -9
- package/dist/gh-tool/pr/review.d.ts +3 -3
- package/dist/gh-tool/pr/review.d.ts.map +1 -1
- package/dist/gh-tool/read/graphql-fallback.d.ts +21 -0
- package/dist/gh-tool/read/graphql-fallback.d.ts.map +1 -0
- package/dist/gh-tool/read/issues.d.ts +11 -1
- package/dist/gh-tool/read/issues.d.ts.map +1 -1
- package/dist/gh-tool/read/pulls.d.ts +13 -12
- package/dist/gh-tool/read/pulls.d.ts.map +1 -1
- package/dist/gh-tool/read/reads.d.ts +22 -5
- package/dist/gh-tool/read/reads.d.ts.map +1 -1
- package/dist/gh-tool/read/rest-porcelain.d.ts +131 -0
- package/dist/gh-tool/read/rest-porcelain.d.ts.map +1 -0
- package/dist/gh-tool/read/review-guards.d.ts +1 -1
- package/dist/gh-tool/read/review-guards.d.ts.map +1 -1
- package/dist/gh-tool/read/review-model.d.ts +1 -1
- package/dist/gh-tool/read/review-model.d.ts.map +1 -1
- package/dist/gh-tool/read/reviews.d.ts.map +1 -1
- package/dist/gh-tool/read/telemetry.d.ts +1 -1
- package/dist/gh-tool/read/telemetry.d.ts.map +1 -1
- package/dist/gh-tool/read/transport.d.ts.map +1 -1
- package/dist/gh-tool/repo.d.ts +3 -0
- package/dist/gh-tool/repo.d.ts.map +1 -1
- package/dist/gh-tool/types.d.ts +2 -2
- package/dist/gh-tool/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/gh-tool/issue/commands.ts +18 -0
- package/src/gh-tool/issue/core.ts +45 -1
- package/src/gh-tool/issue/index.ts +1 -0
- package/src/gh-tool/issue/triage.ts +1 -1
- package/src/gh-tool/main.ts +7 -2
- package/src/gh-tool/pr/commands.ts +15 -6
- package/src/gh-tool/pr/review.ts +27 -7
- package/src/gh-tool/read/graphql-fallback.ts +366 -0
- package/src/gh-tool/read/issues.ts +23 -1
- package/src/gh-tool/read/pulls.ts +60 -10
- package/src/gh-tool/read/reads.ts +12 -1
- package/src/gh-tool/read/rest-porcelain.ts +571 -0
- package/src/gh-tool/read/review-guards.ts +8 -2
- package/src/gh-tool/read/review-model.ts +1 -1
- package/src/gh-tool/read/reviews.ts +19 -3
- package/src/gh-tool/read/telemetry.ts +6 -3
- package/src/gh-tool/read/transport.ts +314 -7
- package/src/gh-tool/repo.ts +7 -1
- package/src/gh-tool/types.ts +2 -2
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { Console, Context, Effect, Schema } from "effect";
|
|
2
|
+
import type { JsonRead } from "./rest-porcelain";
|
|
3
|
+
import type { GhError } from "./transport";
|
|
4
|
+
import { parseRest, restGet, restPages, restPull, restReviewDecision } from "./rest-porcelain";
|
|
5
|
+
import { GitHubCommandError } from "#gh/errors";
|
|
6
|
+
import { ghSubprocessEvent, recordRequest } from "./telemetry";
|
|
7
|
+
|
|
8
|
+
export type Fallback = { source: "REST"; reason: string; unknown: string[] };
|
|
9
|
+
const fallbackResults = new WeakSet<object>();
|
|
10
|
+
export const markFallbackResult = <A extends object>(value: A): A => {
|
|
11
|
+
fallbackResults.add(value);
|
|
12
|
+
return value;
|
|
13
|
+
};
|
|
14
|
+
export const isFallbackResult = (value: unknown): boolean =>
|
|
15
|
+
typeof value === "object" && value !== null && fallbackResults.has(value);
|
|
16
|
+
|
|
17
|
+
export const FallbackState = Context.Reference<{ notices: Fallback[] } | null>("gh/FallbackState", {
|
|
18
|
+
defaultValue: () => null,
|
|
19
|
+
});
|
|
20
|
+
const fallbackMessage = (unknown: string[]) =>
|
|
21
|
+
unknown.length === 0
|
|
22
|
+
? "GitHub GraphQL is blocked (rate limit); answered from REST. All fields were provided."
|
|
23
|
+
: `GitHub GraphQL is blocked (rate limit); answered from REST. Not provided: ${unknown.join(", ")}. A null or UNKNOWN value there means "not provided", never false, unresolved or not approved. If your next step depends on it, retry in a few minutes; if it is still not provided, tell the user. Do not guess.`;
|
|
24
|
+
export const reportFallback = (unknown: string[]) =>
|
|
25
|
+
Effect.gen(function* () {
|
|
26
|
+
const notice: Fallback = { source: "REST", reason: "GitHub GraphQL rate limit", unknown };
|
|
27
|
+
const state = yield* FallbackState;
|
|
28
|
+
state?.notices.push(notice);
|
|
29
|
+
if (state === null) yield* Console.warn(fallbackMessage(unknown));
|
|
30
|
+
yield* recordRequest({
|
|
31
|
+
...ghSubprocessEvent(["api", "graphql"], 0),
|
|
32
|
+
method: "FALLBACK",
|
|
33
|
+
endpoint: "graphql-to-rest",
|
|
34
|
+
outcome: "fallback",
|
|
35
|
+
});
|
|
36
|
+
return notice;
|
|
37
|
+
});
|
|
38
|
+
export const withFallbackOutput = <A, E, R>(program: Effect.Effect<A, E, R>) =>
|
|
39
|
+
Effect.gen(function* () {
|
|
40
|
+
const current = yield* Console.Console;
|
|
41
|
+
const state = { notices: [] as Fallback[] };
|
|
42
|
+
let warned = false;
|
|
43
|
+
const flushWarning = () => {
|
|
44
|
+
if (warned || state.notices.length === 0) return;
|
|
45
|
+
warned = true;
|
|
46
|
+
current.warn(
|
|
47
|
+
fallbackMessage([...new Set(state.notices.flatMap((notice) => notice.unknown))]),
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
const console = {
|
|
51
|
+
...current,
|
|
52
|
+
log: (...args: readonly unknown[]) => {
|
|
53
|
+
flushWarning();
|
|
54
|
+
current.log(
|
|
55
|
+
...args.map((arg) => {
|
|
56
|
+
if (state.notices.length === 0 || typeof arg !== "string") return arg;
|
|
57
|
+
try {
|
|
58
|
+
const value: unknown = JSON.parse(arg);
|
|
59
|
+
if (value === null || typeof value !== "object") return arg;
|
|
60
|
+
const fallback = {
|
|
61
|
+
source: "REST",
|
|
62
|
+
reason: "GitHub GraphQL rate limit",
|
|
63
|
+
unknown: [...new Set(state.notices.flatMap((notice) => notice.unknown))],
|
|
64
|
+
};
|
|
65
|
+
return JSON.stringify(
|
|
66
|
+
Array.isArray(value) ? { items: value, fallback } : { ...value, fallback },
|
|
67
|
+
null,
|
|
68
|
+
arg.includes("\n") ? 2 : undefined,
|
|
69
|
+
);
|
|
70
|
+
} catch {
|
|
71
|
+
return arg;
|
|
72
|
+
}
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
return yield* program.pipe(
|
|
78
|
+
Effect.provideService(FallbackState, state),
|
|
79
|
+
Effect.provideService(Console.Console, console),
|
|
80
|
+
Effect.ensuring(Effect.sync(flushWarning)),
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
const Comment = Schema.Struct({
|
|
84
|
+
id: Schema.Number,
|
|
85
|
+
node_id: Schema.String,
|
|
86
|
+
in_reply_to_id: Schema.optional(Schema.NullOr(Schema.Number)),
|
|
87
|
+
path: Schema.String,
|
|
88
|
+
line: Schema.NullOr(Schema.Number),
|
|
89
|
+
body: Schema.String,
|
|
90
|
+
user: Schema.Struct({ login: Schema.String }),
|
|
91
|
+
commit_id: Schema.String,
|
|
92
|
+
});
|
|
93
|
+
const Reviewer = Schema.Struct({ login: Schema.String, type: Schema.optional(Schema.String) });
|
|
94
|
+
export const restGraphql = (
|
|
95
|
+
query: string,
|
|
96
|
+
variables: Record<string, string | number | null>,
|
|
97
|
+
read: JsonRead,
|
|
98
|
+
): Effect.Effect<unknown, GhError> | null => {
|
|
99
|
+
if (
|
|
100
|
+
(query.includes("mutation") && !query.includes("submitPullRequestReview")) ||
|
|
101
|
+
typeof variables.owner !== "string" ||
|
|
102
|
+
typeof variables.name !== "string" ||
|
|
103
|
+
typeof variables.pr !== "number"
|
|
104
|
+
)
|
|
105
|
+
return null;
|
|
106
|
+
const repo = `${variables.owner}/${variables.name}`;
|
|
107
|
+
const pr = variables.pr;
|
|
108
|
+
const root = `repos/${repo}/pulls/${pr}`;
|
|
109
|
+
if (
|
|
110
|
+
query.includes("states: [PENDING]") ||
|
|
111
|
+
query.includes("... on PullRequestReview {") ||
|
|
112
|
+
query.includes("submitPullRequestReview")
|
|
113
|
+
)
|
|
114
|
+
return pendingRest(query, variables, read, root, repo, variables.pr);
|
|
115
|
+
if (query.includes("reviewThreads"))
|
|
116
|
+
return Effect.gen(function* () {
|
|
117
|
+
const comments = (yield* parseRest(
|
|
118
|
+
Schema.Array(Schema.Array(Comment)),
|
|
119
|
+
yield* restPages(read, `${root}/comments`),
|
|
120
|
+
)).flat();
|
|
121
|
+
const groups = new Map<number, Array<typeof Comment.Type>>();
|
|
122
|
+
for (const comment of comments) {
|
|
123
|
+
const rootId = comment.in_reply_to_id ?? comment.id;
|
|
124
|
+
const group = groups.get(rootId) ?? [];
|
|
125
|
+
group.push(comment);
|
|
126
|
+
groups.set(rootId, group);
|
|
127
|
+
}
|
|
128
|
+
const nodes = [...groups.entries()].map(([rootId, group]) => ({
|
|
129
|
+
id: `rest-thread:${rootId}`,
|
|
130
|
+
isResolved: null,
|
|
131
|
+
comments: {
|
|
132
|
+
nodes: group
|
|
133
|
+
.toSorted((a, b) => Number(b.id === rootId) - Number(a.id === rootId))
|
|
134
|
+
.map((item) => ({
|
|
135
|
+
id: item.node_id,
|
|
136
|
+
databaseId: item.id,
|
|
137
|
+
path: item.path,
|
|
138
|
+
line: item.line,
|
|
139
|
+
body: item.body,
|
|
140
|
+
author: { login: item.user.login },
|
|
141
|
+
commit: { oid: item.commit_id },
|
|
142
|
+
replyTo:
|
|
143
|
+
item.in_reply_to_id === undefined || item.in_reply_to_id === null
|
|
144
|
+
? null
|
|
145
|
+
: { id: String(item.in_reply_to_id) },
|
|
146
|
+
})),
|
|
147
|
+
pageInfo: { hasNextPage: false, endCursor: null },
|
|
148
|
+
},
|
|
149
|
+
}));
|
|
150
|
+
const pull = yield* restPull(read, repo, String(pr));
|
|
151
|
+
const reviewDecision = query.includes("reviewDecision")
|
|
152
|
+
? yield* restReviewDecision(read, {
|
|
153
|
+
repo,
|
|
154
|
+
number: pr,
|
|
155
|
+
baseRefName: pull.baseRefName,
|
|
156
|
+
headSha: pull.headSha,
|
|
157
|
+
})
|
|
158
|
+
: pull.reviewDecision;
|
|
159
|
+
const fallback = yield* reportFallback([
|
|
160
|
+
"review thread resolution",
|
|
161
|
+
...(query.includes("reviewDecision") && reviewDecision === "UNKNOWN"
|
|
162
|
+
? ["reviewDecision"]
|
|
163
|
+
: []),
|
|
164
|
+
]);
|
|
165
|
+
return {
|
|
166
|
+
fallback,
|
|
167
|
+
repository: {
|
|
168
|
+
pullRequest: {
|
|
169
|
+
...pull,
|
|
170
|
+
reviewDecision,
|
|
171
|
+
reviewRequests: {
|
|
172
|
+
nodes: pull.reviewRequests.map((reviewer) => ({ requestedReviewer: reviewer })),
|
|
173
|
+
},
|
|
174
|
+
reviewThreads: { nodes, pageInfo: { hasNextPage: false, endCursor: null } },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
if (query.includes("reviews(last:") && query.includes("reviewRequests"))
|
|
180
|
+
return Effect.gen(function* () {
|
|
181
|
+
const reviews = (yield* parseRest(
|
|
182
|
+
Schema.Array(
|
|
183
|
+
Schema.Array(
|
|
184
|
+
Schema.Struct({
|
|
185
|
+
user: Schema.NullOr(Reviewer),
|
|
186
|
+
state: Schema.String,
|
|
187
|
+
submitted_at: Schema.NullOr(Schema.String),
|
|
188
|
+
}),
|
|
189
|
+
),
|
|
190
|
+
),
|
|
191
|
+
yield* restPages(read, `${root}/reviews`),
|
|
192
|
+
)).flat();
|
|
193
|
+
const requested = yield* parseRest(
|
|
194
|
+
Schema.Struct({
|
|
195
|
+
users: Schema.Array(Reviewer),
|
|
196
|
+
teams: Schema.Array(Schema.Struct({ name: Schema.String })),
|
|
197
|
+
}),
|
|
198
|
+
yield* restGet(read, `${root}/requested_reviewers`),
|
|
199
|
+
);
|
|
200
|
+
const fallback = yield* reportFallback([]);
|
|
201
|
+
return {
|
|
202
|
+
fallback,
|
|
203
|
+
repository: {
|
|
204
|
+
pullRequest: {
|
|
205
|
+
reviewRequests: {
|
|
206
|
+
nodes: [
|
|
207
|
+
...requested.users.map((user) => ({
|
|
208
|
+
requestedReviewer: {
|
|
209
|
+
__typename: user.type === "Bot" ? "Bot" : "User",
|
|
210
|
+
login: user.login,
|
|
211
|
+
},
|
|
212
|
+
})),
|
|
213
|
+
...requested.teams.map((team) => ({
|
|
214
|
+
requestedReviewer: { __typename: "Team", name: team.name },
|
|
215
|
+
})),
|
|
216
|
+
],
|
|
217
|
+
},
|
|
218
|
+
reviews: {
|
|
219
|
+
nodes: reviews.map((review) => ({
|
|
220
|
+
author: review.user === null ? null : { login: review.user.login },
|
|
221
|
+
state: review.state,
|
|
222
|
+
submittedAt: review.submitted_at,
|
|
223
|
+
})),
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
});
|
|
229
|
+
return null;
|
|
230
|
+
};
|
|
231
|
+
export const isGraphqlLimit = (error: unknown) =>
|
|
232
|
+
error instanceof GitHubCommandError &&
|
|
233
|
+
/rate.?limit|already exceeded|secondary limit|abuse detection|HTTP 429/i.test(error.stderr);
|
|
234
|
+
export const rateLimitResponse = (stdout: string): string | null => {
|
|
235
|
+
try {
|
|
236
|
+
const value = Schema.decodeUnknownSync(
|
|
237
|
+
Schema.Struct({
|
|
238
|
+
errors: Schema.optional(Schema.Array(Schema.Unknown)),
|
|
239
|
+
type: Schema.optional(Schema.String),
|
|
240
|
+
code: Schema.optional(Schema.String),
|
|
241
|
+
message: Schema.optional(Schema.String),
|
|
242
|
+
}),
|
|
243
|
+
)(JSON.parse(stdout));
|
|
244
|
+
const detail = JSON.stringify(value.errors ?? value);
|
|
245
|
+
return /RATE_LIMIT|graphql_rate_limit|rate limit|secondary limit/i.test(detail) ? detail : null;
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
export const graphqlLimitError = () =>
|
|
251
|
+
new GitHubCommandError({
|
|
252
|
+
command: "gh api graphql",
|
|
253
|
+
exitCode: 1,
|
|
254
|
+
message: "GraphQL rate limit already exceeded",
|
|
255
|
+
stderr: "GraphQL rate limit already exceeded",
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const PendingReview = Schema.Struct({
|
|
259
|
+
id: Schema.Number,
|
|
260
|
+
node_id: Schema.String,
|
|
261
|
+
state: Schema.String,
|
|
262
|
+
body: Schema.NullOr(Schema.String),
|
|
263
|
+
user: Schema.NullOr(Schema.Struct({ login: Schema.String })),
|
|
264
|
+
pull_request_url: Schema.String,
|
|
265
|
+
});
|
|
266
|
+
const pendingRest = (
|
|
267
|
+
query: string,
|
|
268
|
+
variables: Record<string, string | number | null>,
|
|
269
|
+
read: JsonRead,
|
|
270
|
+
root: string,
|
|
271
|
+
repo: string,
|
|
272
|
+
pr: number,
|
|
273
|
+
): Effect.Effect<unknown, GhError> =>
|
|
274
|
+
Effect.gen(function* () {
|
|
275
|
+
const reviews = (yield* parseRest(
|
|
276
|
+
Schema.Array(Schema.Array(PendingReview)),
|
|
277
|
+
yield* restPages(read, `${root}/reviews`),
|
|
278
|
+
)).flat();
|
|
279
|
+
const viewer = yield* parseRest(
|
|
280
|
+
Schema.Struct({ login: Schema.String }),
|
|
281
|
+
yield* restGet(read, "user"),
|
|
282
|
+
);
|
|
283
|
+
const pageInfo = { hasNextPage: false, endCursor: null };
|
|
284
|
+
if (query.includes("states: [PENDING]")) {
|
|
285
|
+
const fallback = yield* reportFallback([]);
|
|
286
|
+
return {
|
|
287
|
+
fallback,
|
|
288
|
+
viewer,
|
|
289
|
+
repository: {
|
|
290
|
+
pullRequest: {
|
|
291
|
+
reviews: {
|
|
292
|
+
nodes: reviews
|
|
293
|
+
.filter((review) => review.state === "PENDING")
|
|
294
|
+
.map((review) => ({
|
|
295
|
+
id: review.node_id,
|
|
296
|
+
state: review.state,
|
|
297
|
+
author: review.user,
|
|
298
|
+
})),
|
|
299
|
+
pageInfo,
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const review = reviews.find((item) => item.node_id === variables.reviewId);
|
|
306
|
+
if (
|
|
307
|
+
review === undefined ||
|
|
308
|
+
review.state !== "PENDING" ||
|
|
309
|
+
review.user?.login !== viewer.login ||
|
|
310
|
+
review.pull_request_url !== `https://api.github.com/${root}`
|
|
311
|
+
)
|
|
312
|
+
return yield* new GitHubCommandError({
|
|
313
|
+
command: "gh-tool pr submit-review",
|
|
314
|
+
exitCode: 1,
|
|
315
|
+
message: "REST could not verify an owned pending review on this PR",
|
|
316
|
+
stderr: "REST could not verify an owned pending review on this PR",
|
|
317
|
+
});
|
|
318
|
+
if (query.includes("submitPullRequestReview")) {
|
|
319
|
+
if (
|
|
320
|
+
typeof variables.event !== "string" ||
|
|
321
|
+
!["COMMENT", "APPROVE", "REQUEST_CHANGES"].includes(variables.event) ||
|
|
322
|
+
typeof variables.body !== "string"
|
|
323
|
+
)
|
|
324
|
+
return yield* new GitHubCommandError({
|
|
325
|
+
command: "gh-tool pr submit-review",
|
|
326
|
+
exitCode: 1,
|
|
327
|
+
message: "Invalid review event or body",
|
|
328
|
+
stderr: "Invalid review event or body",
|
|
329
|
+
});
|
|
330
|
+
const result = yield* parseRest(
|
|
331
|
+
Schema.Struct({ node_id: Schema.String, state: Schema.String }),
|
|
332
|
+
yield* read([
|
|
333
|
+
"api",
|
|
334
|
+
"--method",
|
|
335
|
+
"POST",
|
|
336
|
+
`${root}/reviews/${review.id}/events`,
|
|
337
|
+
"-f",
|
|
338
|
+
`event=${variables.event}`,
|
|
339
|
+
"-f",
|
|
340
|
+
`body=${variables.body}`,
|
|
341
|
+
]),
|
|
342
|
+
);
|
|
343
|
+
const fallback = yield* reportFallback([]);
|
|
344
|
+
return {
|
|
345
|
+
fallback,
|
|
346
|
+
submitPullRequestReview: { pullRequestReview: { id: result.node_id, state: result.state } },
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
const comments = (yield* parseRest(
|
|
350
|
+
Schema.Array(Schema.Array(Schema.Struct({ body: Schema.String }))),
|
|
351
|
+
yield* restPages(read, `${root}/reviews/${review.id}/comments`),
|
|
352
|
+
)).flat();
|
|
353
|
+
const fallback = yield* reportFallback([]);
|
|
354
|
+
return {
|
|
355
|
+
fallback,
|
|
356
|
+
viewer,
|
|
357
|
+
node: {
|
|
358
|
+
id: review.node_id,
|
|
359
|
+
state: review.state,
|
|
360
|
+
body: review.body,
|
|
361
|
+
author: review.user,
|
|
362
|
+
pullRequest: { number: pr, repository: { nameWithOwner: repo } },
|
|
363
|
+
comments: { nodes: comments, pageInfo },
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
});
|
|
@@ -17,6 +17,7 @@ const IssueListItem = Schema.Struct({
|
|
|
17
17
|
url: Schema.String,
|
|
18
18
|
labels: Schema.mutable(Schema.Array(Label)),
|
|
19
19
|
assignees: Schema.mutable(Schema.Array(Login)),
|
|
20
|
+
author: Schema.NullOr(Login),
|
|
20
21
|
createdAt: Schema.String,
|
|
21
22
|
});
|
|
22
23
|
const IssueView = Schema.Struct({
|
|
@@ -25,6 +26,18 @@ const IssueView = Schema.Struct({
|
|
|
25
26
|
body: Schema.String,
|
|
26
27
|
closedAt: Schema.NullOr(Schema.String),
|
|
27
28
|
});
|
|
29
|
+
const IssueNodeLookup = Schema.Struct({
|
|
30
|
+
repository: Schema.NullOr(
|
|
31
|
+
Schema.Struct({
|
|
32
|
+
issue: Schema.NullOr(
|
|
33
|
+
Schema.Struct({
|
|
34
|
+
id: Schema.String,
|
|
35
|
+
url: Schema.optional(Schema.String),
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
}),
|
|
39
|
+
),
|
|
40
|
+
});
|
|
28
41
|
const IssueComment = Schema.Struct({
|
|
29
42
|
id: Schema.Number,
|
|
30
43
|
user: Login,
|
|
@@ -39,7 +52,7 @@ export type IssueListFilter = {
|
|
|
39
52
|
limit: number;
|
|
40
53
|
includeBody?: boolean;
|
|
41
54
|
};
|
|
42
|
-
export const ISSUE_LIST_FIELDS = "number,title,state,url,labels,assignees,createdAt";
|
|
55
|
+
export const ISSUE_LIST_FIELDS = "number,title,state,url,labels,assignees,author,createdAt";
|
|
43
56
|
export const ISSUE_VIEW_FIELDS =
|
|
44
57
|
"number,title,state,url,labels,assignees,author,body,createdAt,closedAt";
|
|
45
58
|
|
|
@@ -69,6 +82,15 @@ export const issueView = Effect.fn("reads.issueView")(function* (
|
|
|
69
82
|
});
|
|
70
83
|
});
|
|
71
84
|
|
|
85
|
+
export const issueNode = Effect.fn("reads.issueNode")(function* (repo: RepoRef, issue: number) {
|
|
86
|
+
const gh = yield* GitHubService;
|
|
87
|
+
const response = yield* gh.runGraphQL(
|
|
88
|
+
"query($owner: String!, $name: String!, $number: Int!) { repository(owner: $owner, name: $name) { issue(number: $number) { id url } } }",
|
|
89
|
+
{ owner: repo.owner, name: repo.name, number: issue },
|
|
90
|
+
);
|
|
91
|
+
return parsePreserving(IssueNodeLookup)(response).repository?.issue ?? null;
|
|
92
|
+
});
|
|
93
|
+
|
|
72
94
|
export const issueList = Effect.fn("reads.issueList")(function* (
|
|
73
95
|
repo: RepoRef,
|
|
74
96
|
filter: IssueListFilter,
|
|
@@ -8,13 +8,14 @@ import { gatedRead } from "./gated";
|
|
|
8
8
|
import { restSnapshot, snapshotDigest } from "./rest";
|
|
9
9
|
import { OPS } from "./classes";
|
|
10
10
|
import { repoSlug } from "./keys";
|
|
11
|
+
import { isFallbackResult } from "./graphql-fallback";
|
|
11
12
|
import type { RepoRef } from "./keys";
|
|
12
13
|
import { parsePreserving } from "./porcelain";
|
|
13
14
|
|
|
14
15
|
export const PR_VIEW_FIELDS =
|
|
15
|
-
"number,url,title,headRefName,baseRefName,headRefOid,state,isDraft,mergeable,body,author,
|
|
16
|
+
"number,url,title,headRefName,baseRefName,headRefOid,state,isDraft,mergeable,body,author,reviewRequests,createdAt,headRepositoryOwner,mergeCommit,reviewDecision";
|
|
16
17
|
export const PR_LIST_FIELDS =
|
|
17
|
-
"number,url,title,headRefName,baseRefName,state,isDraft,mergeable,author,createdAt,reviewDecision
|
|
18
|
+
"number,url,title,headRefName,baseRefName,state,isDraft,mergeable,author,createdAt,headRepositoryOwner,reviewDecision";
|
|
18
19
|
export type PrListFilter = {
|
|
19
20
|
state: string;
|
|
20
21
|
limit: number;
|
|
@@ -38,7 +39,7 @@ const Pull = Schema.Struct({
|
|
|
38
39
|
),
|
|
39
40
|
createdAt: Schema.optional(Schema.String),
|
|
40
41
|
reviewDecision: Schema.optional(Schema.String),
|
|
41
|
-
headRepositoryOwner: Schema.optional(Schema.Struct({ login: Schema.String })),
|
|
42
|
+
headRepositoryOwner: Schema.optional(Schema.NullOr(Schema.Struct({ login: Schema.String }))),
|
|
42
43
|
});
|
|
43
44
|
|
|
44
45
|
const PullView = Schema.Struct({
|
|
@@ -48,7 +49,13 @@ const PullView = Schema.Struct({
|
|
|
48
49
|
baseSha: Schema.optional(Schema.NullOr(Schema.String)),
|
|
49
50
|
body: Schema.String,
|
|
50
51
|
author: Schema.Struct({ login: Schema.String, is_bot: Schema.Boolean }),
|
|
51
|
-
reviewDecision: Schema.Literals([
|
|
52
|
+
reviewDecision: Schema.Literals([
|
|
53
|
+
"APPROVED",
|
|
54
|
+
"CHANGES_REQUESTED",
|
|
55
|
+
"REVIEW_REQUIRED",
|
|
56
|
+
"UNKNOWN",
|
|
57
|
+
"",
|
|
58
|
+
]),
|
|
52
59
|
reviewRequests: Schema.mutable(
|
|
53
60
|
Schema.Array(
|
|
54
61
|
Schema.Union([
|
|
@@ -159,7 +166,7 @@ export const prView = Effect.fn("reads.prView")(function* (
|
|
|
159
166
|
if (selector.by === "currentBranch" || repo === null) {
|
|
160
167
|
const fence = yield* readFence;
|
|
161
168
|
const data = (yield* fetch()).value;
|
|
162
|
-
if (repo !== null && consistency !== "origin" && fence !== null) {
|
|
169
|
+
if (repo !== null && consistency !== "origin" && fence !== null && !("fallback" in data)) {
|
|
163
170
|
const head = yield* prHead(repo, data.number).pipe(Effect.orElseSucceed(() => null));
|
|
164
171
|
if (head !== null && head.headSha === data.headRefOid)
|
|
165
172
|
yield* seedRead(
|
|
@@ -177,6 +184,7 @@ export const prView = Effect.fn("reads.prView")(function* (
|
|
|
177
184
|
fetch: Effect.suspend(() => gh.runGhJson<PrView>(args)),
|
|
178
185
|
signal: prHead(repo, selector.number).pipe(Effect.map((head) => head.signature)),
|
|
179
186
|
maxAgeMs: () => OPS.prView.maxAgeMs,
|
|
187
|
+
cacheableWhen: (value) => !("fallback" in value),
|
|
180
188
|
});
|
|
181
189
|
});
|
|
182
190
|
|
|
@@ -214,6 +222,8 @@ export const prList = Effect.fn("reads.prList")(function* (
|
|
|
214
222
|
gh
|
|
215
223
|
.runGhJson<PrListItem[]>(args)
|
|
216
224
|
.pipe(Effect.map((value) => ({ kind: "fresh" as const, value }))),
|
|
225
|
+
cacheableWhen: (value) =>
|
|
226
|
+
!isFallbackResult(value) && value.every((item) => !("fallback" in item)),
|
|
217
227
|
});
|
|
218
228
|
});
|
|
219
229
|
|
|
@@ -353,7 +363,7 @@ type PrReviewTriageSummaryPage = {
|
|
|
353
363
|
};
|
|
354
364
|
readonly reviewThreads: {
|
|
355
365
|
readonly nodes: ReadonlyArray<{
|
|
356
|
-
readonly isResolved: boolean;
|
|
366
|
+
readonly isResolved: boolean | null;
|
|
357
367
|
readonly comments: {
|
|
358
368
|
readonly nodes: ReadonlyArray<{ readonly replyTo: { readonly id: string } | null }>;
|
|
359
369
|
};
|
|
@@ -376,7 +386,7 @@ export type PrReviewTriageSummary = {
|
|
|
376
386
|
readonly reviewRequests: ReadonlyArray<{ readonly login: string }>;
|
|
377
387
|
};
|
|
378
388
|
readonly summary: {
|
|
379
|
-
readonly unresolvedReviewThreadsCount: number;
|
|
389
|
+
readonly unresolvedReviewThreadsCount: number | null;
|
|
380
390
|
readonly unrepliedReviewThreadsCount: number;
|
|
381
391
|
};
|
|
382
392
|
readonly checks: ReadonlyArray<{ readonly name: string; readonly bucket: string }>;
|
|
@@ -392,14 +402,16 @@ export const prReviewTriageSummary = Effect.fn("reads.prReviewTriageSummary")(fu
|
|
|
392
402
|
PR_REVIEW_TRIAGE_SUMMARY_QUERY,
|
|
393
403
|
variables,
|
|
394
404
|
)) as PrReviewTriageSummaryPage;
|
|
395
|
-
|
|
405
|
+
let firstPage = first.repository.pullRequest;
|
|
396
406
|
let unresolvedReviewThreadsCount = 0;
|
|
407
|
+
let resolutionUnknown = false;
|
|
397
408
|
let unrepliedReviewThreadsCount = 0;
|
|
398
409
|
const countThreads = (
|
|
399
410
|
threads: PrReviewTriageSummaryPage["repository"]["pullRequest"]["reviewThreads"]["nodes"],
|
|
400
411
|
) => {
|
|
401
412
|
for (const thread of threads) {
|
|
402
|
-
if (
|
|
413
|
+
if (thread.isResolved === null) resolutionUnknown = true;
|
|
414
|
+
if (thread.isResolved === false) unresolvedReviewThreadsCount++;
|
|
403
415
|
if (!thread.comments.nodes.some((comment) => comment.replyTo !== null))
|
|
404
416
|
unrepliedReviewThreadsCount++;
|
|
405
417
|
}
|
|
@@ -413,6 +425,12 @@ export const prReviewTriageSummary = Effect.fn("reads.prReviewTriageSummary")(fu
|
|
|
413
425
|
after: pageInfo.endCursor,
|
|
414
426
|
})) as PrReviewTriageSummaryPage;
|
|
415
427
|
const pullRequest = response.repository.pullRequest;
|
|
428
|
+
if ("fallback" in response) {
|
|
429
|
+
unresolvedReviewThreadsCount = 0;
|
|
430
|
+
unrepliedReviewThreadsCount = 0;
|
|
431
|
+
resolutionUnknown = false;
|
|
432
|
+
firstPage = pullRequest;
|
|
433
|
+
}
|
|
416
434
|
countThreads(pullRequest.reviewThreads.nodes);
|
|
417
435
|
pageInfo = pullRequest.reviewThreads.pageInfo;
|
|
418
436
|
}
|
|
@@ -451,7 +469,10 @@ export const prReviewTriageSummary = Effect.fn("reads.prReviewTriageSummary")(fu
|
|
|
451
469
|
mergeable: firstPage.mergeable,
|
|
452
470
|
reviewRequests,
|
|
453
471
|
},
|
|
454
|
-
summary: {
|
|
472
|
+
summary: {
|
|
473
|
+
unresolvedReviewThreadsCount: resolutionUnknown ? null : unresolvedReviewThreadsCount,
|
|
474
|
+
unrepliedReviewThreadsCount,
|
|
475
|
+
},
|
|
455
476
|
checks,
|
|
456
477
|
};
|
|
457
478
|
});
|
|
@@ -464,3 +485,32 @@ const movedHead = (pr: number) =>
|
|
|
464
485
|
stderr: "PR head changed while collecting checks",
|
|
465
486
|
retryable: true,
|
|
466
487
|
});
|
|
488
|
+
|
|
489
|
+
export const prReviewDecision = Effect.fn("reads.prReviewDecision")(function* (
|
|
490
|
+
repo: RepoRef,
|
|
491
|
+
pr: number,
|
|
492
|
+
) {
|
|
493
|
+
const gh = yield* GitHubService;
|
|
494
|
+
const result = yield* gh.runGhJson<unknown>([
|
|
495
|
+
"pr",
|
|
496
|
+
"view",
|
|
497
|
+
String(pr),
|
|
498
|
+
"--repo",
|
|
499
|
+
repoSlug(repo),
|
|
500
|
+
"--json",
|
|
501
|
+
"reviewDecision",
|
|
502
|
+
]);
|
|
503
|
+
return yield* Effect.try({
|
|
504
|
+
try: () =>
|
|
505
|
+
Schema.decodeUnknownSync(Schema.Struct({ reviewDecision: PullView.fields.reviewDecision }))(
|
|
506
|
+
result,
|
|
507
|
+
).reviewDecision,
|
|
508
|
+
catch: (cause) =>
|
|
509
|
+
new GitHubCommandError({
|
|
510
|
+
command: "pr view",
|
|
511
|
+
exitCode: 1,
|
|
512
|
+
message: "Invalid review decision",
|
|
513
|
+
stderr: String(cause),
|
|
514
|
+
}),
|
|
515
|
+
});
|
|
516
|
+
});
|
|
@@ -5,7 +5,7 @@ import type { CheckRunAnnotation } from "#gh/types";
|
|
|
5
5
|
import { cachedRead, ReadCache } from "./cache";
|
|
6
6
|
import { OPS } from "./classes";
|
|
7
7
|
import { repoSlug } from "./keys";
|
|
8
|
-
import { issueView, issueList, issueComments } from "./issues";
|
|
8
|
+
import { issueView, issueList, issueComments, issueNode } from "./issues";
|
|
9
9
|
import {
|
|
10
10
|
prView,
|
|
11
11
|
prList,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
prBody,
|
|
15
15
|
checkSignal,
|
|
16
16
|
prReviewTriageSummary,
|
|
17
|
+
prReviewDecision,
|
|
17
18
|
} from "./pulls";
|
|
18
19
|
import { prComments, prReviews, reviewThreads, lastHumanReviewer } from "./reviews";
|
|
19
20
|
import { reviewGuards } from "./review-guards";
|
|
@@ -40,6 +41,7 @@ const RateLimitResponse = Schema.Struct({
|
|
|
40
41
|
}),
|
|
41
42
|
),
|
|
42
43
|
});
|
|
44
|
+
const ViewerLogin = Schema.Struct({ viewer: Schema.Struct({ login: Schema.String }) });
|
|
43
45
|
|
|
44
46
|
export type RateLimitResource = {
|
|
45
47
|
readonly limit: number;
|
|
@@ -110,6 +112,12 @@ export const getPRStatusRateLimit = Effect.fn("reads.getPRStatusRateLimit")(func
|
|
|
110
112
|
return { core, graphql } satisfies PRStatusRateLimit;
|
|
111
113
|
});
|
|
112
114
|
|
|
115
|
+
export const viewerLogin = Effect.fn("reads.viewerLogin")(function* () {
|
|
116
|
+
const gh = yield* GitHubService;
|
|
117
|
+
const response = yield* gh.runGraphQL("query { viewer { login } }", {});
|
|
118
|
+
return parsePreserving(ViewerLogin)(response).viewer;
|
|
119
|
+
});
|
|
120
|
+
|
|
113
121
|
const completedAttemptJobs = Effect.fn("reads.completedAttemptJobs")(function* (
|
|
114
122
|
repo: RepoRef,
|
|
115
123
|
runId: number,
|
|
@@ -384,6 +392,7 @@ export const reads = {
|
|
|
384
392
|
prList,
|
|
385
393
|
prChecks,
|
|
386
394
|
prReviewTriageSummary,
|
|
395
|
+
prReviewDecision,
|
|
387
396
|
prHead,
|
|
388
397
|
prBody,
|
|
389
398
|
checkSignal,
|
|
@@ -396,4 +405,6 @@ export const reads = {
|
|
|
396
405
|
issueView,
|
|
397
406
|
issueList,
|
|
398
407
|
issueComments,
|
|
408
|
+
issueNode,
|
|
409
|
+
viewerLogin,
|
|
399
410
|
};
|