Clean up init
This commit is contained in:
@@ -39287,18 +39287,26 @@ var init_buildPullfrogFooter = __esm({
|
||||
// mcp/shared.ts
|
||||
function initMcpContext(state) {
|
||||
mcpInitContext = state;
|
||||
cachedMcpContext = void 0;
|
||||
}
|
||||
function getMcpContext() {
|
||||
async function getMcpContext() {
|
||||
if (!mcpInitContext) {
|
||||
throw new Error("MCP context not initialized. Call initializeMcpContext first.");
|
||||
}
|
||||
return {
|
||||
if (cachedMcpContext) {
|
||||
return cachedMcpContext;
|
||||
}
|
||||
const repoContext = parseRepoContext();
|
||||
const octokit = new Octokit2({
|
||||
auth: getGitHubInstallationToken()
|
||||
});
|
||||
cachedMcpContext = {
|
||||
...mcpInitContext,
|
||||
...parseRepoContext(),
|
||||
octokit: new Octokit2({
|
||||
auth: getGitHubInstallationToken()
|
||||
})
|
||||
...repoContext,
|
||||
octokit,
|
||||
repo: mcpInitContext.repo
|
||||
};
|
||||
return cachedMcpContext;
|
||||
}
|
||||
function isProgressCommentDisabled() {
|
||||
return mcpInitContext?.payload.disableProgressComment === true;
|
||||
@@ -39382,7 +39390,7 @@ function sanitizeTool(tool2) {
|
||||
parameters: wrappedSchema
|
||||
};
|
||||
}
|
||||
var mcpInitContext, tool, addTools, contextualize, handleToolSuccess, handleToolError;
|
||||
var mcpInitContext, cachedMcpContext, tool, addTools, contextualize, handleToolSuccess, handleToolError;
|
||||
var init_shared3 = __esm({
|
||||
"mcp/shared.ts"() {
|
||||
"use strict";
|
||||
@@ -39400,7 +39408,7 @@ var init_shared3 = __esm({
|
||||
contextualize = (executor) => {
|
||||
return async (params) => {
|
||||
try {
|
||||
const ctx = getMcpContext();
|
||||
const ctx = await getMcpContext();
|
||||
const result = await executor(params, ctx);
|
||||
return handleToolSuccess(result);
|
||||
} catch (error41) {
|
||||
@@ -39491,7 +39499,7 @@ function setProgressCommentId(id) {
|
||||
progressCommentIdInitialized = true;
|
||||
}
|
||||
async function reportProgress({ body }) {
|
||||
const ctx = getMcpContext();
|
||||
const ctx = await getMcpContext();
|
||||
const bodyWithFooter = addFooter(body, ctx.payload);
|
||||
const existingCommentId = getProgressCommentId();
|
||||
if (existingCommentId) {
|
||||
@@ -39536,7 +39544,7 @@ async function deleteProgressComment() {
|
||||
if (!existingCommentId) {
|
||||
return false;
|
||||
}
|
||||
const ctx = getMcpContext();
|
||||
const ctx = await getMcpContext();
|
||||
await ctx.octokit.rest.issues.deleteComment({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.name,
|
||||
@@ -39588,7 +39596,7 @@ async function ensureProgressCommentUpdated(payload) {
|
||||
}
|
||||
let resolvedPayload;
|
||||
try {
|
||||
const ctx = getMcpContext();
|
||||
const ctx = await getMcpContext();
|
||||
resolvedPayload = ctx.payload;
|
||||
} catch {
|
||||
resolvedPayload = payload;
|
||||
@@ -99850,6 +99858,7 @@ var agents = {
|
||||
import { mkdtemp as mkdtemp2 } from "node:fs/promises";
|
||||
import { tmpdir as tmpdir2 } from "node:os";
|
||||
import { join as join8 } from "node:path";
|
||||
init_dist_src5();
|
||||
init_out4();
|
||||
init_external();
|
||||
init_comment();
|
||||
@@ -124356,36 +124365,40 @@ function containsSecrets(content, secrets) {
|
||||
|
||||
// mcp/git.ts
|
||||
init_shared3();
|
||||
var CreateBranch = type({
|
||||
branchName: type.string.describe(
|
||||
"The name of the branch to create (e.g., 'pullfrog/123-fix-bug')"
|
||||
),
|
||||
baseBranch: type.string.describe("The base branch to create from (e.g., 'main')").default("main")
|
||||
});
|
||||
var CreateBranchTool = tool({
|
||||
name: "create_branch",
|
||||
description: "Create a new git branch from the specified base branch. The branch will be created locally and pushed to the remote repository.",
|
||||
parameters: CreateBranch,
|
||||
execute: contextualize(async ({ branchName, baseBranch }) => {
|
||||
if (containsSecrets(branchName)) {
|
||||
throw new Error(
|
||||
"Branch creation blocked: secrets detected in branch name. Please remove any sensitive information (API keys, tokens, passwords) before creating a branch."
|
||||
);
|
||||
}
|
||||
log.info(`Creating branch ${branchName} from ${baseBranch}`);
|
||||
$("git", ["fetch", "origin", baseBranch, "--depth=1"]);
|
||||
$("git", ["checkout", "-B", baseBranch, `origin/${baseBranch}`]);
|
||||
$("git", ["checkout", "-b", branchName]);
|
||||
$("git", ["push", "-u", "origin", branchName]);
|
||||
log.info(`Successfully created and pushed branch ${branchName}`);
|
||||
return {
|
||||
success: true,
|
||||
branchName,
|
||||
baseBranch,
|
||||
message: `Branch ${branchName} created from ${baseBranch} and pushed to remote`
|
||||
};
|
||||
})
|
||||
});
|
||||
function CreateBranchTool(ctx) {
|
||||
const defaultBranch = ctx.repo.default_branch || "main";
|
||||
const CreateBranch = type({
|
||||
branchName: type.string.describe(
|
||||
"The name of the branch to create (e.g., 'pullfrog/123-fix-bug')"
|
||||
),
|
||||
baseBranch: type.string.describe(`The base branch to create from (defaults to '${defaultBranch}')`).default(defaultBranch)
|
||||
});
|
||||
return tool({
|
||||
name: "create_branch",
|
||||
description: "Create a new git branch from the specified base branch. The branch will be created locally and pushed to the remote repository.",
|
||||
parameters: CreateBranch,
|
||||
execute: contextualize(async ({ branchName, baseBranch }) => {
|
||||
const resolvedBaseBranch = baseBranch || ctx.repo.default_branch || "main";
|
||||
if (containsSecrets(branchName)) {
|
||||
throw new Error(
|
||||
"Branch creation blocked: secrets detected in branch name. Please remove any sensitive information (API keys, tokens, passwords) before creating a branch."
|
||||
);
|
||||
}
|
||||
log.info(`Creating branch ${branchName} from ${resolvedBaseBranch}`);
|
||||
$("git", ["fetch", "origin", resolvedBaseBranch, "--depth=1"]);
|
||||
$("git", ["checkout", "-B", resolvedBaseBranch, `origin/${resolvedBaseBranch}`]);
|
||||
$("git", ["checkout", "-b", branchName]);
|
||||
$("git", ["push", "-u", "origin", branchName]);
|
||||
log.info(`Successfully created and pushed branch ${branchName}`);
|
||||
return {
|
||||
success: true,
|
||||
branchName,
|
||||
baseBranch: resolvedBaseBranch,
|
||||
message: `Branch ${branchName} created from ${resolvedBaseBranch} and pushed to remote`
|
||||
};
|
||||
})
|
||||
});
|
||||
}
|
||||
var CommitFiles = type({
|
||||
message: type.string.describe("The commit message"),
|
||||
files: type.string.array().describe(
|
||||
@@ -124440,27 +124453,31 @@ 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)
|
||||
});
|
||||
var PushBranchTool = tool({
|
||||
name: "push_branch",
|
||||
description: "Push the current branch (or specified branch) to the remote repository. Never force push unless explicitly requested.",
|
||||
parameters: PushBranch,
|
||||
execute: contextualize(async ({ branchName, force }) => {
|
||||
const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
|
||||
if (force) {
|
||||
log.warning(`Force pushing branch ${branch} - this will overwrite remote history`);
|
||||
$("git", ["push", "--force", "origin", branch]);
|
||||
} else {
|
||||
log.info(`Pushing branch ${branch} to remote`);
|
||||
$("git", ["push", "origin", branch]);
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
branch,
|
||||
force,
|
||||
message: `Successfully pushed branch ${branch} to remote`
|
||||
};
|
||||
})
|
||||
});
|
||||
function PushBranchTool(ctx) {
|
||||
const remote = ctx.pushRemote;
|
||||
return tool({
|
||||
name: "push_branch",
|
||||
description: "Push the current branch (or specified branch) to the remote repository. Never force push unless explicitly requested.",
|
||||
parameters: PushBranch,
|
||||
execute: contextualize(async ({ branchName, force }) => {
|
||||
const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
|
||||
log.info(`Pushing branch ${branch} to ${remote}`);
|
||||
if (force) {
|
||||
log.warning(`Force pushing - this will overwrite remote history`);
|
||||
$("git", ["push", "--force", "-u", remote, branch]);
|
||||
} else {
|
||||
$("git", ["push", "-u", remote, branch]);
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
branch,
|
||||
remote,
|
||||
force,
|
||||
message: `Successfully pushed branch ${branch} to ${remote}`
|
||||
};
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// mcp/issue.ts
|
||||
init_out4();
|
||||
@@ -125043,6 +125060,7 @@ async function startMcpHttpServer(state) {
|
||||
name: ghPullfrogMcpName,
|
||||
version: "0.0.1"
|
||||
});
|
||||
const ctx = await getMcpContext();
|
||||
const tools = [
|
||||
SelectModeTool,
|
||||
CreateCommentTool,
|
||||
@@ -125060,9 +125078,9 @@ async function startMcpHttpServer(state) {
|
||||
GetCheckSuiteLogsTool,
|
||||
DebugShellCommandTool,
|
||||
AddLabelsTool,
|
||||
CreateBranchTool,
|
||||
CreateBranchTool(ctx),
|
||||
CommitFilesTool,
|
||||
PushBranchTool
|
||||
PushBranchTool(ctx)
|
||||
];
|
||||
if (!isProgressCommentDisabled()) {
|
||||
tools.push(ReportProgressTool);
|
||||
@@ -125156,7 +125174,6 @@ init_github();
|
||||
// utils/setup.ts
|
||||
import { execSync } from "node:child_process";
|
||||
init_cli();
|
||||
init_github();
|
||||
function setupGitConfig() {
|
||||
const repoDir = process.cwd();
|
||||
log.info("\u{1F527} Setting up git configuration...");
|
||||
@@ -125176,9 +125193,10 @@ function setupGitConfig() {
|
||||
);
|
||||
}
|
||||
}
|
||||
function setupGitAuth(ctx) {
|
||||
function setupGit(ctx) {
|
||||
const { githubInstallationToken: githubInstallationToken2, payload } = ctx;
|
||||
const repoDir = process.cwd();
|
||||
log.info("\u{1F510} Setting up git authentication...");
|
||||
log.info("\u{1F527} Setting up git configuration...");
|
||||
try {
|
||||
execSync("git config --local --unset-all http.https://github.com/.extraheader", {
|
||||
cwd: repoDir,
|
||||
@@ -125188,24 +125206,39 @@ function setupGitAuth(ctx) {
|
||||
} catch {
|
||||
log.debug("No existing authentication headers to remove");
|
||||
}
|
||||
const remoteUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${ctx.repoContext.owner}/${ctx.repoContext.name}.git`;
|
||||
$("git", ["remote", "set-url", "origin", remoteUrl], { cwd: repoDir });
|
||||
log.info("\u2713 Updated remote URL with authentication token (scoped to repo)");
|
||||
}
|
||||
function setupGitBranch(payload) {
|
||||
$("git", ["config", "--local", "credential.helper", ""], { cwd: repoDir });
|
||||
$("git", ["config", "--local", "--add", "credential.helper", "!gh auth git-credential"], {
|
||||
cwd: repoDir,
|
||||
env: { GH_TOKEN: githubInstallationToken2 }
|
||||
});
|
||||
log.info("\u2713 Configured gh as credential helper");
|
||||
if (payload.event.is_pr !== true || !payload.event.issue_number) {
|
||||
log.debug("Not a PR event, staying on default branch");
|
||||
return;
|
||||
return { pushRemote: "origin" };
|
||||
}
|
||||
const prNumber = payload.event.issue_number;
|
||||
const repoDir = process.cwd();
|
||||
log.info(`\u{1F33F} Checking out PR #${prNumber}...`);
|
||||
const token = getGitHubInstallationToken();
|
||||
$("gh", ["pr", "checkout", prNumber.toString()], {
|
||||
cwd: repoDir,
|
||||
env: { GH_TOKEN: token }
|
||||
env: { GH_TOKEN: githubInstallationToken2 }
|
||||
});
|
||||
log.info(`\u2713 Successfully checked out PR #${prNumber}`);
|
||||
const pushRemote = detectPushRemote();
|
||||
if (pushRemote !== "origin") {
|
||||
log.info(`\u{1F374} Fork PR detected, will push to remote: ${pushRemote}`);
|
||||
}
|
||||
return { pushRemote };
|
||||
}
|
||||
function detectPushRemote() {
|
||||
try {
|
||||
const branch = $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
|
||||
const upstream = $("git", ["rev-parse", "--abbrev-ref", `${branch}@{upstream}`], {
|
||||
log: false
|
||||
});
|
||||
return upstream.split("/")[0];
|
||||
} catch {
|
||||
return "origin";
|
||||
}
|
||||
}
|
||||
|
||||
// utils/timer.ts
|
||||
@@ -125245,13 +125278,11 @@ async function main(inputs) {
|
||||
const partialCtx = await initializeContext(inputs, payload);
|
||||
const ctx = partialCtx;
|
||||
timer.checkpoint("initializeContext");
|
||||
setupGitAuth({
|
||||
githubInstallationToken: ctx.githubInstallationToken,
|
||||
repoContext: ctx.repoContext
|
||||
});
|
||||
const { pushRemote } = setupGit(ctx);
|
||||
ctx.pushRemote = pushRemote;
|
||||
timer.checkpoint("setupGit");
|
||||
await setupTempDirectory(ctx);
|
||||
timer.checkpoint("setupTempDirectory");
|
||||
setupGitBranch(ctx.payload);
|
||||
await startMcpServer(ctx);
|
||||
mcpServerClose = ctx.mcpServerClose;
|
||||
timer.checkpoint("startMcpServer");
|
||||
@@ -125349,6 +125380,21 @@ async function initializeContext(inputs, payload) {
|
||||
setupGitConfig();
|
||||
const githubInstallationToken2 = await setupGitHubInstallationToken();
|
||||
const repoContext = parseRepoContext();
|
||||
const octokit = new Octokit2({
|
||||
auth: githubInstallationToken2
|
||||
});
|
||||
let repo;
|
||||
try {
|
||||
const response = await octokit.repos.get({
|
||||
owner: repoContext.owner,
|
||||
repo: repoContext.name
|
||||
});
|
||||
repo = response.data;
|
||||
} catch {
|
||||
repo = {
|
||||
default_branch: "main"
|
||||
};
|
||||
}
|
||||
const { agentName, agent: agent2 } = await resolveAgent(
|
||||
inputs,
|
||||
payload,
|
||||
@@ -125360,6 +125406,7 @@ async function initializeContext(inputs, payload) {
|
||||
inputs,
|
||||
githubInstallationToken: githubInstallationToken2,
|
||||
repoContext,
|
||||
repo,
|
||||
agentName,
|
||||
agent: agent2,
|
||||
payload: resolvedPayload,
|
||||
@@ -125448,7 +125495,9 @@ async function startMcpServer(ctx) {
|
||||
const { url: url2, close } = await startMcpHttpServer({
|
||||
payload: ctx.payload,
|
||||
modes: allModes,
|
||||
agentName: ctx.agentName
|
||||
agentName: ctx.agentName,
|
||||
repo: ctx.repo,
|
||||
pushRemote: ctx.pushRemote
|
||||
});
|
||||
ctx.mcpServerUrl = url2;
|
||||
ctx.mcpServerClose = close;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { mkdtemp } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { flatMorph } from "@ark/util";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import { encode as toonEncode } from "@toon-format/toon";
|
||||
import { type } from "arktype";
|
||||
import { agents } from "./agents/index.ts";
|
||||
@@ -22,7 +23,7 @@ import {
|
||||
revokeGitHubInstallationToken,
|
||||
setupGitHubInstallationToken,
|
||||
} from "./utils/github.ts";
|
||||
import { setupGitAuth, setupGitBranch, setupGitConfig } from "./utils/setup.ts";
|
||||
import { setupGit, setupGitConfig } from "./utils/setup.ts";
|
||||
import { Timer } from "./utils/timer.ts";
|
||||
|
||||
// runtime validation using agents (needed for ArkType)
|
||||
@@ -64,16 +65,13 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
||||
const ctx = partialCtx as MainContext;
|
||||
timer.checkpoint("initializeContext");
|
||||
|
||||
setupGitAuth({
|
||||
githubInstallationToken: ctx.githubInstallationToken,
|
||||
repoContext: ctx.repoContext,
|
||||
});
|
||||
const { pushRemote } = setupGit(ctx);
|
||||
ctx.pushRemote = pushRemote;
|
||||
timer.checkpoint("setupGit");
|
||||
|
||||
await setupTempDirectory(ctx);
|
||||
timer.checkpoint("setupTempDirectory");
|
||||
|
||||
setupGitBranch(ctx.payload);
|
||||
|
||||
await startMcpServer(ctx);
|
||||
mcpServerClose = ctx.mcpServerClose;
|
||||
timer.checkpoint("startMcpServer");
|
||||
@@ -205,14 +203,16 @@ To fix this, add the required secret to your GitHub repository:
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
interface MainContext {
|
||||
export interface MainContext {
|
||||
inputs: Inputs;
|
||||
githubInstallationToken: string;
|
||||
repoContext: RepoContext;
|
||||
repo: Awaited<ReturnType<Octokit["repos"]["get"]>>["data"];
|
||||
agentName: AgentName;
|
||||
agent: (typeof agents)[AgentName];
|
||||
sharedTempDir: string;
|
||||
payload: Payload;
|
||||
pushRemote: string;
|
||||
mcpServerUrl: string;
|
||||
mcpServerClose: () => Promise<void>;
|
||||
mcpServers: ReturnType<typeof createMcpConfigs>;
|
||||
@@ -227,7 +227,13 @@ async function initializeContext(
|
||||
): Promise<
|
||||
Omit<
|
||||
MainContext,
|
||||
"mcpServerUrl" | "mcpServerClose" | "mcpServers" | "cliPath" | "apiKey" | "apiKeys"
|
||||
| "mcpServerUrl"
|
||||
| "mcpServerClose"
|
||||
| "mcpServers"
|
||||
| "cliPath"
|
||||
| "apiKey"
|
||||
| "apiKeys"
|
||||
| "pushRemote"
|
||||
>
|
||||
> {
|
||||
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
||||
@@ -237,6 +243,24 @@ async function initializeContext(
|
||||
const githubInstallationToken = await setupGitHubInstallationToken();
|
||||
const repoContext = parseRepoContext();
|
||||
|
||||
// fetch repo data
|
||||
const octokit = new Octokit({
|
||||
auth: githubInstallationToken,
|
||||
});
|
||||
let repo: Awaited<ReturnType<Octokit["repos"]["get"]>>["data"];
|
||||
try {
|
||||
const response = await octokit.repos.get({
|
||||
owner: repoContext.owner,
|
||||
repo: repoContext.name,
|
||||
});
|
||||
repo = response.data;
|
||||
} catch {
|
||||
// fallback to minimal repo data if API call fails
|
||||
repo = {
|
||||
default_branch: "main",
|
||||
} as Awaited<ReturnType<Octokit["repos"]["get"]>>["data"];
|
||||
}
|
||||
|
||||
// resolve agent and update payload with resolved agent name
|
||||
const { agentName, agent } = await resolveAgent(
|
||||
inputs,
|
||||
@@ -250,6 +274,7 @@ async function initializeContext(
|
||||
inputs,
|
||||
githubInstallationToken,
|
||||
repoContext,
|
||||
repo,
|
||||
agentName,
|
||||
agent,
|
||||
payload: resolvedPayload,
|
||||
@@ -324,9 +349,7 @@ async function resolveAgent(
|
||||
return { agentName, agent };
|
||||
}
|
||||
|
||||
async function setupTempDirectory(
|
||||
ctx: Omit<MainContext, "payload" | "mcpServers" | "cliPath" | "apiKey">
|
||||
): Promise<void> {
|
||||
async function setupTempDirectory(ctx: MainContext): Promise<void> {
|
||||
ctx.sharedTempDir = await mkdtemp(join(tmpdir(), "pullfrog-"));
|
||||
process.env.PULLFROG_TEMP_DIR = ctx.sharedTempDir;
|
||||
log.info(`📂 PULLFROG_TEMP_DIR has been created at ${ctx.sharedTempDir}`);
|
||||
@@ -372,6 +395,8 @@ async function startMcpServer(ctx: MainContext): Promise<void> {
|
||||
payload: ctx.payload,
|
||||
modes: allModes,
|
||||
agentName: ctx.agentName,
|
||||
repo: ctx.repo,
|
||||
pushRemote: ctx.pushRemote,
|
||||
});
|
||||
ctx.mcpServerUrl = url;
|
||||
ctx.mcpServerClose = close;
|
||||
|
||||
+3
-3
@@ -150,7 +150,7 @@ export async function reportProgress({ body }: { body: string }): Promise<
|
||||
}
|
||||
| undefined
|
||||
> {
|
||||
const ctx = getMcpContext();
|
||||
const ctx = await getMcpContext();
|
||||
|
||||
const bodyWithFooter = addFooter(body, ctx.payload);
|
||||
const existingCommentId = getProgressCommentId();
|
||||
@@ -242,7 +242,7 @@ export async function deleteProgressComment(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ctx = getMcpContext();
|
||||
const ctx = await getMcpContext();
|
||||
|
||||
await ctx.octokit.rest.issues.deleteComment({
|
||||
owner: ctx.owner,
|
||||
@@ -324,7 +324,7 @@ export async function ensureProgressCommentUpdated(payload?: Payload): Promise<v
|
||||
// try to get payload from MCP context if available, otherwise use provided payload
|
||||
let resolvedPayload: Payload | undefined;
|
||||
try {
|
||||
const ctx = getMcpContext();
|
||||
const ctx = await getMcpContext();
|
||||
resolvedPayload = ctx.payload;
|
||||
} catch {
|
||||
// MCP context not initialized, use provided payload
|
||||
|
||||
+74
-61
@@ -2,54 +2,62 @@ import { type } from "arktype";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { containsSecrets } from "../utils/secrets.ts";
|
||||
import { $ } from "../utils/shell.ts";
|
||||
import { contextualize, tool } from "./shared.ts";
|
||||
import { contextualize, type McpContext, tool } from "./shared.ts";
|
||||
|
||||
export const CreateBranch = type({
|
||||
branchName: type.string.describe(
|
||||
"The name of the branch to create (e.g., 'pullfrog/123-fix-bug')"
|
||||
),
|
||||
baseBranch: type.string.describe("The base branch to create from (e.g., 'main')").default("main"),
|
||||
});
|
||||
export function CreateBranchTool(ctx: McpContext) {
|
||||
const defaultBranch = ctx.repo.default_branch || "main";
|
||||
const CreateBranch = type({
|
||||
branchName: type.string.describe(
|
||||
"The name of the branch to create (e.g., 'pullfrog/123-fix-bug')"
|
||||
),
|
||||
baseBranch: type.string
|
||||
.describe(`The base branch to create from (defaults to '${defaultBranch}')`)
|
||||
.default(defaultBranch),
|
||||
});
|
||||
|
||||
export const CreateBranchTool = tool({
|
||||
name: "create_branch",
|
||||
description:
|
||||
"Create a new git branch from the specified base branch. The branch will be created locally and pushed to the remote repository.",
|
||||
parameters: CreateBranch,
|
||||
execute: contextualize(async ({ branchName, baseBranch }) => {
|
||||
// validate branch name for secrets
|
||||
if (containsSecrets(branchName)) {
|
||||
throw new Error(
|
||||
"Branch creation blocked: secrets detected in branch name. " +
|
||||
"Please remove any sensitive information (API keys, tokens, passwords) before creating a branch."
|
||||
);
|
||||
}
|
||||
return tool({
|
||||
name: "create_branch",
|
||||
description:
|
||||
"Create a new git branch from the specified base branch. The branch will be created locally and pushed to the remote repository.",
|
||||
parameters: CreateBranch,
|
||||
execute: contextualize(async ({ branchName, baseBranch }) => {
|
||||
// baseBranch should always be defined due to default, but TypeScript needs help
|
||||
const resolvedBaseBranch = baseBranch || ctx.repo.default_branch || "main";
|
||||
|
||||
log.info(`Creating branch ${branchName} from ${baseBranch}`);
|
||||
// validate branch name for secrets
|
||||
if (containsSecrets(branchName)) {
|
||||
throw new Error(
|
||||
"Branch creation blocked: secrets detected in branch name. " +
|
||||
"Please remove any sensitive information (API keys, tokens, passwords) before creating a branch."
|
||||
);
|
||||
}
|
||||
|
||||
// fetch base branch to ensure we're up to date
|
||||
$("git", ["fetch", "origin", baseBranch, "--depth=1"]);
|
||||
log.info(`Creating branch ${branchName} from ${resolvedBaseBranch}`);
|
||||
|
||||
// checkout base branch, ensuring it matches the remote version
|
||||
// -B creates or resets the branch to match origin/baseBranch
|
||||
$("git", ["checkout", "-B", baseBranch, `origin/${baseBranch}`]);
|
||||
// fetch base branch to ensure we're up to date
|
||||
$("git", ["fetch", "origin", resolvedBaseBranch, "--depth=1"]);
|
||||
|
||||
// create and checkout new branch
|
||||
$("git", ["checkout", "-b", branchName]);
|
||||
// checkout base branch, ensuring it matches the remote version
|
||||
// -B creates or resets the branch to match origin/baseBranch
|
||||
$("git", ["checkout", "-B", resolvedBaseBranch, `origin/${resolvedBaseBranch}`]);
|
||||
|
||||
// push branch to remote (set upstream)
|
||||
$("git", ["push", "-u", "origin", branchName]);
|
||||
// create and checkout new branch
|
||||
$("git", ["checkout", "-b", branchName]);
|
||||
|
||||
log.info(`Successfully created and pushed branch ${branchName}`);
|
||||
// push branch to remote (set upstream)
|
||||
$("git", ["push", "-u", "origin", branchName]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
branchName,
|
||||
baseBranch,
|
||||
message: `Branch ${branchName} created from ${baseBranch} and pushed to remote`,
|
||||
};
|
||||
}),
|
||||
});
|
||||
log.info(`Successfully created and pushed branch ${branchName}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
branchName,
|
||||
baseBranch: resolvedBaseBranch,
|
||||
message: `Branch ${branchName} created from ${resolvedBaseBranch} and pushed to remote`,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export const CommitFiles = type({
|
||||
message: type.string.describe("The commit message"),
|
||||
@@ -129,27 +137,32 @@ export const PushBranch = type({
|
||||
force: type.boolean.describe("Force push (use with caution)").default(false),
|
||||
});
|
||||
|
||||
export const PushBranchTool = tool({
|
||||
name: "push_branch",
|
||||
description:
|
||||
"Push the current branch (or specified branch) to the remote repository. Never force push unless explicitly requested.",
|
||||
parameters: PushBranch,
|
||||
execute: contextualize(async ({ branchName, force }) => {
|
||||
const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
|
||||
export function PushBranchTool(ctx: McpContext) {
|
||||
const remote = ctx.pushRemote;
|
||||
|
||||
if (force) {
|
||||
log.warning(`Force pushing branch ${branch} - this will overwrite remote history`);
|
||||
$("git", ["push", "--force", "origin", branch]);
|
||||
} else {
|
||||
log.info(`Pushing branch ${branch} to remote`);
|
||||
$("git", ["push", "origin", branch]);
|
||||
}
|
||||
return tool({
|
||||
name: "push_branch",
|
||||
description:
|
||||
"Push the current branch (or specified branch) to the remote repository. Never force push unless explicitly requested.",
|
||||
parameters: PushBranch,
|
||||
execute: contextualize(async ({ branchName, force }) => {
|
||||
const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
branch,
|
||||
force,
|
||||
message: `Successfully pushed branch ${branch} to remote`,
|
||||
};
|
||||
}),
|
||||
});
|
||||
log.info(`Pushing branch ${branch} to ${remote}`);
|
||||
if (force) {
|
||||
log.warning(`Force pushing - this will overwrite remote history`);
|
||||
$("git", ["push", "--force", "-u", remote, branch]);
|
||||
} else {
|
||||
$("git", ["push", "-u", remote, branch]);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
branch,
|
||||
remote,
|
||||
force,
|
||||
message: `Successfully pushed branch ${branch} to ${remote}`,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
+6
-2
@@ -24,6 +24,7 @@ import { GetReviewCommentsTool, ListPullRequestReviewsTool } from "./reviewComme
|
||||
import { SelectModeTool } from "./selectMode.ts";
|
||||
import {
|
||||
addTools,
|
||||
getMcpContext,
|
||||
initMcpContext,
|
||||
isProgressCommentDisabled,
|
||||
type McpInitContext,
|
||||
@@ -71,6 +72,9 @@ export async function startMcpHttpServer(
|
||||
version: "0.0.1",
|
||||
});
|
||||
|
||||
// get context for dynamic tool creation
|
||||
const ctx = await getMcpContext();
|
||||
|
||||
const tools: Tool<any, any>[] = [
|
||||
SelectModeTool,
|
||||
CreateCommentTool,
|
||||
@@ -88,9 +92,9 @@ export async function startMcpHttpServer(
|
||||
GetCheckSuiteLogsTool,
|
||||
DebugShellCommandTool,
|
||||
AddLabelsTool,
|
||||
CreateBranchTool,
|
||||
CreateBranchTool(ctx),
|
||||
CommitFilesTool,
|
||||
PushBranchTool,
|
||||
PushBranchTool(ctx),
|
||||
];
|
||||
|
||||
// only include ReportProgressTool if progress comment is not disabled
|
||||
|
||||
+25
-7
@@ -9,30 +9,48 @@ export interface McpInitContext {
|
||||
payload: Payload;
|
||||
modes: Mode[];
|
||||
agentName?: string;
|
||||
repo: Awaited<ReturnType<Octokit["repos"]["get"]>>["data"];
|
||||
pushRemote: string;
|
||||
}
|
||||
|
||||
let mcpInitContext: McpInitContext | undefined;
|
||||
let cachedMcpContext: McpContext | undefined;
|
||||
|
||||
// this must be called on mcp server initialization
|
||||
export function initMcpContext(state: McpInitContext): void {
|
||||
mcpInitContext = state;
|
||||
// clear cached context when reinitializing
|
||||
cachedMcpContext = undefined;
|
||||
}
|
||||
|
||||
export interface McpContext extends McpInitContext, RepoContext {
|
||||
octokit: Octokit;
|
||||
repo: Awaited<ReturnType<Octokit["repos"]["get"]>>["data"];
|
||||
}
|
||||
|
||||
export function getMcpContext(): McpContext {
|
||||
export async function getMcpContext(): Promise<McpContext> {
|
||||
if (!mcpInitContext) {
|
||||
throw new Error("MCP context not initialized. Call initializeMcpContext first.");
|
||||
}
|
||||
return {
|
||||
|
||||
// return cached context if available
|
||||
if (cachedMcpContext) {
|
||||
return cachedMcpContext;
|
||||
}
|
||||
|
||||
const repoContext = parseRepoContext();
|
||||
const octokit = new Octokit({
|
||||
auth: getGitHubInstallationToken(),
|
||||
});
|
||||
|
||||
cachedMcpContext = {
|
||||
...mcpInitContext,
|
||||
...parseRepoContext(),
|
||||
octokit: new Octokit({
|
||||
auth: getGitHubInstallationToken(),
|
||||
}),
|
||||
...repoContext,
|
||||
octokit,
|
||||
repo: mcpInitContext.repo,
|
||||
};
|
||||
|
||||
return cachedMcpContext;
|
||||
}
|
||||
|
||||
export function isProgressCommentDisabled(): boolean {
|
||||
@@ -180,7 +198,7 @@ export const contextualize = <T>(
|
||||
) => {
|
||||
return async (params: T): Promise<ToolResult> => {
|
||||
try {
|
||||
const ctx = getMcpContext();
|
||||
const ctx = await getMcpContext();
|
||||
const result = await executor(params, ctx);
|
||||
return handleToolSuccess(result);
|
||||
} catch (error) {
|
||||
|
||||
+44
-32
@@ -1,8 +1,7 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { existsSync, rmSync } from "node:fs";
|
||||
import type { Payload } from "../external.ts";
|
||||
import type { MainContext } from "../main.ts";
|
||||
import { log } from "./cli.ts";
|
||||
import { getGitHubInstallationToken, type RepoContext } from "./github.ts";
|
||||
import { $ } from "./shell.ts";
|
||||
|
||||
export interface SetupOptions {
|
||||
@@ -63,20 +62,23 @@ export function setupGitConfig(): void {
|
||||
}
|
||||
}
|
||||
|
||||
export type SetupGitResult = {
|
||||
pushRemote: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Setup git authentication using GitHub installation token
|
||||
* Always uses the installation token, scoped to the current repo only
|
||||
* Unified git setup: configures authentication and checks out PR branch if applicable.
|
||||
* Uses gh as credential helper so git push works with any remote (including forks).
|
||||
* For PR events, gh pr checkout sets up proper remote tracking.
|
||||
* Returns the remote to push to (detected from branch tracking after checkout).
|
||||
*/
|
||||
export function setupGitAuth(ctx: {
|
||||
githubInstallationToken: string;
|
||||
repoContext: RepoContext;
|
||||
}): void {
|
||||
export function setupGit(ctx: MainContext): SetupGitResult {
|
||||
const { githubInstallationToken, payload } = ctx;
|
||||
const repoDir = process.cwd();
|
||||
|
||||
log.info("🔐 Setting up git authentication...");
|
||||
log.info("🔧 Setting up git configuration...");
|
||||
|
||||
// Remove existing git auth headers that actions/checkout might have set
|
||||
// Use --local to scope to this repo only
|
||||
// remove existing git auth headers that actions/checkout might have set
|
||||
try {
|
||||
execSync("git config --local --unset-all http.https://github.com/.extraheader", {
|
||||
cwd: repoDir,
|
||||
@@ -87,36 +89,46 @@ export function setupGitAuth(ctx: {
|
||||
log.debug("No existing authentication headers to remove");
|
||||
}
|
||||
|
||||
// Update remote URL to embed the token
|
||||
// This is scoped to the repo's .git/config, not the user's global config
|
||||
const remoteUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${ctx.repoContext.owner}/${ctx.repoContext.name}.git`;
|
||||
$("git", ["remote", "set-url", "origin", remoteUrl], { cwd: repoDir });
|
||||
log.info("✓ Updated remote URL with authentication token (scoped to repo)");
|
||||
}
|
||||
// set up gh as credential helper - this makes git use GH_TOKEN for any remote
|
||||
$("git", ["config", "--local", "credential.helper", ""], { cwd: repoDir });
|
||||
$("git", ["config", "--local", "--add", "credential.helper", "!gh auth git-credential"], {
|
||||
cwd: repoDir,
|
||||
env: { GH_TOKEN: githubInstallationToken },
|
||||
});
|
||||
log.info("✓ Configured gh as credential helper");
|
||||
|
||||
/**
|
||||
* Setup git branch based on payload event context.
|
||||
* For PR events, uses `gh pr checkout` which handles fork PRs automatically.
|
||||
* For non-PR events, stays on the default branch.
|
||||
*/
|
||||
export function setupGitBranch(payload: Payload): void {
|
||||
// only checkout for PR events - use issue_number directly (no dependency on branch field)
|
||||
// non-PR events: stay on default branch, push to origin
|
||||
if (payload.event.is_pr !== true || !payload.event.issue_number) {
|
||||
log.debug("Not a PR event, staying on default branch");
|
||||
return;
|
||||
return { pushRemote: "origin" };
|
||||
}
|
||||
|
||||
// checkout PR branch - gh pr checkout handles fork remotes and tracking automatically
|
||||
const prNumber = payload.event.issue_number;
|
||||
const repoDir = process.cwd();
|
||||
|
||||
log.info(`🌿 Checking out PR #${prNumber}...`);
|
||||
|
||||
// gh pr checkout handles fork PRs by setting up remotes automatically
|
||||
const token = getGitHubInstallationToken();
|
||||
$("gh", ["pr", "checkout", prNumber.toString()], {
|
||||
cwd: repoDir,
|
||||
env: { GH_TOKEN: token },
|
||||
env: { GH_TOKEN: githubInstallationToken },
|
||||
});
|
||||
|
||||
log.info(`✓ Successfully checked out PR #${prNumber}`);
|
||||
|
||||
// detect the push remote from branch tracking (set by gh pr checkout)
|
||||
const pushRemote = detectPushRemote();
|
||||
if (pushRemote !== "origin") {
|
||||
log.info(`🍴 Fork PR detected, will push to remote: ${pushRemote}`);
|
||||
}
|
||||
return { pushRemote };
|
||||
}
|
||||
|
||||
function detectPushRemote(): string {
|
||||
try {
|
||||
const branch = $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
|
||||
const upstream = $("git", ["rev-parse", "--abbrev-ref", `${branch}@{upstream}`], {
|
||||
log: false,
|
||||
});
|
||||
// upstream is like "remote/branch", extract remote name
|
||||
return upstream.split("/")[0];
|
||||
} catch {
|
||||
return "origin";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user