@blogic-cz/agent-tools 1.7.5 → 1.9.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 +2 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/types.d.ts +1 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/credential-guard/index.d.ts.map +1 -1
- package/dist/db-tool/service.d.ts.map +1 -1
- package/dist/gh-tool/branch.d.ts +2 -3
- package/dist/gh-tool/branch.d.ts.map +1 -1
- package/dist/gh-tool/environment.d.ts +1 -1
- package/dist/gh-tool/environment.d.ts.map +1 -1
- package/dist/gh-tool/gist.d.ts +8 -28
- package/dist/gh-tool/gist.d.ts.map +1 -1
- package/dist/gh-tool/issue/commands.d.ts +11 -9
- package/dist/gh-tool/issue/commands.d.ts.map +1 -1
- package/dist/gh-tool/issue/core.d.ts +116 -9
- package/dist/gh-tool/issue/core.d.ts.map +1 -1
- package/dist/gh-tool/issue/triage.d.ts +6 -6
- package/dist/gh-tool/issue/triage.d.ts.map +1 -1
- package/dist/gh-tool/label.d.ts +20 -0
- package/dist/gh-tool/label.d.ts.map +1 -0
- package/dist/gh-tool/pr/commands.d.ts +53 -46
- package/dist/gh-tool/pr/commands.d.ts.map +1 -1
- package/dist/gh-tool/pr/core.d.ts +156 -86
- package/dist/gh-tool/pr/core.d.ts.map +1 -1
- package/dist/gh-tool/pr/review.d.ts +16 -16
- package/dist/gh-tool/pr/review.d.ts.map +1 -1
- package/dist/gh-tool/pr/snapshot.d.ts +9 -0
- package/dist/gh-tool/pr/snapshot.d.ts.map +1 -0
- package/dist/gh-tool/pr/stack-read.d.ts +1 -6
- package/dist/gh-tool/pr/stack-read.d.ts.map +1 -1
- package/dist/gh-tool/pr/stack.d.ts +3 -3
- package/dist/gh-tool/pr/stack.d.ts.map +1 -1
- package/dist/gh-tool/{api.d.ts → read/api.d.ts} +9 -3
- package/dist/gh-tool/read/api.d.ts.map +1 -0
- package/dist/gh-tool/read/cache.d.ts +44 -0
- package/dist/gh-tool/read/cache.d.ts.map +1 -0
- package/dist/gh-tool/read/catalog.d.ts +170 -0
- package/dist/gh-tool/read/catalog.d.ts.map +1 -0
- package/dist/gh-tool/read/classes.d.ts +98 -0
- package/dist/gh-tool/read/classes.d.ts.map +1 -0
- package/dist/gh-tool/read/commands.d.ts +3 -0
- package/dist/gh-tool/read/commands.d.ts.map +1 -0
- package/dist/gh-tool/read/commits.d.ts +10 -0
- package/dist/gh-tool/read/commits.d.ts.map +1 -0
- package/dist/gh-tool/read/gated.d.ts +14 -0
- package/dist/gh-tool/read/gated.d.ts.map +1 -0
- package/dist/gh-tool/read/index.d.ts +5 -0
- package/dist/gh-tool/read/index.d.ts.map +1 -0
- package/dist/gh-tool/read/issues.d.ts +80 -0
- package/dist/gh-tool/read/issues.d.ts.map +1 -0
- package/dist/gh-tool/read/keys.d.ts +64 -0
- package/dist/gh-tool/read/keys.d.ts.map +1 -0
- package/dist/gh-tool/read/porcelain.d.ts +68 -0
- package/dist/gh-tool/read/porcelain.d.ts.map +1 -0
- package/dist/gh-tool/read/pulls.d.ts +137 -0
- package/dist/gh-tool/read/pulls.d.ts.map +1 -0
- package/dist/gh-tool/read/reads.d.ts +220 -0
- package/dist/gh-tool/read/reads.d.ts.map +1 -0
- package/dist/gh-tool/read/rest.d.ts +49 -0
- package/dist/gh-tool/read/rest.d.ts.map +1 -0
- package/dist/gh-tool/read/review-guards.d.ts +20 -0
- package/dist/gh-tool/read/review-guards.d.ts.map +1 -0
- package/dist/gh-tool/read/review-model.d.ts +190 -0
- package/dist/gh-tool/read/review-model.d.ts.map +1 -0
- package/dist/gh-tool/read/reviews.d.ts +9 -0
- package/dist/gh-tool/read/reviews.d.ts.map +1 -0
- package/dist/gh-tool/read/stacks.d.ts +7 -0
- package/dist/gh-tool/read/stacks.d.ts.map +1 -0
- package/dist/gh-tool/read/store.d.ts +70 -0
- package/dist/gh-tool/read/store.d.ts.map +1 -0
- package/dist/gh-tool/{service.d.ts → read/transport.d.ts} +5 -3
- package/dist/gh-tool/read/transport.d.ts.map +1 -0
- package/dist/gh-tool/read/waits.d.ts +22 -0
- package/dist/gh-tool/read/waits.d.ts.map +1 -0
- package/dist/gh-tool/read/writes.d.ts +16 -0
- package/dist/gh-tool/read/writes.d.ts.map +1 -0
- package/dist/gh-tool/release.d.ts +10 -26
- package/dist/gh-tool/release.d.ts.map +1 -1
- package/dist/gh-tool/repo.d.ts +9 -4
- package/dist/gh-tool/repo.d.ts.map +1 -1
- package/dist/gh-tool/types.d.ts +1 -1
- package/dist/gh-tool/types.d.ts.map +1 -1
- package/dist/gh-tool/workflow.d.ts +43 -21
- package/dist/gh-tool/workflow.d.ts.map +1 -1
- package/dist/observability-tool/shared.d.ts.map +1 -1
- package/dist/shared/content-security.d.ts +2 -0
- package/dist/shared/content-security.d.ts.map +1 -1
- package/package.json +7 -6
- package/schemas/agent-tools.schema.json +5 -0
- package/src/api-tool/proxy-entry.ts +3 -1
- package/src/config/loader.ts +1 -0
- package/src/config/types.ts +1 -0
- package/src/credential-guard/index.ts +169 -1
- package/src/db-tool/service.ts +4 -1
- package/src/gh-tool/branch.ts +5 -8
- package/src/gh-tool/environment.ts +2 -1
- package/src/gh-tool/gist.ts +8 -35
- package/src/gh-tool/issue/commands.ts +12 -2
- package/src/gh-tool/issue/core.ts +58 -122
- package/src/gh-tool/issue/triage.ts +4 -26
- package/src/gh-tool/label.ts +57 -0
- package/src/gh-tool/main.ts +24 -4
- package/src/gh-tool/pr/commands.ts +17 -15
- package/src/gh-tool/pr/core.ts +486 -582
- package/src/gh-tool/pr/review.ts +106 -454
- package/src/gh-tool/pr/snapshot.ts +23 -0
- package/src/gh-tool/pr/stack-read.ts +1 -73
- package/src/gh-tool/pr/stack.ts +37 -36
- package/src/gh-tool/{api.ts → read/api.ts} +40 -7
- package/src/gh-tool/read/cache.ts +192 -0
- package/src/gh-tool/read/catalog.ts +310 -0
- package/src/gh-tool/read/classes.ts +31 -0
- package/src/gh-tool/read/commands.ts +25 -0
- package/src/gh-tool/read/commits.ts +28 -0
- package/src/gh-tool/read/gated.ts +56 -0
- package/src/gh-tool/read/index.ts +4 -0
- package/src/gh-tool/read/issues.ts +124 -0
- package/src/gh-tool/read/keys.ts +104 -0
- package/src/gh-tool/read/porcelain.ts +58 -0
- package/src/gh-tool/read/pulls.ts +266 -0
- package/src/gh-tool/read/reads.ts +306 -0
- package/src/gh-tool/read/rest.ts +111 -0
- package/src/gh-tool/read/review-guards.ts +61 -0
- package/src/gh-tool/read/review-model.ts +286 -0
- package/src/gh-tool/read/reviews.ts +114 -0
- package/src/gh-tool/read/stacks.ts +73 -0
- package/src/gh-tool/read/store.ts +504 -0
- package/src/gh-tool/{service.ts → read/transport.ts} +26 -6
- package/src/gh-tool/read/waits.ts +52 -0
- package/src/gh-tool/read/writes.ts +37 -0
- package/src/gh-tool/release.ts +15 -168
- package/src/gh-tool/repo.ts +26 -78
- package/src/gh-tool/types.ts +1 -1
- package/src/gh-tool/workflow.ts +123 -214
- package/src/observability-tool/shared.ts +2 -1
- package/src/shared/content-security.ts +10 -1
- package/src/shared/credential-patterns.ts +1 -1
- package/dist/gh-tool/api.d.ts.map +0 -1
- package/dist/gh-tool/pr/check-status-cache.d.ts +0 -6
- package/dist/gh-tool/pr/check-status-cache.d.ts.map +0 -1
- package/dist/gh-tool/service.d.ts.map +0 -1
- package/src/gh-tool/pr/check-status-cache.ts +0 -142
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { GitHubService } from "#gh/read/transport";
|
|
3
|
+
import { GitHubCommandError } from "#gh/errors";
|
|
4
|
+
import { cachedRead, ReadCache } from "./cache";
|
|
5
|
+
import { OPS } from "./classes";
|
|
6
|
+
import { digest } from "./keys";
|
|
7
|
+
import type { ReadKey } from "./keys";
|
|
8
|
+
import { parsePreserving } from "./porcelain";
|
|
9
|
+
|
|
10
|
+
const Page = Schema.Struct({
|
|
11
|
+
path: Schema.String,
|
|
12
|
+
etag: Schema.NullOr(Schema.String),
|
|
13
|
+
body: Schema.Unknown,
|
|
14
|
+
});
|
|
15
|
+
const Snapshot = Schema.Struct({ pages: Schema.Array(Page) });
|
|
16
|
+
export type RestSnapshot = typeof Snapshot.Type;
|
|
17
|
+
export type RestSource =
|
|
18
|
+
| { readonly path: string; readonly kind: "single" }
|
|
19
|
+
| {
|
|
20
|
+
readonly path: string;
|
|
21
|
+
readonly kind: "pages";
|
|
22
|
+
readonly items: (body: unknown) => readonly unknown[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const restSnapshot = Effect.fn("reads.restSnapshot")(function* (
|
|
26
|
+
key: ReadKey & { op: "prHead" | "checkSignal" | "issueComments" | "prComments" | "prReviews" },
|
|
27
|
+
sources: readonly RestSource[],
|
|
28
|
+
consistency: "cached" | "origin" = "cached",
|
|
29
|
+
) {
|
|
30
|
+
const gh = yield* GitHubService;
|
|
31
|
+
const session = yield* ReadCache;
|
|
32
|
+
return yield* cachedRead({
|
|
33
|
+
key,
|
|
34
|
+
policy: OPS[key.op],
|
|
35
|
+
consistency,
|
|
36
|
+
validateOrigin: true,
|
|
37
|
+
decode: parsePreserving(Snapshot),
|
|
38
|
+
fetch: (previous: { value: RestSnapshot } | null) =>
|
|
39
|
+
Effect.gen(function* () {
|
|
40
|
+
const pages: Array<typeof Page.Type> = [];
|
|
41
|
+
let unchanged = previous !== null;
|
|
42
|
+
for (const source of sources) {
|
|
43
|
+
let page = 1;
|
|
44
|
+
while (true) {
|
|
45
|
+
const path =
|
|
46
|
+
source.kind === "single"
|
|
47
|
+
? source.path
|
|
48
|
+
: `${source.path}${source.path.includes("?") ? "&" : "?"}per_page=100&page=${page}`;
|
|
49
|
+
const prior = previous?.value.pages.find((item) => item.path === path);
|
|
50
|
+
const request = {
|
|
51
|
+
path,
|
|
52
|
+
...(prior?.etag === null || prior?.etag === undefined
|
|
53
|
+
? {}
|
|
54
|
+
: { ifNoneMatch: prior.etag }),
|
|
55
|
+
};
|
|
56
|
+
const response = yield* session?.store === null || session === null
|
|
57
|
+
? gh.runGh(["api", path]).pipe(
|
|
58
|
+
Effect.flatMap((result) =>
|
|
59
|
+
Effect.try({
|
|
60
|
+
try: () => ({
|
|
61
|
+
status: 200,
|
|
62
|
+
body: JSON.parse(result.stdout) as unknown,
|
|
63
|
+
etag: undefined,
|
|
64
|
+
}),
|
|
65
|
+
catch: (error) =>
|
|
66
|
+
new GitHubCommandError({
|
|
67
|
+
command: `GET ${path}`,
|
|
68
|
+
exitCode: 0,
|
|
69
|
+
message: `Failed to parse response: ${String(error)}`,
|
|
70
|
+
stderr: `Failed to parse response: ${String(error)}`,
|
|
71
|
+
}),
|
|
72
|
+
}),
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
: gh.apiRequest<unknown>(request);
|
|
76
|
+
if (response.status === 304 && prior === undefined)
|
|
77
|
+
return yield* new GitHubCommandError({
|
|
78
|
+
command: `GET ${path}`,
|
|
79
|
+
exitCode: 304,
|
|
80
|
+
message: "304 without a cached page",
|
|
81
|
+
stderr: "304 without a cached page",
|
|
82
|
+
});
|
|
83
|
+
const body =
|
|
84
|
+
response.status === 304 && prior !== undefined ? prior.body : response.body;
|
|
85
|
+
const etag = response.etag ?? (response.status === 304 ? (prior?.etag ?? null) : null);
|
|
86
|
+
unchanged = unchanged && response.status === 304;
|
|
87
|
+
pages.push({ path, etag, body });
|
|
88
|
+
if (source.kind === "single") break;
|
|
89
|
+
const count = yield* Effect.try({
|
|
90
|
+
try: () => source.items(body).length,
|
|
91
|
+
catch: (error) =>
|
|
92
|
+
new GitHubCommandError({
|
|
93
|
+
command: `GET ${path}`,
|
|
94
|
+
exitCode: 0,
|
|
95
|
+
message: `Invalid paginated response: ${String(error)}`,
|
|
96
|
+
stderr: `Invalid paginated response: ${String(error)}`,
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
if (count < 100) break;
|
|
100
|
+
page += 1;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return unchanged
|
|
104
|
+
? { kind: "unchanged" as const }
|
|
105
|
+
: { kind: "fresh" as const, value: { pages } };
|
|
106
|
+
}),
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
export const snapshotDigest = (snapshot: RestSnapshot): string =>
|
|
111
|
+
digest(JSON.stringify(snapshot.pages.map((page) => page.body)));
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { GitHubService } from "#gh/read/transport";
|
|
3
|
+
import type { RepoRef } from "./keys";
|
|
4
|
+
import { repoSlug } from "./keys";
|
|
5
|
+
import { PENDING_REVIEWS_QUERY, PENDING_REVIEW_CONTENT_QUERY } from "./review-model";
|
|
6
|
+
import type { EditableComment, ReviewCommentById } from "./review-model";
|
|
7
|
+
|
|
8
|
+
export const reviewGuards = {
|
|
9
|
+
requestedReviewers: (repo: RepoRef, pr: number) =>
|
|
10
|
+
Effect.gen(function* () {
|
|
11
|
+
const gh = yield* GitHubService;
|
|
12
|
+
return yield* gh.runGhJson<{ requested_reviewers?: unknown }>([
|
|
13
|
+
"api",
|
|
14
|
+
`repos/${repoSlug(repo)}/pulls/${pr}`,
|
|
15
|
+
]);
|
|
16
|
+
}),
|
|
17
|
+
commentById: (commentId: number) =>
|
|
18
|
+
Effect.gen(function* () {
|
|
19
|
+
const gh = yield* GitHubService;
|
|
20
|
+
return yield* gh.runGhJson<ReviewCommentById>([
|
|
21
|
+
"api",
|
|
22
|
+
`repos/{owner}/{repo}/pulls/comments/${commentId}`,
|
|
23
|
+
]);
|
|
24
|
+
}),
|
|
25
|
+
editableComment: (repo: RepoRef, kind: "review" | "issue", commentId: number) =>
|
|
26
|
+
Effect.gen(function* () {
|
|
27
|
+
const gh = yield* GitHubService;
|
|
28
|
+
return yield* gh.runGhJson<EditableComment>([
|
|
29
|
+
"api",
|
|
30
|
+
`repos/${repoSlug(repo)}/${kind === "review" ? "pulls" : "issues"}/comments/${commentId}`,
|
|
31
|
+
]);
|
|
32
|
+
}),
|
|
33
|
+
viewer: () =>
|
|
34
|
+
Effect.gen(function* () {
|
|
35
|
+
const gh = yield* GitHubService;
|
|
36
|
+
return yield* gh.runGhJson<{ login: string }>(["api", "user"]);
|
|
37
|
+
}),
|
|
38
|
+
pullNumber: (repo: RepoRef, pr: number) =>
|
|
39
|
+
Effect.gen(function* () {
|
|
40
|
+
const gh = yield* GitHubService;
|
|
41
|
+
return yield* gh.runGhJson<{ number: number }>([
|
|
42
|
+
"api",
|
|
43
|
+
`repos/${repoSlug(repo)}/pulls/${pr}`,
|
|
44
|
+
]);
|
|
45
|
+
}),
|
|
46
|
+
pendingReviews: (repo: RepoRef, pr: number, after: string | null) =>
|
|
47
|
+
Effect.gen(function* () {
|
|
48
|
+
const gh = yield* GitHubService;
|
|
49
|
+
return yield* gh.runGraphQL(PENDING_REVIEWS_QUERY, {
|
|
50
|
+
owner: repo.owner,
|
|
51
|
+
name: repo.name,
|
|
52
|
+
pr,
|
|
53
|
+
after,
|
|
54
|
+
});
|
|
55
|
+
}),
|
|
56
|
+
pendingReviewContent: (reviewId: string, after: string | null) =>
|
|
57
|
+
Effect.gen(function* () {
|
|
58
|
+
const gh = yield* GitHubService;
|
|
59
|
+
return yield* gh.runGraphQL(PENDING_REVIEW_CONTENT_QUERY, { reviewId, after });
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
export const REVIEW_THREADS_QUERY = `
|
|
2
|
+
query($owner: String!, $name: String!, $pr: Int!, $after: String) {
|
|
3
|
+
repository(owner: $owner, name: $name) {
|
|
4
|
+
pullRequest(number: $pr) {
|
|
5
|
+
reviewThreads(first: 100, after: $after) {
|
|
6
|
+
nodes {
|
|
7
|
+
id
|
|
8
|
+
isResolved
|
|
9
|
+
comments(first: 100) {
|
|
10
|
+
nodes {
|
|
11
|
+
id
|
|
12
|
+
databaseId
|
|
13
|
+
path
|
|
14
|
+
line
|
|
15
|
+
body
|
|
16
|
+
author { login }
|
|
17
|
+
commit { oid }
|
|
18
|
+
}
|
|
19
|
+
pageInfo { hasNextPage endCursor }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
pageInfo {
|
|
23
|
+
hasNextPage
|
|
24
|
+
endCursor
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
export const REVIEW_THREAD_COMMENTS_QUERY = `
|
|
33
|
+
query($threadId: ID!, $after: String) {
|
|
34
|
+
node(id: $threadId) {
|
|
35
|
+
... on PullRequestReviewThread {
|
|
36
|
+
comments(first: 100, after: $after) {
|
|
37
|
+
nodes {
|
|
38
|
+
id
|
|
39
|
+
databaseId
|
|
40
|
+
path
|
|
41
|
+
line
|
|
42
|
+
body
|
|
43
|
+
author { login }
|
|
44
|
+
commit { oid }
|
|
45
|
+
}
|
|
46
|
+
pageInfo { hasNextPage endCursor }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
export const RESOLVE_THREAD_MUTATION = `
|
|
54
|
+
mutation($threadId: ID!) {
|
|
55
|
+
resolveReviewThread(input: {threadId: $threadId}) {
|
|
56
|
+
thread { id isResolved }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
export const PENDING_REVIEWS_QUERY = `
|
|
62
|
+
query($owner: String!, $name: String!, $pr: Int!, $after: String) {
|
|
63
|
+
viewer { login }
|
|
64
|
+
repository(owner: $owner, name: $name) {
|
|
65
|
+
pullRequest(number: $pr) {
|
|
66
|
+
reviews(first: 100, after: $after, states: [PENDING]) {
|
|
67
|
+
nodes {
|
|
68
|
+
id
|
|
69
|
+
state
|
|
70
|
+
author { login }
|
|
71
|
+
}
|
|
72
|
+
pageInfo { hasNextPage endCursor }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
export const PENDING_REVIEW_CONTENT_QUERY = `
|
|
80
|
+
query($reviewId: ID!, $after: String) {
|
|
81
|
+
viewer { login }
|
|
82
|
+
node(id: $reviewId) {
|
|
83
|
+
... on PullRequestReview {
|
|
84
|
+
id
|
|
85
|
+
state
|
|
86
|
+
body
|
|
87
|
+
author { login }
|
|
88
|
+
pullRequest {
|
|
89
|
+
number
|
|
90
|
+
repository { nameWithOwner }
|
|
91
|
+
}
|
|
92
|
+
comments(first: 100, after: $after) {
|
|
93
|
+
nodes { body }
|
|
94
|
+
pageInfo { hasNextPage endCursor }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
export const SUBMIT_REVIEW_MUTATION = `
|
|
102
|
+
mutation($reviewId: ID!, $event: PullRequestReviewEvent!, $body: String) {
|
|
103
|
+
submitPullRequestReview(input: { pullRequestReviewId: $reviewId, event: $event, body: $body }) {
|
|
104
|
+
pullRequestReview { id state }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
export const LAST_HUMAN_REVIEWER_QUERY = `
|
|
110
|
+
query($owner: String!, $name: String!, $pr: Int!) {
|
|
111
|
+
repository(owner: $owner, name: $name) {
|
|
112
|
+
pullRequest(number: $pr) {
|
|
113
|
+
reviewRequests(first: 100) {
|
|
114
|
+
nodes {
|
|
115
|
+
requestedReviewer {
|
|
116
|
+
__typename
|
|
117
|
+
... on User { login }
|
|
118
|
+
... on Team { name }
|
|
119
|
+
... on Bot { login }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
reviews(last: 100) {
|
|
124
|
+
nodes {
|
|
125
|
+
author { login }
|
|
126
|
+
state
|
|
127
|
+
submittedAt
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Internal types
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
export type ThreadNode = {
|
|
140
|
+
id: string;
|
|
141
|
+
isResolved: boolean;
|
|
142
|
+
comments: {
|
|
143
|
+
nodes: Array<{
|
|
144
|
+
id: string;
|
|
145
|
+
databaseId: number;
|
|
146
|
+
path: string;
|
|
147
|
+
line: number;
|
|
148
|
+
body: string;
|
|
149
|
+
author: { login: string };
|
|
150
|
+
commit: { oid: string } | null;
|
|
151
|
+
}>;
|
|
152
|
+
pageInfo?: { hasNextPage: boolean; endCursor: string | null };
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export type ThreadCommentsQueryResult = {
|
|
157
|
+
node: { comments: ThreadNode["comments"] } | null;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export type ThreadsQueryResult = {
|
|
161
|
+
repository: {
|
|
162
|
+
pullRequest: {
|
|
163
|
+
reviewThreads: {
|
|
164
|
+
nodes: ThreadNode[];
|
|
165
|
+
pageInfo: {
|
|
166
|
+
hasNextPage: boolean;
|
|
167
|
+
endCursor: string | null;
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export type ResolveThreadResult = {
|
|
175
|
+
resolveReviewThread: {
|
|
176
|
+
thread: { id: string; isResolved: boolean };
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export type PendingReviewsQueryResult = {
|
|
181
|
+
viewer: { login: string };
|
|
182
|
+
repository: {
|
|
183
|
+
pullRequest: {
|
|
184
|
+
reviews: {
|
|
185
|
+
nodes: Array<{
|
|
186
|
+
id: string;
|
|
187
|
+
state: string;
|
|
188
|
+
author: { login: string };
|
|
189
|
+
}>;
|
|
190
|
+
pageInfo: { hasNextPage: boolean; endCursor: string | null };
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export type PendingReviewContentQueryResult = {
|
|
197
|
+
viewer: { login: string };
|
|
198
|
+
node: {
|
|
199
|
+
id: string;
|
|
200
|
+
state: string;
|
|
201
|
+
body: string | null;
|
|
202
|
+
author: { login: string };
|
|
203
|
+
pullRequest: { number: number; repository: { nameWithOwner: string } };
|
|
204
|
+
comments: {
|
|
205
|
+
nodes: Array<{ body: string }>;
|
|
206
|
+
pageInfo: { hasNextPage: boolean; endCursor: string | null };
|
|
207
|
+
};
|
|
208
|
+
} | null;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export type SubmitReviewResult = {
|
|
212
|
+
submitPullRequestReview: {
|
|
213
|
+
pullRequestReview: { id: string; state: string };
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export type RequestedReviewerNode = {
|
|
218
|
+
__typename: string;
|
|
219
|
+
login?: string;
|
|
220
|
+
name?: string;
|
|
221
|
+
} | null;
|
|
222
|
+
|
|
223
|
+
export type LastHumanReviewerQueryResult = {
|
|
224
|
+
repository: {
|
|
225
|
+
pullRequest: {
|
|
226
|
+
reviewRequests: {
|
|
227
|
+
nodes: Array<{ requestedReviewer: RequestedReviewerNode }>;
|
|
228
|
+
};
|
|
229
|
+
reviews: {
|
|
230
|
+
nodes: Array<{
|
|
231
|
+
author: { login: string } | null;
|
|
232
|
+
state: string;
|
|
233
|
+
submittedAt: string | null;
|
|
234
|
+
}>;
|
|
235
|
+
};
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
export type RawReviewComment = {
|
|
241
|
+
id: number;
|
|
242
|
+
in_reply_to_id: number | null;
|
|
243
|
+
user: { login: string };
|
|
244
|
+
body: string;
|
|
245
|
+
path: string;
|
|
246
|
+
line: number;
|
|
247
|
+
created_at: string;
|
|
248
|
+
updated_at: string;
|
|
249
|
+
pull_request_review_id?: number | null;
|
|
250
|
+
commit_id?: string | null;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export type RawIssueComment = {
|
|
254
|
+
id: number;
|
|
255
|
+
user: { login: string };
|
|
256
|
+
body: string;
|
|
257
|
+
created_at: string;
|
|
258
|
+
html_url: string;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
export type RawPullRequestReview = {
|
|
262
|
+
id: number;
|
|
263
|
+
user: { login: string } | null;
|
|
264
|
+
state: string;
|
|
265
|
+
body: string | null;
|
|
266
|
+
submitted_at: string | null;
|
|
267
|
+
html_url: string;
|
|
268
|
+
commit_id?: string | null;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export type ReviewCommentById = {
|
|
272
|
+
id: number;
|
|
273
|
+
in_reply_to_id: number | null;
|
|
274
|
+
pull_request_url: string;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
export type EditableComment = {
|
|
278
|
+
id: number;
|
|
279
|
+
user: { login: string };
|
|
280
|
+
html_url: string;
|
|
281
|
+
updated_at: string;
|
|
282
|
+
pull_request_url?: string;
|
|
283
|
+
issue_url?: string;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export const REST_PAGE_SIZE = 100;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { GitHubService } from "#gh/read/transport";
|
|
3
|
+
import { cachedRead } from "./cache";
|
|
4
|
+
import { gatedRead } from "./gated";
|
|
5
|
+
import { restSnapshot, snapshotDigest } from "./rest";
|
|
6
|
+
import { OPS } from "./classes";
|
|
7
|
+
import { repoSlug } from "./keys";
|
|
8
|
+
import type { RepoRef } from "./keys";
|
|
9
|
+
import {
|
|
10
|
+
REVIEW_THREADS_QUERY,
|
|
11
|
+
REVIEW_THREAD_COMMENTS_QUERY,
|
|
12
|
+
LAST_HUMAN_REVIEWER_QUERY,
|
|
13
|
+
} from "./review-model";
|
|
14
|
+
import type {
|
|
15
|
+
ThreadNode,
|
|
16
|
+
ThreadsQueryResult,
|
|
17
|
+
ThreadCommentsQueryResult,
|
|
18
|
+
LastHumanReviewerQueryResult,
|
|
19
|
+
RawReviewComment,
|
|
20
|
+
RawPullRequestReview,
|
|
21
|
+
} from "./review-model";
|
|
22
|
+
|
|
23
|
+
const originThreads = Effect.fn("pr.fetchAllThreadNodes")(function* (
|
|
24
|
+
repoInfo: RepoRef,
|
|
25
|
+
pr: number,
|
|
26
|
+
) {
|
|
27
|
+
const service = yield* GitHubService;
|
|
28
|
+
|
|
29
|
+
const nodes: ThreadNode[] = [];
|
|
30
|
+
let after: string | null = null;
|
|
31
|
+
|
|
32
|
+
while (true) {
|
|
33
|
+
const response = (yield* service.runGraphQL(REVIEW_THREADS_QUERY, {
|
|
34
|
+
owner: repoInfo.owner,
|
|
35
|
+
name: repoInfo.name,
|
|
36
|
+
pr,
|
|
37
|
+
after,
|
|
38
|
+
})) as ThreadsQueryResult;
|
|
39
|
+
|
|
40
|
+
const page = response.repository.pullRequest.reviewThreads;
|
|
41
|
+
for (const node of page.nodes) {
|
|
42
|
+
let commentsAfter = node.comments.pageInfo?.endCursor ?? null;
|
|
43
|
+
while (node.comments.pageInfo?.hasNextPage && commentsAfter !== null) {
|
|
44
|
+
const commentsResponse = (yield* service.runGraphQL(REVIEW_THREAD_COMMENTS_QUERY, {
|
|
45
|
+
threadId: node.id,
|
|
46
|
+
after: commentsAfter,
|
|
47
|
+
})) as ThreadCommentsQueryResult;
|
|
48
|
+
if (commentsResponse.node === null) break;
|
|
49
|
+
node.comments.nodes.push(...commentsResponse.node.comments.nodes);
|
|
50
|
+
node.comments.pageInfo = commentsResponse.node.comments.pageInfo;
|
|
51
|
+
commentsAfter = commentsResponse.node.comments.pageInfo?.endCursor ?? null;
|
|
52
|
+
}
|
|
53
|
+
nodes.push(node);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!page.pageInfo.hasNextPage || page.pageInfo.endCursor === null) {
|
|
57
|
+
return nodes;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
after = page.pageInfo.endCursor;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const reviewPages = (repo: RepoRef, pr: number, op: "prComments" | "prReviews") =>
|
|
65
|
+
restSnapshot({ op, repo, pr }, [
|
|
66
|
+
{
|
|
67
|
+
kind: "pages",
|
|
68
|
+
path: `repos/${repoSlug(repo)}/pulls/${pr}/${op === "prComments" ? "comments" : "reviews"}`,
|
|
69
|
+
items: Schema.decodeUnknownSync(Schema.Array(Schema.Unknown)),
|
|
70
|
+
},
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
export const prComments = Effect.fn("reads.prComments")(function* (repo: RepoRef, pr: number) {
|
|
74
|
+
const snapshot = yield* reviewPages(repo, pr, "prComments");
|
|
75
|
+
return snapshot.pages.flatMap((page) => page.body as RawReviewComment[]);
|
|
76
|
+
});
|
|
77
|
+
export const prReviews = Effect.fn("reads.prReviews")(function* (repo: RepoRef, pr: number) {
|
|
78
|
+
const snapshot = yield* reviewPages(repo, pr, "prReviews");
|
|
79
|
+
return snapshot.pages.flatMap((page) => page.body as RawPullRequestReview[]);
|
|
80
|
+
});
|
|
81
|
+
export const reviewThreads = Effect.fn("reads.reviewThreads")(function* (
|
|
82
|
+
repo: RepoRef,
|
|
83
|
+
pr: number,
|
|
84
|
+
consistency: "cached" | "origin" = "cached",
|
|
85
|
+
) {
|
|
86
|
+
return yield* gatedRead({
|
|
87
|
+
key: { op: "reviewThreads", repo, pr },
|
|
88
|
+
consistency,
|
|
89
|
+
decode: (raw) => Schema.decodeUnknownSync(Schema.Array(Schema.Unknown))(raw) as ThreadNode[],
|
|
90
|
+
fetch: originThreads(repo, pr),
|
|
91
|
+
signal: reviewPages(repo, pr, "prComments").pipe(Effect.map(snapshotDigest)),
|
|
92
|
+
maxAgeMs: () => OPS.reviewThreads.maxAgeMs,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
export const lastHumanReviewer = Effect.fn("reads.lastHumanReviewer")(function* (
|
|
96
|
+
repo: RepoRef,
|
|
97
|
+
pr: number,
|
|
98
|
+
) {
|
|
99
|
+
const service = yield* GitHubService;
|
|
100
|
+
return yield* cachedRead({
|
|
101
|
+
key: { op: "lastHumanReviewer", repo, pr },
|
|
102
|
+
policy: OPS.lastHumanReviewer,
|
|
103
|
+
decode: (raw) => raw as LastHumanReviewerQueryResult,
|
|
104
|
+
fetch: () =>
|
|
105
|
+
service
|
|
106
|
+
.runGraphQL(LAST_HUMAN_REVIEWER_QUERY, { owner: repo.owner, name: repo.name, pr })
|
|
107
|
+
.pipe(
|
|
108
|
+
Effect.map((value) => ({
|
|
109
|
+
kind: "fresh" as const,
|
|
110
|
+
value: value as LastHumanReviewerQueryResult,
|
|
111
|
+
})),
|
|
112
|
+
),
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
|
|
3
|
+
import { GitHubService } from "#gh/read/transport";
|
|
4
|
+
|
|
5
|
+
import type { StackMember, StackView } from "#gh/types";
|
|
6
|
+
|
|
7
|
+
type StacksListResponse = Array<{ number: number }>;
|
|
8
|
+
|
|
9
|
+
type StackResponse = {
|
|
10
|
+
number: number;
|
|
11
|
+
base: { ref: string };
|
|
12
|
+
open: boolean;
|
|
13
|
+
pull_requests: Array<{
|
|
14
|
+
number: number;
|
|
15
|
+
title: string;
|
|
16
|
+
state: "open" | "closed";
|
|
17
|
+
merged_at: string | null;
|
|
18
|
+
draft: boolean;
|
|
19
|
+
html_url: string;
|
|
20
|
+
head: { ref: string };
|
|
21
|
+
base: { ref: string };
|
|
22
|
+
}>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const readStack = Effect.fn("pr.readStack")(function* (opts: { pr: number }) {
|
|
26
|
+
const gh = yield* GitHubService;
|
|
27
|
+
const repo = yield* gh.getRepoInfo();
|
|
28
|
+
const base = `repos/${repo.owner}/${repo.name}`;
|
|
29
|
+
|
|
30
|
+
const unstacked: StackView = {
|
|
31
|
+
pr: opts.pr,
|
|
32
|
+
isStacked: false,
|
|
33
|
+
stackNumber: null,
|
|
34
|
+
baseRef: null,
|
|
35
|
+
members: [],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// A 404 is the repository having no stacks surface at all, which reads the same as a PR
|
|
39
|
+
// that belongs to no stack. Every caller gets that reading from here, not its own.
|
|
40
|
+
const stacks = yield* gh
|
|
41
|
+
.apiRequest<StacksListResponse>({ path: `${base}/stacks?pull_request=${opts.pr}` })
|
|
42
|
+
.pipe(Effect.catchTag("GitHubNotFoundError", () => Effect.succeed(null)));
|
|
43
|
+
|
|
44
|
+
if (stacks === null) {
|
|
45
|
+
return unstacked;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const stackNumber = stacks.body?.[0]?.number;
|
|
49
|
+
if (stackNumber === undefined) {
|
|
50
|
+
return unstacked;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const stack = yield* gh.apiRequest<StackResponse>({ path: `${base}/stacks/${stackNumber}` });
|
|
54
|
+
|
|
55
|
+
const members: StackMember[] = stack.body.pull_requests.map((member, index) => ({
|
|
56
|
+
position: index + 1,
|
|
57
|
+
number: member.number,
|
|
58
|
+
title: member.title,
|
|
59
|
+
headRefName: member.head.ref,
|
|
60
|
+
baseRefName: member.base.ref,
|
|
61
|
+
state: member.merged_at === null ? member.state : "merged",
|
|
62
|
+
isDraft: member.draft,
|
|
63
|
+
url: member.html_url,
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
pr: opts.pr,
|
|
68
|
+
isStacked: true,
|
|
69
|
+
stackNumber: stack.body.number,
|
|
70
|
+
baseRef: stack.body.base.ref,
|
|
71
|
+
members,
|
|
72
|
+
} satisfies StackView;
|
|
73
|
+
});
|