From efb4ad186f559553d6cd4e23f9441fb1968fb71a Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Mon, 15 Dec 2025 23:56:47 -0800 Subject: [PATCH] Improve remote tracking --- entry | 56 ++++++++++++++++++++++------------------------ main.ts | 2 +- mcp/git.ts | 16 +++++++++----- package.json | 2 +- utils/setup.ts | 60 +++++++++++++++++++++++++------------------------- 5 files changed, 68 insertions(+), 68 deletions(-) diff --git a/entry b/entry index 8d1d9d5..7a992c7 100755 --- a/entry +++ b/entry @@ -83327,7 +83327,7 @@ function query({ // package.json var package_default = { name: "@pullfrog/action", - version: "0.0.142", + version: "0.0.143", type: "module", files: [ "index.js", @@ -123148,19 +123148,19 @@ function PushBranchTool(ctx) { parameters: PushBranch, execute: execute(ctx, async ({ branchName, force }) => { const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false }); - log.info(`Pushing branch ${branch} to ${remote}`); + const isUrl = remote.startsWith("https://"); + const args3 = force ? ["push", "--force", ...isUrl ? [] : ["-u"], remote, branch] : ["push", ...isUrl ? [] : ["-u"], remote, branch]; + log.info(`Pushing branch ${branch} to ${isUrl ? "(fork URL)" : remote}`); if (force) { log.warning(`Force pushing - this will overwrite remote history`); - $("git", ["push", "--force", "-u", remote, branch]); - } else { - $("git", ["push", "-u", remote, branch]); } + $("git", args3); return { success: true, branch, - remote, + remote: isUrl ? "(fork URL)" : remote, force, - message: `Successfully pushed branch ${branch} to ${remote}` + message: `Successfully pushed branch ${branch}` }; }) }); @@ -123859,7 +123859,7 @@ function setupGitConfig() { ); } } -function setupGit(ctx) { +async function setupGit(ctx) { const repoDir = process.cwd(); log.info("\u{1F527} Setting up git authentication..."); try { @@ -123885,29 +123885,25 @@ function setupGit(ctx) { env: { GH_TOKEN: ctx.githubInstallationToken } }); 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}`); - const forkUrl = $("git", ["remote", "get-url", pushRemote], { cwd: repoDir, log: false }); - const authedForkUrl = forkUrl.replace( - "https://github.com/", - `https://x-access-token:${ctx.githubInstallationToken}@github.com/` + const pr = await ctx.octokit.rest.pulls.get({ + owner: ctx.owner, + repo: ctx.name, + pull_number: prNumber + }); + const headRepo = pr.data.head.repo; + const baseRepo = pr.data.base.repo; + if (!headRepo || headRepo.full_name === baseRepo.full_name) { + return { pushRemote: "origin" }; + } + if (!pr.data.maintainer_can_modify) { + log.warning( + `\u26A0\uFE0F Fork PR from ${headRepo.owner.login} does not allow maintainer edits. Push may fail.` ); - $("git", ["remote", "set-url", pushRemote, authedForkUrl], { cwd: repoDir }); - log.info(`\u2713 Updated ${pushRemote} URL with authentication token`); - } - 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"; } + const token = process.env.GITHUB_TOKEN || ctx.githubInstallationToken; + const forkUrl = `https://x-access-token:${token}@github.com/${headRepo.full_name}.git`; + log.info(`\u{1F374} Fork PR detected, will push to: ${headRepo.full_name}`); + return { pushRemote: forkUrl }; } // utils/timer.ts @@ -123946,7 +123942,7 @@ async function main(inputs) { const partialCtx = await initializeContext(inputs, payload); const ctx = partialCtx; timer.checkpoint("initializeContext"); - const { pushRemote } = setupGit(ctx); + const { pushRemote } = await setupGit(ctx); ctx.pushRemote = pushRemote; timer.checkpoint("setupGit"); await setupTempDirectory(ctx); diff --git a/main.ts b/main.ts index 90e1a14..92ede1c 100644 --- a/main.ts +++ b/main.ts @@ -64,7 +64,7 @@ export async function main(inputs: Inputs): Promise { const ctx = partialCtx as Context; timer.checkpoint("initializeContext"); - const { pushRemote } = setupGit(ctx); + const { pushRemote } = await setupGit(ctx); ctx.pushRemote = pushRemote; timer.checkpoint("setupGit"); diff --git a/mcp/git.ts b/mcp/git.ts index c8c9f68..9d78c13 100644 --- a/mcp/git.ts +++ b/mcp/git.ts @@ -152,20 +152,24 @@ export function PushBranchTool(ctx: Context) { execute: execute(ctx, async ({ branchName, force }) => { const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false }); - log.info(`Pushing branch ${branch} to ${remote}`); + // skip -u flag when pushing to URL (can't set upstream without a remote name) + const isUrl = remote.startsWith("https://"); + const args = force + ? ["push", "--force", ...(isUrl ? [] : ["-u"]), remote, branch] + : ["push", ...(isUrl ? [] : ["-u"]), remote, branch]; + + log.info(`Pushing branch ${branch} to ${isUrl ? "(fork URL)" : remote}`); if (force) { log.warning(`Force pushing - this will overwrite remote history`); - $("git", ["push", "--force", "-u", remote, branch]); - } else { - $("git", ["push", "-u", remote, branch]); } + $("git", args); return { success: true, branch, - remote, + remote: isUrl ? "(fork URL)" : remote, force, - message: `Successfully pushed branch ${branch} to ${remote}`, + message: `Successfully pushed branch ${branch}`, }; }), }); diff --git a/package.json b/package.json index 863ea36..f281a92 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pullfrog/action", - "version": "0.0.142", + "version": "0.0.143", "type": "module", "files": [ "index.js", diff --git a/utils/setup.ts b/utils/setup.ts index c60eca4..8247a2e 100644 --- a/utils/setup.ts +++ b/utils/setup.ts @@ -68,11 +68,10 @@ export type SetupGitResult = { /** * 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). + * For fork PRs, returns a full URL to push to (since gh pr checkout doesn't set up remotes in Actions). + * For same-repo PRs, returns "origin". */ -export function setupGit(ctx: Context): SetupGitResult { +export async function setupGit(ctx: Context): Promise { const repoDir = process.cwd(); log.info("🔧 Setting up git authentication..."); @@ -99,7 +98,7 @@ export function setupGit(ctx: Context): SetupGitResult { return { pushRemote: "origin" }; } - // checkout PR branch - gh pr checkout handles fork remotes and tracking automatically + // checkout PR branch const prNumber = ctx.payload.event.issue_number; log.info(`🌿 Checking out PR #${prNumber}...`); $("gh", ["pr", "checkout", prNumber.toString()], { @@ -108,31 +107,32 @@ export function setupGit(ctx: Context): SetupGitResult { }); 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}`); - // embed token in fork remote URL too - const forkUrl = $("git", ["remote", "get-url", pushRemote], { cwd: repoDir, log: false }); - const authedForkUrl = forkUrl.replace( - "https://github.com/", - `https://x-access-token:${ctx.githubInstallationToken}@github.com/` - ); - $("git", ["remote", "set-url", pushRemote, authedForkUrl], { cwd: repoDir }); - log.info(`✓ Updated ${pushRemote} URL with authentication token`); - } - return { pushRemote }; -} + // check if this is a fork PR - gh pr checkout in Actions doesn't set up remotes for forks + const pr = await ctx.octokit.rest.pulls.get({ + owner: ctx.owner, + repo: ctx.name, + pull_number: prNumber, + }); -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"; + const headRepo = pr.data.head.repo; + const baseRepo = pr.data.base.repo; + + // not a fork - push to origin + if (!headRepo || headRepo.full_name === baseRepo.full_name) { + return { pushRemote: "origin" }; } + + // fork PR - return the full URL with auth token embedded + // git push accepts URLs directly, no remote needed + if (!pr.data.maintainer_can_modify) { + log.warning( + `⚠️ Fork PR from ${headRepo.owner.login} does not allow maintainer edits. Push may fail.` + ); + } + + // use GITHUB_TOKEN for fork push - it has the right permissions in Actions + const token = process.env.GITHUB_TOKEN || ctx.githubInstallationToken; + const forkUrl = `https://x-access-token:${token}@github.com/${headRepo.full_name}.git`; + log.info(`🍴 Fork PR detected, will push to: ${headRepo.full_name}`); + return { pushRemote: forkUrl }; }