fix(github): complete PR reactions for CodeRabbit reviews (#13456)

* feat(github): add PR comment reaction controls

* feat(github): add full PR comment reaction picker

* fix(github): cover all reactable PR comment paths

* fix(github): reconcile comment reactions with main

* fix(github): preserve focus on failed reaction removal
This commit is contained in:
Neil
2026-08-09 23:14:17 -07:00
committed by GitHub
parent d6362deb04
commit 016df33f00
7 changed files with 237 additions and 34 deletions
+54
View File
@@ -4044,6 +4044,60 @@ describe('GitHub GraphQL rate-limit guard', () => {
expect(noteRateLimitSpendMock).not.toHaveBeenCalledWith('graphql')
})
it('maps review summary reaction subjects from GraphQL', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock
.mockResolvedValueOnce({
stdout: JSON.stringify({
data: {
repository: {
pullRequest: {
reviewThreads: { nodes: [] },
comments: { nodes: [] },
reviews: {
nodes: [
{
id: 'PRR_44',
databaseId: 44,
author: {
__typename: 'Bot',
login: 'coderabbitai',
avatarUrl: 'https://avatar'
},
body: 'Automated review summary',
createdAt: '2026-04-01T00:00:00Z',
url: 'https://github.com/acme/widgets/pull/7#pullrequestreview-44',
reactionGroups: [
{
content: 'ROCKET',
viewerHasReacted: true,
reactors: { totalCount: 2 }
}
]
}
]
}
}
}
}
})
})
.mockResolvedValueOnce({ stdout: '[]' })
.mockResolvedValueOnce({ stdout: '[]' })
await expect(getPRComments('/repo-root', 7)).resolves.toEqual([
expect.objectContaining({
id: 44,
reactionSubjectId: 'PRR_44',
isBot: true,
reactions: [{ content: 'rocket', count: 2, viewerHasReacted: true }]
})
])
expect(ghExecFileAsyncMock.mock.calls[0]?.[0]).toEqual(
expect.arrayContaining([expect.stringContaining('reviews(first: 100)')])
)
})
it('uses explicit PR repo for comments when a fork PR is discovered', async () => {
rateLimitGuardMock.mockImplementation(((bucket: string) =>
bucket === 'graphql'
+37 -1
View File
@@ -4157,6 +4157,23 @@ query($owner: String!, $repo: String!, $pr: Int!) {
}
}
}
reviews(first: 100) {
nodes {
id
databaseId
author { __typename login avatarUrl(size: 48) }
body
createdAt
url
reactionGroups {
content
viewerHasReacted
reactors {
totalCount
}
}
}
}
}
}
}`
@@ -4284,6 +4301,7 @@ export async function getPRComments(
url: string
reactionGroups?: GitHubGraphQLReactionGroup[] | null
}
let graphQLReviewSummaries: PRComment[] | undefined
const reviewComments: PRComment[] = []
if (threadsResult.status === 'fulfilled' && threadsResult.value) {
const threadsData = JSON.parse(threadsResult.value.stdout) as {
@@ -4292,6 +4310,7 @@ export async function getPRComments(
pullRequest: {
reviewThreads: { nodes: GQLThread[] }
comments?: { nodes: GQLIssueComment[] }
reviews?: { nodes: GQLIssueComment[] }
}
}
}
@@ -4313,6 +4332,21 @@ export async function getPRComments(
if (graphQLIssueComments.length > 0) {
issueComments = graphQLIssueComments
}
graphQLReviewSummaries = (pullRequest.reviews?.nodes ?? [])
.filter((review) => review.body?.trim())
.map(
(review): PRComment => ({
id: review.databaseId,
author: review.author?.login ?? 'ghost',
authorAvatarUrl: review.author?.avatarUrl ?? '',
body: review.body,
createdAt: review.createdAt,
url: review.url,
isBot: review.author?.__typename === 'Bot',
reactionSubjectId: review.id,
reactions: mapGraphQLReactionGroups(review.reactionGroups)
})
)
const threads = pullRequest.reviewThreads.nodes
for (const thread of threads) {
@@ -4353,7 +4387,9 @@ export async function getPRComments(
html_url: string
}
let reviewSummaries: PRComment[] = []
if (reviewsResult.status === 'fulfilled') {
if (graphQLReviewSummaries) {
reviewSummaries = graphQLReviewSummaries
} else if (reviewsResult.status === 'fulfilled') {
reviewSummaries = (JSON.parse(reviewsResult.value.stdout) as RESTReview[])
.filter((r) => r.body?.trim())
.map(