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