@blogic-cz/agent-tools 1.7.4 → 1.8.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 (128) hide show
  1. package/README.md +2 -1
  2. package/dist/db-tool/service.d.ts.map +1 -1
  3. package/dist/gh-tool/branch.d.ts +2 -3
  4. package/dist/gh-tool/branch.d.ts.map +1 -1
  5. package/dist/gh-tool/environment.d.ts +1 -1
  6. package/dist/gh-tool/environment.d.ts.map +1 -1
  7. package/dist/gh-tool/gist.d.ts +8 -28
  8. package/dist/gh-tool/gist.d.ts.map +1 -1
  9. package/dist/gh-tool/issue/commands.d.ts +9 -9
  10. package/dist/gh-tool/issue/commands.d.ts.map +1 -1
  11. package/dist/gh-tool/issue/core.d.ts +103 -9
  12. package/dist/gh-tool/issue/core.d.ts.map +1 -1
  13. package/dist/gh-tool/issue/triage.d.ts +6 -6
  14. package/dist/gh-tool/issue/triage.d.ts.map +1 -1
  15. package/dist/gh-tool/pr/commands.d.ts +53 -46
  16. package/dist/gh-tool/pr/commands.d.ts.map +1 -1
  17. package/dist/gh-tool/pr/core.d.ts +179 -74
  18. package/dist/gh-tool/pr/core.d.ts.map +1 -1
  19. package/dist/gh-tool/pr/review.d.ts +16 -16
  20. package/dist/gh-tool/pr/review.d.ts.map +1 -1
  21. package/dist/gh-tool/pr/snapshot.d.ts +9 -0
  22. package/dist/gh-tool/pr/snapshot.d.ts.map +1 -0
  23. package/dist/gh-tool/pr/stack-read.d.ts +1 -6
  24. package/dist/gh-tool/pr/stack-read.d.ts.map +1 -1
  25. package/dist/gh-tool/pr/stack.d.ts +3 -3
  26. package/dist/gh-tool/pr/stack.d.ts.map +1 -1
  27. package/dist/gh-tool/{api.d.ts → read/api.d.ts} +9 -3
  28. package/dist/gh-tool/read/api.d.ts.map +1 -0
  29. package/dist/gh-tool/read/cache.d.ts +44 -0
  30. package/dist/gh-tool/read/cache.d.ts.map +1 -0
  31. package/dist/gh-tool/read/catalog.d.ts +170 -0
  32. package/dist/gh-tool/read/catalog.d.ts.map +1 -0
  33. package/dist/gh-tool/read/classes.d.ts +94 -0
  34. package/dist/gh-tool/read/classes.d.ts.map +1 -0
  35. package/dist/gh-tool/read/commands.d.ts +3 -0
  36. package/dist/gh-tool/read/commands.d.ts.map +1 -0
  37. package/dist/gh-tool/read/gated.d.ts +14 -0
  38. package/dist/gh-tool/read/gated.d.ts.map +1 -0
  39. package/dist/gh-tool/read/index.d.ts +5 -0
  40. package/dist/gh-tool/read/index.d.ts.map +1 -0
  41. package/dist/gh-tool/read/issues.d.ts +68 -0
  42. package/dist/gh-tool/read/issues.d.ts.map +1 -0
  43. package/dist/gh-tool/read/keys.d.ts +61 -0
  44. package/dist/gh-tool/read/keys.d.ts.map +1 -0
  45. package/dist/gh-tool/read/porcelain.d.ts +68 -0
  46. package/dist/gh-tool/read/porcelain.d.ts.map +1 -0
  47. package/dist/gh-tool/read/pulls.d.ts +137 -0
  48. package/dist/gh-tool/read/pulls.d.ts.map +1 -0
  49. package/dist/gh-tool/read/reads.d.ts +209 -0
  50. package/dist/gh-tool/read/reads.d.ts.map +1 -0
  51. package/dist/gh-tool/read/rest.d.ts +49 -0
  52. package/dist/gh-tool/read/rest.d.ts.map +1 -0
  53. package/dist/gh-tool/read/review-guards.d.ts +20 -0
  54. package/dist/gh-tool/read/review-guards.d.ts.map +1 -0
  55. package/dist/gh-tool/read/review-model.d.ts +190 -0
  56. package/dist/gh-tool/read/review-model.d.ts.map +1 -0
  57. package/dist/gh-tool/read/reviews.d.ts +9 -0
  58. package/dist/gh-tool/read/reviews.d.ts.map +1 -0
  59. package/dist/gh-tool/read/stacks.d.ts +7 -0
  60. package/dist/gh-tool/read/stacks.d.ts.map +1 -0
  61. package/dist/gh-tool/read/store.d.ts +70 -0
  62. package/dist/gh-tool/read/store.d.ts.map +1 -0
  63. package/dist/gh-tool/{service.d.ts → read/transport.d.ts} +5 -3
  64. package/dist/gh-tool/read/transport.d.ts.map +1 -0
  65. package/dist/gh-tool/read/waits.d.ts +22 -0
  66. package/dist/gh-tool/read/waits.d.ts.map +1 -0
  67. package/dist/gh-tool/read/writes.d.ts +16 -0
  68. package/dist/gh-tool/read/writes.d.ts.map +1 -0
  69. package/dist/gh-tool/release.d.ts +10 -26
  70. package/dist/gh-tool/release.d.ts.map +1 -1
  71. package/dist/gh-tool/repo.d.ts +4 -4
  72. package/dist/gh-tool/repo.d.ts.map +1 -1
  73. package/dist/gh-tool/types.d.ts +1 -1
  74. package/dist/gh-tool/types.d.ts.map +1 -1
  75. package/dist/gh-tool/workflow.d.ts +43 -21
  76. package/dist/gh-tool/workflow.d.ts.map +1 -1
  77. package/dist/observability-tool/shared.d.ts.map +1 -1
  78. package/dist/shared/content-security.d.ts +2 -0
  79. package/dist/shared/content-security.d.ts.map +1 -1
  80. package/package.json +6 -6
  81. package/src/api-tool/proxy-entry.ts +3 -1
  82. package/src/db-tool/service.ts +4 -1
  83. package/src/gh-tool/branch.ts +5 -8
  84. package/src/gh-tool/environment.ts +2 -1
  85. package/src/gh-tool/gist.ts +8 -35
  86. package/src/gh-tool/issue/commands.ts +1 -1
  87. package/src/gh-tool/issue/core.ts +55 -122
  88. package/src/gh-tool/issue/triage.ts +4 -26
  89. package/src/gh-tool/main.ts +5 -2
  90. package/src/gh-tool/pr/commands.ts +19 -23
  91. package/src/gh-tool/pr/core.ts +542 -582
  92. package/src/gh-tool/pr/review.ts +106 -454
  93. package/src/gh-tool/pr/snapshot.ts +23 -0
  94. package/src/gh-tool/pr/stack-read.ts +1 -73
  95. package/src/gh-tool/pr/stack.ts +37 -36
  96. package/src/gh-tool/{api.ts → read/api.ts} +40 -7
  97. package/src/gh-tool/read/cache.ts +192 -0
  98. package/src/gh-tool/read/catalog.ts +310 -0
  99. package/src/gh-tool/read/classes.ts +30 -0
  100. package/src/gh-tool/read/commands.ts +25 -0
  101. package/src/gh-tool/read/gated.ts +56 -0
  102. package/src/gh-tool/read/index.ts +4 -0
  103. package/src/gh-tool/read/issues.ts +107 -0
  104. package/src/gh-tool/read/keys.ts +102 -0
  105. package/src/gh-tool/read/porcelain.ts +58 -0
  106. package/src/gh-tool/read/pulls.ts +266 -0
  107. package/src/gh-tool/read/reads.ts +306 -0
  108. package/src/gh-tool/read/rest.ts +111 -0
  109. package/src/gh-tool/read/review-guards.ts +61 -0
  110. package/src/gh-tool/read/review-model.ts +286 -0
  111. package/src/gh-tool/read/reviews.ts +114 -0
  112. package/src/gh-tool/read/stacks.ts +73 -0
  113. package/src/gh-tool/read/store.ts +504 -0
  114. package/src/gh-tool/{service.ts → read/transport.ts} +26 -6
  115. package/src/gh-tool/read/waits.ts +52 -0
  116. package/src/gh-tool/read/writes.ts +37 -0
  117. package/src/gh-tool/release.ts +15 -168
  118. package/src/gh-tool/repo.ts +5 -78
  119. package/src/gh-tool/types.ts +1 -1
  120. package/src/gh-tool/workflow.ts +123 -214
  121. package/src/observability-tool/shared.ts +2 -1
  122. package/src/shared/content-security.ts +10 -1
  123. package/src/shared/credential-patterns.ts +1 -1
  124. package/dist/gh-tool/api.d.ts.map +0 -1
  125. package/dist/gh-tool/pr/check-status-cache.d.ts +0 -6
  126. package/dist/gh-tool/pr/check-status-cache.d.ts.map +0 -1
  127. package/dist/gh-tool/service.d.ts.map +0 -1
  128. package/src/gh-tool/pr/check-status-cache.ts +0 -142
@@ -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
+ });