* One GraphQL round-trip returning the PR's inline review threads AND the set of * author logins that are bots (`__typename === "Bot"`) across comments, reviews, * and thread comments. GraphQL is the only place GitHub exposes author type — * `gh pr view --json` gives the login only. The caller us
( runtime: PRRuntime, ref: GhPRRef, )
| 421 | * the top-level comments/reviews; thread comments are tagged inline here. |
| 422 | */ |
| 423 | async function fetchGhThreadsAndBots( |
| 424 | runtime: PRRuntime, |
| 425 | ref: GhPRRef, |
| 426 | ): Promise<{ threads: PRReviewThread[]; botLogins: Set<string>; avatarByLogin: Map<string, string> }> { |
| 427 | const result = await runtime.runCommand("gh", hostnameArgs(ref.host, [ |
| 428 | "api", "graphql", |
| 429 | "-f", `query=${PR_CONTEXT_GRAPHQL_QUERY}`, |
| 430 | "-f", `owner=${ref.owner}`, |
| 431 | "-f", `repo=${ref.repo}`, |
| 432 | "-F", `number=${ref.number}`, |
| 433 | ])); |
| 434 | |
| 435 | const empty = { threads: [] as PRReviewThread[], botLogins: new Set<string>(), avatarByLogin: new Map<string, string>() }; |
| 436 | if (result.exitCode !== 0) return empty; |
| 437 | |
| 438 | const data = JSON.parse(result.stdout); |
| 439 | const pr = data?.data?.repository?.pullRequest; |
| 440 | if (!pr) return empty; |
| 441 | |
| 442 | const botLogins = new Set<string>(); |
| 443 | const avatarByLogin = new Map<string, string>(); |
| 444 | const collect = (author: any) => { |
| 445 | if (!author?.login) return; |
| 446 | const login = String(author.login); |
| 447 | if (author.__typename === 'Bot') botLogins.add(login); |
| 448 | if (author.avatarUrl && !avatarByLogin.has(login)) avatarByLogin.set(login, String(author.avatarUrl)); |
| 449 | }; |
| 450 | for (const n of (pr.comments?.nodes ?? [])) collect(n?.author); |
| 451 | for (const n of (pr.reviews?.nodes ?? [])) collect(n?.author); |
| 452 | |
| 453 | const threadNodes = pr.reviewThreads?.nodes; |
| 454 | const threads: PRReviewThread[] = Array.isArray(threadNodes) |
| 455 | ? threadNodes.map((t: any): PRReviewThread => ({ |
| 456 | id: String(t.id ?? ''), |
| 457 | isResolved: t.isResolved === true, |
| 458 | isOutdated: t.isOutdated === true, |
| 459 | path: String(t.path ?? ''), |
| 460 | line: typeof t.line === 'number' ? t.line : null, |
| 461 | startLine: typeof t.startLine === 'number' ? t.startLine : null, |
| 462 | diffSide: t.diffSide === 'LEFT' || t.diffSide === 'RIGHT' ? t.diffSide : null, |
| 463 | comments: Array.isArray(t.comments?.nodes) |
| 464 | ? t.comments.nodes.map((c: any): PRThreadComment => { |
| 465 | collect(c?.author); |
| 466 | return { |
| 467 | id: String(c.id ?? ''), |
| 468 | author: c.author?.login ? String(c.author.login) : '', |
| 469 | ...(c.author?.__typename === 'Bot' ? { isBot: true } : {}), |
| 470 | ...(c.author?.avatarUrl ? { avatarUrl: String(c.author.avatarUrl) } : {}), |
| 471 | body: String(c.body ?? ''), |
| 472 | createdAt: String(c.createdAt ?? ''), |
| 473 | url: String(c.url ?? ''), |
| 474 | ...(c.diffHunk ? { diffHunk: String(c.diffHunk) } : {}), |
| 475 | }; |
| 476 | }) |
| 477 | : [], |
| 478 | })) |
| 479 | : []; |
| 480 |
no test coverage detected