Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b038fc574f | |||
| 316b6cb83c | |||
| a19ae49224 |
@@ -39511,7 +39511,7 @@ async function reportProgress({ body }) {
|
|||||||
}
|
}
|
||||||
const issueNumber = ctx.payload.event.issue_number;
|
const issueNumber = ctx.payload.event.issue_number;
|
||||||
if (issueNumber === void 0) {
|
if (issueNumber === void 0) {
|
||||||
throw new Error("cannot create progress comment: no issue_number found in the payload event");
|
return void 0;
|
||||||
}
|
}
|
||||||
const result = await ctx.octokit.rest.issues.createComment({
|
const result = await ctx.octokit.rest.issues.createComment({
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
@@ -39679,6 +39679,12 @@ var init_comment = __esm({
|
|||||||
parameters: ReportProgress,
|
parameters: ReportProgress,
|
||||||
execute: contextualize(async ({ body }) => {
|
execute: contextualize(async ({ body }) => {
|
||||||
const result = await reportProgress({ body });
|
const result = await reportProgress({ body });
|
||||||
|
if (!result) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "cannot create progress comment: no issue_number found in the payload event. this may occur for workflow_dispatch events or when there is no associated issue/PR. if you need to comment on a specific issue or PR, use create_issue_comment with an explicit issueNumber."
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
...result
|
...result
|
||||||
@@ -97443,7 +97449,7 @@ function query({
|
|||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "@pullfrog/action",
|
name: "@pullfrog/action",
|
||||||
version: "0.0.137",
|
version: "0.0.139",
|
||||||
type: "module",
|
type: "module",
|
||||||
files: [
|
files: [
|
||||||
"index.js",
|
"index.js",
|
||||||
@@ -124233,7 +124239,8 @@ function $(cmd, args3, options) {
|
|||||||
const result = spawnSync4(cmd, args3, {
|
const result = spawnSync4(cmd, args3, {
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
encoding,
|
encoding,
|
||||||
cwd: options?.cwd
|
cwd: options?.cwd,
|
||||||
|
env: options?.env ? { ...process.env, ...options.env } : void 0
|
||||||
});
|
});
|
||||||
const stdout = result.stdout ?? "";
|
const stdout = result.stdout ?? "";
|
||||||
const stderr = result.stderr ?? "";
|
const stderr = result.stderr ?? "";
|
||||||
@@ -124718,14 +124725,13 @@ var PullRequestTool = tool({
|
|||||||
|
|
||||||
// mcp/prInfo.ts
|
// mcp/prInfo.ts
|
||||||
init_out4();
|
init_out4();
|
||||||
init_cli();
|
|
||||||
init_shared3();
|
init_shared3();
|
||||||
var PullRequestInfo = type({
|
var PullRequestInfo = type({
|
||||||
pull_number: type.number.describe("The pull request number to fetch")
|
pull_number: type.number.describe("The pull request number to fetch")
|
||||||
});
|
});
|
||||||
var PullRequestInfoTool = tool({
|
var PullRequestInfoTool = tool({
|
||||||
name: "get_pull_request",
|
name: "get_pull_request",
|
||||||
description: "Retrieve PR information. Automatically fetches and checks out the PR branch.",
|
description: "Retrieve PR information (metadata only). PR branch is already checked out during setup.",
|
||||||
parameters: PullRequestInfo,
|
parameters: PullRequestInfo,
|
||||||
execute: contextualize(async ({ pull_number }, ctx) => {
|
execute: contextualize(async ({ pull_number }, ctx) => {
|
||||||
const pr = await ctx.octokit.rest.pulls.get({
|
const pr = await ctx.octokit.rest.pulls.get({
|
||||||
@@ -124734,26 +124740,7 @@ var PullRequestInfoTool = tool({
|
|||||||
pull_number
|
pull_number
|
||||||
});
|
});
|
||||||
const data = pr.data;
|
const data = pr.data;
|
||||||
const baseBranch = data.base.ref;
|
const isFork = data.head.repo.full_name !== data.base.repo.full_name;
|
||||||
const headBranch = data.head.ref;
|
|
||||||
if (!baseBranch) {
|
|
||||||
throw new Error(`Base branch not found for PR #${pull_number}`);
|
|
||||||
}
|
|
||||||
const baseRepo = data.base.repo.full_name;
|
|
||||||
const headRepo = data.head.repo.full_name;
|
|
||||||
const isFork = headRepo !== baseRepo;
|
|
||||||
log.info(`Checking out PR #${pull_number} using gh pr checkout`);
|
|
||||||
$("gh", ["pr", "checkout", pull_number.toString()]);
|
|
||||||
log.info(`Fetching base branch: origin/${baseBranch}`);
|
|
||||||
$("git", ["fetch", "origin", baseBranch, "--depth=20"]);
|
|
||||||
const currentBranch = $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false }).trim();
|
|
||||||
const currentSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
|
|
||||||
const baseSha = $("git", ["rev-parse", `origin/${baseBranch}`], { log: false }).trim();
|
|
||||||
const summary2 = `PR branch has been fetched and checked out:
|
|
||||||
- Base branch: \`origin/${baseBranch}\` (${baseSha.substring(0, 7)})
|
|
||||||
- PR branch: \`${headBranch}\` (checked out locally, ${currentSha.substring(0, 7)})
|
|
||||||
- Current branch: \`${currentBranch}\`
|
|
||||||
- View diff: \`git diff origin/${baseBranch}...HEAD\``;
|
|
||||||
return {
|
return {
|
||||||
number: data.number,
|
number: data.number,
|
||||||
url: data.html_url,
|
url: data.html_url,
|
||||||
@@ -124761,10 +124748,9 @@ var PullRequestInfoTool = tool({
|
|||||||
state: data.state,
|
state: data.state,
|
||||||
draft: data.draft,
|
draft: data.draft,
|
||||||
merged: data.merged,
|
merged: data.merged,
|
||||||
base: baseBranch,
|
base: data.base.ref,
|
||||||
head: headBranch,
|
head: data.head.ref,
|
||||||
isFork,
|
isFork
|
||||||
summary: summary2
|
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -124862,41 +124848,108 @@ var ReviewTool = tool({
|
|||||||
// mcp/reviewComments.ts
|
// mcp/reviewComments.ts
|
||||||
init_out4();
|
init_out4();
|
||||||
init_shared3();
|
init_shared3();
|
||||||
|
var REVIEW_THREADS_QUERY = `
|
||||||
|
query ($owner: String!, $repo: String!, $pullNumber: Int!) {
|
||||||
|
repository(owner: $owner, name: $repo) {
|
||||||
|
pullRequest(number: $pullNumber) {
|
||||||
|
reviewThreads(first: 100) {
|
||||||
|
nodes {
|
||||||
|
comments(first: 100) {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
databaseId
|
||||||
|
body
|
||||||
|
path
|
||||||
|
line
|
||||||
|
startLine
|
||||||
|
diffSide
|
||||||
|
startSide
|
||||||
|
url
|
||||||
|
author {
|
||||||
|
login
|
||||||
|
}
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
pullRequestReview {
|
||||||
|
databaseId
|
||||||
|
}
|
||||||
|
replyTo {
|
||||||
|
databaseId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
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")
|
||||||
});
|
});
|
||||||
var GetReviewCommentsTool = tool({
|
var GetReviewCommentsTool = tool({
|
||||||
name: "get_review_comments",
|
name: "get_review_comments",
|
||||||
description: "Get all review comments for a specific pull request review. Returns line-by-line comments that were left on specific code locations.",
|
description: "Get all review comments and their replies for a specific pull request review. Returns line-by-line comments that were left on specific code locations, including any threaded replies.",
|
||||||
parameters: GetReviewComments,
|
parameters: GetReviewComments,
|
||||||
execute: contextualize(async ({ pull_number, review_id }, ctx) => {
|
execute: contextualize(async ({ pull_number, review_id }, ctx) => {
|
||||||
const comments = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listCommentsForReview, {
|
const response = await ctx.octokit.graphql(REVIEW_THREADS_QUERY, {
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
repo: ctx.name,
|
repo: ctx.name,
|
||||||
pull_number,
|
pullNumber: pull_number
|
||||||
review_id
|
|
||||||
});
|
});
|
||||||
|
const pullRequest = response.repository?.pullRequest;
|
||||||
|
if (!pullRequest) {
|
||||||
|
return {
|
||||||
|
review_id,
|
||||||
|
pull_number,
|
||||||
|
comments: [],
|
||||||
|
count: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const threadNodes = pullRequest.reviewThreads?.nodes;
|
||||||
|
if (!threadNodes) {
|
||||||
|
return {
|
||||||
|
review_id,
|
||||||
|
pull_number,
|
||||||
|
comments: [],
|
||||||
|
count: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const allComments = [];
|
||||||
|
for (const thread of threadNodes) {
|
||||||
|
if (!thread?.comments?.nodes) continue;
|
||||||
|
const threadComments = thread.comments.nodes.filter(
|
||||||
|
(c) => c !== null
|
||||||
|
);
|
||||||
|
if (threadComments.length === 0) continue;
|
||||||
|
const rootComment = threadComments.find((c) => c.replyTo === null);
|
||||||
|
if (!rootComment) continue;
|
||||||
|
const threadBelongsToReview = rootComment.pullRequestReview?.databaseId === review_id;
|
||||||
|
if (!threadBelongsToReview) continue;
|
||||||
|
for (const comment of threadComments) {
|
||||||
|
allComments.push({
|
||||||
|
id: comment.databaseId,
|
||||||
|
body: comment.body,
|
||||||
|
path: comment.path,
|
||||||
|
line: comment.line,
|
||||||
|
start_line: comment.startLine,
|
||||||
|
side: comment.diffSide,
|
||||||
|
start_side: comment.startSide,
|
||||||
|
user: comment.author?.login ?? null,
|
||||||
|
created_at: comment.createdAt,
|
||||||
|
updated_at: comment.updatedAt,
|
||||||
|
html_url: comment.url,
|
||||||
|
in_reply_to_id: comment.replyTo?.databaseId ?? null,
|
||||||
|
pull_request_review_id: comment.pullRequestReview?.databaseId ?? null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
review_id,
|
review_id,
|
||||||
pull_number,
|
pull_number,
|
||||||
comments: comments.map((comment) => ({
|
comments: allComments,
|
||||||
id: comment.id,
|
count: allComments.length
|
||||||
body: comment.body,
|
|
||||||
path: comment.path,
|
|
||||||
line: comment.line,
|
|
||||||
side: comment.side,
|
|
||||||
start_line: comment.start_line,
|
|
||||||
start_side: comment.start_side,
|
|
||||||
user: typeof comment.user === "string" ? comment.user : comment.user?.login,
|
|
||||||
created_at: comment.created_at,
|
|
||||||
updated_at: comment.updated_at,
|
|
||||||
html_url: comment.html_url,
|
|
||||||
in_reply_to_id: comment.in_reply_to_id,
|
|
||||||
diff_hunk: comment.diff_hunk,
|
|
||||||
reactions: comment.reactions
|
|
||||||
})),
|
|
||||||
count: comments.length
|
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -125103,6 +125156,7 @@ init_github();
|
|||||||
// utils/setup.ts
|
// utils/setup.ts
|
||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
init_cli();
|
init_cli();
|
||||||
|
init_github();
|
||||||
function setupGitConfig() {
|
function setupGitConfig() {
|
||||||
const repoDir = process.cwd();
|
const repoDir = process.cwd();
|
||||||
log.info("\u{1F527} Setting up git configuration...");
|
log.info("\u{1F527} Setting up git configuration...");
|
||||||
@@ -125139,46 +125193,19 @@ function setupGitAuth(ctx) {
|
|||||||
log.info("\u2713 Updated remote URL with authentication token (scoped to repo)");
|
log.info("\u2713 Updated remote URL with authentication token (scoped to repo)");
|
||||||
}
|
}
|
||||||
function setupGitBranch(payload) {
|
function setupGitBranch(payload) {
|
||||||
const branch = payload.event.branch;
|
if (payload.event.is_pr !== true || !payload.event.issue_number) {
|
||||||
const repoDir = process.cwd();
|
log.debug("Not a PR event, staying on default branch");
|
||||||
if (!branch) {
|
|
||||||
log.debug("No branch specified in payload, using default branch");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.info(`\u{1F33F} Setting up git branch: ${branch}`);
|
const prNumber = payload.event.issue_number;
|
||||||
if (payload.event.is_pr === true && payload.event.issue_number) {
|
const repoDir = process.cwd();
|
||||||
const prNumber = payload.event.issue_number;
|
log.info(`\u{1F33F} Checking out PR #${prNumber}...`);
|
||||||
try {
|
const token = getGitHubInstallationToken();
|
||||||
log.debug(`Checking out PR #${prNumber} using gh pr checkout`);
|
$("gh", ["pr", "checkout", prNumber.toString()], {
|
||||||
execSync(`gh pr checkout ${prNumber}`, {
|
cwd: repoDir,
|
||||||
cwd: repoDir,
|
env: { GH_TOKEN: token }
|
||||||
stdio: "pipe"
|
});
|
||||||
});
|
log.info(`\u2713 Successfully checked out PR #${prNumber}`);
|
||||||
log.info(`\u2713 Successfully checked out PR branch: ${branch}`);
|
|
||||||
return;
|
|
||||||
} catch (error41) {
|
|
||||||
log.debug(
|
|
||||||
`gh pr checkout failed, falling back to branch name fetch: ${error41 instanceof Error ? error41.message : String(error41)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
log.debug(`Fetching branch from origin: ${branch}`);
|
|
||||||
execSync(`git fetch origin ${branch}`, {
|
|
||||||
cwd: repoDir,
|
|
||||||
stdio: "pipe"
|
|
||||||
});
|
|
||||||
log.debug(`Checking out branch: ${branch}`);
|
|
||||||
execSync(`git checkout -B ${branch} origin/${branch}`, {
|
|
||||||
cwd: repoDir,
|
|
||||||
stdio: "pipe"
|
|
||||||
});
|
|
||||||
log.info(`\u2713 Successfully checked out branch: ${branch}`);
|
|
||||||
} catch (error41) {
|
|
||||||
log.warning(
|
|
||||||
`Failed to checkout branch ${branch}: ${error41 instanceof Error ? error41.message : String(error41)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// utils/timer.ts
|
// utils/timer.ts
|
||||||
|
|||||||
@@ -193,7 +193,6 @@ interface FixReviewEvent extends BasePayloadEvent {
|
|||||||
review_id: number;
|
review_id: number;
|
||||||
/** "all" to fix all comments, or specific comment IDs to fix */
|
/** "all" to fix all comments, or specific comment IDs to fix */
|
||||||
comment_ids: number[] | "all";
|
comment_ids: number[] | "all";
|
||||||
branch: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UnknownEvent extends BasePayloadEvent {
|
interface UnknownEvent extends BasePayloadEvent {
|
||||||
|
|||||||
+3
-3
@@ -30,7 +30,7 @@ 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 for a specific pull request review.
|
get all line-by-line comments and their replies for a specific pull request review.
|
||||||
|
|
||||||
**parameters:**
|
**parameters:**
|
||||||
- `pull_number` (number): the pull request number
|
- `pull_number` (number): the pull request number
|
||||||
@@ -39,11 +39,11 @@ get all line-by-line comments for a specific pull request review.
|
|||||||
**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:
|
array of review comments including threaded replies:
|
||||||
- file path, line number, comment body
|
- file path, line number, comment body
|
||||||
- side (LEFT/RIGHT) and position in diff
|
- side (LEFT/RIGHT) and position in diff
|
||||||
- user, timestamps, html_url
|
- user, timestamps, html_url
|
||||||
- in_reply_to_id for threaded comments
|
- in_reply_to_id for threaded comments (replies have this set to the parent comment id)
|
||||||
|
|
||||||
**example:**
|
**example:**
|
||||||
```typescript
|
```typescript
|
||||||
|
|||||||
+12
-1
@@ -177,7 +177,8 @@ export async function reportProgress({ body }: { body: string }): Promise<
|
|||||||
// no existing comment - create one
|
// no existing comment - create one
|
||||||
const issueNumber = ctx.payload.event.issue_number;
|
const issueNumber = ctx.payload.event.issue_number;
|
||||||
if (issueNumber === undefined) {
|
if (issueNumber === undefined) {
|
||||||
throw new Error("cannot create progress comment: no issue_number found in the payload event");
|
// cannot create comment without issue_number (e.g., workflow_dispatch events)
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await ctx.octokit.rest.issues.createComment({
|
const result = await ctx.octokit.rest.issues.createComment({
|
||||||
@@ -207,6 +208,16 @@ export const ReportProgressTool = tool({
|
|||||||
execute: contextualize(async ({ body }) => {
|
execute: contextualize(async ({ body }) => {
|
||||||
const result = await reportProgress({ body });
|
const result = await reportProgress({ body });
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
// gracefully handle case where no comment can be created
|
||||||
|
// this happens for workflow_dispatch events or when there's no associated issue/PR
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message:
|
||||||
|
"cannot create progress comment: no issue_number found in the payload event. this may occur for workflow_dispatch events or when there is no associated issue/PR. if you need to comment on a specific issue or PR, use create_issue_comment with an explicit issueNumber.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
...result,
|
...result,
|
||||||
|
|||||||
+5
-37
@@ -1,6 +1,4 @@
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { log } from "../utils/cli.ts";
|
|
||||||
import { $ } from "../utils/shell.ts";
|
|
||||||
import { contextualize, tool } from "./shared.ts";
|
import { contextualize, tool } from "./shared.ts";
|
||||||
|
|
||||||
export const PullRequestInfo = type({
|
export const PullRequestInfo = type({
|
||||||
@@ -9,7 +7,8 @@ export const PullRequestInfo = type({
|
|||||||
|
|
||||||
export const PullRequestInfoTool = tool({
|
export const PullRequestInfoTool = tool({
|
||||||
name: "get_pull_request",
|
name: "get_pull_request",
|
||||||
description: "Retrieve PR information. Automatically fetches and checks out the PR branch.",
|
description:
|
||||||
|
"Retrieve PR information (metadata only). PR branch is already checked out during setup.",
|
||||||
parameters: PullRequestInfo,
|
parameters: PullRequestInfo,
|
||||||
execute: contextualize(async ({ pull_number }, ctx) => {
|
execute: contextualize(async ({ pull_number }, ctx) => {
|
||||||
const pr = await ctx.octokit.rest.pulls.get({
|
const pr = await ctx.octokit.rest.pulls.get({
|
||||||
@@ -20,38 +19,8 @@ export const PullRequestInfoTool = tool({
|
|||||||
|
|
||||||
const data = pr.data;
|
const data = pr.data;
|
||||||
|
|
||||||
const baseBranch = data.base.ref;
|
|
||||||
const headBranch = data.head.ref;
|
|
||||||
|
|
||||||
if (!baseBranch) {
|
|
||||||
throw new Error(`Base branch not found for PR #${pull_number}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// detect fork PRs - head repo differs from base repo
|
// detect fork PRs - head repo differs from base repo
|
||||||
const baseRepo = data.base.repo.full_name;
|
const isFork = data.head.repo.full_name !== data.base.repo.full_name;
|
||||||
const headRepo = data.head.repo.full_name;
|
|
||||||
const isFork = headRepo !== baseRepo;
|
|
||||||
|
|
||||||
// use gh pr checkout which handles fork PRs automatically
|
|
||||||
// it adds the fork as a remote if needed and checks out the PR branch
|
|
||||||
log.info(`Checking out PR #${pull_number} using gh pr checkout`);
|
|
||||||
$("gh", ["pr", "checkout", pull_number.toString()]);
|
|
||||||
|
|
||||||
// fetch base branch for diff comparison
|
|
||||||
log.info(`Fetching base branch: origin/${baseBranch}`);
|
|
||||||
$("git", ["fetch", "origin", baseBranch, "--depth=20"]);
|
|
||||||
|
|
||||||
// get current git status for summary
|
|
||||||
const currentBranch = $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false }).trim();
|
|
||||||
const currentSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
|
|
||||||
const baseSha = $("git", ["rev-parse", `origin/${baseBranch}`], { log: false }).trim();
|
|
||||||
|
|
||||||
// build summary
|
|
||||||
const summary = `PR branch has been fetched and checked out:
|
|
||||||
- Base branch: \`origin/${baseBranch}\` (${baseSha.substring(0, 7)})
|
|
||||||
- PR branch: \`${headBranch}\` (checked out locally, ${currentSha.substring(0, 7)})
|
|
||||||
- Current branch: \`${currentBranch}\`
|
|
||||||
- View diff: \`git diff origin/${baseBranch}...HEAD\``;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
number: data.number,
|
number: data.number,
|
||||||
@@ -60,10 +29,9 @@ export const PullRequestInfoTool = tool({
|
|||||||
state: data.state,
|
state: data.state,
|
||||||
draft: data.draft,
|
draft: data.draft,
|
||||||
merged: data.merged,
|
merged: data.merged,
|
||||||
base: baseBranch,
|
base: data.base.ref,
|
||||||
head: headBranch,
|
head: data.head.ref,
|
||||||
isFork,
|
isFork,
|
||||||
summary,
|
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
+158
-21
@@ -1,6 +1,84 @@
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { contextualize, tool } from "./shared.ts";
|
import { contextualize, tool } from "./shared.ts";
|
||||||
|
|
||||||
|
// graphql query to fetch all review threads with comments and replies
|
||||||
|
const REVIEW_THREADS_QUERY = `
|
||||||
|
query ($owner: String!, $repo: String!, $pullNumber: Int!) {
|
||||||
|
repository(owner: $owner, name: $repo) {
|
||||||
|
pullRequest(number: $pullNumber) {
|
||||||
|
reviewThreads(first: 100) {
|
||||||
|
nodes {
|
||||||
|
comments(first: 100) {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
databaseId
|
||||||
|
body
|
||||||
|
path
|
||||||
|
line
|
||||||
|
startLine
|
||||||
|
diffSide
|
||||||
|
startSide
|
||||||
|
url
|
||||||
|
author {
|
||||||
|
login
|
||||||
|
}
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
pullRequestReview {
|
||||||
|
databaseId
|
||||||
|
}
|
||||||
|
replyTo {
|
||||||
|
databaseId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// graphql response types (nodes arrays can contain nulls per GitHub GraphQL spec)
|
||||||
|
type GraphQLReviewComment = {
|
||||||
|
id: string;
|
||||||
|
databaseId: number;
|
||||||
|
body: string;
|
||||||
|
path: string;
|
||||||
|
line: number | null;
|
||||||
|
startLine: number | null;
|
||||||
|
diffSide: "LEFT" | "RIGHT";
|
||||||
|
startSide: "LEFT" | "RIGHT" | null;
|
||||||
|
url: string;
|
||||||
|
author: {
|
||||||
|
login: string;
|
||||||
|
} | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
pullRequestReview: {
|
||||||
|
databaseId: number;
|
||||||
|
} | null;
|
||||||
|
replyTo: {
|
||||||
|
databaseId: number;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GraphQLReviewThread = {
|
||||||
|
comments: {
|
||||||
|
nodes: (GraphQLReviewComment | null)[] | null;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GraphQLResponse = {
|
||||||
|
repository: {
|
||||||
|
pullRequest: {
|
||||||
|
reviewThreads: {
|
||||||
|
nodes: (GraphQLReviewThread | null)[] | null;
|
||||||
|
} | null;
|
||||||
|
} | null;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
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"),
|
||||||
@@ -9,36 +87,95 @@ export const GetReviewComments = type({
|
|||||||
export const GetReviewCommentsTool = tool({
|
export const GetReviewCommentsTool = tool({
|
||||||
name: "get_review_comments",
|
name: "get_review_comments",
|
||||||
description:
|
description:
|
||||||
"Get all review comments for a specific pull request review. Returns line-by-line comments that were left on specific code locations.",
|
"Get all review comments and their replies for a specific pull request review. Returns line-by-line comments that were left on specific code locations, including any threaded replies.",
|
||||||
parameters: GetReviewComments,
|
parameters: GetReviewComments,
|
||||||
execute: contextualize(async ({ pull_number, review_id }, ctx) => {
|
execute: contextualize(async ({ pull_number, review_id }, ctx) => {
|
||||||
const comments = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listCommentsForReview, {
|
// fetch all review threads using graphql
|
||||||
|
const response = await ctx.octokit.graphql<GraphQLResponse>(REVIEW_THREADS_QUERY, {
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
repo: ctx.name,
|
repo: ctx.name,
|
||||||
pull_number,
|
pullNumber: pull_number,
|
||||||
review_id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pullRequest = response.repository?.pullRequest;
|
||||||
|
if (!pullRequest) {
|
||||||
|
return {
|
||||||
|
review_id,
|
||||||
|
pull_number,
|
||||||
|
comments: [],
|
||||||
|
count: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const threadNodes = pullRequest.reviewThreads?.nodes;
|
||||||
|
if (!threadNodes) {
|
||||||
|
return {
|
||||||
|
review_id,
|
||||||
|
pull_number,
|
||||||
|
comments: [],
|
||||||
|
count: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const allComments: {
|
||||||
|
id: number;
|
||||||
|
body: string;
|
||||||
|
path: string;
|
||||||
|
line: number | null;
|
||||||
|
side: "LEFT" | "RIGHT";
|
||||||
|
start_line: number | null;
|
||||||
|
start_side: "LEFT" | "RIGHT" | null;
|
||||||
|
user: string | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
html_url: string;
|
||||||
|
in_reply_to_id: number | null;
|
||||||
|
pull_request_review_id: number | null;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
// iterate through all threads (filter out nulls)
|
||||||
|
for (const thread of threadNodes) {
|
||||||
|
if (!thread?.comments?.nodes) continue;
|
||||||
|
|
||||||
|
// filter out null comments
|
||||||
|
const threadComments = thread.comments.nodes.filter(
|
||||||
|
(c): c is GraphQLReviewComment => c !== null
|
||||||
|
);
|
||||||
|
if (threadComments.length === 0) continue;
|
||||||
|
|
||||||
|
// find the root comment (the one with replyTo == null) to determine thread ownership
|
||||||
|
const rootComment = threadComments.find((c) => c.replyTo === null);
|
||||||
|
if (!rootComment) continue;
|
||||||
|
|
||||||
|
// check if this thread belongs to the target review using the root comment
|
||||||
|
const threadBelongsToReview = rootComment.pullRequestReview?.databaseId === review_id;
|
||||||
|
if (!threadBelongsToReview) continue;
|
||||||
|
|
||||||
|
// include all comments from this thread (original + replies)
|
||||||
|
for (const comment of threadComments) {
|
||||||
|
allComments.push({
|
||||||
|
id: comment.databaseId,
|
||||||
|
body: comment.body,
|
||||||
|
path: comment.path,
|
||||||
|
line: comment.line,
|
||||||
|
start_line: comment.startLine,
|
||||||
|
side: comment.diffSide,
|
||||||
|
start_side: comment.startSide,
|
||||||
|
user: comment.author?.login ?? null,
|
||||||
|
created_at: comment.createdAt,
|
||||||
|
updated_at: comment.updatedAt,
|
||||||
|
html_url: comment.url,
|
||||||
|
in_reply_to_id: comment.replyTo?.databaseId ?? null,
|
||||||
|
pull_request_review_id: comment.pullRequestReview?.databaseId ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
review_id,
|
review_id,
|
||||||
pull_number,
|
pull_number,
|
||||||
comments: comments.map((comment) => ({
|
comments: allComments,
|
||||||
id: comment.id,
|
count: allComments.length,
|
||||||
body: comment.body,
|
|
||||||
path: comment.path,
|
|
||||||
line: comment.line,
|
|
||||||
side: comment.side,
|
|
||||||
start_line: comment.start_line,
|
|
||||||
start_side: comment.start_side,
|
|
||||||
user: typeof comment.user === "string" ? comment.user : comment.user?.login,
|
|
||||||
created_at: comment.created_at,
|
|
||||||
updated_at: comment.updated_at,
|
|
||||||
html_url: comment.html_url,
|
|
||||||
in_reply_to_id: comment.in_reply_to_id,
|
|
||||||
diff_hunk: comment.diff_hunk,
|
|
||||||
reactions: comment.reactions,
|
|
||||||
})),
|
|
||||||
count: comments.length,
|
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/action",
|
"name": "@pullfrog/action",
|
||||||
"version": "0.0.137",
|
"version": "0.0.139",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
|
|||||||
+17
-51
@@ -2,7 +2,7 @@ import { execSync } from "node:child_process";
|
|||||||
import { existsSync, rmSync } from "node:fs";
|
import { existsSync, rmSync } from "node:fs";
|
||||||
import type { Payload } from "../external.ts";
|
import type { Payload } from "../external.ts";
|
||||||
import { log } from "./cli.ts";
|
import { log } from "./cli.ts";
|
||||||
import type { RepoContext } from "./github.ts";
|
import { getGitHubInstallationToken, type RepoContext } from "./github.ts";
|
||||||
import { $ } from "./shell.ts";
|
import { $ } from "./shell.ts";
|
||||||
|
|
||||||
export interface SetupOptions {
|
export interface SetupOptions {
|
||||||
@@ -95,62 +95,28 @@ export function setupGitAuth(ctx: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup git branch based on payload event context
|
* Setup git branch based on payload event context.
|
||||||
* Automatically checks out the appropriate branch before agent execution
|
* For PR events, uses `gh pr checkout` which handles fork PRs automatically.
|
||||||
|
* For non-PR events, stays on the default branch.
|
||||||
*/
|
*/
|
||||||
export function setupGitBranch(payload: Payload): void {
|
export function setupGitBranch(payload: Payload): void {
|
||||||
const branch = payload.event.branch;
|
// only checkout for PR events - use issue_number directly (no dependency on branch field)
|
||||||
const repoDir = process.cwd();
|
if (payload.event.is_pr !== true || !payload.event.issue_number) {
|
||||||
|
log.debug("Not a PR event, staying on default branch");
|
||||||
if (!branch) {
|
|
||||||
log.debug("No branch specified in payload, using default branch");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`🌿 Setting up git branch: ${branch}`);
|
const prNumber = payload.event.issue_number;
|
||||||
|
const repoDir = process.cwd();
|
||||||
|
|
||||||
// if this is a PR-related event, use gh pr checkout which handles fork PRs automatically
|
log.info(`🌿 Checking out PR #${prNumber}...`);
|
||||||
if (payload.event.is_pr === true && payload.event.issue_number) {
|
|
||||||
const prNumber = payload.event.issue_number;
|
|
||||||
try {
|
|
||||||
// gh pr checkout handles fork PRs by setting up remotes automatically
|
|
||||||
log.debug(`Checking out PR #${prNumber} using gh pr checkout`);
|
|
||||||
execSync(`gh pr checkout ${prNumber}`, {
|
|
||||||
cwd: repoDir,
|
|
||||||
stdio: "pipe",
|
|
||||||
});
|
|
||||||
|
|
||||||
log.info(`✓ Successfully checked out PR branch: ${branch}`);
|
// gh pr checkout handles fork PRs by setting up remotes automatically
|
||||||
return;
|
const token = getGitHubInstallationToken();
|
||||||
} catch (error) {
|
$("gh", ["pr", "checkout", prNumber.toString()], {
|
||||||
// if gh pr checkout fails, fall back to branch name fetch
|
cwd: repoDir,
|
||||||
log.debug(
|
env: { GH_TOKEN: token },
|
||||||
`gh pr checkout failed, falling back to branch name fetch: ${error instanceof Error ? error.message : String(error)}`
|
});
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fallback: fetch by branch name (for non-PR contexts or if PR ref fetch failed)
|
log.info(`✓ Successfully checked out PR #${prNumber}`);
|
||||||
try {
|
|
||||||
log.debug(`Fetching branch from origin: ${branch}`);
|
|
||||||
execSync(`git fetch origin ${branch}`, {
|
|
||||||
cwd: repoDir,
|
|
||||||
stdio: "pipe",
|
|
||||||
});
|
|
||||||
|
|
||||||
// checkout the branch, creating local tracking branch
|
|
||||||
log.debug(`Checking out branch: ${branch}`);
|
|
||||||
execSync(`git checkout -B ${branch} origin/${branch}`, {
|
|
||||||
cwd: repoDir,
|
|
||||||
stdio: "pipe",
|
|
||||||
});
|
|
||||||
|
|
||||||
log.info(`✓ Successfully checked out branch: ${branch}`);
|
|
||||||
} catch (error) {
|
|
||||||
// if git operations fail, log warning but don't fail the action
|
|
||||||
// the agent might still be able to work with the default branch
|
|
||||||
log.warning(
|
|
||||||
`Failed to checkout branch ${branch}: ${error instanceof Error ? error.message : String(error)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface ShellOptions {
|
|||||||
| "ucs2"
|
| "ucs2"
|
||||||
| "utf16le";
|
| "utf16le";
|
||||||
log?: boolean;
|
log?: boolean;
|
||||||
|
env?: Record<string, string>;
|
||||||
onError?: (result: { status: number; stdout: string; stderr: string }) => void;
|
onError?: (result: { status: number; stdout: string; stderr: string }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ export function $(cmd: string, args: string[], options?: ShellOptions): string {
|
|||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
encoding,
|
encoding,
|
||||||
cwd: options?.cwd,
|
cwd: options?.cwd,
|
||||||
|
env: options?.env ? { ...process.env, ...options.env } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const stdout = result.stdout ?? "";
|
const stdout = result.stdout ?? "";
|
||||||
|
|||||||
Reference in New Issue
Block a user