overhaul git setup
This commit is contained in:
+2
-2
@@ -14,11 +14,11 @@ export const claude = agent({
|
||||
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
|
||||
delete process.env.ANTHROPIC_API_KEY;
|
||||
|
||||
const prompt = addInstructions({ payload, prepResults });
|
||||
const prompt = addInstructions({ payload, prepResults, repo });
|
||||
console.log(prompt);
|
||||
|
||||
// configure sandbox mode if enabled
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@ export const codex = agent({
|
||||
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
|
||||
const tempHome = process.env.PULLFROG_TEMP_DIR!;
|
||||
const configDir = join(tempHome, ".config", "codex");
|
||||
@@ -61,7 +61,7 @@ export const codex = agent({
|
||||
);
|
||||
|
||||
try {
|
||||
const streamedTurn = await thread.runStreamed(addInstructions({ payload, prepResults }));
|
||||
const streamedTurn = await thread.runStreamed(addInstructions({ payload, prepResults, repo }));
|
||||
|
||||
let finalOutput = "";
|
||||
for await (const event of streamedTurn.events) {
|
||||
|
||||
+2
-2
@@ -91,7 +91,7 @@ export const cursor = agent({
|
||||
executableName: "cursor-agent",
|
||||
});
|
||||
},
|
||||
run: async ({ payload, apiKey, cliPath, mcpServers, prepResults }) => {
|
||||
run: async ({ payload, apiKey, cliPath, mcpServers, prepResults, repo }) => {
|
||||
configureCursorMcpServers({ mcpServers, cliPath });
|
||||
configureCursorSandbox({ sandbox: payload.sandbox ?? false });
|
||||
|
||||
@@ -166,7 +166,7 @@ export const cursor = agent({
|
||||
};
|
||||
|
||||
try {
|
||||
const fullPrompt = addInstructions({ payload, prepResults });
|
||||
const fullPrompt = addInstructions({ payload, prepResults, repo });
|
||||
|
||||
// configure sandbox mode if enabled
|
||||
// in sandbox mode: remove --force flag and rely on cli-config.json sandbox settings
|
||||
|
||||
+2
-2
@@ -154,14 +154,14 @@ export const gemini = agent({
|
||||
...(githubInstallationToken && { githubInstallationToken }),
|
||||
});
|
||||
},
|
||||
run: async ({ payload, apiKey, mcpServers, cliPath, prepResults }) => {
|
||||
run: async ({ payload, apiKey, mcpServers, cliPath, prepResults, repo }) => {
|
||||
configureGeminiMcpServers({ mcpServers, cliPath });
|
||||
|
||||
if (!apiKey) {
|
||||
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)}...`);
|
||||
|
||||
// configure sandbox mode if enabled
|
||||
|
||||
+73
-12
@@ -1,3 +1,4 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { encode as toonEncode } from "@toon-format/toon";
|
||||
import type { Payload } from "../external.ts";
|
||||
import { ghPullfrogMcpName } from "../external.ts";
|
||||
@@ -62,19 +63,74 @@ function formatPrepResults(results: PrepResult[]): string {
|
||||
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 {
|
||||
payload: Payload;
|
||||
prepResults: PrepResult[];
|
||||
repo: RepoInfo;
|
||||
}
|
||||
|
||||
export const addInstructions = ({ payload, prepResults }: AddInstructionsParams) => {
|
||||
export const addInstructions = ({ payload, prepResults, repo }: AddInstructionsParams) => {
|
||||
let encodedEvent = "";
|
||||
|
||||
const eventKeys = Object.keys(payload.event);
|
||||
@@ -86,7 +142,7 @@ export const addInstructions = ({ payload, prepResults }: AddInstructionsParams)
|
||||
encodedEvent = toonEncode(payload.event);
|
||||
}
|
||||
|
||||
const envSetup = formatPrepResults(prepResults);
|
||||
const runtimeContext = buildRuntimeContext({ repo, prepResults });
|
||||
const dependenciesPreinstalled = prepResults.every((r) => r.dependenciesInstalled) || undefined;
|
||||
|
||||
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.
|
||||
|
||||
**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}/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
|
||||
|
||||
**Workflow for making code changes**:
|
||||
1. Use file operations to create/modify files
|
||||
2. Use \`${ghPullfrogMcpName}/create_branch\` to create a new branch
|
||||
**Workflow for working on an existing PR**:
|
||||
1. Use \`${ghPullfrogMcpName}/checkout_pr\` to checkout the PR 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
|
||||
4. Use \`${ghPullfrogMcpName}/push_branch\` to push the branch
|
||||
5. Use \`${ghPullfrogMcpName}/create_pull_request\` to create a PR
|
||||
@@ -221,7 +284,5 @@ ${encodedEvent}`
|
||||
|
||||
************* RUNTIME CONTEXT *************
|
||||
|
||||
working_directory: ${process.cwd()}
|
||||
|
||||
${envSetup}`;
|
||||
${runtimeContext}`;
|
||||
};
|
||||
|
||||
+2
-2
@@ -304,7 +304,7 @@ export const opencode = agent({
|
||||
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
|
||||
const tempHome = process.env.PULLFROG_TEMP_DIR!;
|
||||
const configDir = join(tempHome, ".config", "opencode");
|
||||
@@ -313,7 +313,7 @@ export const opencode = agent({
|
||||
configureOpenCodeMcpServers({ mcpServers });
|
||||
configureOpenCodeSandbox({ sandbox: payload.sandbox ?? false });
|
||||
|
||||
const prompt = addInstructions({ payload, prepResults });
|
||||
const prompt = addInstructions({ payload, prepResults, repo });
|
||||
const args = ["run", "--format", "json", prompt];
|
||||
|
||||
if (payload.sandbox) {
|
||||
|
||||
@@ -21,6 +21,15 @@ export interface AgentResult {
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repo info for agent context
|
||||
*/
|
||||
export interface RepoInfo {
|
||||
owner: string;
|
||||
name: string;
|
||||
defaultBranch: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for agent creation
|
||||
*/
|
||||
@@ -31,6 +40,7 @@ export interface AgentConfig {
|
||||
mcpServers: Record<string, McpHttpServerConfig>;
|
||||
cliPath: string;
|
||||
prepResults: PrepResult[];
|
||||
repo: RepoInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -80489,6 +80489,9 @@ function formatIndentedField(label, content) {
|
||||
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
|
||||
var LIST_ITEM_MARKER = "-";
|
||||
var LIST_ITEM_PREFIX = "- ";
|
||||
@@ -89416,10 +89419,11 @@ function getModes({
|
||||
name: "Address Reviews",
|
||||
description: "Address PR review feedback; respond to reviewer comments; make requested changes to an existing PR",
|
||||
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.
|
||||
- **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.
|
||||
|
||||
@@ -89429,7 +89433,7 @@ function getModes({
|
||||
|
||||
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 ? "" : `
|
||||
8. ${reportProgressInstruction}
|
||||
|
||||
@@ -89439,9 +89443,9 @@ ${disableProgressComment ? "" : `
|
||||
name: "Review",
|
||||
description: "Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
|
||||
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.
|
||||
|
||||
@@ -89449,7 +89453,8 @@ ${disableProgressComment ? "" : `
|
||||
|
||||
**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)
|
||||
- Only comment on lines that appear in the diff. GitHub will reject comments on unchanged lines.
|
||||
- **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
|
||||
- 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 leave any comments that are not potentially actionable.
|
||||
- The review should be thoughtful. When evaluating complex changes, consider the following conceptual approach:
|
||||
- 1) conceptualize the changes made. make sure you understand it.
|
||||
- 2) evaluate conceptual approach. leave feedback as needed.
|
||||
@@ -89554,20 +89558,47 @@ function formatPrepResults(results) {
|
||||
if (lines.length === 0) {
|
||||
return "";
|
||||
}
|
||||
return `************* ENVIRONMENT SETUP *************
|
||||
|
||||
${lines.join("\n")}
|
||||
|
||||
`;
|
||||
return 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 = "";
|
||||
const eventKeys = Object.keys(payload.event);
|
||||
if (eventKeys.length === 1 && eventKeys[0] === "trigger") {
|
||||
} else {
|
||||
encodedEvent = encode(payload.event);
|
||||
}
|
||||
const envSetup = formatPrepResults(prepResults);
|
||||
const runtimeContext = buildRuntimeContext({ repo, prepResults });
|
||||
const dependenciesPreinstalled = prepResults.every((r) => r.dependenciesInstalled) || void 0;
|
||||
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.
|
||||
|
||||
**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}/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
|
||||
|
||||
**Workflow for making code changes**:
|
||||
1. Use file operations to create/modify files
|
||||
2. Use \`${ghPullfrogMcpName}/create_branch\` to create a new branch
|
||||
**Workflow for working on an existing PR**:
|
||||
1. Use \`${ghPullfrogMcpName}/checkout_pr\` to checkout the PR 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
|
||||
4. Use \`${ghPullfrogMcpName}/push_branch\` to push the branch
|
||||
5. Use \`${ghPullfrogMcpName}/create_pull_request\` to create a PR
|
||||
@@ -89694,9 +89732,7 @@ ${encodedEvent}` : ""}
|
||||
|
||||
************* RUNTIME CONTEXT *************
|
||||
|
||||
working_directory: ${process.cwd()}
|
||||
|
||||
${envSetup}`;
|
||||
${runtimeContext}`;
|
||||
};
|
||||
|
||||
// agents/shared.ts
|
||||
@@ -90117,9 +90153,9 @@ var claude = agent({
|
||||
executablePath: "cli.js"
|
||||
});
|
||||
},
|
||||
run: async ({ payload, mcpServers, apiKey, cliPath, prepResults }) => {
|
||||
run: async ({ payload, mcpServers, apiKey, cliPath, prepResults, repo }) => {
|
||||
delete process.env.ANTHROPIC_API_KEY;
|
||||
const prompt = addInstructions({ payload, prepResults });
|
||||
const prompt = addInstructions({ payload, prepResults, repo });
|
||||
console.log(prompt);
|
||||
const sandboxOptions = payload.sandbox ? {
|
||||
permissionMode: "default",
|
||||
@@ -90575,7 +90611,7 @@ var codex = agent({
|
||||
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 configDir = join5(tempHome, ".config", "codex");
|
||||
mkdirSync2(configDir, { recursive: true });
|
||||
@@ -90605,7 +90641,7 @@ var codex = agent({
|
||||
}
|
||||
);
|
||||
try {
|
||||
const streamedTurn = await thread.runStreamed(addInstructions({ payload, prepResults }));
|
||||
const streamedTurn = await thread.runStreamed(addInstructions({ payload, prepResults, repo }));
|
||||
let finalOutput2 = "";
|
||||
for await (const event of streamedTurn.events) {
|
||||
const handler2 = messageHandlers2[event.type];
|
||||
@@ -90747,7 +90783,7 @@ var cursor = agent({
|
||||
executableName: "cursor-agent"
|
||||
});
|
||||
},
|
||||
run: async ({ payload, apiKey, cliPath, mcpServers, prepResults }) => {
|
||||
run: async ({ payload, apiKey, cliPath, mcpServers, prepResults, repo }) => {
|
||||
configureCursorMcpServers({ mcpServers, cliPath });
|
||||
configureCursorSandbox({ sandbox: payload.sandbox ?? false });
|
||||
const loggedModelCallIds = /* @__PURE__ */ new Set();
|
||||
@@ -90800,7 +90836,7 @@ var cursor = agent({
|
||||
}
|
||||
};
|
||||
try {
|
||||
const fullPrompt = addInstructions({ payload, prepResults });
|
||||
const fullPrompt = addInstructions({ payload, prepResults, repo });
|
||||
const cursorArgs = payload.sandbox ? [
|
||||
"--print",
|
||||
fullPrompt,
|
||||
@@ -91104,12 +91140,12 @@ var gemini = agent({
|
||||
...githubInstallationToken2 && { githubInstallationToken: githubInstallationToken2 }
|
||||
});
|
||||
},
|
||||
run: async ({ payload, apiKey, mcpServers, cliPath, prepResults }) => {
|
||||
run: async ({ payload, apiKey, mcpServers, cliPath, prepResults, repo }) => {
|
||||
configureGeminiMcpServers({ mcpServers, cliPath });
|
||||
if (!apiKey) {
|
||||
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)}...`);
|
||||
const args3 = payload.sandbox ? [
|
||||
"--allowed-tools",
|
||||
@@ -91365,13 +91401,13 @@ var opencode = agent({
|
||||
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 configDir = join7(tempHome, ".config", "opencode");
|
||||
mkdirSync4(configDir, { recursive: true });
|
||||
configureOpenCodeMcpServers({ mcpServers });
|
||||
configureOpenCodeSandbox({ sandbox: payload.sandbox ?? false });
|
||||
const prompt = addInstructions({ payload, prepResults });
|
||||
const prompt = addInstructions({ payload, prepResults, repo });
|
||||
const args3 = ["run", "--format", "json", prompt];
|
||||
if (payload.sandbox) {
|
||||
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})`);
|
||||
}
|
||||
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})`);
|
||||
}
|
||||
if (params.customParts) {
|
||||
@@ -123934,6 +123971,70 @@ function $(cmd, args3, options) {
|
||||
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
|
||||
var DebugShellCommand = type({});
|
||||
var DebugShellCommandTool = tool({
|
||||
@@ -124079,27 +124180,30 @@ var PushBranch = type({
|
||||
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)
|
||||
});
|
||||
function PushBranchTool(ctx) {
|
||||
const remote = ctx.pushRemote;
|
||||
function PushBranchTool(_ctx) {
|
||||
return tool({
|
||||
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,
|
||||
execute: execute(ctx, async ({ branchName, force }) => {
|
||||
execute: execute(_ctx, async ({ branchName, force }) => {
|
||||
const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
|
||||
const isUrl = remote.startsWith("https://");
|
||||
const args3 = force ? ["push", "--force", ...isUrl ? [] : ["-u"], remote, branch] : ["push", ...isUrl ? [] : ["-u"], remote, branch];
|
||||
log.info(`Pushing branch ${branch} to ${isUrl ? "(fork URL)" : remote}`);
|
||||
let remote = "origin";
|
||||
try {
|
||||
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) {
|
||||
log.warning(`Force pushing - this will overwrite remote history`);
|
||||
log.warning(`force pushing - this will overwrite remote history`);
|
||||
}
|
||||
$("git", args3);
|
||||
return {
|
||||
success: true,
|
||||
branch,
|
||||
remote: isUrl ? "(fork URL)" : remote,
|
||||
remote,
|
||||
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"),
|
||||
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) {
|
||||
return tool({
|
||||
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."
|
||||
);
|
||||
}
|
||||
const diff = $("git", ["diff", `origin/${base}...HEAD`], { log: false });
|
||||
const diff = $("git", ["diff", `origin/${base}..HEAD`], { log: false });
|
||||
if (containsSecrets(diff)) {
|
||||
throw new Error(
|
||||
"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({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.name,
|
||||
title,
|
||||
body,
|
||||
body: bodyWithFooter,
|
||||
head: currentBranch,
|
||||
base
|
||||
});
|
||||
@@ -124372,7 +124490,7 @@ var PullRequestInfo = type({
|
||||
function PullRequestInfoTool(ctx) {
|
||||
return tool({
|
||||
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,
|
||||
execute: execute(ctx, async ({ pull_number }) => {
|
||||
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()
|
||||
}).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()
|
||||
});
|
||||
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 fixApprovedUrl = `${apiUrl}/trigger/${ctx.owner}/${ctx.name}/${pull_number}?action=fix-approved&review_id=${reviewId}`;
|
||||
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})`]
|
||||
});
|
||||
const updatedBody = (body || "") + footer;
|
||||
@@ -124695,6 +124816,7 @@ async function startMcpHttpServer(ctx) {
|
||||
PullRequestTool(ctx),
|
||||
ReviewTool(ctx),
|
||||
PullRequestInfoTool(ctx),
|
||||
CheckoutPrTool(ctx),
|
||||
GetReviewCommentsTool(ctx),
|
||||
ListPullRequestReviewsTool(ctx),
|
||||
GetCheckSuiteLogsTool(ctx),
|
||||
@@ -125328,16 +125450,16 @@ ${error42}` : `\u274C ${error42}`;
|
||||
}
|
||||
|
||||
// utils/setup.ts
|
||||
import { execSync } from "node:child_process";
|
||||
import { execSync as execSync2 } from "node:child_process";
|
||||
function setupGitConfig() {
|
||||
const repoDir = process.cwd();
|
||||
log.info("\u{1F527} Setting up git configuration...");
|
||||
try {
|
||||
execSync('git config --local user.email "team@pullfrog.com"', {
|
||||
execSync2('git config --local user.email "team@pullfrog.com"', {
|
||||
cwd: repoDir,
|
||||
stdio: "pipe"
|
||||
});
|
||||
execSync('git config --local user.name "pullfrog"', {
|
||||
execSync2('git config --local user.name "pullfrog"', {
|
||||
cwd: repoDir,
|
||||
stdio: "pipe"
|
||||
});
|
||||
@@ -125350,44 +125472,19 @@ function setupGitConfig() {
|
||||
}
|
||||
async function setupGit(ctx) {
|
||||
const repoDir = process.cwd();
|
||||
log.info("\u{1F527} Setting up git authentication...");
|
||||
log.info("\u{1F527} setting up git authentication...");
|
||||
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,
|
||||
stdio: "pipe"
|
||||
});
|
||||
log.info("\u2713 Removed existing authentication headers");
|
||||
log.info("\u2713 removed existing authentication headers");
|
||||
} 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`;
|
||||
$("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
|
||||
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 };
|
||||
log.info("\u2713 updated origin URL with authentication token");
|
||||
}
|
||||
|
||||
// utils/timer.ts
|
||||
@@ -125426,8 +125523,7 @@ async function main(inputs) {
|
||||
const partialCtx = await initializeContext(inputs, payload);
|
||||
const ctx = partialCtx;
|
||||
timer.checkpoint("initializeContext");
|
||||
const { pushRemote } = await setupGit(ctx);
|
||||
ctx.pushRemote = pushRemote;
|
||||
await setupGit(ctx);
|
||||
timer.checkpoint("setupGit");
|
||||
await setupTempDirectory(ctx);
|
||||
timer.checkpoint("setupTempDirectory");
|
||||
@@ -125636,11 +125732,26 @@ function parsePayload(inputs) {
|
||||
}
|
||||
async function startMcpServer(ctx) {
|
||||
const runId = process.env.GITHUB_RUN_ID;
|
||||
if (runId) {
|
||||
const workflowRunInfo = await fetchWorkflowRunInfo(runId);
|
||||
if (workflowRunInfo.progressCommentId) {
|
||||
process.env.PULLFROG_PROGRESS_COMMENT_ID = workflowRunInfo.progressCommentId;
|
||||
log.info(`\u{1F4DD} Using pre-created progress comment: ${workflowRunInfo.progressCommentId}`);
|
||||
if (!runId) {
|
||||
throw new Error("GITHUB_RUN_ID environment variable is required");
|
||||
}
|
||||
ctx.runId = runId;
|
||||
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);
|
||||
@@ -125695,7 +125806,12 @@ ${encode(eventWithoutContext)}`;
|
||||
apiKey: ctx.apiKey,
|
||||
apiKeys: ctx.apiKeys,
|
||||
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) {
|
||||
|
||||
+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;
|
||||
timer.checkpoint("initializeContext");
|
||||
|
||||
const { pushRemote } = await setupGit(ctx);
|
||||
ctx.pushRemote = pushRemote;
|
||||
await setupGit(ctx);
|
||||
timer.checkpoint("setupGit");
|
||||
|
||||
await setupTempDirectory(ctx);
|
||||
@@ -225,7 +224,6 @@ export interface Context {
|
||||
modes: Mode[];
|
||||
|
||||
// setup fields
|
||||
pushRemote: string;
|
||||
sharedTempDir: string;
|
||||
|
||||
// mcp fields
|
||||
@@ -240,6 +238,10 @@ export interface Context {
|
||||
|
||||
// prep phase results
|
||||
prepResults: PrepResult[];
|
||||
|
||||
// workflow run info
|
||||
runId: string;
|
||||
jobId: string | undefined;
|
||||
}
|
||||
|
||||
async function initializeContext(
|
||||
@@ -254,8 +256,9 @@ async function initializeContext(
|
||||
| "cliPath"
|
||||
| "apiKey"
|
||||
| "apiKeys"
|
||||
| "pushRemote"
|
||||
| "prepResults"
|
||||
| "runId"
|
||||
| "jobId"
|
||||
>
|
||||
> {
|
||||
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
||||
@@ -407,14 +410,32 @@ function parsePayload(inputs: Inputs): Payload {
|
||||
}
|
||||
|
||||
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
|
||||
// this must be set BEFORE starting the MCP server so comment.ts can read it
|
||||
const runId = process.env.GITHUB_RUN_ID;
|
||||
if (runId) {
|
||||
const workflowRunInfo = await fetchWorkflowRunInfo(runId);
|
||||
if (workflowRunInfo.progressCommentId) {
|
||||
process.env.PULLFROG_PROGRESS_COMMENT_ID = workflowRunInfo.progressCommentId;
|
||||
log.info(`📝 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(`📝 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,
|
||||
cliPath: ctx.cliPath,
|
||||
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),
|
||||
});
|
||||
|
||||
export function PushBranchTool(ctx: Context) {
|
||||
const remote = ctx.pushRemote;
|
||||
|
||||
export function PushBranchTool(_ctx: Context) {
|
||||
return tool({
|
||||
name: "push_branch",
|
||||
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,
|
||||
execute: execute(ctx, async ({ branchName, force }) => {
|
||||
execute: execute(_ctx, async ({ branchName, force }) => {
|
||||
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)
|
||||
const isUrl = remote.startsWith("https://");
|
||||
const args = force
|
||||
? ["push", "--force", ...(isUrl ? [] : ["-u"]), remote, branch]
|
||||
: ["push", ...(isUrl ? [] : ["-u"]), remote, branch];
|
||||
// check if branch has a configured pushRemote
|
||||
let remote = "origin";
|
||||
try {
|
||||
remote = $("git", ["config", `branch.${branch}.pushRemote`], { log: false }).trim();
|
||||
} 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) {
|
||||
log.warning(`Force pushing - this will overwrite remote history`);
|
||||
log.warning(`force pushing - this will overwrite remote history`);
|
||||
}
|
||||
$("git", args);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
branch,
|
||||
remote: isUrl ? "(fork URL)" : remote,
|
||||
remote,
|
||||
force,
|
||||
message: `Successfully pushed branch ${branch}`,
|
||||
message: `successfully pushed branch ${branch}`,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { type } from "arktype";
|
||||
import { agentsManifest } from "../external.ts";
|
||||
import type { Context } from "../main.ts";
|
||||
import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { parseRepoContext } from "../utils/github.ts";
|
||||
import { containsSecrets } from "../utils/secrets.ts";
|
||||
import { $ } from "../utils/shell.ts";
|
||||
import { execute, tool } from "./shared.ts";
|
||||
@@ -11,6 +14,23 @@ export const PullRequest = type({
|
||||
base: type.string.describe("the base branch to merge into (e.g., 'main')"),
|
||||
});
|
||||
|
||||
function buildPrBodyWithFooter(ctx: Context, body: string): string {
|
||||
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 } : undefined,
|
||||
workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : undefined,
|
||||
});
|
||||
|
||||
const bodyWithoutFooter = stripExistingFooter(body);
|
||||
return `${bodyWithoutFooter}${footer}`;
|
||||
}
|
||||
|
||||
export function PullRequestTool(ctx: Context) {
|
||||
return tool({
|
||||
name: "create_pull_request",
|
||||
@@ -29,7 +49,9 @@ export function PullRequestTool(ctx: Context) {
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
throw new Error(
|
||||
"PR creation blocked: secrets detected in changes. " +
|
||||
@@ -37,11 +59,13 @@ export function PullRequestTool(ctx: Context) {
|
||||
);
|
||||
}
|
||||
|
||||
const bodyWithFooter = buildPrBodyWithFooter(ctx, body);
|
||||
|
||||
const result = await ctx.octokit.rest.pulls.create({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.name,
|
||||
title: title,
|
||||
body: body,
|
||||
body: bodyWithFooter,
|
||||
head: currentBranch,
|
||||
base: base,
|
||||
});
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ export function PullRequestInfoTool(ctx: Context) {
|
||||
return tool({
|
||||
name: "get_pull_request",
|
||||
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,
|
||||
execute: execute(ctx, async ({ pull_number }) => {
|
||||
const pr = await ctx.octokit.rest.pulls.get({
|
||||
|
||||
+4
-1
@@ -35,7 +35,9 @@ export const Review = type({
|
||||
})
|
||||
.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(),
|
||||
});
|
||||
@@ -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 footer = buildPullfrogFooter({
|
||||
workflowRun: { owner: ctx.owner, repo: ctx.name, runId: ctx.runId, jobId: ctx.jobId },
|
||||
customParts: [`[Fix all ➔](${fixAllUrl})`, `[Fix 👍s ➔](${fixApprovedUrl})`],
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { FastMCP, type Tool } from "fastmcp";
|
||||
import { ghPullfrogMcpName } from "../external.ts";
|
||||
import type { Context } from "../main.ts";
|
||||
import { GetCheckSuiteLogsTool } from "./checkSuite.ts";
|
||||
import { CheckoutPrTool } from "./checkout.ts";
|
||||
import {
|
||||
CreateCommentTool,
|
||||
EditCommentTool,
|
||||
@@ -78,6 +79,7 @@ export async function startMcpHttpServer(
|
||||
PullRequestTool(ctx),
|
||||
ReviewTool(ctx),
|
||||
PullRequestInfoTool(ctx),
|
||||
CheckoutPrTool(ctx),
|
||||
GetReviewCommentsTool(ctx),
|
||||
ListPullRequestReviewsTool(ctx),
|
||||
GetCheckSuiteLogsTool(ctx),
|
||||
|
||||
@@ -64,10 +64,11 @@ export function getModes({
|
||||
description:
|
||||
"Address PR review feedback; respond to reviewer comments; make requested changes to an existing PR",
|
||||
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.
|
||||
- **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.
|
||||
|
||||
@@ -77,7 +78,7 @@ export function getModes({
|
||||
|
||||
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
|
||||
? ""
|
||||
@@ -92,9 +93,9 @@ ${
|
||||
description:
|
||||
"Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
|
||||
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.
|
||||
|
||||
@@ -102,7 +103,8 @@ ${
|
||||
|
||||
**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)
|
||||
- Only comment on lines that appear in the diff. GitHub will reject comments on unchanged lines.
|
||||
- **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
|
||||
- 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 leave any comments that are not potentially actionable.
|
||||
- The review should be thoughtful. When evaluating complex changes, consider the following conceptual approach:
|
||||
- 1) conceptualize the changes made. make sure you understand it.
|
||||
- 2) evaluate conceptual approach. leave feedback as needed.
|
||||
|
||||
@@ -7,12 +7,12 @@ export interface AgentInfo {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface WorkflowRunInfo {
|
||||
export interface WorkflowRunFooterInfo {
|
||||
owner: string;
|
||||
repo: string;
|
||||
runId: string;
|
||||
/** optional job URL - if provided, will be used instead of building from runId */
|
||||
htmlUrl?: string;
|
||||
/** optional job ID - if provided, will append /job/{jobId} to the workflow run URL */
|
||||
jobId?: string | undefined;
|
||||
}
|
||||
|
||||
export interface BuildPullfrogFooterParams {
|
||||
@@ -21,7 +21,7 @@ export interface BuildPullfrogFooterParams {
|
||||
/** add "Using [agent](url)" link */
|
||||
agent?: AgentInfo | undefined;
|
||||
/** add "View workflow run" link */
|
||||
workflowRun?: WorkflowRunInfo | undefined;
|
||||
workflowRun?: WorkflowRunFooterInfo | undefined;
|
||||
/** arbitrary custom parts (e.g., action links) */
|
||||
customParts?: string[];
|
||||
}
|
||||
@@ -42,7 +42,8 @@ export function buildPullfrogFooter(params: BuildPullfrogFooterParams): string {
|
||||
}
|
||||
|
||||
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})`);
|
||||
}
|
||||
|
||||
|
||||
+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.
|
||||
* For PR events, always returns a push URL constructed from the PR's head repo.
|
||||
* This works for both same-repo and fork PRs with a single code path.
|
||||
* Setup git authentication for the repository.
|
||||
* PR checkout is handled dynamically by the checkout_pr MCP tool.
|
||||
*
|
||||
* 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();
|
||||
|
||||
log.info("🔧 Setting up git authentication...");
|
||||
log.info("🔧 setting up git authentication...");
|
||||
|
||||
// remove existing git auth headers that actions/checkout might have set
|
||||
try {
|
||||
@@ -82,47 +82,13 @@ export async function setupGit(ctx: Context): Promise<SetupGitResult> {
|
||||
cwd: repoDir,
|
||||
stdio: "pipe",
|
||||
});
|
||||
log.info("✓ Removed existing authentication headers");
|
||||
log.info("✓ removed existing authentication headers");
|
||||
} 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`;
|
||||
$("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
|
||||
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 };
|
||||
log.info("✓ updated origin URL with authentication token");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user