Merge pull request #7 from pullfrog/git-setup-overhaul
overhaul git setup
This commit is contained in:
+3
-3
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
@@ -1 +1 @@
|
|||||||
Tell me a joke
|
Implement .omit() in a new PR
|
||||||
@@ -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
@@ -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
@@ -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}`,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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})`],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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 };
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user