From e031505eb2d97ed82d5e66ad2cf9cca532ddece8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 19:49:22 +0000 Subject: [PATCH 1/2] Initial plan From 89a8fcdeaac38ae33360854ef9af5cbc6c4fd94f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:11:27 +0000 Subject: [PATCH 2/2] Fix git fetch auth after clean_git_credentials.sh for push_to_pull_request_branch and create_pull_request Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- ...-git-fetch-auth-after-clean-credentials.md | 5 +++ actions/setup/js/generate_git_patch.cjs | 14 ++++++-- .../setup/js/git_patch_integration.test.cjs | 36 +++++++++++++++++++ actions/setup/js/safe_outputs_handlers.cjs | 13 ++++++- 4 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 .changeset/patch-fix-git-fetch-auth-after-clean-credentials.md diff --git a/.changeset/patch-fix-git-fetch-auth-after-clean-credentials.md b/.changeset/patch-fix-git-fetch-auth-after-clean-credentials.md new file mode 100644 index 00000000000..d1776200f1f --- /dev/null +++ b/.changeset/patch-fix-git-fetch-auth-after-clean-credentials.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Fix `push_to_pull_request_branch` and `create_pull_request` so all git fetches in `generate_git_patch.cjs` authenticate using `GIT_CONFIG_*` environment variables, ensuring they succeed after `clean_git_credentials.sh` strips credentials from the git remote URL. Also pass per-handler `github-token` (when configured) through to `generateGitPatch` so cross-repo PATs are used for git fetch operations. diff --git a/actions/setup/js/generate_git_patch.cjs b/actions/setup/js/generate_git_patch.cjs index 200298e813e..b4fff957ba2 100644 --- a/actions/setup/js/generate_git_patch.cjs +++ b/actions/setup/js/generate_git_patch.cjs @@ -89,6 +89,8 @@ function getPatchPathForRepo(branchName, repoSlug) { * Use this for multi-repo scenarios where repos are checked out to subdirectories. * @param {string} [options.repoSlug] - Repository slug (owner/repo) to include in patch filename for disambiguation. * Required for multi-repo scenarios to prevent patch file collisions. + * @param {string} [options.token] - GitHub token for git authentication. Falls back to GITHUB_TOKEN env var. + * Use this for cross-repo scenarios where a custom PAT with access to the target repo is needed. * @returns {Promise} Object with patch info or error */ async function generateGitPatch(branchName, baseBranch, options = {}) { @@ -146,9 +148,10 @@ async function generateGitPatch(branchName, baseBranch, options = {}) { // Configure git authentication via GIT_CONFIG_* environment variables. // This ensures the fetch works when .git/config credentials are unavailable // (e.g. after clean_git_credentials.sh) and on GitHub Enterprise Server (GHES). + // Use options.token when provided (cross-repo PAT), falling back to GITHUB_TOKEN. // SECURITY: The auth header is passed via env vars so it is never written to // .git/config on disk, preventing file-monitoring attacks. - const fetchEnv = { ...process.env, ...getGitAuthEnv() }; + const fetchEnv = { ...process.env, ...getGitAuthEnv(options.token) }; try { // Explicitly fetch origin/branchName to ensure we have the latest @@ -192,8 +195,15 @@ async function generateGitPatch(branchName, baseBranch, options = {}) { // origin/ doesn't exist locally, try to fetch it debugLog(`Strategy 1 (full): origin/${defaultBranch} not found locally, attempting fetch`); try { + // Configure git authentication via GIT_CONFIG_* environment variables. + // This ensures the fetch works when .git/config credentials are unavailable + // (e.g. after clean_git_credentials.sh) and on GitHub Enterprise Server (GHES). + // Use options.token when provided (cross-repo PAT), falling back to GITHUB_TOKEN. + // SECURITY: The auth header is passed via env vars so it is never written to + // .git/config on disk, preventing file-monitoring attacks. + const fullFetchEnv = { ...process.env, ...getGitAuthEnv(options.token) }; // Use "--" to prevent branch names starting with "-" from being interpreted as options - execGitSync(["fetch", "origin", "--", defaultBranch], { cwd }); + execGitSync(["fetch", "origin", "--", defaultBranch], { cwd, env: fullFetchEnv }); hasLocalDefaultBranch = true; debugLog(`Strategy 1 (full): Successfully fetched origin/${defaultBranch}`); } catch (fetchErr) { diff --git a/actions/setup/js/git_patch_integration.test.cjs b/actions/setup/js/git_patch_integration.test.cjs index 584d601edf5..17cbb8f8374 100644 --- a/actions/setup/js/git_patch_integration.test.cjs +++ b/actions/setup/js/git_patch_integration.test.cjs @@ -703,6 +703,42 @@ describe("git patch integration tests", () => { } }); + it("should use options.token instead of GITHUB_TOKEN when provided", async () => { + // Set up a feature branch with a commit to push + execGit(["checkout", "-b", "token-option-test"], { cwd: workingRepo }); + fs.writeFileSync(path.join(workingRepo, "token-test.txt"), "token option test\n"); + execGit(["add", "token-test.txt"], { cwd: workingRepo }); + execGit(["commit", "-m", "Token option base commit"], { cwd: workingRepo }); + execGit(["push", "-u", "origin", "token-option-test"], { cwd: workingRepo }); + + // Add a second commit that will become the incremental patch + fs.writeFileSync(path.join(workingRepo, "token-test2.txt"), "token option test 2\n"); + execGit(["add", "token-test2.txt"], { cwd: workingRepo }); + execGit(["commit", "-m", "Token option new commit"], { cwd: workingRepo }); + + // Delete the tracking ref so generateGitPatch has to re-fetch + execGit(["update-ref", "-d", "refs/remotes/origin/token-option-test"], { cwd: workingRepo }); + + const restore = setTestEnv(workingRepo); + try { + // Pass a custom token via options.token — the local git server ignores auth so the + // fetch still succeeds, but we verify no credentials are written to disk. + const result = await generateGitPatch("token-option-test", "main", { + mode: "incremental", + token: "ghs_custom_token_for_cross_repo", + }); + + expect(result.success).toBe(true); + + // Verify the extraheader was never written to git config (auth is passed via env vars only) + const configCheck = spawnSync("git", ["config", "--local", "--get", "http.https://github.example.com/.extraheader"], { cwd: workingRepo, encoding: "utf8" }); + // exit status 1 means the key does not exist — that is what we want + expect(configCheck.status).toBe(1); + } finally { + restore(); + } + }); + it("should include all commits in full mode even when origin/branch exists", async () => { // Create a feature branch with first commit execGit(["checkout", "-b", "full-mode-branch"], { cwd: workingRepo }); diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index c8588efc571..8db48a8574e 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -319,6 +319,11 @@ function createHandlers(server, appendSafeOutput, config = {}) { if (repoSlug) { patchOptions.repoSlug = repoSlug; } + // Pass per-handler token so cross-repo PATs are used for git fetch when configured. + // Falls back to GITHUB_TOKEN if not set. + if (prConfig["github-token"]) { + patchOptions.token = prConfig["github-token"]; + } const patchResult = await generateGitPatch(entry.branch, baseBranch, patchOptions); if (!patchResult.success) { @@ -423,7 +428,13 @@ function createHandlers(server, appendSafeOutput, config = {}) { // Incremental mode only includes commits since origin/branchName, // preventing patches that include already-existing commits server.debug(`Generating incremental patch for push_to_pull_request_branch with branch: ${entry.branch}, baseBranch: ${baseBranch}`); - const patchResult = await generateGitPatch(entry.branch, baseBranch, { mode: "incremental" }); + // Pass per-handler token so cross-repo PATs are used for git fetch when configured. + // Falls back to GITHUB_TOKEN if not set. + const pushPatchOptions = { mode: "incremental" }; + if (pushConfig["github-token"]) { + pushPatchOptions.token = pushConfig["github-token"]; + } + const patchResult = await generateGitPatch(entry.branch, baseBranch, pushPatchOptions); if (!patchResult.success) { // Patch generation failed or patch is empty