Compare commits

...

3 Commits

Author SHA1 Message Date
Colin McDonnell efb4ad186f Improve remote tracking 2025-12-15 23:56:47 -08:00
Colin McDonnell c2cedce1bc 0.0.142 2025-12-15 23:38:46 -08:00
Colin McDonnell e383dd33dd Clean up destructuring 2025-12-15 23:32:02 -08:00
6 changed files with 89 additions and 83 deletions
+36 -36
View File
@@ -83327,7 +83327,7 @@ function query({
// package.json // package.json
var package_default = { var package_default = {
name: "@pullfrog/action", name: "@pullfrog/action",
version: "0.0.141", version: "0.0.143",
type: "module", type: "module",
files: [ files: [
"index.js", "index.js",
@@ -98180,8 +98180,9 @@ function buildPullfrogFooter(params) {
parts.push(`Using [${params.agent.displayName}](${params.agent.url})`); parts.push(`Using [${params.agent.displayName}](${params.agent.url})`);
} }
if (params.workflowRun) { if (params.workflowRun) {
const { owner, repo, runId } = params.workflowRun; parts.push(
parts.push(`[View workflow run](https://github.com/${owner}/${repo}/actions/runs/${runId})`); `[View workflow run](https://github.com/${params.workflowRun.owner}/${params.workflowRun.repo}/actions/runs/${params.workflowRun.runId})`
);
} }
if (params.customParts) { if (params.customParts) {
parts.push(...params.customParts); parts.push(...params.customParts);
@@ -123147,19 +123148,19 @@ function PushBranchTool(ctx) {
parameters: PushBranch, parameters: PushBranch,
execute: execute(ctx, async ({ branchName, force }) => { execute: execute(ctx, async ({ branchName, force }) => {
const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false }); const 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) { if (force) {
log.warning(`Force pushing - this will overwrite remote history`); log.warning(`Force pushing - this will overwrite remote history`);
$("git", ["push", "--force", "-u", remote, branch]);
} else {
$("git", ["push", "-u", remote, branch]);
} }
$("git", args3);
return { return {
success: true, success: true,
branch, branch,
remote, remote: isUrl ? "(fork URL)" : remote,
force, force,
message: `Successfully pushed branch ${branch} to ${remote}` message: `Successfully pushed branch ${branch}`
}; };
}) })
}); });
@@ -123858,10 +123859,9 @@ function setupGitConfig() {
); );
} }
} }
function setupGit(ctx) { async function setupGit(ctx) {
const { githubInstallationToken: githubInstallationToken2, payload } = ctx;
const repoDir = process.cwd(); const repoDir = process.cwd();
log.info("\u{1F527} Setting up git configuration..."); log.info("\u{1F527} Setting up git authentication...");
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,
@@ -123871,39 +123871,39 @@ function setupGit(ctx) {
} catch { } catch {
log.debug("No existing authentication headers to remove"); log.debug("No existing authentication headers to remove");
} }
process.env.GH_TOKEN = githubInstallationToken2; const originUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${ctx.owner}/${ctx.name}.git`;
$("git", ["config", "--local", "credential.helper", ""], { cwd: repoDir }); $("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
$("git", ["config", "--local", "--add", "credential.helper", "!gh auth git-credential"], { log.info("\u2713 Updated origin URL with authentication token");
cwd: repoDir if (ctx.payload.event.is_pr !== true || !ctx.payload.event.issue_number) {
});
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"); log.debug("Not a PR event, staying on default branch");
return { pushRemote: "origin" }; return { pushRemote: "origin" };
} }
const prNumber = payload.event.issue_number; const prNumber = ctx.payload.event.issue_number;
log.info(`\u{1F33F} Checking out PR #${prNumber}...`); log.info(`\u{1F33F} Checking out PR #${prNumber}...`);
$("gh", ["pr", "checkout", prNumber.toString()], { $("gh", ["pr", "checkout", prNumber.toString()], {
cwd: repoDir, cwd: repoDir,
env: { GH_TOKEN: githubInstallationToken2 } env: { GH_TOKEN: ctx.githubInstallationToken }
}); });
log.info(`\u2713 Successfully checked out PR #${prNumber}`); log.info(`\u2713 Successfully checked out PR #${prNumber}`);
const pushRemote = detectPushRemote(); const pr = await ctx.octokit.rest.pulls.get({
if (pushRemote !== "origin") { owner: ctx.owner,
log.info(`\u{1F374} Fork PR detected, will push to remote: ${pushRemote}`); 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" };
} }
return { pushRemote }; if (!pr.data.maintainer_can_modify) {
} log.warning(
function detectPushRemote() { `\u26A0\uFE0F Fork PR from ${headRepo.owner.login} does not allow maintainer edits. Push may fail.`
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 // utils/timer.ts
@@ -123942,7 +123942,7 @@ 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");
const { pushRemote } = setupGit(ctx); const { pushRemote } = await setupGit(ctx);
ctx.pushRemote = pushRemote; ctx.pushRemote = pushRemote;
timer.checkpoint("setupGit"); timer.checkpoint("setupGit");
await setupTempDirectory(ctx); await setupTempDirectory(ctx);
+1 -1
View File
@@ -64,7 +64,7 @@ export async function main(inputs: Inputs): Promise<MainResult> {
const ctx = partialCtx as Context; const ctx = partialCtx as Context;
timer.checkpoint("initializeContext"); timer.checkpoint("initializeContext");
const { pushRemote } = setupGit(ctx); const { pushRemote } = await setupGit(ctx);
ctx.pushRemote = pushRemote; ctx.pushRemote = pushRemote;
timer.checkpoint("setupGit"); timer.checkpoint("setupGit");
+10 -6
View File
@@ -152,20 +152,24 @@ export function PushBranchTool(ctx: Context) {
execute: execute(ctx, async ({ branchName, force }) => { execute: execute(ctx, async ({ branchName, force }) => {
const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false }); 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) { if (force) {
log.warning(`Force pushing - this will overwrite remote history`); log.warning(`Force pushing - this will overwrite remote history`);
$("git", ["push", "--force", "-u", remote, branch]);
} else {
$("git", ["push", "-u", remote, branch]);
} }
$("git", args);
return { return {
success: true, success: true,
branch, branch,
remote, remote: isUrl ? "(fork URL)" : remote,
force, force,
message: `Successfully pushed branch ${branch} to ${remote}`, message: `Successfully pushed branch ${branch}`,
}; };
}), }),
}); });
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@pullfrog/action", "name": "@pullfrog/action",
"version": "0.0.141", "version": "0.0.143",
"type": "module", "type": "module",
"files": [ "files": [
"index.js", "index.js",
+3 -2
View File
@@ -40,8 +40,9 @@ export function buildPullfrogFooter(params: BuildPullfrogFooterParams): string {
} }
if (params.workflowRun) { if (params.workflowRun) {
const { owner, repo, runId } = params.workflowRun; parts.push(
parts.push(`[View workflow run](https://github.com/${owner}/${repo}/actions/runs/${runId})`); `[View workflow run](https://github.com/${params.workflowRun.owner}/${params.workflowRun.repo}/actions/runs/${params.workflowRun.runId})`
);
} }
if (params.customParts) { if (params.customParts) {
+38 -37
View File
@@ -68,15 +68,13 @@ export type SetupGitResult = {
/** /**
* Unified git setup: configures authentication and checks out PR branch if applicable. * 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 fork PRs, returns a full URL to push to (since gh pr checkout doesn't set up remotes in Actions).
* For PR events, gh pr checkout sets up proper remote tracking. * For same-repo PRs, returns "origin".
* Returns the remote to push to (detected from branch tracking after checkout).
*/ */
export function setupGit(ctx: Context): SetupGitResult { export async function setupGit(ctx: Context): Promise<SetupGitResult> {
const { githubInstallationToken, payload } = ctx;
const repoDir = process.cwd(); const repoDir = process.cwd();
log.info("🔧 Setting up git configuration..."); log.info("🔧 Setting up git authentication...");
// remove existing git auth headers that actions/checkout might have set // remove existing git auth headers that actions/checkout might have set
try { try {
@@ -89,49 +87,52 @@ export function setupGit(ctx: Context): SetupGitResult {
log.debug("No existing authentication headers to remove"); log.debug("No existing authentication headers to remove");
} }
// set GH_TOKEN in process.env so gh auth git-credential uses the installation token // embed token directly in origin URL - simple and doesn't expose token in env
// (not the default GITHUB_TOKEN which has different permissions) const originUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${ctx.owner}/${ctx.name}.git`;
process.env.GH_TOKEN = githubInstallationToken; $("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
log.info("✓ Updated origin URL with authentication token");
// 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,
});
log.info("✓ Configured gh as credential helper");
// non-PR events: stay on default branch, push to origin // non-PR events: stay on default branch, push to origin
if (payload.event.is_pr !== true || !payload.event.issue_number) { if (ctx.payload.event.is_pr !== true || !ctx.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 { pushRemote: "origin" }; return { pushRemote: "origin" };
} }
// checkout PR branch - gh pr checkout handles fork remotes and tracking automatically // checkout PR branch
const prNumber = payload.event.issue_number; const prNumber = ctx.payload.event.issue_number;
log.info(`🌿 Checking out PR #${prNumber}...`); log.info(`🌿 Checking out PR #${prNumber}...`);
$("gh", ["pr", "checkout", prNumber.toString()], { $("gh", ["pr", "checkout", prNumber.toString()], {
cwd: repoDir, cwd: repoDir,
env: { GH_TOKEN: githubInstallationToken }, env: { GH_TOKEN: ctx.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) // check if this is a fork PR - gh pr checkout in Actions doesn't set up remotes for forks
const pushRemote = detectPushRemote(); const pr = await ctx.octokit.rest.pulls.get({
if (pushRemote !== "origin") { owner: ctx.owner,
log.info(`🍴 Fork PR detected, will push to remote: ${pushRemote}`); repo: ctx.name,
} pull_number: prNumber,
return { pushRemote }; });
}
function detectPushRemote(): string { const headRepo = pr.data.head.repo;
try { const baseRepo = pr.data.base.repo;
const branch = $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
const upstream = $("git", ["rev-parse", "--abbrev-ref", `${branch}@{upstream}`], { // not a fork - push to origin
log: false, if (!headRepo || headRepo.full_name === baseRepo.full_name) {
}); return { pushRemote: "origin" };
// upstream is like "remote/branch", extract remote name
return upstream.split("/")[0];
} catch {
return "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 };
} }