Compare commits

...

4 Commits

Author SHA1 Message Date
Colin McDonnell 316b6cb83c 0.0.138 2025-12-15 21:21:57 -08:00
Colin McDonnell a19ae49224 Determinstically set up PR branch 2025-12-15 21:12:55 -08:00
Colin McDonnell 1d69f0f3e4 0.0.137 2025-12-15 20:22:12 -08:00
Colin McDonnell 2f16d2ef0e Improve repo setup with gh cli 2025-12-15 20:21:56 -08:00
10 changed files with 303 additions and 400 deletions
+72 -72
View File
@@ -8,84 +8,84 @@ import { getModes } from "../modes.ts";
* Removes verbose GitHub API metadata (user objects, repository metadata, etc.) * Removes verbose GitHub API metadata (user objects, repository metadata, etc.)
* and keeps only the fields agents actually need. * and keeps only the fields agents actually need.
*/ */
function extractEssentialEventData(event: Payload["event"]): Record<string, unknown> { // function extractEssentialEventData(event: Payload["event"]): Record<string, unknown> {
const trigger = event.trigger; // const trigger = event.trigger;
const essential: Record<string, unknown> = { trigger }; // const essential: Record<string, unknown> = { trigger };
// common fields // // common fields
if ("issue_number" in event) { // if ("issue_number" in event) {
essential.issue_number = event.issue_number; // essential.issue_number = event.issue_number;
} // }
if ("branch" in event && event.branch) { // if ("branch" in event && event.branch) {
essential.branch = event.branch; // essential.branch = event.branch;
} // }
// trigger-specific fields // // trigger-specific fields
switch (trigger) { // switch (trigger) {
case "issue_comment_created": // case "issue_comment_created":
if ("comment_id" in event) essential.comment_id = event.comment_id; // if ("comment_id" in event) essential.comment_id = event.comment_id;
if ("comment_body" in event) essential.comment_body = event.comment_body; // if ("comment_body" in event) essential.comment_body = event.comment_body;
// include issue title/body if available in context (but not the entire context object) // // include issue title/body if available in context (but not the entire context object)
if ("context" in event && event.context && typeof event.context === "object") { // if ("context" in event && event.context && typeof event.context === "object") {
const ctx = event.context as Record<string, unknown>; // const ctx = event.context as Record<string, unknown>;
if (ctx.issue && typeof ctx.issue === "object") { // if (ctx.issue && typeof ctx.issue === "object") {
const issue = ctx.issue as Record<string, unknown>; // const issue = ctx.issue as Record<string, unknown>;
if (issue.title) essential.issue_title = issue.title; // if (issue.title) essential.issue_title = issue.title;
if (issue.body) essential.issue_body = issue.body; // if (issue.body) essential.issue_body = issue.body;
} // }
} // }
break; // break;
case "issues_opened": // case "issues_opened":
case "issues_assigned": // case "issues_assigned":
case "issues_labeled": // case "issues_labeled":
if ("issue_title" in event) essential.issue_title = event.issue_title; // if ("issue_title" in event) essential.issue_title = event.issue_title;
if ("issue_body" in event) essential.issue_body = event.issue_body; // if ("issue_body" in event) essential.issue_body = event.issue_body;
break; // break;
case "pull_request_opened": // case "pull_request_opened":
case "pull_request_review_requested": // case "pull_request_review_requested":
if ("pr_title" in event) essential.pr_title = event.pr_title; // if ("pr_title" in event) essential.pr_title = event.pr_title;
if ("pr_body" in event) essential.pr_body = event.pr_body; // if ("pr_body" in event) essential.pr_body = event.pr_body;
if ("branch" in event) essential.branch = event.branch; // if ("branch" in event) essential.branch = event.branch;
break; // break;
case "pull_request_review_submitted": // case "pull_request_review_submitted":
if ("review_id" in event) essential.review_id = event.review_id; // if ("review_id" in event) essential.review_id = event.review_id;
if ("review_body" in event) essential.review_body = event.review_body; // if ("review_body" in event) essential.review_body = event.review_body;
if ("review_state" in event) essential.review_state = event.review_state; // if ("review_state" in event) essential.review_state = event.review_state;
if ("branch" in event) essential.branch = event.branch; // if ("branch" in event) essential.branch = event.branch;
break; // break;
case "pull_request_review_comment_created": // case "pull_request_review_comment_created":
if ("comment_id" in event) essential.comment_id = event.comment_id; // if ("comment_id" in event) essential.comment_id = event.comment_id;
if ("comment_body" in event) essential.comment_body = event.comment_body; // if ("comment_body" in event) essential.comment_body = event.comment_body;
if ("pr_title" in event) essential.pr_title = event.pr_title; // if ("pr_title" in event) essential.pr_title = event.pr_title;
if ("branch" in event) essential.branch = event.branch; // if ("branch" in event) essential.branch = event.branch;
break; // break;
case "check_suite_completed": // case "check_suite_completed":
if ("pr_title" in event) essential.pr_title = event.pr_title; // if ("pr_title" in event) essential.pr_title = event.pr_title;
if ("pr_body" in event) essential.pr_body = event.pr_body; // if ("pr_body" in event) essential.pr_body = event.pr_body;
if ("branch" in event) essential.branch = event.branch; // if ("branch" in event) essential.branch = event.branch;
if ("check_suite" in event) { // if ("check_suite" in event) {
essential.check_suite = { // essential.check_suite = {
id: event.check_suite.id, // id: event.check_suite.id,
head_sha: event.check_suite.head_sha, // head_sha: event.check_suite.head_sha,
head_branch: event.check_suite.head_branch, // head_branch: event.check_suite.head_branch,
status: event.check_suite.status, // status: event.check_suite.status,
conclusion: event.check_suite.conclusion, // conclusion: event.check_suite.conclusion,
}; // };
} // }
break; // break;
case "workflow_dispatch": // case "workflow_dispatch":
if ("inputs" in event) essential.inputs = event.inputs; // if ("inputs" in event) essential.inputs = event.inputs;
break; // break;
} // }
return essential; // return essential;
} // }
export const addInstructions = (payload: Payload) => { export const addInstructions = (payload: Payload) => {
let encodedEvent = ""; let encodedEvent = "";
@@ -95,8 +95,8 @@ export const addInstructions = (payload: Payload) => {
// no meaningful event data to encode // no meaningful event data to encode
} else { } else {
// extract only essential fields to reduce token usage // extract only essential fields to reduce token usage
const essentialEvent = extractEssentialEventData(payload.event); // const essentialEvent = payload.event;
encodedEvent = toonEncode(essentialEvent); encodedEvent = toonEncode(payload.event);
} }
return ` return `
@@ -168,7 +168,7 @@ MCP servers provide tools you can call. Inspect your available MCP servers at st
Tool names may be formatted as \`(server name)/(tool name)\`, for example: \`${ghPullfrogMcpName}/create_issue_comment\` Tool names may be formatted as \`(server name)/(tool name)\`, for example: \`${ghPullfrogMcpName}/create_issue_comment\`
**GitHub CLI prohibition**: Do not use the \`gh\` CLI under any circumstances. Use the corresponding tool from ${ghPullfrogMcpName} instead. **GitHub CLI**: Prefer using MCP tools from ${ghPullfrogMcpName} for GitHub operations. The \`gh\` CLI is available as a fallback if needed, but MCP tools handle authentication and provide better integration.
**Git operations**: All git operations must use ${ghPullfrogMcpName} MCP tools to ensure proper authentication and commit attribution. Do NOT use git commands directly (e.g., \`git commit\`, \`git push\`, \`git checkout\`, \`git branch\`) - these will use incorrect credentials and attribute commits to the wrong author. **Git operations**: All git operations must use ${ghPullfrogMcpName} MCP tools to ensure proper authentication and commit attribution. Do NOT use git commands directly (e.g., \`git commit\`, \`git push\`, \`git checkout\`, \`git branch\`) - these will use incorrect credentials and attribute commits to the wrong author.
+30 -132
View File
@@ -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.136", version: "0.0.138",
type: "module", type: "module",
files: [ files: [
"index.js", "index.js",
@@ -97972,7 +97978,7 @@ ${disableProgressComment ? "" : `
prompt: `Follow these steps: prompt: `Follow these steps:
1. Get PR info with ${ghPullfrogMcpName}/get_pull_request (this automatically prepares the repository by fetching and checking out the PR branch) 1. Get PR info with ${ghPullfrogMcpName}/get_pull_request (this automatically prepares the repository by fetching and checking out the PR branch)
2. View diff: \`git diff origin/<base>...origin/<head>\` (replace <base> and <head> with 'base' and 'head' from PR info) 2. **IMPORTANT**: After calling ${ghPullfrogMcpName}/get_pull_request, the PR branch is already checked out locally. View diff using: \`git diff origin/<base>...HEAD\` (replace <base> with 'base' from PR info). Do NOT use \`origin/<head>\` - the branch is checked out locally, not as a remote tracking branch.
3. Read files from the checked-out PR branch to understand the implementation. Always use **relative paths** from repo root (e.g., \`src/index.ts\`), never absolute paths. 3. Read files from the checked-out PR branch to understand the implementation. Always use **relative paths** from repo root (e.g., \`src/index.ts\`), never absolute paths.
@@ -98034,79 +98040,12 @@ ${disableProgressComment ? "" : `
var modes = getModes({ disableProgressComment: void 0 }); var modes = getModes({ disableProgressComment: void 0 });
// agents/instructions.ts // agents/instructions.ts
function extractEssentialEventData(event) {
const trigger = event.trigger;
const essential = { trigger };
if ("issue_number" in event) {
essential.issue_number = event.issue_number;
}
if ("branch" in event && event.branch) {
essential.branch = event.branch;
}
switch (trigger) {
case "issue_comment_created":
if ("comment_id" in event) essential.comment_id = event.comment_id;
if ("comment_body" in event) essential.comment_body = event.comment_body;
if ("context" in event && event.context && typeof event.context === "object") {
const ctx = event.context;
if (ctx.issue && typeof ctx.issue === "object") {
const issue2 = ctx.issue;
if (issue2.title) essential.issue_title = issue2.title;
if (issue2.body) essential.issue_body = issue2.body;
}
}
break;
case "issues_opened":
case "issues_assigned":
case "issues_labeled":
if ("issue_title" in event) essential.issue_title = event.issue_title;
if ("issue_body" in event) essential.issue_body = event.issue_body;
break;
case "pull_request_opened":
case "pull_request_review_requested":
if ("pr_title" in event) essential.pr_title = event.pr_title;
if ("pr_body" in event) essential.pr_body = event.pr_body;
if ("branch" in event) essential.branch = event.branch;
break;
case "pull_request_review_submitted":
if ("review_id" in event) essential.review_id = event.review_id;
if ("review_body" in event) essential.review_body = event.review_body;
if ("review_state" in event) essential.review_state = event.review_state;
if ("branch" in event) essential.branch = event.branch;
break;
case "pull_request_review_comment_created":
if ("comment_id" in event) essential.comment_id = event.comment_id;
if ("comment_body" in event) essential.comment_body = event.comment_body;
if ("pr_title" in event) essential.pr_title = event.pr_title;
if ("branch" in event) essential.branch = event.branch;
break;
case "check_suite_completed":
if ("pr_title" in event) essential.pr_title = event.pr_title;
if ("pr_body" in event) essential.pr_body = event.pr_body;
if ("branch" in event) essential.branch = event.branch;
if ("check_suite" in event) {
essential.check_suite = {
id: event.check_suite.id,
head_sha: event.check_suite.head_sha,
head_branch: event.check_suite.head_branch,
status: event.check_suite.status,
conclusion: event.check_suite.conclusion
};
}
break;
case "workflow_dispatch":
if ("inputs" in event) essential.inputs = event.inputs;
break;
}
return essential;
}
var addInstructions = (payload) => { var addInstructions = (payload) => {
let encodedEvent = ""; let encodedEvent = "";
const eventKeys = Object.keys(payload.event); const eventKeys = Object.keys(payload.event);
if (eventKeys.length === 1 && eventKeys[0] === "trigger") { if (eventKeys.length === 1 && eventKeys[0] === "trigger") {
} else { } else {
const essentialEvent = extractEssentialEventData(payload.event); encodedEvent = encode(payload.event);
encodedEvent = encode(essentialEvent);
} }
return ` return `
*********************************************** ***********************************************
@@ -98177,7 +98116,7 @@ MCP servers provide tools you can call. Inspect your available MCP servers at st
Tool names may be formatted as \`(server name)/(tool name)\`, for example: \`${ghPullfrogMcpName}/create_issue_comment\` Tool names may be formatted as \`(server name)/(tool name)\`, for example: \`${ghPullfrogMcpName}/create_issue_comment\`
**GitHub CLI prohibition**: Do not use the \`gh\` CLI under any circumstances. Use the corresponding tool from ${ghPullfrogMcpName} instead. **GitHub CLI**: Prefer using MCP tools from ${ghPullfrogMcpName} for GitHub operations. The \`gh\` CLI is available as a fallback if needed, but MCP tools handle authentication and provide better integration.
**Git operations**: All git operations must use ${ghPullfrogMcpName} MCP tools to ensure proper authentication and commit attribution. Do NOT use git commands directly (e.g., \`git commit\`, \`git push\`, \`git checkout\`, \`git branch\`) - these will use incorrect credentials and attribute commits to the wrong author. **Git operations**: All git operations must use ${ghPullfrogMcpName} MCP tools to ensure proper authentication and commit attribution. Do NOT use git commands directly (e.g., \`git commit\`, \`git push\`, \`git checkout\`, \`git branch\`) - these will use incorrect credentials and attribute commits to the wrong author.
@@ -124300,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 ?? "";
@@ -124785,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 and automatically prepare the repository for review by fetching and checking 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({
@@ -124801,17 +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}`);
}
log.info(`Fetching base branch: origin/${baseBranch}`);
$("git", ["fetch", "origin", baseBranch, "--depth=20"]);
log.info(`Fetching PR #${pull_number} using refs/pull/${pull_number}/head`);
$("git", ["fetch", "origin", `refs/pull/${pull_number}/head`]);
log.info(`Checking out PR branch: ${headBranch}`);
$("git", ["checkout", "-B", headBranch, "FETCH_HEAD"]);
return { return {
number: data.number, number: data.number,
url: data.html_url, url: data.html_url,
@@ -124819,8 +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
}; };
}) })
}); });
@@ -125159,6 +125089,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...");
@@ -125195,52 +125126,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;
const issueNumber = "issue_number" in payload.event ? payload.event.issue_number : void 0; const repoDir = process.cwd();
const isLikelyPR = issueNumber !== void 0 && branch !== void 0; log.info(`\u{1F33F} Checking out PR #${prNumber}...`);
if (isLikelyPR) { const token = getGitHubInstallationToken();
try { $("gh", ["pr", "checkout", prNumber.toString()], {
log.debug(`Fetching PR #${issueNumber} using refs/pull/${issueNumber}/head`); cwd: repoDir,
execSync(`git fetch origin refs/pull/${issueNumber}/head`, { env: { GH_TOKEN: token }
cwd: repoDir, });
stdio: "pipe" log.info(`\u2713 Successfully checked out PR #${prNumber}`);
});
log.debug(`Checking out branch: ${branch}`);
execSync(`git checkout -B ${branch} FETCH_HEAD`, {
cwd: repoDir,
stdio: "pipe"
});
log.info(`\u2713 Successfully checked out PR branch: ${branch}`);
return;
} catch (error41) {
log.debug(
`PR ref fetch 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
+161 -108
View File
@@ -51,117 +51,170 @@ export const AgentName = type.enumerated(...Object.keys(agentsManifest));
export type AgentApiKeyName = (typeof agentsManifest)[AgentName]["apiKeyNames"][number]; export type AgentApiKeyName = (typeof agentsManifest)[AgentName]["apiKeyNames"][number];
// base interface for common payload event fields
interface BasePayloadEvent {
issue_number?: number;
is_pr?: boolean;
branch?: string;
pr_title?: string;
pr_body?: string | null;
issue_title?: string;
issue_body?: string | null;
comment_id?: number;
comment_body?: string;
review_id?: number;
review_body?: string | null;
review_state?: string;
review_comments?: any[];
context?: any;
thread?: any;
pull_request?: any;
check_suite?: {
id: number;
head_sha: string;
head_branch: string | null;
status: string | null;
conclusion: string | null;
url: string;
};
comment_ids?: number[] | "all";
[key: string]: any;
}
interface PullRequestOpenedEvent extends BasePayloadEvent {
trigger: "pull_request_opened";
issue_number: number;
is_pr: true;
pr_title: string;
pr_body: string | null;
branch: string;
}
interface PullRequestReadyForReviewEvent extends BasePayloadEvent {
trigger: "pull_request_ready_for_review";
issue_number: number;
is_pr: true;
pr_title: string;
pr_body: string | null;
branch: string;
}
interface PullRequestReviewRequestedEvent extends BasePayloadEvent {
trigger: "pull_request_review_requested";
issue_number: number;
is_pr: true;
pr_title: string;
pr_body: string | null;
branch: string;
}
interface PullRequestReviewSubmittedEvent extends BasePayloadEvent {
trigger: "pull_request_review_submitted";
issue_number: number;
is_pr: true;
review_id: number;
review_body: string | null;
review_state: string;
review_comments: any[];
context: any;
branch: string;
}
interface PullRequestReviewCommentCreatedEvent extends BasePayloadEvent {
trigger: "pull_request_review_comment_created";
issue_number: number;
is_pr: true;
pr_title: string;
comment_id: number;
comment_body: string;
thread?: any;
branch: string;
}
interface IssuesOpenedEvent extends BasePayloadEvent {
trigger: "issues_opened";
issue_number: number;
issue_title: string;
issue_body: string | null;
}
interface IssuesAssignedEvent extends BasePayloadEvent {
trigger: "issues_assigned";
issue_number: number;
issue_title: string;
issue_body: string | null;
}
interface IssuesLabeledEvent extends BasePayloadEvent {
trigger: "issues_labeled";
issue_number: number;
issue_title: string;
issue_body: string | null;
}
interface IssueCommentCreatedEvent extends BasePayloadEvent {
trigger: "issue_comment_created";
comment_id: number;
comment_body: string;
issue_number: number;
// PR-specific fields (only present when is_pr is true)
is_pr?: true;
branch?: string;
pr_title?: string;
pr_body?: string | null;
}
interface CheckSuiteCompletedEvent extends BasePayloadEvent {
trigger: "check_suite_completed";
issue_number: number;
is_pr: true;
pr_title: string;
pr_body: string | null;
pull_request: any;
branch: string;
check_suite: {
id: number;
head_sha: string;
head_branch: string | null;
status: string | null;
conclusion: string | null;
url: string;
};
}
interface WorkflowDispatchEvent extends BasePayloadEvent {
trigger: "workflow_dispatch";
}
interface FixReviewEvent extends BasePayloadEvent {
trigger: "fix_review";
issue_number: number;
is_pr: true;
review_id: number;
/** "all" to fix all comments, or specific comment IDs to fix */
comment_ids: number[] | "all";
}
interface UnknownEvent extends BasePayloadEvent {
trigger: "unknown";
}
// discriminated union for payload event based on trigger // discriminated union for payload event based on trigger
// note: all events use issue_number for consistency (PRs are issues in GitHub's API) // note: all events use issue_number for consistency (PRs are issues in GitHub's API)
export type PayloadEvent = export type PayloadEvent =
| { | PullRequestOpenedEvent
trigger: "pull_request_opened"; | PullRequestReadyForReviewEvent
issue_number: number; | PullRequestReviewRequestedEvent
pr_title: string; | PullRequestReviewSubmittedEvent
pr_body: string | null; | PullRequestReviewCommentCreatedEvent
branch: string; | IssuesOpenedEvent
[key: string]: any; | IssuesAssignedEvent
} | IssuesLabeledEvent
| { | IssueCommentCreatedEvent
trigger: "pull_request_ready_for_review"; | CheckSuiteCompletedEvent
issue_number: number; | WorkflowDispatchEvent
pr_title: string; | FixReviewEvent
pr_body: string | null; | UnknownEvent;
branch: string;
[key: string]: any;
}
| {
trigger: "pull_request_review_requested";
issue_number: number;
pr_title: string;
pr_body: string | null;
branch: string;
[key: string]: any;
}
| {
trigger: "pull_request_review_submitted";
issue_number: number;
review_id: number;
review_body: string | null;
review_state: string;
review_comments: any[];
context: any;
branch: string;
[key: string]: any;
}
| {
trigger: "pull_request_review_comment_created";
issue_number: number;
pr_title: string;
comment_id: number;
comment_body: string;
thread?: any;
branch: string;
[key: string]: any;
}
| {
trigger: "issues_opened";
issue_number: number;
issue_title: string;
issue_body: string | null;
[key: string]: any;
}
| {
trigger: "issues_assigned";
issue_number: number;
issue_title: string;
issue_body: string | null;
[key: string]: any;
}
| {
trigger: "issues_labeled";
issue_number: number;
issue_title: string;
issue_body: string | null;
[key: string]: any;
}
| {
trigger: "issue_comment_created";
comment_id: number;
comment_body: string;
issue_number: number;
branch?: string;
[key: string]: any;
}
| {
trigger: "check_suite_completed";
issue_number: number;
pr_title: string;
pr_body: string | null;
pull_request: any;
branch: string;
check_suite: {
id: number;
head_sha: string;
head_branch: string | null;
status: string | null;
conclusion: string | null;
url: string;
};
[key: string]: any;
}
| {
trigger: "workflow_dispatch";
[key: string]: any;
}
| {
trigger: "fix_review";
issue_number: number;
review_id: number;
/** "all" to fix all comments, or specific comment IDs to fix */
comment_ids: number[] | "all";
branch: string;
[key: string]: any;
}
| {
trigger: "unknown";
[key: string]: any;
};
export interface DispatchOptions { export interface DispatchOptions {
/** /**
+1 -1
View File
@@ -111,7 +111,7 @@ see individual files for documentation on other tools:
## usage in agents ## usage in agents
agents should never use the `gh` cli. instead, they should use the mcp tools provided by this server. agents should prefer using the mcp tools provided by this server. the `gh` cli is available as a fallback if needed, but mcp tools handle authentication and provide better integration.
the agent instructions automatically include guidance on using these tools. the agent instructions automatically include guidance on using these tools.
+12 -1
View File
@@ -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,
+6 -24
View File
@@ -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({
@@ -10,7 +8,7 @@ export const PullRequestInfo = type({
export const PullRequestInfoTool = tool({ export const PullRequestInfoTool = tool({
name: "get_pull_request", name: "get_pull_request",
description: description:
"Retrieve PR information and automatically prepare the repository for review by fetching and checking out the PR branch.", "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({
@@ -21,25 +19,8 @@ export const PullRequestInfoTool = tool({
const data = pr.data; const data = pr.data;
const baseBranch = data.base.ref; // detect fork PRs - head repo differs from base repo
const headBranch = data.head.ref; const isFork = data.head.repo.full_name !== data.base.repo.full_name;
if (!baseBranch) {
throw new Error(`Base branch not found for PR #${pull_number}`);
}
// Automatically fetch and checkout branches for review
log.info(`Fetching base branch: origin/${baseBranch}`);
$("git", ["fetch", "origin", baseBranch, "--depth=20"]);
// use GitHub's PR ref which works for both fork and non-fork PRs
// refs/pull/{number}/head always points to the PR head commit
log.info(`Fetching PR #${pull_number} using refs/pull/${pull_number}/head`);
$("git", ["fetch", "origin", `refs/pull/${pull_number}/head`]);
log.info(`Checking out PR branch: ${headBranch}`);
// check out a local branch from FETCH_HEAD (the PR ref we just fetched)
$("git", ["checkout", "-B", headBranch, "FETCH_HEAD"]);
return { return {
number: data.number, number: data.number,
@@ -48,8 +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,
}; };
}), }),
}); });
+1 -1
View File
@@ -85,7 +85,7 @@ ${
prompt: `Follow these steps: prompt: `Follow these steps:
1. Get PR info with ${ghPullfrogMcpName}/get_pull_request (this automatically prepares the repository by fetching and checking out the PR branch) 1. Get PR info with ${ghPullfrogMcpName}/get_pull_request (this automatically prepares the repository by fetching and checking out the PR branch)
2. View diff: \`git diff origin/<base>...origin/<head>\` (replace <base> and <head> with 'base' and 'head' from PR info) 2. **IMPORTANT**: After calling ${ghPullfrogMcpName}/get_pull_request, the PR branch is already checked out locally. View diff using: \`git diff origin/<base>...HEAD\` (replace <base> with 'base' from PR info). Do NOT use \`origin/<head>\` - the branch is checked out locally, not as a remote tracking branch.
3. Read files from the checked-out PR branch to understand the implementation. Always use **relative paths** from repo root (e.g., \`src/index.ts\`), never absolute paths. 3. Read files from the checked-out PR branch to understand the implementation. Always use **relative paths** from repo root (e.g., \`src/index.ts\`), never absolute paths.
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@pullfrog/action", "name": "@pullfrog/action",
"version": "0.0.136", "version": "0.0.138",
"type": "module", "type": "module",
"files": [ "files": [
"index.js", "index.js",
+17 -60
View File
@@ -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,71 +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 event has issue_number and branch, it's likely a PR - try PR ref first (works for forks) log.info(`🌿 Checking out PR #${prNumber}...`);
const issueNumber = "issue_number" in payload.event ? payload.event.issue_number : undefined;
const isLikelyPR = issueNumber !== undefined && branch !== undefined;
if (isLikelyPR) { // gh pr checkout handles fork PRs by setting up remotes automatically
try { const token = getGitHubInstallationToken();
// use GitHub's PR ref which works for both fork and non-fork PRs $("gh", ["pr", "checkout", prNumber.toString()], {
log.debug(`Fetching PR #${issueNumber} using refs/pull/${issueNumber}/head`); cwd: repoDir,
execSync(`git fetch origin refs/pull/${issueNumber}/head`, { env: { GH_TOKEN: token },
cwd: repoDir, });
stdio: "pipe",
});
// checkout from FETCH_HEAD (the PR ref we just fetched) log.info(`✓ Successfully checked out PR #${prNumber}`);
log.debug(`Checking out branch: ${branch}`);
execSync(`git checkout -B ${branch} FETCH_HEAD`, {
cwd: repoDir,
stdio: "pipe",
});
log.info(`✓ Successfully checked out PR branch: ${branch}`);
return;
} catch (error) {
// if PR ref fetch fails, fall back to branch name fetch
log.debug(
`PR ref fetch 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)
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)}`
);
}
} }
+2
View File
@@ -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 ?? "";