Merge pull request #7 from pullfrog/git-setup-overhaul

overhaul git setup
This commit is contained in:
Colin McDonnell
2025-12-16 19:01:23 -08:00
committed by GitHub
20 changed files with 530 additions and 197 deletions
+3 -3
View File
@@ -14,12 +14,12 @@ export const claude = agent({
executablePath: "cli.js", executablePath: "cli.js",
}); });
}, },
run: async ({ payload, mcpServers, apiKey, cliPath, prepResults }) => { run: async ({ payload, mcpServers, apiKey, cliPath, prepResults, repo }) => {
// Ensure API key is NOT in process.env - only pass via SDK's env option // Ensure API key is NOT in process.env - only pass via SDK's env option
delete process.env.ANTHROPIC_API_KEY; delete process.env.ANTHROPIC_API_KEY;
const prompt = addInstructions({ payload, prepResults }); const prompt = addInstructions({ payload, prepResults, repo });
console.log(prompt); log.group("Full prompt", () => log.info(prompt));
// configure sandbox mode if enabled // configure sandbox mode if enabled
const sandboxOptions: Options = payload.sandbox const sandboxOptions: Options = payload.sandbox
+2 -2
View File
@@ -20,7 +20,7 @@ export const codex = agent({
executablePath: "bin/codex.js", executablePath: "bin/codex.js",
}); });
}, },
run: async ({ payload, mcpServers, apiKey, cliPath, prepResults }) => { run: async ({ payload, mcpServers, apiKey, cliPath, prepResults, repo }) => {
// create config directory for codex before setting HOME // create config directory for codex before setting HOME
const tempHome = process.env.PULLFROG_TEMP_DIR!; const tempHome = process.env.PULLFROG_TEMP_DIR!;
const configDir = join(tempHome, ".config", "codex"); const configDir = join(tempHome, ".config", "codex");
@@ -61,7 +61,7 @@ export const codex = agent({
); );
try { try {
const streamedTurn = await thread.runStreamed(addInstructions({ payload, prepResults })); const streamedTurn = await thread.runStreamed(addInstructions({ payload, prepResults, repo }));
let finalOutput = ""; let finalOutput = "";
for await (const event of streamedTurn.events) { for await (const event of streamedTurn.events) {
+3 -2
View File
@@ -91,7 +91,7 @@ export const cursor = agent({
executableName: "cursor-agent", executableName: "cursor-agent",
}); });
}, },
run: async ({ payload, apiKey, cliPath, mcpServers, prepResults }) => { run: async ({ payload, apiKey, cliPath, mcpServers, prepResults, repo }) => {
configureCursorMcpServers({ mcpServers, cliPath }); configureCursorMcpServers({ mcpServers, cliPath });
configureCursorSandbox({ sandbox: payload.sandbox ?? false }); configureCursorSandbox({ sandbox: payload.sandbox ?? false });
@@ -166,7 +166,8 @@ export const cursor = agent({
}; };
try { try {
const fullPrompt = addInstructions({ payload, prepResults }); const fullPrompt = addInstructions({ payload, prepResults, repo });
log.group("Full prompt", () => log.info(fullPrompt));
// configure sandbox mode if enabled // configure sandbox mode if enabled
// in sandbox mode: remove --force flag and rely on cli-config.json sandbox settings // in sandbox mode: remove --force flag and rely on cli-config.json sandbox settings
+3 -3
View File
@@ -154,15 +154,15 @@ export const gemini = agent({
...(githubInstallationToken && { githubInstallationToken }), ...(githubInstallationToken && { githubInstallationToken }),
}); });
}, },
run: async ({ payload, apiKey, mcpServers, cliPath, prepResults }) => { run: async ({ payload, apiKey, mcpServers, cliPath, prepResults, repo }) => {
configureGeminiMcpServers({ mcpServers, cliPath }); configureGeminiMcpServers({ mcpServers, cliPath });
if (!apiKey) { if (!apiKey) {
throw new Error("google_api_key or gemini_api_key is required for gemini agent"); throw new Error("google_api_key or gemini_api_key is required for gemini agent");
} }
const sessionPrompt = addInstructions({ payload, prepResults }); const sessionPrompt = addInstructions({ payload, prepResults, repo });
log.info(`Starting Gemini CLI with prompt: ${payload.prompt.substring(0, 100)}...`); log.group("Full prompt", () => log.info(sessionPrompt));
// configure sandbox mode if enabled // configure sandbox mode if enabled
// --allowed-tools restricts which tools are available (removes others from registry entirely) // --allowed-tools restricts which tools are available (removes others from registry entirely)
+73 -12
View File
@@ -1,3 +1,4 @@
import { execSync } from "node:child_process";
import { encode as toonEncode } from "@toon-format/toon"; import { encode as toonEncode } from "@toon-format/toon";
import type { Payload } from "../external.ts"; import type { Payload } from "../external.ts";
import { ghPullfrogMcpName } from "../external.ts"; import { ghPullfrogMcpName } from "../external.ts";
@@ -62,19 +63,74 @@ function formatPrepResults(results: PrepResult[]): string {
return ""; return "";
} }
return `************* ENVIRONMENT SETUP ************* return lines.join("\n");
}
${lines.join("\n")} interface RepoInfo {
owner: string;
name: string;
defaultBranch: string;
}
`; interface BuildRuntimeContextParams {
repo: RepoInfo;
prepResults: PrepResult[];
}
/**
* Build runtime context string with git status, repo data, and GitHub Actions variables
*/
function buildRuntimeContext({ repo, prepResults }: BuildRuntimeContextParams): string {
const lines: string[] = [];
// working directory
lines.push(`working_directory: ${process.cwd()}`);
// git status (try to get it, but don't fail if git isn't available)
try {
const gitStatus = execSync("git status --short", { encoding: "utf-8", stdio: "pipe" }).trim();
lines.push(`git_status: ${gitStatus || "(clean)"}`);
} catch {
// git not available or not in a repo
}
// repo data
lines.push(`repo: ${repo.owner}/${repo.name}`);
lines.push(`default_branch: ${repo.defaultBranch}`);
// GitHub Actions variables (when running in CI)
const ghVars: Record<string, string | undefined> = {
github_event_name: process.env.GITHUB_EVENT_NAME,
github_ref: process.env.GITHUB_REF,
github_sha: process.env.GITHUB_SHA?.slice(0, 7),
github_actor: process.env.GITHUB_ACTOR,
github_run_id: process.env.GITHUB_RUN_ID,
github_workflow: process.env.GITHUB_WORKFLOW,
};
for (const [key, value] of Object.entries(ghVars)) {
if (value) {
lines.push(`${key}: ${value}`);
}
}
// environment setup (dependency installation results)
const envSetup = formatPrepResults(prepResults);
if (envSetup) {
lines.push("");
lines.push("environment_setup:");
lines.push(envSetup);
}
return lines.join("\n");
} }
interface AddInstructionsParams { interface AddInstructionsParams {
payload: Payload; payload: Payload;
prepResults: PrepResult[]; prepResults: PrepResult[];
repo: RepoInfo;
} }
export const addInstructions = ({ payload, prepResults }: AddInstructionsParams) => { export const addInstructions = ({ payload, prepResults, repo }: AddInstructionsParams) => {
let encodedEvent = ""; let encodedEvent = "";
const eventKeys = Object.keys(payload.event); const eventKeys = Object.keys(payload.event);
@@ -86,7 +142,7 @@ export const addInstructions = ({ payload, prepResults }: AddInstructionsParams)
encodedEvent = toonEncode(payload.event); encodedEvent = toonEncode(payload.event);
} }
const envSetup = formatPrepResults(prepResults); const runtimeContext = buildRuntimeContext({ repo, prepResults });
const dependenciesPreinstalled = prepResults.every((r) => r.dependenciesInstalled) || undefined; const dependenciesPreinstalled = prepResults.every((r) => r.dependenciesInstalled) || undefined;
return ` return `
@@ -163,14 +219,21 @@ Tool names may be formatted as \`(server name)/(tool name)\`, for example: \`${g
**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.
**Available git MCP tools**: **Available git MCP tools**:
- \`${ghPullfrogMcpName}/checkout_pr\` - Checkout an existing PR branch locally (handles fork PRs automatically)
- \`${ghPullfrogMcpName}/create_branch\` - Create a new branch from a base branch - \`${ghPullfrogMcpName}/create_branch\` - Create a new branch from a base branch
- \`${ghPullfrogMcpName}/commit_files\` - Stage and commit files with proper authentication - \`${ghPullfrogMcpName}/commit_files\` - Stage and commit files with proper authentication
- \`${ghPullfrogMcpName}/push_branch\` - Push a branch to the remote repository - \`${ghPullfrogMcpName}/push_branch\` - Push a branch to the remote (automatically uses correct remote for fork PRs)
- \`${ghPullfrogMcpName}/create_pull_request\` - Create a PR from the current branch - \`${ghPullfrogMcpName}/create_pull_request\` - Create a PR from the current branch
**Workflow for making code changes**: **Workflow for working on an existing PR**:
1. Use file operations to create/modify files 1. Use \`${ghPullfrogMcpName}/checkout_pr\` to checkout the PR branch
2. Use \`${ghPullfrogMcpName}/create_branch\` to create a new branch 2. Make your changes using file operations
3. Use \`${ghPullfrogMcpName}/commit_files\` to commit your changes
4. Use \`${ghPullfrogMcpName}/push_branch\` to push (automatically pushes to fork for fork PRs)
**Workflow for creating new changes**:
1. Use \`${ghPullfrogMcpName}/create_branch\` to create a new branch
2. Make your changes using file operations
3. Use \`${ghPullfrogMcpName}/commit_files\` to commit your changes 3. Use \`${ghPullfrogMcpName}/commit_files\` to commit your changes
4. Use \`${ghPullfrogMcpName}/push_branch\` to push the branch 4. Use \`${ghPullfrogMcpName}/push_branch\` to push the branch
5. Use \`${ghPullfrogMcpName}/create_pull_request\` to create a PR 5. Use \`${ghPullfrogMcpName}/create_pull_request\` to create a PR
@@ -221,7 +284,5 @@ ${encodedEvent}`
************* RUNTIME CONTEXT ************* ************* RUNTIME CONTEXT *************
working_directory: ${process.cwd()} ${runtimeContext}`;
${envSetup}`;
}; };
+3 -2
View File
@@ -304,7 +304,7 @@ export const opencode = agent({
installDependencies: true, installDependencies: true,
}); });
}, },
run: async ({ payload, apiKey: _apiKey, apiKeys, mcpServers, cliPath, prepResults }) => { run: async ({ payload, apiKey: _apiKey, apiKeys, mcpServers, cliPath, prepResults, repo }) => {
// 1. configure home/config directory // 1. configure home/config directory
const tempHome = process.env.PULLFROG_TEMP_DIR!; const tempHome = process.env.PULLFROG_TEMP_DIR!;
const configDir = join(tempHome, ".config", "opencode"); const configDir = join(tempHome, ".config", "opencode");
@@ -313,7 +313,8 @@ export const opencode = agent({
configureOpenCodeMcpServers({ mcpServers }); configureOpenCodeMcpServers({ mcpServers });
configureOpenCodeSandbox({ sandbox: payload.sandbox ?? false }); configureOpenCodeSandbox({ sandbox: payload.sandbox ?? false });
const prompt = addInstructions({ payload, prepResults }); const prompt = addInstructions({ payload, prepResults, repo });
log.group("Full prompt", () => log.info(prompt));
const args = ["run", "--format", "json", prompt]; const args = ["run", "--format", "json", prompt];
if (payload.sandbox) { if (payload.sandbox) {
+10
View File
@@ -21,6 +21,15 @@ export interface AgentResult {
metadata?: Record<string, unknown>; metadata?: Record<string, unknown>;
} }
/**
* Repo info for agent context
*/
export interface RepoInfo {
owner: string;
name: string;
defaultBranch: string;
}
/** /**
* Configuration for agent creation * Configuration for agent creation
*/ */
@@ -31,6 +40,7 @@ export interface AgentConfig {
mcpServers: Record<string, McpHttpServerConfig>; mcpServers: Record<string, McpHttpServerConfig>;
cliPath: string; cliPath: string;
prepResults: PrepResult[]; prepResults: PrepResult[];
repo: RepoInfo;
} }
/** /**
+202 -86
View File
@@ -80489,6 +80489,9 @@ function formatIndentedField(label, content) {
return formatted; return formatted;
} }
// agents/instructions.ts
import { execSync } from "node:child_process";
// ../node_modules/.pnpm/@toon-format+toon@1.0.0/node_modules/@toon-format/toon/dist/index.js // ../node_modules/.pnpm/@toon-format+toon@1.0.0/node_modules/@toon-format/toon/dist/index.js
var LIST_ITEM_MARKER = "-"; var LIST_ITEM_MARKER = "-";
var LIST_ITEM_PREFIX = "- "; var LIST_ITEM_PREFIX = "- ";
@@ -89416,10 +89419,11 @@ function getModes({
name: "Address Reviews", name: "Address Reviews",
description: "Address PR review feedback; respond to reviewer comments; make requested changes to an existing PR", description: "Address PR review feedback; respond to reviewer comments; make requested changes to an existing PR",
prompt: `Follow these steps: prompt: `Follow these steps:
1. Get PR info with ${ghPullfrogMcpName}/get_pull_request (this automatically fetches and checks out the PR branch) 1. Checkout the PR using ${ghPullfrogMcpName}/checkout_pr with the PR number. This fetches the PR branch and configures push settings (including for fork PRs).
2. Review the feedback provided. Understand each review comment and what changes are being requested. 2. Review the feedback provided. Understand each review comment and what changes are being requested.
- **EVENT DATA may contain review comment details**: If available, \`approved_comments\` are comments to address, \`unapproved_comments\` are for context only. The \`triggerer\` field indicates who initiated this action - prioritize their replies when deciding how to implement fixes. - **EVENT DATA may contain review comment details**: If available, \`approved_comments\` are comments to address, \`unapproved_comments\` are for context only. The \`triggerer\` field indicates who initiated this action - prioritize their replies when deciding how to implement fixes.
- You can use ${ghPullfrogMcpName}/get_pull_request to get PR metadata if needed.
3. If the request requires understanding the codebase structure, dependencies, or conventions, gather relevant context. Read AGENTS.md if it exists. 3. If the request requires understanding the codebase structure, dependencies, or conventions, gather relevant context. Read AGENTS.md if it exists.
@@ -89429,7 +89433,7 @@ function getModes({
6. Test your changes to ensure they work correctly. 6. Test your changes to ensure they work correctly.
7. When done, commit and push your changes to the existing PR branch. Do not create a new branch or PR - you are updating an existing one. 7. When done, commit your changes with ${ghPullfrogMcpName}/commit_files, then push with ${ghPullfrogMcpName}/push_branch. The push will automatically go to the correct remote (including fork repos). Do not create a new branch or PR - you are updating an existing one.
${disableProgressComment ? "" : ` ${disableProgressComment ? "" : `
8. ${reportProgressInstruction} 8. ${reportProgressInstruction}
@@ -89439,9 +89443,9 @@ ${disableProgressComment ? "" : `
name: "Review", name: "Review",
description: "Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness", description: "Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
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. Checkout the PR using ${ghPullfrogMcpName}/checkout_pr with the PR number. This fetches the PR branch and base branch, preparing the repo for review.
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. 2. **IMPORTANT**: After calling checkout_pr, the PR branch is checked out locally. View diff using: \`git diff origin/<base>..HEAD\` (replace <base> with 'base' from checkout_pr result, e.g., \`git diff origin/main..HEAD\`). Use two dots (..) not three dots (...) for reliable diffs. Do NOT use \`origin/<head>\` - the branch is checked out locally, not as a remote tracking branch. This works for both same-repo and fork PRs.
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.
@@ -89449,7 +89453,8 @@ ${disableProgressComment ? "" : `
**GENERAL GUIDANCE** **GENERAL GUIDANCE**
- *CRITICAL* \u2014\xA0Use **relative paths** from repo root (e.g., \`packages/core/src/utils.ts\`) - Do not leave any comments that are not potentially actionable. Do not leave complimentary comments just to be nice.
- *CRITICAL* \u2014 Use **relative paths** from repo root (e.g., \`packages/core/src/utils.ts\`)
- For line numbers, use the NEW file line number from the diff (shown after \`+\` in hunk headers like \`@@ -10,5 +12,8 @@\` means new file starts at line 12) - For line numbers, use the NEW file line number from the diff (shown after \`+\` in hunk headers like \`@@ -10,5 +12,8 @@\` means new file starts at line 12)
- Only comment on lines that appear in the diff. GitHub will reject comments on unchanged lines. - Only comment on lines that appear in the diff. GitHub will reject comments on unchanged lines.
- **CRITICAL: Prioritize per-line feedback over summary text.** - **CRITICAL: Prioritize per-line feedback over summary text.**
@@ -89459,7 +89464,6 @@ ${disableProgressComment ? "" : `
- 95%+ of review content should be in per-line comments; the body should be just a couple sentences - 95%+ of review content should be in per-line comments; the body should be just a couple sentences
- The review body will include quick action links for addressing feedback, so keep it concise - The review body will include quick action links for addressing feedback, so keep it concise
- Do not nitpick unless instructed explicity to do so by the user's additional instructions. This includes: requesting documentation/docstrings/JSDoc. - Do not nitpick unless instructed explicity to do so by the user's additional instructions. This includes: requesting documentation/docstrings/JSDoc.
- Do not leave any comments that are not potentially actionable.
- The review should be thoughtful. When evaluating complex changes, consider the following conceptual approach: - The review should be thoughtful. When evaluating complex changes, consider the following conceptual approach:
- 1) conceptualize the changes made. make sure you understand it. - 1) conceptualize the changes made. make sure you understand it.
- 2) evaluate conceptual approach. leave feedback as needed. - 2) evaluate conceptual approach. leave feedback as needed.
@@ -89554,20 +89558,47 @@ function formatPrepResults(results) {
if (lines.length === 0) { if (lines.length === 0) {
return ""; return "";
} }
return `************* ENVIRONMENT SETUP ************* return lines.join("\n");
${lines.join("\n")}
`;
} }
var addInstructions = ({ payload, prepResults }) => { function buildRuntimeContext({ repo, prepResults }) {
const lines = [];
lines.push(`working_directory: ${process.cwd()}`);
try {
const gitStatus = execSync("git status --short", { encoding: "utf-8", stdio: "pipe" }).trim();
lines.push(`git_status: ${gitStatus || "(clean)"}`);
} catch {
}
lines.push(`repo: ${repo.owner}/${repo.name}`);
lines.push(`default_branch: ${repo.defaultBranch}`);
const ghVars = {
github_event_name: process.env.GITHUB_EVENT_NAME,
github_ref: process.env.GITHUB_REF,
github_sha: process.env.GITHUB_SHA?.slice(0, 7),
github_actor: process.env.GITHUB_ACTOR,
github_run_id: process.env.GITHUB_RUN_ID,
github_workflow: process.env.GITHUB_WORKFLOW
};
for (const [key, value2] of Object.entries(ghVars)) {
if (value2) {
lines.push(`${key}: ${value2}`);
}
}
const envSetup = formatPrepResults(prepResults);
if (envSetup) {
lines.push("");
lines.push("environment_setup:");
lines.push(envSetup);
}
return lines.join("\n");
}
var addInstructions = ({ payload, prepResults, repo }) => {
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 {
encodedEvent = encode(payload.event); encodedEvent = encode(payload.event);
} }
const envSetup = formatPrepResults(prepResults); const runtimeContext = buildRuntimeContext({ repo, prepResults });
const dependenciesPreinstalled = prepResults.every((r) => r.dependenciesInstalled) || void 0; const dependenciesPreinstalled = prepResults.every((r) => r.dependenciesInstalled) || void 0;
return ` return `
*********************************************** ***********************************************
@@ -89643,14 +89674,21 @@ Tool names may be formatted as \`(server name)/(tool name)\`, for example: \`${g
**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.
**Available git MCP tools**: **Available git MCP tools**:
- \`${ghPullfrogMcpName}/checkout_pr\` - Checkout an existing PR branch locally (handles fork PRs automatically)
- \`${ghPullfrogMcpName}/create_branch\` - Create a new branch from a base branch - \`${ghPullfrogMcpName}/create_branch\` - Create a new branch from a base branch
- \`${ghPullfrogMcpName}/commit_files\` - Stage and commit files with proper authentication - \`${ghPullfrogMcpName}/commit_files\` - Stage and commit files with proper authentication
- \`${ghPullfrogMcpName}/push_branch\` - Push a branch to the remote repository - \`${ghPullfrogMcpName}/push_branch\` - Push a branch to the remote (automatically uses correct remote for fork PRs)
- \`${ghPullfrogMcpName}/create_pull_request\` - Create a PR from the current branch - \`${ghPullfrogMcpName}/create_pull_request\` - Create a PR from the current branch
**Workflow for making code changes**: **Workflow for working on an existing PR**:
1. Use file operations to create/modify files 1. Use \`${ghPullfrogMcpName}/checkout_pr\` to checkout the PR branch
2. Use \`${ghPullfrogMcpName}/create_branch\` to create a new branch 2. Make your changes using file operations
3. Use \`${ghPullfrogMcpName}/commit_files\` to commit your changes
4. Use \`${ghPullfrogMcpName}/push_branch\` to push (automatically pushes to fork for fork PRs)
**Workflow for creating new changes**:
1. Use \`${ghPullfrogMcpName}/create_branch\` to create a new branch
2. Make your changes using file operations
3. Use \`${ghPullfrogMcpName}/commit_files\` to commit your changes 3. Use \`${ghPullfrogMcpName}/commit_files\` to commit your changes
4. Use \`${ghPullfrogMcpName}/push_branch\` to push the branch 4. Use \`${ghPullfrogMcpName}/push_branch\` to push the branch
5. Use \`${ghPullfrogMcpName}/create_pull_request\` to create a PR 5. Use \`${ghPullfrogMcpName}/create_pull_request\` to create a PR
@@ -89694,9 +89732,7 @@ ${encodedEvent}` : ""}
************* RUNTIME CONTEXT ************* ************* RUNTIME CONTEXT *************
working_directory: ${process.cwd()} ${runtimeContext}`;
${envSetup}`;
}; };
// agents/shared.ts // agents/shared.ts
@@ -90117,9 +90153,9 @@ var claude = agent({
executablePath: "cli.js" executablePath: "cli.js"
}); });
}, },
run: async ({ payload, mcpServers, apiKey, cliPath, prepResults }) => { run: async ({ payload, mcpServers, apiKey, cliPath, prepResults, repo }) => {
delete process.env.ANTHROPIC_API_KEY; delete process.env.ANTHROPIC_API_KEY;
const prompt = addInstructions({ payload, prepResults }); const prompt = addInstructions({ payload, prepResults, repo });
console.log(prompt); console.log(prompt);
const sandboxOptions = payload.sandbox ? { const sandboxOptions = payload.sandbox ? {
permissionMode: "default", permissionMode: "default",
@@ -90575,7 +90611,7 @@ var codex = agent({
executablePath: "bin/codex.js" executablePath: "bin/codex.js"
}); });
}, },
run: async ({ payload, mcpServers, apiKey, cliPath, prepResults }) => { run: async ({ payload, mcpServers, apiKey, cliPath, prepResults, repo }) => {
const tempHome = process.env.PULLFROG_TEMP_DIR; const tempHome = process.env.PULLFROG_TEMP_DIR;
const configDir = join5(tempHome, ".config", "codex"); const configDir = join5(tempHome, ".config", "codex");
mkdirSync2(configDir, { recursive: true }); mkdirSync2(configDir, { recursive: true });
@@ -90605,7 +90641,7 @@ var codex = agent({
} }
); );
try { try {
const streamedTurn = await thread.runStreamed(addInstructions({ payload, prepResults })); const streamedTurn = await thread.runStreamed(addInstructions({ payload, prepResults, repo }));
let finalOutput2 = ""; let finalOutput2 = "";
for await (const event of streamedTurn.events) { for await (const event of streamedTurn.events) {
const handler2 = messageHandlers2[event.type]; const handler2 = messageHandlers2[event.type];
@@ -90747,7 +90783,7 @@ var cursor = agent({
executableName: "cursor-agent" executableName: "cursor-agent"
}); });
}, },
run: async ({ payload, apiKey, cliPath, mcpServers, prepResults }) => { run: async ({ payload, apiKey, cliPath, mcpServers, prepResults, repo }) => {
configureCursorMcpServers({ mcpServers, cliPath }); configureCursorMcpServers({ mcpServers, cliPath });
configureCursorSandbox({ sandbox: payload.sandbox ?? false }); configureCursorSandbox({ sandbox: payload.sandbox ?? false });
const loggedModelCallIds = /* @__PURE__ */ new Set(); const loggedModelCallIds = /* @__PURE__ */ new Set();
@@ -90800,7 +90836,7 @@ var cursor = agent({
} }
}; };
try { try {
const fullPrompt = addInstructions({ payload, prepResults }); const fullPrompt = addInstructions({ payload, prepResults, repo });
const cursorArgs = payload.sandbox ? [ const cursorArgs = payload.sandbox ? [
"--print", "--print",
fullPrompt, fullPrompt,
@@ -91104,12 +91140,12 @@ var gemini = agent({
...githubInstallationToken2 && { githubInstallationToken: githubInstallationToken2 } ...githubInstallationToken2 && { githubInstallationToken: githubInstallationToken2 }
}); });
}, },
run: async ({ payload, apiKey, mcpServers, cliPath, prepResults }) => { run: async ({ payload, apiKey, mcpServers, cliPath, prepResults, repo }) => {
configureGeminiMcpServers({ mcpServers, cliPath }); configureGeminiMcpServers({ mcpServers, cliPath });
if (!apiKey) { if (!apiKey) {
throw new Error("google_api_key or gemini_api_key is required for gemini agent"); throw new Error("google_api_key or gemini_api_key is required for gemini agent");
} }
const sessionPrompt = addInstructions({ payload, prepResults }); const sessionPrompt = addInstructions({ payload, prepResults, repo });
log.info(`Starting Gemini CLI with prompt: ${payload.prompt.substring(0, 100)}...`); log.info(`Starting Gemini CLI with prompt: ${payload.prompt.substring(0, 100)}...`);
const args3 = payload.sandbox ? [ const args3 = payload.sandbox ? [
"--allowed-tools", "--allowed-tools",
@@ -91365,13 +91401,13 @@ var opencode = agent({
installDependencies: true installDependencies: true
}); });
}, },
run: async ({ payload, apiKey: _apiKey, apiKeys, mcpServers, cliPath, prepResults }) => { run: async ({ payload, apiKey: _apiKey, apiKeys, mcpServers, cliPath, prepResults, repo }) => {
const tempHome = process.env.PULLFROG_TEMP_DIR; const tempHome = process.env.PULLFROG_TEMP_DIR;
const configDir = join7(tempHome, ".config", "opencode"); const configDir = join7(tempHome, ".config", "opencode");
mkdirSync4(configDir, { recursive: true }); mkdirSync4(configDir, { recursive: true });
configureOpenCodeMcpServers({ mcpServers }); configureOpenCodeMcpServers({ mcpServers });
configureOpenCodeSandbox({ sandbox: payload.sandbox ?? false }); configureOpenCodeSandbox({ sandbox: payload.sandbox ?? false });
const prompt = addInstructions({ payload, prepResults }); const prompt = addInstructions({ payload, prepResults, repo });
const args3 = ["run", "--format", "json", prompt]; const args3 = ["run", "--format", "json", prompt];
if (payload.sandbox) { if (payload.sandbox) {
log.info("\u{1F512} sandbox mode enabled: restricting to read-only operations"); log.info("\u{1F512} sandbox mode enabled: restricting to read-only operations");
@@ -95064,7 +95100,8 @@ function buildPullfrogFooter(params) {
parts.push(`Using [${params.agent.displayName}](${params.agent.url})`); parts.push(`Using [${params.agent.displayName}](${params.agent.url})`);
} }
if (params.workflowRun) { if (params.workflowRun) {
const url2 = params.workflowRun.htmlUrl ?? `https://github.com/${params.workflowRun.owner}/${params.workflowRun.repo}/actions/runs/${params.workflowRun.runId}`; const baseUrl = `https://github.com/${params.workflowRun.owner}/${params.workflowRun.repo}/actions/runs/${params.workflowRun.runId}`;
const url2 = params.workflowRun.jobId ? `${baseUrl}/job/${params.workflowRun.jobId}` : baseUrl;
parts.push(`[View workflow run](${url2})`); parts.push(`[View workflow run](${url2})`);
} }
if (params.customParts) { if (params.customParts) {
@@ -123934,6 +123971,70 @@ function $(cmd, args3, options) {
return stdout.trim(); return stdout.trim();
} }
// mcp/checkout.ts
var CheckoutPr = type({
pull_number: type.number.describe("the pull request number to checkout")
});
function CheckoutPrTool(ctx) {
return tool({
name: "checkout_pr",
description: "Checkout a pull request branch locally. This fetches the PR branch and sets up push configuration for fork PRs. Use this when you need to work on an existing PR.",
parameters: CheckoutPr,
execute: execute(ctx, async ({ pull_number }) => {
log.info(`\u{1F500} checking out PR #${pull_number}...`);
const pr = await ctx.octokit.rest.pulls.get({
owner: ctx.owner,
repo: ctx.name,
pull_number
});
const headRepo = pr.data.head.repo;
if (!headRepo) {
throw new Error(`PR #${pull_number} source repository was deleted`);
}
const isFork = headRepo.full_name !== pr.data.base.repo.full_name;
const baseBranch = pr.data.base.ref;
const headBranch = pr.data.head.ref;
log.info(`\u{1F4E5} fetching base branch (${baseBranch})...`);
$("git", ["fetch", "--no-tags", "origin", baseBranch]);
log.info(`\u{1F33F} fetching PR #${pull_number} (${headBranch})...`);
$("git", ["fetch", "--no-tags", "origin", `pull/${pull_number}/head:${headBranch}`]);
$("git", ["checkout", headBranch]);
log.info(`\u2713 checked out PR #${pull_number}`);
if (isFork) {
const remoteName = `pr-${pull_number}`;
const forkUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${headRepo.full_name}.git`;
try {
$("git", ["remote", "add", remoteName, forkUrl]);
log.info(`\u{1F4CC} added remote '${remoteName}' for fork ${headRepo.full_name}`);
} catch {
$("git", ["remote", "set-url", remoteName, forkUrl]);
log.info(`\u{1F4CC} updated remote '${remoteName}' for fork ${headRepo.full_name}`);
}
$("git", ["config", `branch.${headBranch}.pushRemote`, remoteName]);
log.info(`\u{1F4CC} configured branch '${headBranch}' to push to '${remoteName}'`);
if (!pr.data.maintainer_can_modify) {
log.warning(
`\u26A0\uFE0F fork PR has maintainer_can_modify=false - push operations will fail. ask the PR author to enable "Allow edits from maintainers" or the fork may be owned by an organization.`
);
}
} else {
$("git", ["config", `branch.${headBranch}.pushRemote`, "origin"]);
}
return {
success: true,
number: pr.data.number,
title: pr.data.title,
base: baseBranch,
head: headBranch,
isFork,
maintainerCanModify: pr.data.maintainer_can_modify,
url: pr.data.html_url,
headRepo: headRepo.full_name
};
})
});
}
// mcp/debug.ts // mcp/debug.ts
var DebugShellCommand = type({}); var DebugShellCommand = type({});
var DebugShellCommandTool = tool({ var DebugShellCommandTool = tool({
@@ -124079,27 +124180,30 @@ var PushBranch = type({
branchName: type.string.describe("The branch name to push (defaults to current branch)").optional(), branchName: type.string.describe("The branch name to push (defaults to current branch)").optional(),
force: type.boolean.describe("Force push (use with caution)").default(false) force: type.boolean.describe("Force push (use with caution)").default(false)
}); });
function PushBranchTool(ctx) { function PushBranchTool(_ctx) {
const remote = ctx.pushRemote;
return tool({ return tool({
name: "push_branch", name: "push_branch",
description: "Push the current branch (or specified branch) to the remote repository. Never force push unless explicitly requested.", description: "Push the current branch (or specified branch) to the remote repository. Git automatically determines the correct remote based on branch config (set by checkout_pr for fork PRs). Never force push unless explicitly requested.",
parameters: PushBranch, parameters: PushBranch,
execute: execute(ctx, async ({ branchName, force }) => { execute: execute(_ctx, async ({ branchName, force }) => {
const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false }); const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
const isUrl = remote.startsWith("https://"); let remote = "origin";
const args3 = force ? ["push", "--force", ...isUrl ? [] : ["-u"], remote, branch] : ["push", ...isUrl ? [] : ["-u"], remote, branch]; try {
log.info(`Pushing branch ${branch} to ${isUrl ? "(fork URL)" : remote}`); remote = $("git", ["config", `branch.${branch}.pushRemote`], { log: false }).trim();
} catch {
}
const args3 = force ? ["push", "--force", "-u", remote, branch] : ["push", "-u", remote, branch];
log.info(`pushing branch ${branch} to ${remote}`);
if (force) { if (force) {
log.warning(`Force pushing - this will overwrite remote history`); log.warning(`force pushing - this will overwrite remote history`);
} }
$("git", args3); $("git", args3);
return { return {
success: true, success: true,
branch, branch,
remote: isUrl ? "(fork URL)" : remote, remote,
force, force,
message: `Successfully pushed branch ${branch}` message: `successfully pushed branch ${branch}`
}; };
}) })
}); });
@@ -124325,6 +124429,19 @@ var PullRequest = type({
body: type.string.describe("the body content of the pull request"), body: type.string.describe("the body content of the pull request"),
base: type.string.describe("the base branch to merge into (e.g., 'main')") base: type.string.describe("the base branch to merge into (e.g., 'main')")
}); });
function buildPrBodyWithFooter(ctx, body) {
const repoContext = parseRepoContext();
const runId = process.env.GITHUB_RUN_ID;
const agentName = ctx.payload.agent;
const agentInfo = agentName ? agentsManifest[agentName] : null;
const footer = buildPullfrogFooter({
triggeredBy: true,
agent: agentInfo ? { displayName: agentInfo.displayName, url: agentInfo.url } : void 0,
workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : void 0
});
const bodyWithoutFooter = stripExistingFooter(body);
return `${bodyWithoutFooter}${footer}`;
}
function PullRequestTool(ctx) { function PullRequestTool(ctx) {
return tool({ return tool({
name: "create_pull_request", name: "create_pull_request",
@@ -124338,17 +124455,18 @@ function PullRequestTool(ctx) {
"PR creation blocked: secrets detected in PR title or body. Please remove any sensitive information (API keys, tokens, passwords) before creating a PR." "PR creation blocked: secrets detected in PR title or body. Please remove any sensitive information (API keys, tokens, passwords) before creating a PR."
); );
} }
const diff = $("git", ["diff", `origin/${base}...HEAD`], { log: false }); const diff = $("git", ["diff", `origin/${base}..HEAD`], { log: false });
if (containsSecrets(diff)) { if (containsSecrets(diff)) {
throw new Error( throw new Error(
"PR creation blocked: secrets detected in changes. Please remove any sensitive information (API keys, tokens, passwords) before creating a PR." "PR creation blocked: secrets detected in changes. Please remove any sensitive information (API keys, tokens, passwords) before creating a PR."
); );
} }
const bodyWithFooter = buildPrBodyWithFooter(ctx, body);
const result = await ctx.octokit.rest.pulls.create({ const result = await ctx.octokit.rest.pulls.create({
owner: ctx.owner, owner: ctx.owner,
repo: ctx.name, repo: ctx.name,
title, title,
body, body: bodyWithFooter,
head: currentBranch, head: currentBranch,
base base
}); });
@@ -124372,7 +124490,7 @@ var PullRequestInfo = type({
function PullRequestInfoTool(ctx) { function PullRequestInfoTool(ctx) {
return tool({ return tool({
name: "get_pull_request", name: "get_pull_request",
description: "Retrieve PR information (metadata only). PR branch is already checked out during setup.", description: "Retrieve PR metadata (number, title, state, base/head branches, fork status). To checkout a PR branch locally, use checkout_pr instead.",
parameters: PullRequestInfo, parameters: PullRequestInfo,
execute: execute(ctx, async ({ pull_number }) => { execute: execute(ctx, async ({ pull_number }) => {
const pr = await ctx.octokit.rest.pulls.get({ const pr = await ctx.octokit.rest.pulls.get({
@@ -124417,7 +124535,9 @@ var Review = type({
), ),
start_line: type.number.describe("Start line for multi-line comments (optional, for commenting on ranges)").optional() start_line: type.number.describe("Start line for multi-line comments (optional, for commenting on ranges)").optional()
}).array().describe( }).array().describe(
"PRIMARY location for ALL feedback. 95%+ of review content should be here. Use 'git diff origin/<base>...origin/<head>' to find correct line numbers (RIGHT side for new code, LEFT for old)." // FORK PR NOTE: use HEAD not origin/<head> - for fork PRs, origin/<head> doesn't exist
// because the head branch is in a different repo (the fork). HEAD is the locally checked out PR branch.
"PRIMARY location for ALL feedback. 95%+ of review content should be here. Use 'git diff origin/<base>..HEAD' to find correct line numbers (RIGHT side for new code, LEFT for old). Works for both fork and same-repo PRs."
).optional() ).optional()
}); });
function ReviewTool(ctx) { function ReviewTool(ctx) {
@@ -124462,6 +124582,7 @@ function ReviewTool(ctx) {
const fixAllUrl = `${apiUrl}/trigger/${ctx.owner}/${ctx.name}/${pull_number}?action=fix&review_id=${reviewId}`; const fixAllUrl = `${apiUrl}/trigger/${ctx.owner}/${ctx.name}/${pull_number}?action=fix&review_id=${reviewId}`;
const fixApprovedUrl = `${apiUrl}/trigger/${ctx.owner}/${ctx.name}/${pull_number}?action=fix-approved&review_id=${reviewId}`; const fixApprovedUrl = `${apiUrl}/trigger/${ctx.owner}/${ctx.name}/${pull_number}?action=fix-approved&review_id=${reviewId}`;
const footer = buildPullfrogFooter({ const footer = buildPullfrogFooter({
workflowRun: { owner: ctx.owner, repo: ctx.name, runId: ctx.runId, jobId: ctx.jobId },
customParts: [`[Fix all \u2794](${fixAllUrl})`, `[Fix \u{1F44D}s \u2794](${fixApprovedUrl})`] customParts: [`[Fix all \u2794](${fixAllUrl})`, `[Fix \u{1F44D}s \u2794](${fixApprovedUrl})`]
}); });
const updatedBody = (body || "") + footer; const updatedBody = (body || "") + footer;
@@ -124695,6 +124816,7 @@ async function startMcpHttpServer(ctx) {
PullRequestTool(ctx), PullRequestTool(ctx),
ReviewTool(ctx), ReviewTool(ctx),
PullRequestInfoTool(ctx), PullRequestInfoTool(ctx),
CheckoutPrTool(ctx),
GetReviewCommentsTool(ctx), GetReviewCommentsTool(ctx),
ListPullRequestReviewsTool(ctx), ListPullRequestReviewsTool(ctx),
GetCheckSuiteLogsTool(ctx), GetCheckSuiteLogsTool(ctx),
@@ -125328,16 +125450,16 @@ ${error42}` : `\u274C ${error42}`;
} }
// utils/setup.ts // utils/setup.ts
import { execSync } from "node:child_process"; import { execSync as execSync2 } from "node:child_process";
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...");
try { try {
execSync('git config --local user.email "team@pullfrog.com"', { execSync2('git config --local user.email "team@pullfrog.com"', {
cwd: repoDir, cwd: repoDir,
stdio: "pipe" stdio: "pipe"
}); });
execSync('git config --local user.name "pullfrog"', { execSync2('git config --local user.name "pullfrog"', {
cwd: repoDir, cwd: repoDir,
stdio: "pipe" stdio: "pipe"
}); });
@@ -125350,44 +125472,19 @@ function setupGitConfig() {
} }
async function setupGit(ctx) { async function setupGit(ctx) {
const repoDir = process.cwd(); const repoDir = process.cwd();
log.info("\u{1F527} Setting up git authentication..."); log.info("\u{1F527} setting up git authentication...");
try { try {
execSync("git config --local --unset-all http.https://github.com/.extraheader", { execSync2("git config --local --unset-all http.https://github.com/.extraheader", {
cwd: repoDir, cwd: repoDir,
stdio: "pipe" stdio: "pipe"
}); });
log.info("\u2713 Removed existing authentication headers"); log.info("\u2713 removed existing authentication headers");
} catch { } catch {
log.debug("No existing authentication headers to remove"); log.debug("no existing authentication headers to remove");
} }
const originUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${ctx.owner}/${ctx.name}.git`; const originUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${ctx.owner}/${ctx.name}.git`;
$("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir }); $("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
log.info("\u2713 Updated origin URL with authentication token"); log.info("\u2713 updated origin URL with authentication token");
if (ctx.payload.event.is_pr !== true || !ctx.payload.event.issue_number) {
return { pushRemote: "origin" };
}
const prNumber = ctx.payload.event.issue_number;
const pr = await ctx.octokit.rest.pulls.get({
owner: ctx.owner,
repo: ctx.name,
pull_number: prNumber
});
const headRepo = pr.data.head.repo;
if (!headRepo) {
throw new Error(`PR #${prNumber} source repository was deleted`);
}
const branch = pr.data.head.ref;
log.info(`\u{1F33F} Checking out PR #${prNumber} (${branch})...`);
$("git", ["fetch", "origin", `pull/${prNumber}/head:${branch}`], { cwd: repoDir });
$("git", ["checkout", branch], { cwd: repoDir });
log.info(`\u2713 Successfully checked out PR #${prNumber}`);
const token = process.env.GITHUB_TOKEN || ctx.githubInstallationToken;
const pushUrl = `https://x-access-token:${token}@github.com/${headRepo.full_name}.git`;
const isFork = headRepo.full_name !== pr.data.base.repo.full_name;
if (isFork) {
log.info(`\u{1F374} Fork PR detected, will push to: ${headRepo.full_name}`);
}
return { pushRemote: pushUrl };
} }
// utils/timer.ts // utils/timer.ts
@@ -125426,8 +125523,7 @@ async function main(inputs) {
const partialCtx = await initializeContext(inputs, payload); const partialCtx = await initializeContext(inputs, payload);
const ctx = partialCtx; const ctx = partialCtx;
timer.checkpoint("initializeContext"); timer.checkpoint("initializeContext");
const { pushRemote } = await setupGit(ctx); await setupGit(ctx);
ctx.pushRemote = pushRemote;
timer.checkpoint("setupGit"); timer.checkpoint("setupGit");
await setupTempDirectory(ctx); await setupTempDirectory(ctx);
timer.checkpoint("setupTempDirectory"); timer.checkpoint("setupTempDirectory");
@@ -125636,11 +125732,26 @@ function parsePayload(inputs) {
} }
async function startMcpServer(ctx) { async function startMcpServer(ctx) {
const runId = process.env.GITHUB_RUN_ID; const runId = process.env.GITHUB_RUN_ID;
if (runId) { if (!runId) {
const workflowRunInfo = await fetchWorkflowRunInfo(runId); throw new Error("GITHUB_RUN_ID environment variable is required");
if (workflowRunInfo.progressCommentId) { }
process.env.PULLFROG_PROGRESS_COMMENT_ID = workflowRunInfo.progressCommentId; ctx.runId = runId;
log.info(`\u{1F4DD} Using pre-created progress comment: ${workflowRunInfo.progressCommentId}`); const workflowRunInfo = await fetchWorkflowRunInfo(ctx.runId);
if (workflowRunInfo.progressCommentId) {
process.env.PULLFROG_PROGRESS_COMMENT_ID = workflowRunInfo.progressCommentId;
log.info(`\u{1F4DD} Using pre-created progress comment: ${workflowRunInfo.progressCommentId}`);
}
const jobName = process.env.GITHUB_JOB;
if (jobName) {
const jobs = await ctx.octokit.rest.actions.listJobsForWorkflowRun({
owner: ctx.owner,
repo: ctx.name,
run_id: parseInt(ctx.runId, 10)
});
const matchingJob = jobs.data.jobs.find((job) => job.name === jobName);
if (matchingJob) {
ctx.jobId = String(matchingJob.id);
log.info(`\u{1F4CB} Found job ID: ${ctx.jobId}`);
} }
} }
const { url: url2, close } = await startMcpHttpServer(ctx); const { url: url2, close } = await startMcpHttpServer(ctx);
@@ -125695,7 +125806,12 @@ ${encode(eventWithoutContext)}`;
apiKey: ctx.apiKey, apiKey: ctx.apiKey,
apiKeys: ctx.apiKeys, apiKeys: ctx.apiKeys,
cliPath: ctx.cliPath, cliPath: ctx.cliPath,
prepResults: ctx.prepResults prepResults: ctx.prepResults,
repo: {
owner: ctx.owner,
name: ctx.name,
defaultBranch: ctx.repo.default_branch
}
}); });
} }
async function handleAgentResult(result) { async function handleAgentResult(result) {
+1 -1
View File
@@ -1 +1 @@
Tell me a joke Implement .omit() in a new PR
+36 -10
View File
@@ -65,8 +65,7 @@ export async function main(inputs: Inputs): Promise<MainResult> {
const ctx = partialCtx as Context; const ctx = partialCtx as Context;
timer.checkpoint("initializeContext"); timer.checkpoint("initializeContext");
const { pushRemote } = await setupGit(ctx); await setupGit(ctx);
ctx.pushRemote = pushRemote;
timer.checkpoint("setupGit"); timer.checkpoint("setupGit");
await setupTempDirectory(ctx); await setupTempDirectory(ctx);
@@ -225,7 +224,6 @@ export interface Context {
modes: Mode[]; modes: Mode[];
// setup fields // setup fields
pushRemote: string;
sharedTempDir: string; sharedTempDir: string;
// mcp fields // mcp fields
@@ -240,6 +238,10 @@ export interface Context {
// prep phase results // prep phase results
prepResults: PrepResult[]; prepResults: PrepResult[];
// workflow run info
runId: string;
jobId: string | undefined;
} }
async function initializeContext( async function initializeContext(
@@ -254,8 +256,9 @@ async function initializeContext(
| "cliPath" | "cliPath"
| "apiKey" | "apiKey"
| "apiKeys" | "apiKeys"
| "pushRemote"
| "prepResults" | "prepResults"
| "runId"
| "jobId"
> >
> { > {
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`); log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
@@ -407,14 +410,32 @@ function parsePayload(inputs: Inputs): Payload {
} }
async function startMcpServer(ctx: Context): Promise<void> { async function startMcpServer(ctx: Context): Promise<void> {
const runId = process.env.GITHUB_RUN_ID;
if (!runId) {
throw new Error("GITHUB_RUN_ID environment variable is required");
}
ctx.runId = runId;
// fetch the pre-created progress comment ID from the database // fetch the pre-created progress comment ID from the database
// this must be set BEFORE starting the MCP server so comment.ts can read it // this must be set BEFORE starting the MCP server so comment.ts can read it
const runId = process.env.GITHUB_RUN_ID; const workflowRunInfo = await fetchWorkflowRunInfo(ctx.runId);
if (runId) { if (workflowRunInfo.progressCommentId) {
const workflowRunInfo = await fetchWorkflowRunInfo(runId); process.env.PULLFROG_PROGRESS_COMMENT_ID = workflowRunInfo.progressCommentId;
if (workflowRunInfo.progressCommentId) { log.info(`📝 Using pre-created progress comment: ${workflowRunInfo.progressCommentId}`);
process.env.PULLFROG_PROGRESS_COMMENT_ID = workflowRunInfo.progressCommentId; }
log.info(`📝 Using pre-created progress comment: ${workflowRunInfo.progressCommentId}`);
// fetch job ID by matching GITHUB_JOB name
const jobName = process.env.GITHUB_JOB;
if (jobName) {
const jobs = await ctx.octokit.rest.actions.listJobsForWorkflowRun({
owner: ctx.owner,
repo: ctx.name,
run_id: parseInt(ctx.runId, 10),
});
const matchingJob = jobs.data.jobs.find((job) => job.name === jobName);
if (matchingJob) {
ctx.jobId = String(matchingJob.id);
log.info(`📋 Found job ID: ${ctx.jobId}`);
} }
} }
@@ -485,6 +506,11 @@ async function runAgent(ctx: Context): Promise<AgentResult> {
apiKeys: ctx.apiKeys, apiKeys: ctx.apiKeys,
cliPath: ctx.cliPath, cliPath: ctx.cliPath,
prepResults: ctx.prepResults, prepResults: ctx.prepResults,
repo: {
owner: ctx.owner,
name: ctx.name,
defaultBranch: ctx.repo.default_branch,
},
}); });
} }
+105
View File
@@ -0,0 +1,105 @@
import { type } from "arktype";
import type { Context } from "../main.ts";
import { log } from "../utils/cli.ts";
import { $ } from "../utils/shell.ts";
import { execute, tool } from "./shared.ts";
export const CheckoutPr = type({
pull_number: type.number.describe("the pull request number to checkout"),
});
export type CheckoutPrResult = {
success: true;
number: number;
title: string;
base: string;
head: string;
isFork: boolean;
maintainerCanModify: boolean;
url: string;
headRepo: string;
};
export function CheckoutPrTool(ctx: Context) {
return tool({
name: "checkout_pr",
description:
"Checkout a pull request branch locally. This fetches the PR branch and sets up push configuration for fork PRs. Use this when you need to work on an existing PR.",
parameters: CheckoutPr,
execute: execute(ctx, async ({ pull_number }) => {
log.info(`🔀 checking out PR #${pull_number}...`);
// fetch PR metadata
const pr = await ctx.octokit.rest.pulls.get({
owner: ctx.owner,
repo: ctx.name,
pull_number,
});
const headRepo = pr.data.head.repo;
if (!headRepo) {
throw new Error(`PR #${pull_number} source repository was deleted`);
}
const isFork = headRepo.full_name !== pr.data.base.repo.full_name;
const baseBranch = pr.data.base.ref;
const headBranch = pr.data.head.ref;
// fetch base branch so origin/<base> exists for diff operations
log.info(`📥 fetching base branch (${baseBranch})...`);
$("git", ["fetch", "--no-tags", "origin", baseBranch]);
// fetch PR branch using pull/{n}/head refspec (works for both fork and same-repo PRs)
log.info(`🌿 fetching PR #${pull_number} (${headBranch})...`);
$("git", ["fetch", "--no-tags", "origin", `pull/${pull_number}/head:${headBranch}`]);
// checkout the branch
$("git", ["checkout", headBranch]);
log.info(`✓ checked out PR #${pull_number}`);
// configure push remote for this branch
if (isFork) {
const remoteName = `pr-${pull_number}`;
const forkUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${headRepo.full_name}.git`;
// add fork as a named remote (ignore error if already exists)
try {
$("git", ["remote", "add", remoteName, forkUrl]);
log.info(`📌 added remote '${remoteName}' for fork ${headRepo.full_name}`);
} catch {
// remote already exists, update its URL
$("git", ["remote", "set-url", remoteName, forkUrl]);
log.info(`📌 updated remote '${remoteName}' for fork ${headRepo.full_name}`);
}
// set branch push config so `git push` knows where to push
$("git", ["config", `branch.${headBranch}.pushRemote`, remoteName]);
log.info(`📌 configured branch '${headBranch}' to push to '${remoteName}'`);
// warn if maintainer can't modify (push will likely fail)
if (!pr.data.maintainer_can_modify) {
log.warning(
`⚠️ fork PR has maintainer_can_modify=false - push operations will fail. ` +
`ask the PR author to enable "Allow edits from maintainers" or the fork may be owned by an organization.`
);
}
} else {
// for same-repo PRs, push to origin
$("git", ["config", `branch.${headBranch}.pushRemote`, "origin"]);
}
return {
success: true,
number: pr.data.number,
title: pr.data.title,
base: baseBranch,
head: headBranch,
isFork,
maintainerCanModify: pr.data.maintainer_can_modify,
url: pr.data.html_url,
headRepo: headRepo.full_name,
} satisfies CheckoutPrResult;
}),
});
}
+18 -14
View File
@@ -141,35 +141,39 @@ export const PushBranch = type({
force: type.boolean.describe("Force push (use with caution)").default(false), force: type.boolean.describe("Force push (use with caution)").default(false),
}); });
export function PushBranchTool(ctx: Context) { export function PushBranchTool(_ctx: Context) {
const remote = ctx.pushRemote;
return tool({ return tool({
name: "push_branch", name: "push_branch",
description: description:
"Push the current branch (or specified branch) to the remote repository. Never force push unless explicitly requested.", "Push the current branch (or specified branch) to the remote repository. Git automatically determines the correct remote based on branch config (set by checkout_pr for fork PRs). Never force push unless explicitly requested.",
parameters: PushBranch, parameters: PushBranch,
execute: execute(ctx, async ({ branchName, force }) => { execute: execute(_ctx, async ({ branchName, force }) => {
const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false }); const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
// skip -u flag when pushing to URL (can't set upstream without a remote name) // check if branch has a configured pushRemote
const isUrl = remote.startsWith("https://"); let remote = "origin";
const args = force try {
? ["push", "--force", ...(isUrl ? [] : ["-u"]), remote, branch] remote = $("git", ["config", `branch.${branch}.pushRemote`], { log: false }).trim();
: ["push", ...(isUrl ? [] : ["-u"]), remote, branch]; } catch {
// no configured pushRemote, default to origin
}
log.info(`Pushing branch ${branch} to ${isUrl ? "(fork URL)" : remote}`); const args = force
? ["push", "--force", "-u", remote, branch]
: ["push", "-u", remote, branch];
log.info(`pushing branch ${branch} to ${remote}`);
if (force) { if (force) {
log.warning(`Force pushing - this will overwrite remote history`); log.warning(`force pushing - this will overwrite remote history`);
} }
$("git", args); $("git", args);
return { return {
success: true, success: true,
branch, branch,
remote: isUrl ? "(fork URL)" : remote, remote,
force, force,
message: `Successfully pushed branch ${branch}`, message: `successfully pushed branch ${branch}`,
}; };
}), }),
}); });
+24 -2
View File
@@ -1,5 +1,7 @@
import { type } from "arktype"; import { type } from "arktype";
import { agentsManifest } from "../external.ts";
import type { Context } from "../main.ts"; import type { Context } from "../main.ts";
import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
import { log } from "../utils/cli.ts"; import { log } from "../utils/cli.ts";
import { containsSecrets } from "../utils/secrets.ts"; import { containsSecrets } from "../utils/secrets.ts";
import { $ } from "../utils/shell.ts"; import { $ } from "../utils/shell.ts";
@@ -11,6 +13,22 @@ export const PullRequest = type({
base: type.string.describe("the base branch to merge into (e.g., 'main')"), base: type.string.describe("the base branch to merge into (e.g., 'main')"),
}); });
function buildPrBodyWithFooter(ctx: Context, body: string): string {
const agentName = ctx.payload.agent;
const agentInfo = agentName ? agentsManifest[agentName] : null;
const footer = buildPullfrogFooter({
triggeredBy: true,
agent: agentInfo ? { displayName: agentInfo.displayName, url: agentInfo.url } : undefined,
workflowRun: ctx.runId
? { owner: ctx.owner, repo: ctx.name, runId: ctx.runId, jobId: ctx.jobId }
: undefined,
});
const bodyWithoutFooter = stripExistingFooter(body);
return `${bodyWithoutFooter}${footer}`;
}
export function PullRequestTool(ctx: Context) { export function PullRequestTool(ctx: Context) {
return tool({ return tool({
name: "create_pull_request", name: "create_pull_request",
@@ -29,7 +47,9 @@ export function PullRequestTool(ctx: Context) {
} }
// validate all changes that would be in the PR (from base to HEAD) // validate all changes that would be in the PR (from base to HEAD)
const diff = $("git", ["diff", `origin/${base}...HEAD`], { log: false }); // FORK PR NOTE: origin/<base> is fetched by setupGit, so this works for both fork and same-repo PRs
// use two-dot (..) not three-dot (...) for reliable diffs with shallow clones
const diff = $("git", ["diff", `origin/${base}..HEAD`], { log: false });
if (containsSecrets(diff)) { if (containsSecrets(diff)) {
throw new Error( throw new Error(
"PR creation blocked: secrets detected in changes. " + "PR creation blocked: secrets detected in changes. " +
@@ -37,11 +57,13 @@ export function PullRequestTool(ctx: Context) {
); );
} }
const bodyWithFooter = buildPrBodyWithFooter(ctx, body);
const result = await ctx.octokit.rest.pulls.create({ const result = await ctx.octokit.rest.pulls.create({
owner: ctx.owner, owner: ctx.owner,
repo: ctx.name, repo: ctx.name,
title: title, title: title,
body: body, body: bodyWithFooter,
head: currentBranch, head: currentBranch,
base: base, base: base,
}); });
+1 -1
View File
@@ -10,7 +10,7 @@ export function PullRequestInfoTool(ctx: Context) {
return tool({ return tool({
name: "get_pull_request", name: "get_pull_request",
description: description:
"Retrieve PR information (metadata only). PR branch is already checked out during setup.", "Retrieve PR metadata (number, title, state, base/head branches, fork status). To checkout a PR branch locally, use checkout_pr instead.",
parameters: PullRequestInfo, parameters: PullRequestInfo,
execute: execute(ctx, async ({ pull_number }) => { execute: execute(ctx, async ({ pull_number }) => {
const pr = await ctx.octokit.rest.pulls.get({ const pr = await ctx.octokit.rest.pulls.get({
+4 -1
View File
@@ -35,7 +35,9 @@ export const Review = type({
}) })
.array() .array()
.describe( .describe(
"PRIMARY location for ALL feedback. 95%+ of review content should be here. Use 'git diff origin/<base>...origin/<head>' to find correct line numbers (RIGHT side for new code, LEFT for old)." // FORK PR NOTE: use HEAD not origin/<head> - for fork PRs, origin/<head> doesn't exist
// because the head branch is in a different repo (the fork). HEAD is the locally checked out PR branch.
"PRIMARY location for ALL feedback. 95%+ of review content should be here. Use 'git diff origin/<base>..HEAD' to find correct line numbers (RIGHT side for new code, LEFT for old). Works for both fork and same-repo PRs."
) )
.optional(), .optional(),
}); });
@@ -93,6 +95,7 @@ export function ReviewTool(ctx: Context) {
const fixApprovedUrl = `${apiUrl}/trigger/${ctx.owner}/${ctx.name}/${pull_number}?action=fix-approved&review_id=${reviewId}`; const fixApprovedUrl = `${apiUrl}/trigger/${ctx.owner}/${ctx.name}/${pull_number}?action=fix-approved&review_id=${reviewId}`;
const footer = buildPullfrogFooter({ const footer = buildPullfrogFooter({
workflowRun: { owner: ctx.owner, repo: ctx.name, runId: ctx.runId, jobId: ctx.jobId },
customParts: [`[Fix all ➔](${fixAllUrl})`, `[Fix 👍s ➔](${fixApprovedUrl})`], customParts: [`[Fix all ➔](${fixAllUrl})`, `[Fix 👍s ➔](${fixApprovedUrl})`],
}); });
+2
View File
@@ -5,6 +5,7 @@ import { FastMCP, type Tool } from "fastmcp";
import { ghPullfrogMcpName } from "../external.ts"; import { ghPullfrogMcpName } from "../external.ts";
import type { Context } from "../main.ts"; import type { Context } from "../main.ts";
import { GetCheckSuiteLogsTool } from "./checkSuite.ts"; import { GetCheckSuiteLogsTool } from "./checkSuite.ts";
import { CheckoutPrTool } from "./checkout.ts";
import { import {
CreateCommentTool, CreateCommentTool,
EditCommentTool, EditCommentTool,
@@ -78,6 +79,7 @@ export async function startMcpHttpServer(
PullRequestTool(ctx), PullRequestTool(ctx),
ReviewTool(ctx), ReviewTool(ctx),
PullRequestInfoTool(ctx), PullRequestInfoTool(ctx),
CheckoutPrTool(ctx),
GetReviewCommentsTool(ctx), GetReviewCommentsTool(ctx),
ListPullRequestReviewsTool(ctx), ListPullRequestReviewsTool(ctx),
GetCheckSuiteLogsTool(ctx), GetCheckSuiteLogsTool(ctx),
+7 -6
View File
@@ -64,10 +64,11 @@ export function getModes({
description: description:
"Address PR review feedback; respond to reviewer comments; make requested changes to an existing PR", "Address PR review feedback; respond to reviewer comments; make requested changes to an existing PR",
prompt: `Follow these steps: prompt: `Follow these steps:
1. Get PR info with ${ghPullfrogMcpName}/get_pull_request (this automatically fetches and checks out the PR branch) 1. Checkout the PR using ${ghPullfrogMcpName}/checkout_pr with the PR number. This fetches the PR branch and configures push settings (including for fork PRs).
2. Review the feedback provided. Understand each review comment and what changes are being requested. 2. Review the feedback provided. Understand each review comment and what changes are being requested.
- **EVENT DATA may contain review comment details**: If available, \`approved_comments\` are comments to address, \`unapproved_comments\` are for context only. The \`triggerer\` field indicates who initiated this action - prioritize their replies when deciding how to implement fixes. - **EVENT DATA may contain review comment details**: If available, \`approved_comments\` are comments to address, \`unapproved_comments\` are for context only. The \`triggerer\` field indicates who initiated this action - prioritize their replies when deciding how to implement fixes.
- You can use ${ghPullfrogMcpName}/get_pull_request to get PR metadata if needed.
3. If the request requires understanding the codebase structure, dependencies, or conventions, gather relevant context. Read AGENTS.md if it exists. 3. If the request requires understanding the codebase structure, dependencies, or conventions, gather relevant context. Read AGENTS.md if it exists.
@@ -77,7 +78,7 @@ export function getModes({
6. Test your changes to ensure they work correctly. 6. Test your changes to ensure they work correctly.
7. When done, commit and push your changes to the existing PR branch. Do not create a new branch or PR - you are updating an existing one. 7. When done, commit your changes with ${ghPullfrogMcpName}/commit_files, then push with ${ghPullfrogMcpName}/push_branch. The push will automatically go to the correct remote (including fork repos). Do not create a new branch or PR - you are updating an existing one.
${ ${
disableProgressComment disableProgressComment
? "" ? ""
@@ -92,9 +93,9 @@ ${
description: description:
"Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness", "Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
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. Checkout the PR using ${ghPullfrogMcpName}/checkout_pr with the PR number. This fetches the PR branch and base branch, preparing the repo for review.
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. 2. **IMPORTANT**: After calling checkout_pr, the PR branch is checked out locally. View diff using: \`git diff origin/<base>..HEAD\` (replace <base> with 'base' from checkout_pr result, e.g., \`git diff origin/main..HEAD\`). Use two dots (..) not three dots (...) for reliable diffs. Do NOT use \`origin/<head>\` - the branch is checked out locally, not as a remote tracking branch. This works for both same-repo and fork PRs.
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.
@@ -102,7 +103,8 @@ ${
**GENERAL GUIDANCE** **GENERAL GUIDANCE**
- *CRITICAL* — Use **relative paths** from repo root (e.g., \`packages/core/src/utils.ts\`) - Do not leave any comments that are not potentially actionable. Do not leave complimentary comments just to be nice.
- *CRITICAL* — Use **relative paths** from repo root (e.g., \`packages/core/src/utils.ts\`)
- For line numbers, use the NEW file line number from the diff (shown after \`+\` in hunk headers like \`@@ -10,5 +12,8 @@\` means new file starts at line 12) - For line numbers, use the NEW file line number from the diff (shown after \`+\` in hunk headers like \`@@ -10,5 +12,8 @@\` means new file starts at line 12)
- Only comment on lines that appear in the diff. GitHub will reject comments on unchanged lines. - Only comment on lines that appear in the diff. GitHub will reject comments on unchanged lines.
- **CRITICAL: Prioritize per-line feedback over summary text.** - **CRITICAL: Prioritize per-line feedback over summary text.**
@@ -112,7 +114,6 @@ ${
- 95%+ of review content should be in per-line comments; the body should be just a couple sentences - 95%+ of review content should be in per-line comments; the body should be just a couple sentences
- The review body will include quick action links for addressing feedback, so keep it concise - The review body will include quick action links for addressing feedback, so keep it concise
- Do not nitpick unless instructed explicity to do so by the user's additional instructions. This includes: requesting documentation/docstrings/JSDoc. - Do not nitpick unless instructed explicity to do so by the user's additional instructions. This includes: requesting documentation/docstrings/JSDoc.
- Do not leave any comments that are not potentially actionable.
- The review should be thoughtful. When evaluating complex changes, consider the following conceptual approach: - The review should be thoughtful. When evaluating complex changes, consider the following conceptual approach:
- 1) conceptualize the changes made. make sure you understand it. - 1) conceptualize the changes made. make sure you understand it.
- 2) evaluate conceptual approach. leave feedback as needed. - 2) evaluate conceptual approach. leave feedback as needed.
+6 -5
View File
@@ -7,12 +7,12 @@ export interface AgentInfo {
url: string; url: string;
} }
export interface WorkflowRunInfo { export interface WorkflowRunFooterInfo {
owner: string; owner: string;
repo: string; repo: string;
runId: string; runId: string;
/** optional job URL - if provided, will be used instead of building from runId */ /** optional job ID - if provided, will append /job/{jobId} to the workflow run URL */
htmlUrl?: string; jobId?: string | undefined;
} }
export interface BuildPullfrogFooterParams { export interface BuildPullfrogFooterParams {
@@ -21,7 +21,7 @@ export interface BuildPullfrogFooterParams {
/** add "Using [agent](url)" link */ /** add "Using [agent](url)" link */
agent?: AgentInfo | undefined; agent?: AgentInfo | undefined;
/** add "View workflow run" link */ /** add "View workflow run" link */
workflowRun?: WorkflowRunInfo | undefined; workflowRun?: WorkflowRunFooterInfo | undefined;
/** arbitrary custom parts (e.g., action links) */ /** arbitrary custom parts (e.g., action links) */
customParts?: string[]; customParts?: string[];
} }
@@ -42,7 +42,8 @@ export function buildPullfrogFooter(params: BuildPullfrogFooterParams): string {
} }
if (params.workflowRun) { if (params.workflowRun) {
const url = params.workflowRun.htmlUrl ?? `https://github.com/${params.workflowRun.owner}/${params.workflowRun.repo}/actions/runs/${params.workflowRun.runId}`; const baseUrl = `https://github.com/${params.workflowRun.owner}/${params.workflowRun.repo}/actions/runs/${params.workflowRun.runId}`;
const url = params.workflowRun.jobId ? `${baseUrl}/job/${params.workflowRun.jobId}` : baseUrl;
parts.push(`[View workflow run](${url})`); parts.push(`[View workflow run](${url})`);
} }
+14
View File
@@ -32,6 +32,15 @@ function endGroup(): void {
} }
} }
/**
* Run a callback within a collapsed group
*/
function group(name: string, fn: () => void): void {
startGroup(name);
fn();
endGroup();
}
/** /**
* Print a formatted box with text (for console output) * Print a formatted box with text (for console output)
*/ */
@@ -316,6 +325,11 @@ export const log = {
*/ */
endGroup, endGroup,
/**
* Run a callback within a collapsed group
*/
group,
/** /**
* Log tool call information to console with formatted output * Log tool call information to console with formatted output
*/ */
+13 -47
View File
@@ -62,19 +62,19 @@ export function setupGitConfig(): void {
} }
} }
export type SetupGitResult = {
pushRemote: string;
};
/** /**
* Unified git setup: configures authentication and checks out PR branch if applicable. * Setup git authentication for the repository.
* For PR events, always returns a push URL constructed from the PR's head repo. * PR checkout is handled dynamically by the checkout_pr MCP tool.
* This works for both same-repo and fork PRs with a single code path. *
* FORK PR ARCHITECTURE (handled by checkout_pr tool):
* - origin: always points to BASE REPO (where PR targets)
* - checkout_pr sets per-branch pushRemote config for fork PRs
* - diff operations use: git diff origin/<base>..HEAD
*/ */
export async function setupGit(ctx: Context): Promise<SetupGitResult> { export async function setupGit(ctx: Context): Promise<void> {
const repoDir = process.cwd(); const repoDir = process.cwd();
log.info("🔧 Setting up git authentication..."); log.info("🔧 setting up git authentication...");
// remove existing git auth headers that actions/checkout might have set // remove existing git auth headers that actions/checkout might have set
try { try {
@@ -82,47 +82,13 @@ export async function setupGit(ctx: Context): Promise<SetupGitResult> {
cwd: repoDir, cwd: repoDir,
stdio: "pipe", stdio: "pipe",
}); });
log.info("✓ Removed existing authentication headers"); log.info("✓ removed existing authentication headers");
} catch { } catch {
log.debug("No existing authentication headers to remove"); log.debug("no existing authentication headers to remove");
} }
// set up origin with token (needed for fetch operations) // authenticate origin - needed for all fetch operations including PR fetches
const originUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${ctx.owner}/${ctx.name}.git`; const originUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${ctx.owner}/${ctx.name}.git`;
$("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir }); $("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
log.info("✓ Updated origin URL with authentication token"); log.info("✓ updated origin URL with authentication token");
// non-PR events: stay on default branch
if (ctx.payload.event.is_pr !== true || !ctx.payload.event.issue_number) {
return { pushRemote: "origin" };
}
// PR event: fetch PR info to get branch name and head repo
const prNumber = ctx.payload.event.issue_number;
const pr = await ctx.octokit.rest.pulls.get({
owner: ctx.owner,
repo: ctx.name,
pull_number: prNumber,
});
const headRepo = pr.data.head.repo;
if (!headRepo) {
throw new Error(`PR #${prNumber} source repository was deleted`);
}
// checkout PR branch using plain git (no gh cli needed)
const branch = pr.data.head.ref;
log.info(`🌿 Checking out PR #${prNumber} (${branch})...`);
$("git", ["fetch", "--no-tags", "origin", `pull/${prNumber}/head:${branch}`], { cwd: repoDir });
$("git", ["checkout", branch], { cwd: repoDir });
log.info(`✓ Successfully checked out PR #${prNumber}`);
// unified push URL - works for both same-repo and fork PRs
const token = process.env.GITHUB_TOKEN || ctx.githubInstallationToken;
const pushUrl = `https://x-access-token:${token}@github.com/${headRepo.full_name}.git`;
const isFork = headRepo.full_name !== pr.data.base.repo.full_name;
if (isFork) {
log.info(`🍴 Fork PR detected, will push to: ${headRepo.full_name}`);
}
return { pushRemote: pushUrl };
} }