Switch back to grpahql for review threads
This commit is contained in:
committed by
pullfrog[bot]
parent
a5fffc97a5
commit
d98f6c8029
@@ -121223,6 +121223,67 @@ function CreatePullRequestReviewTool(ctx) {
|
||||
// mcp/reviewComments.ts
|
||||
import { writeFileSync as writeFileSync3 } from "node:fs";
|
||||
import { join as join5 } from "node:path";
|
||||
var REVIEW_THREADS_QUERY = `
|
||||
query ($owner: String!, $repo: String!, $pullNumber: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $pullNumber) {
|
||||
reviewThreads(first: 100) {
|
||||
nodes {
|
||||
diffSide
|
||||
startDiffSide
|
||||
comments(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
databaseId
|
||||
body
|
||||
path
|
||||
line
|
||||
startLine
|
||||
url
|
||||
author {
|
||||
login
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
pullRequestReview {
|
||||
databaseId
|
||||
}
|
||||
replyTo {
|
||||
databaseId
|
||||
}
|
||||
reactionGroups {
|
||||
content
|
||||
reactors(first: 10) {
|
||||
nodes {
|
||||
... on Actor {
|
||||
login
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
var MAX_BODY_PREVIEW = 80;
|
||||
function truncateBody(body) {
|
||||
const oneLine = body.replace(/\n/g, " ").trim();
|
||||
if (oneLine.length <= MAX_BODY_PREVIEW) return oneLine;
|
||||
return oneLine.slice(0, MAX_BODY_PREVIEW - 3) + "...";
|
||||
}
|
||||
function escapeXml(text) {
|
||||
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||
}
|
||||
function hasThumbsUpFrom(comment, username) {
|
||||
if (!comment.reactionGroups) return false;
|
||||
const thumbsUp = comment.reactionGroups.find((g) => g.content === "THUMBS_UP");
|
||||
if (!thumbsUp?.reactors?.nodes) return false;
|
||||
return thumbsUp.reactors.nodes.some((r) => r?.login === username);
|
||||
}
|
||||
var GetReviewComments = type({
|
||||
pull_number: type.number.describe("The pull request number"),
|
||||
review_id: type.number.describe("The review ID to get comments for"),
|
||||
@@ -121231,71 +121292,112 @@ var GetReviewComments = type({
|
||||
function GetReviewCommentsTool(ctx) {
|
||||
return tool({
|
||||
name: "get_review_comments",
|
||||
description: "Get review comments for a pull request review, including diff context. When approved_by is provided, only returns comments that user approved with \u{1F44D}. Returns commentsPath pointing to a file with full comment details.",
|
||||
description: "Get review comments for a pull request review, including thread context. When approved_by is provided, only returns comments that user approved with \u{1F44D}. Returns commentsPath pointing to a file with full comment details in XML format.",
|
||||
parameters: GetReviewComments,
|
||||
execute: execute(async ({ pull_number, review_id, approved_by }) => {
|
||||
const allComments = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviewComments, {
|
||||
const review = await ctx.octokit.rest.pulls.getReview({
|
||||
owner: ctx.repo.owner,
|
||||
repo: ctx.repo.name,
|
||||
pull_number
|
||||
pull_number,
|
||||
review_id
|
||||
});
|
||||
let reviewComments = allComments.filter((c) => c.pull_request_review_id === review_id);
|
||||
if (approved_by) {
|
||||
const approvedIds = /* @__PURE__ */ new Set();
|
||||
for (const comment of reviewComments) {
|
||||
const reactions = await ctx.octokit.rest.reactions.listForPullRequestReviewComment({
|
||||
owner: ctx.repo.owner,
|
||||
repo: ctx.repo.name,
|
||||
comment_id: comment.id
|
||||
});
|
||||
const hasThumbsUp = reactions.data.some(
|
||||
(r) => r.content === "+1" && r.user?.login === approved_by
|
||||
);
|
||||
if (hasThumbsUp) approvedIds.add(comment.id);
|
||||
}
|
||||
reviewComments = reviewComments.filter((c) => approvedIds.has(c.id));
|
||||
}
|
||||
if (reviewComments.length === 0) {
|
||||
const reviewer = review.data.user?.login ?? "unknown";
|
||||
const response = await ctx.octokit.graphql(REVIEW_THREADS_QUERY, {
|
||||
owner: ctx.repo.owner,
|
||||
repo: ctx.repo.name,
|
||||
pullNumber: pull_number
|
||||
});
|
||||
const pullRequest = response.repository?.pullRequest;
|
||||
if (!pullRequest?.reviewThreads?.nodes) {
|
||||
return {
|
||||
review_id,
|
||||
pull_number,
|
||||
reviewer,
|
||||
count: 0,
|
||||
commentsPath: null,
|
||||
message: "No review threads found"
|
||||
};
|
||||
}
|
||||
const leafComments = [];
|
||||
for (const thread of pullRequest.reviewThreads.nodes) {
|
||||
if (!thread?.comments?.nodes) continue;
|
||||
const threadComments = thread.comments.nodes.filter(
|
||||
(c) => c !== null
|
||||
);
|
||||
if (threadComments.length === 0) continue;
|
||||
for (const comment of threadComments) {
|
||||
if (comment.pullRequestReview?.databaseId !== review_id) continue;
|
||||
if (approved_by && !hasThumbsUpFrom(comment, approved_by)) continue;
|
||||
const threadContext = threadComments.filter(
|
||||
(c) => c.createdAt < comment.createdAt || c.createdAt === comment.createdAt && c.databaseId < comment.databaseId
|
||||
);
|
||||
leafComments.push({
|
||||
comment,
|
||||
thread: threadContext,
|
||||
side: thread.diffSide
|
||||
});
|
||||
}
|
||||
}
|
||||
if (leafComments.length === 0) {
|
||||
return {
|
||||
review_id,
|
||||
pull_number,
|
||||
reviewer,
|
||||
count: 0,
|
||||
commentsPath: null,
|
||||
message: approved_by ? `No comments with \u{1F44D} from ${approved_by}` : "No comments found for this review"
|
||||
};
|
||||
}
|
||||
const lines = [];
|
||||
for (const comment of reviewComments) {
|
||||
lines.push(`${"=".repeat(60)}`);
|
||||
lines.push(`COMMENT #${comment.id} by @${comment.user?.login ?? "unknown"}`);
|
||||
lines.push(`File: ${comment.path}:${comment.line ?? comment.original_line ?? "?"}`);
|
||||
if (comment.in_reply_to_id) {
|
||||
lines.push(`Reply to: #${comment.in_reply_to_id}`);
|
||||
lines.push(
|
||||
`<review_comments count="${leafComments.length}" reviewer="${escapeXml(reviewer)}">`
|
||||
);
|
||||
lines.push("");
|
||||
lines.push("<summary>");
|
||||
for (const leaf of leafComments) {
|
||||
const line = leaf.comment.line ?? leaf.comment.startLine ?? 0;
|
||||
const preview = escapeXml(truncateBody(leaf.comment.body));
|
||||
lines.push(
|
||||
` <comment id="${leaf.comment.databaseId}" file="${escapeXml(leaf.comment.path)}" line="${line}">${preview}</comment>`
|
||||
);
|
||||
}
|
||||
lines.push("</summary>");
|
||||
lines.push("");
|
||||
for (const leaf of leafComments) {
|
||||
const line = leaf.comment.line ?? leaf.comment.startLine ?? 0;
|
||||
const author = leaf.comment.author?.login ?? "unknown";
|
||||
lines.push(
|
||||
`<comment id="${leaf.comment.databaseId}" file="${escapeXml(leaf.comment.path)}" line="${line}" author="${escapeXml(author)}">`
|
||||
);
|
||||
if (leaf.thread.length > 0) {
|
||||
lines.push(" <thread>");
|
||||
for (const msg of leaf.thread) {
|
||||
const msgAuthor = msg.author?.login ?? "unknown";
|
||||
lines.push(
|
||||
` <message id="${msg.databaseId}" author="${escapeXml(msgAuthor)}">${escapeXml(msg.body)}</message>`
|
||||
);
|
||||
}
|
||||
lines.push(" </thread>");
|
||||
}
|
||||
lines.push("");
|
||||
if (comment.diff_hunk) {
|
||||
lines.push("```diff");
|
||||
lines.push(comment.diff_hunk);
|
||||
lines.push("```");
|
||||
lines.push("");
|
||||
}
|
||||
lines.push("Comment:");
|
||||
lines.push(comment.body);
|
||||
lines.push(` <body>${escapeXml(leaf.comment.body)}</body>`);
|
||||
lines.push("</comment>");
|
||||
lines.push("");
|
||||
}
|
||||
lines.push("</review_comments>");
|
||||
const content = lines.join("\n");
|
||||
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
||||
if (!tempDir) {
|
||||
throw new Error("PULLFROG_TEMP_DIR not set");
|
||||
}
|
||||
const filename = approved_by ? `review-${review_id}-approved-by-${approved_by}.txt` : `review-${review_id}-comments.txt`;
|
||||
const filename = approved_by ? `review-${review_id}-approved-by-${approved_by}.xml` : `review-${review_id}-comments.xml`;
|
||||
const commentsPath = join5(tempDir, filename);
|
||||
writeFileSync3(commentsPath, content);
|
||||
log.debug(`wrote ${reviewComments.length} comments to ${commentsPath}`);
|
||||
log.debug(`wrote ${leafComments.length} comments to ${commentsPath}`);
|
||||
return {
|
||||
review_id,
|
||||
pull_number,
|
||||
count: reviewComments.length,
|
||||
reviewer,
|
||||
count: leafComments.length,
|
||||
commentsPath
|
||||
};
|
||||
})
|
||||
@@ -121534,7 +121636,8 @@ function computeModes() {
|
||||
|
||||
3. Fetch review comments using ${ghPullfrogMcpName}/get_review_comments with \`pull_number\` and \`review_id\` from EVENT DATA. This returns \`commentsPath\` - read that file for full comment details with diff context. If EVENT DATA contains a \`triggerer\` field (indicating who requested fixes), you can pass \`approved_by\` to filter to only comments they approved with \u{1F44D}.
|
||||
|
||||
4. Review the feedback provided. Understand each review comment and what changes are being requested.
|
||||
4. Review the feedback provided. Understand each review comment and what changes are being requested.
|
||||
- **If there are no actionable comments** (e.g., the review is an approval with no specific feedback to address), do NOT post a progress comment. Simply exit without taking action.
|
||||
|
||||
5. If the request requires understanding the codebase structure or conventions, gather relevant context. Read AGENTS.md if it exists.
|
||||
|
||||
@@ -139997,12 +140100,18 @@ ${ctx.error}` : ctx.error;
|
||||
}
|
||||
const repoContext = parseRepoContext();
|
||||
const octokit = createOctokit(getGitHubInstallationToken());
|
||||
const runId = process.env.GITHUB_RUN_ID;
|
||||
const footer = buildPullfrogFooter({
|
||||
triggeredBy: true,
|
||||
workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : void 0
|
||||
});
|
||||
await octokit.rest.issues.updateComment({
|
||||
owner: repoContext.owner,
|
||||
repo: repoContext.name,
|
||||
comment_id: commentId,
|
||||
body: formattedError
|
||||
body: `${formattedError}${footer}`
|
||||
});
|
||||
ctx.toolState.progressComment.wasUpdated = true;
|
||||
}
|
||||
|
||||
// utils/instructions.ts
|
||||
|
||||
+36
-6
@@ -30,20 +30,50 @@ await mcp.call("gh_pullfrog/get_check_suite_logs", {
|
||||
### review tools
|
||||
|
||||
#### `get_review_comments`
|
||||
get all line-by-line comments and their replies for a specific pull request review.
|
||||
get all line-by-line comments for a specific pull request review, including full thread context for replies.
|
||||
|
||||
**parameters:**
|
||||
- `pull_number` (number): the pull request number
|
||||
- `review_id` (number): the id from review.id in the webhook payload
|
||||
- `approved_by` (string, optional): only return comments this user gave a 👍 to
|
||||
|
||||
**replaces:** `gh api repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/comments`
|
||||
|
||||
**returns:**
|
||||
array of review comments including threaded replies:
|
||||
- file path, line number, comment body
|
||||
- side (LEFT/RIGHT) and position in diff
|
||||
- user, timestamps, html_url
|
||||
- in_reply_to_id for threaded comments (replies have this set to the parent comment id)
|
||||
- `commentsPath`: path to XML file with full comment details
|
||||
- `reviewer`: github username of the review author
|
||||
- `count`: number of comments to address
|
||||
|
||||
**output format (XML):**
|
||||
```xml
|
||||
<review_comments count="2" reviewer="colinmcd94">
|
||||
|
||||
<summary>
|
||||
<comment id="67890" file="src/utils/auth.ts" line="42">Actually, can you use a type guard...</comment>
|
||||
<comment id="67891" file="src/api/handler.ts" line="15">This should handle the error case</comment>
|
||||
</summary>
|
||||
|
||||
<comment id="67890" file="src/utils/auth.ts" line="42" author="colinmcd94">
|
||||
<thread>
|
||||
<message id="12345" author="colinmcd94">Please add null checking here</message>
|
||||
<message id="23456" author="octocat">What about using optional chaining?</message>
|
||||
</thread>
|
||||
<diff>
|
||||
@@ -40,7 +40,7 @@
|
||||
const user = getUser(id);
|
||||
- return user.name;
|
||||
+ return user?.name;
|
||||
</diff>
|
||||
<body>Actually, can you use a type guard instead?</body>
|
||||
</comment>
|
||||
|
||||
</review_comments>
|
||||
```
|
||||
|
||||
- `<summary>` lists all comments to address with truncated preview
|
||||
- `<thread>` shows parent comments (when replying to existing thread)
|
||||
- `<diff>` contains the diff hunk around the commented line
|
||||
- `<body>` is the actual comment text to address
|
||||
|
||||
**example:**
|
||||
```typescript
|
||||
|
||||
+230
-45
@@ -5,6 +5,124 @@ import { log } from "../utils/log.ts";
|
||||
import type { ToolContext } from "./server.ts";
|
||||
import { execute, tool } from "./shared.ts";
|
||||
|
||||
// graphql query to fetch all review threads with comments, replies, and reactions
|
||||
const REVIEW_THREADS_QUERY = `
|
||||
query ($owner: String!, $repo: String!, $pullNumber: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $pullNumber) {
|
||||
reviewThreads(first: 100) {
|
||||
nodes {
|
||||
diffSide
|
||||
startDiffSide
|
||||
comments(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
databaseId
|
||||
body
|
||||
path
|
||||
line
|
||||
startLine
|
||||
url
|
||||
author {
|
||||
login
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
pullRequestReview {
|
||||
databaseId
|
||||
}
|
||||
replyTo {
|
||||
databaseId
|
||||
}
|
||||
reactionGroups {
|
||||
content
|
||||
reactors(first: 10) {
|
||||
nodes {
|
||||
... on Actor {
|
||||
login
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type GraphQLReviewComment = {
|
||||
id: string;
|
||||
databaseId: number;
|
||||
body: string;
|
||||
path: string;
|
||||
line: number | null;
|
||||
startLine: number | null;
|
||||
url: string;
|
||||
author: {
|
||||
login: string;
|
||||
} | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
pullRequestReview: {
|
||||
databaseId: number;
|
||||
} | null;
|
||||
replyTo: {
|
||||
databaseId: number;
|
||||
} | null;
|
||||
reactionGroups:
|
||||
| {
|
||||
content: string;
|
||||
reactors: {
|
||||
nodes: ({ login: string } | null)[] | null;
|
||||
};
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
|
||||
type GraphQLReviewThread = {
|
||||
diffSide: "LEFT" | "RIGHT";
|
||||
startDiffSide: "LEFT" | "RIGHT" | null;
|
||||
comments: {
|
||||
nodes: (GraphQLReviewComment | null)[] | null;
|
||||
} | null;
|
||||
} | null;
|
||||
|
||||
type GraphQLResponse = {
|
||||
repository: {
|
||||
pullRequest: {
|
||||
reviewThreads: {
|
||||
nodes: (GraphQLReviewThread | null)[] | null;
|
||||
} | null;
|
||||
} | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
const MAX_BODY_PREVIEW = 80;
|
||||
|
||||
function truncateBody(body: string): string {
|
||||
const oneLine = body.replace(/\n/g, " ").trim();
|
||||
if (oneLine.length <= MAX_BODY_PREVIEW) return oneLine;
|
||||
return oneLine.slice(0, MAX_BODY_PREVIEW - 3) + "...";
|
||||
}
|
||||
|
||||
function escapeXml(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function hasThumbsUpFrom(comment: GraphQLReviewComment, username: string): boolean {
|
||||
if (!comment.reactionGroups) return false;
|
||||
const thumbsUp = comment.reactionGroups.find((g) => g.content === "THUMBS_UP");
|
||||
if (!thumbsUp?.reactors?.nodes) return false;
|
||||
return thumbsUp.reactors.nodes.some((r) => r?.login === username);
|
||||
}
|
||||
|
||||
export const GetReviewComments = type({
|
||||
pull_number: type.number.describe("The pull request number"),
|
||||
review_id: type.number.describe("The review ID to get comments for"),
|
||||
@@ -17,42 +135,82 @@ export function GetReviewCommentsTool(ctx: ToolContext) {
|
||||
return tool({
|
||||
name: "get_review_comments",
|
||||
description:
|
||||
"Get review comments for a pull request review, including diff context. " +
|
||||
"Get review comments for a pull request review, including thread context. " +
|
||||
"When approved_by is provided, only returns comments that user approved with 👍. " +
|
||||
"Returns commentsPath pointing to a file with full comment details.",
|
||||
"Returns commentsPath pointing to a file with full comment details in XML format.",
|
||||
parameters: GetReviewComments,
|
||||
execute: execute(async ({ pull_number, review_id, approved_by }) => {
|
||||
// fetch all review comments via REST API (includes diff_hunk)
|
||||
const allComments = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviewComments, {
|
||||
// fetch the review to get reviewer username
|
||||
const review = await ctx.octokit.rest.pulls.getReview({
|
||||
owner: ctx.repo.owner,
|
||||
repo: ctx.repo.name,
|
||||
pull_number,
|
||||
review_id,
|
||||
});
|
||||
const reviewer = review.data.user?.login ?? "unknown";
|
||||
|
||||
// fetch all review threads using graphql (single API call)
|
||||
const response = await ctx.octokit.graphql<GraphQLResponse>(REVIEW_THREADS_QUERY, {
|
||||
owner: ctx.repo.owner,
|
||||
repo: ctx.repo.name,
|
||||
pullNumber: pull_number,
|
||||
});
|
||||
|
||||
// filter to target review
|
||||
let reviewComments = allComments.filter((c) => c.pull_request_review_id === review_id);
|
||||
|
||||
// filter by thumbs up if approved_by is specified
|
||||
if (approved_by) {
|
||||
const approvedIds = new Set<number>();
|
||||
for (const comment of reviewComments) {
|
||||
const reactions = await ctx.octokit.rest.reactions.listForPullRequestReviewComment({
|
||||
owner: ctx.repo.owner,
|
||||
repo: ctx.repo.name,
|
||||
comment_id: comment.id,
|
||||
});
|
||||
const hasThumbsUp = reactions.data.some(
|
||||
(r) => r.content === "+1" && r.user?.login === approved_by
|
||||
);
|
||||
if (hasThumbsUp) approvedIds.add(comment.id);
|
||||
}
|
||||
reviewComments = reviewComments.filter((c) => approvedIds.has(c.id));
|
||||
}
|
||||
|
||||
if (reviewComments.length === 0) {
|
||||
const pullRequest = response.repository?.pullRequest;
|
||||
if (!pullRequest?.reviewThreads?.nodes) {
|
||||
return {
|
||||
review_id,
|
||||
pull_number,
|
||||
reviewer,
|
||||
count: 0,
|
||||
commentsPath: null,
|
||||
message: "No review threads found",
|
||||
};
|
||||
}
|
||||
|
||||
// collect leaf comments (from target review) with their thread context
|
||||
type LeafComment = {
|
||||
comment: GraphQLReviewComment;
|
||||
thread: GraphQLReviewComment[]; // parent comments in order (oldest first)
|
||||
side: "LEFT" | "RIGHT";
|
||||
};
|
||||
const leafComments: LeafComment[] = [];
|
||||
|
||||
for (const thread of pullRequest.reviewThreads.nodes) {
|
||||
if (!thread?.comments?.nodes) continue;
|
||||
|
||||
const threadComments = thread.comments.nodes.filter(
|
||||
(c): c is GraphQLReviewComment => c !== null
|
||||
);
|
||||
if (threadComments.length === 0) continue;
|
||||
|
||||
// find comments from the target review (these are the "leaf" comments to address)
|
||||
for (const comment of threadComments) {
|
||||
if (comment.pullRequestReview?.databaseId !== review_id) continue;
|
||||
|
||||
// filter by approved_by if specified
|
||||
if (approved_by && !hasThumbsUpFrom(comment, approved_by)) continue;
|
||||
|
||||
// get thread context (all comments before this one in the thread)
|
||||
const threadContext = threadComments.filter(
|
||||
(c) =>
|
||||
c.createdAt < comment.createdAt ||
|
||||
(c.createdAt === comment.createdAt && c.databaseId < comment.databaseId)
|
||||
);
|
||||
|
||||
leafComments.push({
|
||||
comment,
|
||||
thread: threadContext,
|
||||
side: thread.diffSide,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (leafComments.length === 0) {
|
||||
return {
|
||||
review_id,
|
||||
pull_number,
|
||||
reviewer,
|
||||
count: 0,
|
||||
commentsPath: null,
|
||||
message: approved_by
|
||||
@@ -61,27 +219,53 @@ export function GetReviewCommentsTool(ctx: ToolContext) {
|
||||
};
|
||||
}
|
||||
|
||||
// format comments with diff context
|
||||
// build XML output
|
||||
const lines: string[] = [];
|
||||
for (const comment of reviewComments) {
|
||||
lines.push(`${"=".repeat(60)}`);
|
||||
lines.push(`COMMENT #${comment.id} by @${comment.user?.login ?? "unknown"}`);
|
||||
lines.push(`File: ${comment.path}:${comment.line ?? comment.original_line ?? "?"}`);
|
||||
if (comment.in_reply_to_id) {
|
||||
lines.push(`Reply to: #${comment.in_reply_to_id}`);
|
||||
lines.push(
|
||||
`<review_comments count="${leafComments.length}" reviewer="${escapeXml(reviewer)}">`
|
||||
);
|
||||
lines.push("");
|
||||
|
||||
// summary section
|
||||
lines.push("<summary>");
|
||||
for (const leaf of leafComments) {
|
||||
const line = leaf.comment.line ?? leaf.comment.startLine ?? 0;
|
||||
const preview = escapeXml(truncateBody(leaf.comment.body));
|
||||
lines.push(
|
||||
` <comment id="${leaf.comment.databaseId}" file="${escapeXml(leaf.comment.path)}" line="${line}">${preview}</comment>`
|
||||
);
|
||||
}
|
||||
lines.push("</summary>");
|
||||
lines.push("");
|
||||
|
||||
// detailed comments with thread context
|
||||
for (const leaf of leafComments) {
|
||||
const line = leaf.comment.line ?? leaf.comment.startLine ?? 0;
|
||||
const author = leaf.comment.author?.login ?? "unknown";
|
||||
lines.push(
|
||||
`<comment id="${leaf.comment.databaseId}" file="${escapeXml(leaf.comment.path)}" line="${line}" author="${escapeXml(author)}">`
|
||||
);
|
||||
|
||||
// thread history (parent comments)
|
||||
if (leaf.thread.length > 0) {
|
||||
lines.push(" <thread>");
|
||||
for (const msg of leaf.thread) {
|
||||
const msgAuthor = msg.author?.login ?? "unknown";
|
||||
lines.push(
|
||||
` <message id="${msg.databaseId}" author="${escapeXml(msgAuthor)}">${escapeXml(msg.body)}</message>`
|
||||
);
|
||||
}
|
||||
lines.push(" </thread>");
|
||||
}
|
||||
lines.push("");
|
||||
if (comment.diff_hunk) {
|
||||
lines.push("```diff");
|
||||
lines.push(comment.diff_hunk);
|
||||
lines.push("```");
|
||||
lines.push("");
|
||||
}
|
||||
lines.push("Comment:");
|
||||
lines.push(comment.body);
|
||||
|
||||
// the actual comment body to address
|
||||
lines.push(` <body>${escapeXml(leaf.comment.body)}</body>`);
|
||||
lines.push("</comment>");
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
lines.push("</review_comments>");
|
||||
|
||||
const content = lines.join("\n");
|
||||
|
||||
// write to temp file
|
||||
@@ -90,16 +274,17 @@ export function GetReviewCommentsTool(ctx: ToolContext) {
|
||||
throw new Error("PULLFROG_TEMP_DIR not set");
|
||||
}
|
||||
const filename = approved_by
|
||||
? `review-${review_id}-approved-by-${approved_by}.txt`
|
||||
: `review-${review_id}-comments.txt`;
|
||||
? `review-${review_id}-approved-by-${approved_by}.xml`
|
||||
: `review-${review_id}-comments.xml`;
|
||||
const commentsPath = join(tempDir, filename);
|
||||
writeFileSync(commentsPath, content);
|
||||
log.debug(`wrote ${reviewComments.length} comments to ${commentsPath}`);
|
||||
log.debug(`wrote ${leafComments.length} comments to ${commentsPath}`);
|
||||
|
||||
return {
|
||||
review_id,
|
||||
pull_number,
|
||||
count: reviewComments.length,
|
||||
reviewer,
|
||||
count: leafComments.length,
|
||||
commentsPath,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -75,7 +75,8 @@ export function computeModes(): Mode[] {
|
||||
|
||||
3. Fetch review comments using ${ghPullfrogMcpName}/get_review_comments with \`pull_number\` and \`review_id\` from EVENT DATA. This returns \`commentsPath\` - read that file for full comment details with diff context. If EVENT DATA contains a \`triggerer\` field (indicating who requested fixes), you can pass \`approved_by\` to filter to only comments they approved with 👍.
|
||||
|
||||
4. Review the feedback provided. Understand each review comment and what changes are being requested.
|
||||
4. Review the feedback provided. Understand each review comment and what changes are being requested.
|
||||
- **If there are no actionable comments** (e.g., the review is an approval with no specific feedback to address), do NOT post a progress comment. Simply exit without taking action.
|
||||
|
||||
5. If the request requires understanding the codebase structure or conventions, gather relevant context. Read AGENTS.md if it exists.
|
||||
|
||||
|
||||
+14
-1
@@ -1,4 +1,5 @@
|
||||
import type { ToolState } from "../mcp/server.ts";
|
||||
import { buildPullfrogFooter } from "./buildPullfrogFooter.ts";
|
||||
import { createOctokit, parseRepoContext } from "./github.ts";
|
||||
import { getGitHubInstallationToken } from "./token.ts";
|
||||
|
||||
@@ -18,11 +19,23 @@ export async function reportErrorToComment(ctx: ReportErrorParams): Promise<void
|
||||
|
||||
const repoContext = parseRepoContext();
|
||||
const octokit = createOctokit(getGitHubInstallationToken());
|
||||
const runId = process.env.GITHUB_RUN_ID;
|
||||
|
||||
// build footer with workflow run link
|
||||
const footer = buildPullfrogFooter({
|
||||
triggeredBy: true,
|
||||
workflowRun: runId
|
||||
? { owner: repoContext.owner, repo: repoContext.name, runId }
|
||||
: undefined,
|
||||
});
|
||||
|
||||
await octokit.rest.issues.updateComment({
|
||||
owner: repoContext.owner,
|
||||
repo: repoContext.name,
|
||||
comment_id: commentId,
|
||||
body: formattedError,
|
||||
body: `${formattedError}${footer}`,
|
||||
});
|
||||
|
||||
// mark as updated so ensureProgressCommentUpdated doesn't try to update again
|
||||
ctx.toolState.progressComment.wasUpdated = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user