Switch back to grpahql for review threads

This commit is contained in:
Colin McDonnell
2026-01-20 23:59:52 +00:00
committed by pullfrog[bot]
parent a5fffc97a5
commit d98f6c8029
5 changed files with 431 additions and 93 deletions
+149 -40
View File
@@ -121223,6 +121223,67 @@ function CreatePullRequestReviewTool(ctx) {
// mcp/reviewComments.ts // mcp/reviewComments.ts
import { writeFileSync as writeFileSync3 } from "node:fs"; import { writeFileSync as writeFileSync3 } from "node:fs";
import { join as join5 } from "node:path"; 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
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({ var GetReviewComments = type({
pull_number: type.number.describe("The pull request number"), pull_number: type.number.describe("The pull request number"),
review_id: type.number.describe("The review ID to get comments for"), review_id: type.number.describe("The review ID to get comments for"),
@@ -121231,71 +121292,112 @@ var GetReviewComments = type({
function GetReviewCommentsTool(ctx) { function GetReviewCommentsTool(ctx) {
return tool({ return tool({
name: "get_review_comments", 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, parameters: GetReviewComments,
execute: execute(async ({ pull_number, review_id, approved_by }) => { 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, owner: ctx.repo.owner,
repo: ctx.repo.name, repo: ctx.repo.name,
pull_number pull_number,
review_id
}); });
let reviewComments = allComments.filter((c) => c.pull_request_review_id === review_id); const reviewer = review.data.user?.login ?? "unknown";
if (approved_by) { const response = await ctx.octokit.graphql(REVIEW_THREADS_QUERY, {
const approvedIds = /* @__PURE__ */ new Set(); owner: ctx.repo.owner,
for (const comment of reviewComments) { repo: ctx.repo.name,
const reactions = await ctx.octokit.rest.reactions.listForPullRequestReviewComment({ pullNumber: pull_number
owner: ctx.repo.owner, });
repo: ctx.repo.name, const pullRequest = response.repository?.pullRequest;
comment_id: comment.id if (!pullRequest?.reviewThreads?.nodes) {
});
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) {
return { return {
review_id, review_id,
pull_number, 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, count: 0,
commentsPath: null, commentsPath: null,
message: approved_by ? `No comments with \u{1F44D} from ${approved_by}` : "No comments found for this review" message: approved_by ? `No comments with \u{1F44D} from ${approved_by}` : "No comments found for this review"
}; };
} }
const lines = []; const lines = [];
for (const comment of reviewComments) { lines.push(
lines.push(`${"=".repeat(60)}`); `<review_comments count="${leafComments.length}" reviewer="${escapeXml(reviewer)}">`
lines.push(`COMMENT #${comment.id} by @${comment.user?.login ?? "unknown"}`); );
lines.push(`File: ${comment.path}:${comment.line ?? comment.original_line ?? "?"}`); lines.push("");
if (comment.in_reply_to_id) { lines.push("<summary>");
lines.push(`Reply to: #${comment.in_reply_to_id}`); 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(""); lines.push(` <body>${escapeXml(leaf.comment.body)}</body>`);
if (comment.diff_hunk) { lines.push("</comment>");
lines.push("```diff");
lines.push(comment.diff_hunk);
lines.push("```");
lines.push("");
}
lines.push("Comment:");
lines.push(comment.body);
lines.push(""); lines.push("");
} }
lines.push("</review_comments>");
const content = lines.join("\n"); const content = lines.join("\n");
const tempDir = process.env.PULLFROG_TEMP_DIR; const tempDir = process.env.PULLFROG_TEMP_DIR;
if (!tempDir) { if (!tempDir) {
throw new Error("PULLFROG_TEMP_DIR not set"); 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); const commentsPath = join5(tempDir, filename);
writeFileSync3(commentsPath, content); writeFileSync3(commentsPath, content);
log.debug(`wrote ${reviewComments.length} comments to ${commentsPath}`); log.debug(`wrote ${leafComments.length} comments to ${commentsPath}`);
return { return {
review_id, review_id,
pull_number, pull_number,
count: reviewComments.length, reviewer,
count: leafComments.length,
commentsPath 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}. 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. 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 repoContext = parseRepoContext();
const octokit = createOctokit(getGitHubInstallationToken()); 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({ await octokit.rest.issues.updateComment({
owner: repoContext.owner, owner: repoContext.owner,
repo: repoContext.name, repo: repoContext.name,
comment_id: commentId, comment_id: commentId,
body: formattedError body: `${formattedError}${footer}`
}); });
ctx.toolState.progressComment.wasUpdated = true;
} }
// utils/instructions.ts // utils/instructions.ts
+36 -6
View File
@@ -30,20 +30,50 @@ await mcp.call("gh_pullfrog/get_check_suite_logs", {
### review tools ### review tools
#### `get_review_comments` #### `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:** **parameters:**
- `pull_number` (number): the pull request number - `pull_number` (number): the pull request number
- `review_id` (number): the id from review.id in the webhook payload - `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` **replaces:** `gh api repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/comments`
**returns:** **returns:**
array of review comments including threaded replies: - `commentsPath`: path to XML file with full comment details
- file path, line number, comment body - `reviewer`: github username of the review author
- side (LEFT/RIGHT) and position in diff - `count`: number of comments to address
- user, timestamps, html_url
- in_reply_to_id for threaded comments (replies have this set to the parent comment id) **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:** **example:**
```typescript ```typescript
+230 -45
View File
@@ -5,6 +5,124 @@ import { log } from "../utils/log.ts";
import type { ToolContext } from "./server.ts"; import type { ToolContext } from "./server.ts";
import { execute, tool } from "./shared.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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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({ export const GetReviewComments = type({
pull_number: type.number.describe("The pull request number"), pull_number: type.number.describe("The pull request number"),
review_id: type.number.describe("The review ID to get comments for"), review_id: type.number.describe("The review ID to get comments for"),
@@ -17,42 +135,82 @@ export function GetReviewCommentsTool(ctx: ToolContext) {
return tool({ return tool({
name: "get_review_comments", name: "get_review_comments",
description: 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 👍. " + "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, parameters: GetReviewComments,
execute: execute(async ({ pull_number, review_id, approved_by }) => { execute: execute(async ({ pull_number, review_id, approved_by }) => {
// fetch all review comments via REST API (includes diff_hunk) // fetch the review to get reviewer username
const allComments = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviewComments, { const review = await ctx.octokit.rest.pulls.getReview({
owner: ctx.repo.owner, owner: ctx.repo.owner,
repo: ctx.repo.name, repo: ctx.repo.name,
pull_number, 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 const pullRequest = response.repository?.pullRequest;
let reviewComments = allComments.filter((c) => c.pull_request_review_id === review_id); if (!pullRequest?.reviewThreads?.nodes) {
// 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) {
return { return {
review_id, review_id,
pull_number, 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, count: 0,
commentsPath: null, commentsPath: null,
message: approved_by message: approved_by
@@ -61,27 +219,53 @@ export function GetReviewCommentsTool(ctx: ToolContext) {
}; };
} }
// format comments with diff context // build XML output
const lines: string[] = []; const lines: string[] = [];
for (const comment of reviewComments) { lines.push(
lines.push(`${"=".repeat(60)}`); `<review_comments count="${leafComments.length}" reviewer="${escapeXml(reviewer)}">`
lines.push(`COMMENT #${comment.id} by @${comment.user?.login ?? "unknown"}`); );
lines.push(`File: ${comment.path}:${comment.line ?? comment.original_line ?? "?"}`); lines.push("");
if (comment.in_reply_to_id) {
lines.push(`Reply to: #${comment.in_reply_to_id}`); // 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) { // the actual comment body to address
lines.push("```diff"); lines.push(` <body>${escapeXml(leaf.comment.body)}</body>`);
lines.push(comment.diff_hunk); lines.push("</comment>");
lines.push("```");
lines.push("");
}
lines.push("Comment:");
lines.push(comment.body);
lines.push(""); lines.push("");
} }
lines.push("</review_comments>");
const content = lines.join("\n"); const content = lines.join("\n");
// write to temp file // write to temp file
@@ -90,16 +274,17 @@ export function GetReviewCommentsTool(ctx: ToolContext) {
throw new Error("PULLFROG_TEMP_DIR not set"); throw new Error("PULLFROG_TEMP_DIR not set");
} }
const filename = approved_by const filename = approved_by
? `review-${review_id}-approved-by-${approved_by}.txt` ? `review-${review_id}-approved-by-${approved_by}.xml`
: `review-${review_id}-comments.txt`; : `review-${review_id}-comments.xml`;
const commentsPath = join(tempDir, filename); const commentsPath = join(tempDir, filename);
writeFileSync(commentsPath, content); writeFileSync(commentsPath, content);
log.debug(`wrote ${reviewComments.length} comments to ${commentsPath}`); log.debug(`wrote ${leafComments.length} comments to ${commentsPath}`);
return { return {
review_id, review_id,
pull_number, pull_number,
count: reviewComments.length, reviewer,
count: leafComments.length,
commentsPath, commentsPath,
}; };
}), }),
+2 -1
View File
@@ -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 👍. 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. 5. If the request requires understanding the codebase structure or conventions, gather relevant context. Read AGENTS.md if it exists.
+14 -1
View File
@@ -1,4 +1,5 @@
import type { ToolState } from "../mcp/server.ts"; import type { ToolState } from "../mcp/server.ts";
import { buildPullfrogFooter } from "./buildPullfrogFooter.ts";
import { createOctokit, parseRepoContext } from "./github.ts"; import { createOctokit, parseRepoContext } from "./github.ts";
import { getGitHubInstallationToken } from "./token.ts"; import { getGitHubInstallationToken } from "./token.ts";
@@ -18,11 +19,23 @@ export async function reportErrorToComment(ctx: ReportErrorParams): Promise<void
const repoContext = parseRepoContext(); const repoContext = parseRepoContext();
const octokit = createOctokit(getGitHubInstallationToken()); 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({ await octokit.rest.issues.updateComment({
owner: repoContext.owner, owner: repoContext.owner,
repo: repoContext.name, repo: repoContext.name,
comment_id: commentId, comment_id: commentId,
body: formattedError, body: `${formattedError}${footer}`,
}); });
// mark as updated so ensureProgressCommentUpdated doesn't try to update again
ctx.toolState.progressComment.wasUpdated = true;
} }