Clean up init

This commit is contained in:
Colin McDonnell
2025-12-15 22:21:47 -08:00
parent 6f96458e2d
commit 0fced1dfa6
7 changed files with 320 additions and 199 deletions
+131 -82
View File
@@ -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;
+37 -12
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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";
}
}