From 85ec30f8733dd475099fe0529234a6ba2ae80692 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:26:13 +0000 Subject: [PATCH 1/9] Initial plan From a202a9e5c5a8861b62ca61ff7a2f353c88b6eee1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:56:56 +0000 Subject: [PATCH 2/9] Fix GH_HOST mismatch in gh pr checkout by deriving host from GITHUB_SERVER_URL When the DIFC proxy is active, start_difc_proxy.sh sets GH_HOST=localhost:18443 in GITHUB_ENV. This causes `gh pr checkout` to fail because GH_HOST doesn't match any git remote (origin points to github.com or a GHE host). Fix: derive the correct GH_HOST from GITHUB_SERVER_URL and pass it as an env override when calling `exec.exec("gh", ["pr", "checkout", ...])`. This ensures: - DIFC proxy's localhost:18443 GH_HOST is overridden with the real GitHub host - GHES hosts correctly use their actual hostname - github.com uses github.com Also adds new tests for the GH_HOST override behavior and updates existing test expectations to match the new 3-argument exec.exec call. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/55bc2334-76f1-49a8-9009-1219a7a6cf87 Co-authored-by: dsyme <7204669+dsyme@users.noreply.github.com> --- actions/setup/js/checkout_pr_branch.cjs | 12 ++- actions/setup/js/checkout_pr_branch.test.cjs | 86 +++++++++++++++----- 2 files changed, 77 insertions(+), 21 deletions(-) diff --git a/actions/setup/js/checkout_pr_branch.cjs b/actions/setup/js/checkout_pr_branch.cjs index 2396ac946be..f9ede0018f8 100644 --- a/actions/setup/js/checkout_pr_branch.cjs +++ b/actions/setup/js/checkout_pr_branch.cjs @@ -176,7 +176,17 @@ async function main() { } core.info(`Checking out PR #${prNumber} using gh CLI`); - await exec.exec("gh", ["pr", "checkout", prNumber.toString()]); + + // Derive the correct GH_HOST from GITHUB_SERVER_URL so that gh CLI targets the + // actual GitHub instance for remote matching. When the DIFC proxy is active, + // GH_HOST is overridden to localhost:18443 (the proxy address) in GITHUB_ENV, + // which doesn't match any git remote (origin points to github.com or a GHE host). + // We must use the real GitHub host so gh pr checkout can resolve the repository. + const serverUrl = process.env.GITHUB_SERVER_URL || "https://github.com"; + const ghHost = serverUrl.replace(/^https?:\/\//, "").replace(/\/+$/, ""); + await exec.exec("gh", ["pr", "checkout", prNumber.toString()], { + env: { ...process.env, GH_HOST: ghHost }, + }); // Log the resulting branch after checkout let currentBranch = ""; diff --git a/actions/setup/js/checkout_pr_branch.test.cjs b/actions/setup/js/checkout_pr_branch.test.cjs index b8056c6df6d..e7bcd753d8a 100644 --- a/actions/setup/js/checkout_pr_branch.test.cjs +++ b/actions/setup/js/checkout_pr_branch.test.cjs @@ -67,6 +67,7 @@ describe("checkout_pr_branch.cjs", () => { global.exec = mockExec; global.context = mockContext; process.env.GITHUB_TOKEN = "test-token"; + process.env.GITHUB_SERVER_URL = "https://github.com"; }); afterEach(() => { @@ -75,6 +76,7 @@ describe("checkout_pr_branch.cjs", () => { delete global.context; delete global.github; delete process.env.GITHUB_TOKEN; + delete process.env.GITHUB_SERVER_URL; vi.clearAllMocks(); }); @@ -241,8 +243,8 @@ If the pull request is still open, verify that: expect(mockCore.info).toHaveBeenCalledWith("Strategy: gh pr checkout"); expect(mockCore.info).toHaveBeenCalledWith("Reason: pull_request event from fork repository; head branch exists only in fork, not in origin"); - // Verify gh pr checkout is used instead of git fetch - expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"]); + // Verify gh pr checkout is used instead of git fetch, with GH_HOST override + expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"], expect.objectContaining({ env: expect.objectContaining({ GH_HOST: "github.com" }) })); expect(mockExec.exec).not.toHaveBeenCalledWith("git", ["fetch", "origin", "feature-branch", "--depth=2"]); expect(mockCore.setFailed).not.toHaveBeenCalled(); @@ -301,8 +303,8 @@ If the pull request is still open, verify that: expect(mockCore.info).toHaveBeenCalledWith("Checking out PR #123 using gh CLI"); - // Updated expectation: no env options passed, GH_TOKEN comes from step environment - expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"]); + // GH_HOST is overridden with value derived from GITHUB_SERVER_URL to avoid proxy/stale values + expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"], expect.objectContaining({ env: expect.objectContaining({ GH_HOST: "github.com" }) })); expect(mockCore.info).toHaveBeenCalledWith("✅ Successfully checked out PR #123"); expect(mockCore.setFailed).not.toHaveBeenCalled(); @@ -324,16 +326,14 @@ If the pull request is still open, verify that: expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_API}: Failed to checkout PR branch: gh pr checkout failed`); }); - it("should pass environment variables to gh command", async () => { - // This test is no longer relevant since we don't pass env options explicitly - // The GH_TOKEN is now set at the step level, not in the exec options - // Keeping the test but updating to verify the call without env options + it("should pass GH_HOST derived from GITHUB_SERVER_URL to gh command", async () => { + // GH_HOST is always derived from GITHUB_SERVER_URL to avoid stale/proxy values process.env.CUSTOM_VAR = "custom-value"; await runScript(); - // Verify exec is called without env options - expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"]); + // Verify exec is called with GH_HOST derived from GITHUB_SERVER_URL + expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"], expect.objectContaining({ env: expect.objectContaining({ GH_HOST: "github.com" }) })); delete process.env.CUSTOM_VAR; }); @@ -378,9 +378,8 @@ If the pull request is still open, verify that: await runScript(); expect(mockCore.info).toHaveBeenCalledWith("Event: pull_request_target"); - // pull_request_target uses gh pr checkout, not git - // Updated expectation: no third argument (env options removed) - expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"]); + // pull_request_target uses gh pr checkout with GH_HOST override + expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"], expect.objectContaining({ env: expect.objectContaining({ GH_HOST: "github.com" }) })); }); it("should handle pull_request_review event", async () => { @@ -389,9 +388,8 @@ If the pull request is still open, verify that: await runScript(); expect(mockCore.info).toHaveBeenCalledWith("Event: pull_request_review"); - // pull_request_review uses gh pr checkout, not git - // Updated expectation: no third argument (env options removed) - expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"]); + // pull_request_review uses gh pr checkout with GH_HOST override + expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"], expect.objectContaining({ env: expect.objectContaining({ GH_HOST: "github.com" }) })); }); it("should handle pull_request_review_comment event", async () => { @@ -399,8 +397,8 @@ If the pull request is still open, verify that: await runScript(); - // Updated expectation: no third argument (env options removed) - expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"]); + // pull_request_review_comment uses gh pr checkout with GH_HOST override + expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"], expect.objectContaining({ env: expect.objectContaining({ GH_HOST: "github.com" }) })); }); }); @@ -500,7 +498,7 @@ If the pull request is still open, verify that: // Verify fork detection logging with reason expect(mockCore.info).toHaveBeenCalledWith("Is fork PR: true (different repository names)"); expect(mockCore.warning).toHaveBeenCalledWith("⚠️ Fork PR detected - gh pr checkout will fetch from fork repository"); - expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"]); + expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"], expect.objectContaining({ env: expect.objectContaining({ GH_HOST: "github.com" }) })); }); it("should NOT detect fork when repo has fork flag but same full_name", async () => { @@ -516,7 +514,7 @@ If the pull request is still open, verify that: expect(mockCore.info).toHaveBeenCalledWith("Is fork PR: false (same repository)"); expect(mockCore.warning).not.toHaveBeenCalledWith(expect.stringContaining("Fork PR detected")); // Still uses gh pr checkout because pull_request_target always does - expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"]); + expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"], expect.objectContaining({ env: expect.objectContaining({ GH_HOST: "github.com" }) })); }); it("should detect non-fork PRs in pull_request_target events", async () => { @@ -887,4 +885,52 @@ If the pull request is still open, verify that: expect(mockCore.setFailed).not.toHaveBeenCalled(); }); }); + + describe("GH_HOST override for gh pr checkout", () => { + it("should override DIFC proxy GH_HOST (localhost:18443) with actual GitHub host", async () => { + // Simulate active DIFC proxy that set GH_HOST=localhost:18443 in env + process.env.GH_HOST = "localhost:18443"; + process.env.GITHUB_SERVER_URL = "https://github.com"; + mockContext.eventName = "issue_comment"; + + await runScript(); + + // GH_HOST should be overridden to github.com, not localhost:18443 + expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"], expect.objectContaining({ env: expect.objectContaining({ GH_HOST: "github.com" }) })); + + delete process.env.GH_HOST; + }); + + it("should use GHE host from GITHUB_SERVER_URL for gh pr checkout", async () => { + process.env.GITHUB_SERVER_URL = "https://myorg.ghe.com"; + mockContext.eventName = "pull_request_target"; + + await runScript(); + + expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"], expect.objectContaining({ env: expect.objectContaining({ GH_HOST: "myorg.ghe.com" }) })); + + process.env.GITHUB_SERVER_URL = "https://github.com"; + }); + + it("should strip https:// protocol from GITHUB_SERVER_URL when deriving GH_HOST", async () => { + process.env.GITHUB_SERVER_URL = "https://github.com"; + mockContext.eventName = "pull_request_target"; + + await runScript(); + + // Should not include the protocol in GH_HOST + expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"], expect.objectContaining({ env: expect.objectContaining({ GH_HOST: "github.com" }) })); + }); + + it("should default to github.com when GITHUB_SERVER_URL is not set", async () => { + delete process.env.GITHUB_SERVER_URL; + mockContext.eventName = "issue_comment"; + + await runScript(); + + expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"], expect.objectContaining({ env: expect.objectContaining({ GH_HOST: "github.com" }) })); + + process.env.GITHUB_SERVER_URL = "https://github.com"; + }); + }); }); From 13fa414bdbd19c23c73ea80bac4a104edf489174 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:59:16 +0000 Subject: [PATCH 3/9] Address code review: combine regex, remove redundant env cleanup in tests Agent-Logs-Url: https://github.com/github/gh-aw/sessions/55bc2334-76f1-49a8-9009-1219a7a6cf87 Co-authored-by: dsyme <7204669+dsyme@users.noreply.github.com> --- actions/setup/js/checkout_pr_branch.cjs | 2 +- actions/setup/js/checkout_pr_branch.test.cjs | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/actions/setup/js/checkout_pr_branch.cjs b/actions/setup/js/checkout_pr_branch.cjs index f9ede0018f8..de39fbc3f80 100644 --- a/actions/setup/js/checkout_pr_branch.cjs +++ b/actions/setup/js/checkout_pr_branch.cjs @@ -183,7 +183,7 @@ async function main() { // which doesn't match any git remote (origin points to github.com or a GHE host). // We must use the real GitHub host so gh pr checkout can resolve the repository. const serverUrl = process.env.GITHUB_SERVER_URL || "https://github.com"; - const ghHost = serverUrl.replace(/^https?:\/\//, "").replace(/\/+$/, ""); + const ghHost = serverUrl.replace(/^https?:\/\/|\/+$/g, ""); await exec.exec("gh", ["pr", "checkout", prNumber.toString()], { env: { ...process.env, GH_HOST: ghHost }, }); diff --git a/actions/setup/js/checkout_pr_branch.test.cjs b/actions/setup/js/checkout_pr_branch.test.cjs index e7bcd753d8a..62a9499374c 100644 --- a/actions/setup/js/checkout_pr_branch.test.cjs +++ b/actions/setup/js/checkout_pr_branch.test.cjs @@ -908,8 +908,6 @@ If the pull request is still open, verify that: await runScript(); expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"], expect.objectContaining({ env: expect.objectContaining({ GH_HOST: "myorg.ghe.com" }) })); - - process.env.GITHUB_SERVER_URL = "https://github.com"; }); it("should strip https:// protocol from GITHUB_SERVER_URL when deriving GH_HOST", async () => { @@ -929,8 +927,6 @@ If the pull request is still open, verify that: await runScript(); expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"], expect.objectContaining({ env: expect.objectContaining({ GH_HOST: "github.com" }) })); - - process.env.GITHUB_SERVER_URL = "https://github.com"; }); }); }); From 4af69218c98d2a2ca819a1d60cce2c6cd6c7f96b Mon Sep 17 00:00:00 2001 From: Don Syme Date: Tue, 14 Apr 2026 06:45:41 +1000 Subject: [PATCH 4/9] Update actions/setup/js/checkout_pr_branch.test.cjs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- actions/setup/js/checkout_pr_branch.test.cjs | 30 +++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/actions/setup/js/checkout_pr_branch.test.cjs b/actions/setup/js/checkout_pr_branch.test.cjs index 62a9499374c..2cf2713c3f9 100644 --- a/actions/setup/js/checkout_pr_branch.test.cjs +++ b/actions/setup/js/checkout_pr_branch.test.cjs @@ -888,17 +888,25 @@ If the pull request is still open, verify that: describe("GH_HOST override for gh pr checkout", () => { it("should override DIFC proxy GH_HOST (localhost:18443) with actual GitHub host", async () => { - // Simulate active DIFC proxy that set GH_HOST=localhost:18443 in env - process.env.GH_HOST = "localhost:18443"; - process.env.GITHUB_SERVER_URL = "https://github.com"; - mockContext.eventName = "issue_comment"; - - await runScript(); - - // GH_HOST should be overridden to github.com, not localhost:18443 - expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"], expect.objectContaining({ env: expect.objectContaining({ GH_HOST: "github.com" }) })); - - delete process.env.GH_HOST; + const previousGhHost = process.env.GH_HOST; + + try { + // Simulate active DIFC proxy that set GH_HOST=localhost:18443 in env + process.env.GH_HOST = "localhost:18443"; + process.env.GITHUB_SERVER_URL = "https://github.com"; + mockContext.eventName = "issue_comment"; + + await runScript(); + + // GH_HOST should be overridden to github.com, not localhost:18443 + expect(mockExec.exec).toHaveBeenCalledWith("gh", ["pr", "checkout", "123"], expect.objectContaining({ env: expect.objectContaining({ GH_HOST: "github.com" }) })); + } finally { + if (previousGhHost === undefined) { + delete process.env.GH_HOST; + } else { + process.env.GH_HOST = previousGhHost; + } + } }); it("should use GHE host from GITHUB_SERVER_URL for gh pr checkout", async () => { From 830cdaa512b73cc9234ec046979322edf015881b Mon Sep 17 00:00:00 2001 From: Don Syme Date: Tue, 14 Apr 2026 07:50:37 +1000 Subject: [PATCH 5/9] refactor: extract shared getGhEnv() helper for GH_HOST derivation Centralize GH_HOST derivation from GITHUB_SERVER_URL into git_helpers.cjs with getGitHubHost() and getGhEnv() helpers, replacing scattered inline overrides across checkout_pr_branch.cjs, assign_issue.cjs, and apply_safe_outputs_replay.cjs. --- .../setup/js/apply_safe_outputs_replay.cjs | 3 +- actions/setup/js/assign_issue.cjs | 3 +- actions/setup/js/checkout_pr_branch.cjs | 13 +++----- actions/setup/js/checkout_pr_branch.test.cjs | 3 ++ actions/setup/js/git_helpers.cjs | 32 +++++++++++++++++++ 5 files changed, 44 insertions(+), 10 deletions(-) diff --git a/actions/setup/js/apply_safe_outputs_replay.cjs b/actions/setup/js/apply_safe_outputs_replay.cjs index 5583b0a1878..f73c289cffd 100644 --- a/actions/setup/js/apply_safe_outputs_replay.cjs +++ b/actions/setup/js/apply_safe_outputs_replay.cjs @@ -23,6 +23,7 @@ const fs = require("fs"); const path = require("path"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { getGhEnv } = require("./git_helpers.cjs"); const { ERR_CONFIG, ERR_SYSTEM, ERR_VALIDATION } = require("./error_codes.cjs"); const { AGENT_OUTPUT_FILENAME, TMP_GH_AW_PATH } = require("./constants.cjs"); @@ -77,7 +78,7 @@ async function downloadAgentArtifact(runId, destDir, repoSlug) { args.push("--repo", repoSlug); } - const exitCode = await exec.exec("gh", args); + const exitCode = await exec.exec("gh", args, { env: getGhEnv() }); if (exitCode !== 0) { throw new Error(`${ERR_SYSTEM}: Failed to download agent artifact from run ${runId}`); } diff --git a/actions/setup/js/assign_issue.cjs b/actions/setup/js/assign_issue.cjs index 5bbf202b1f0..d140059caa0 100644 --- a/actions/setup/js/assign_issue.cjs +++ b/actions/setup/js/assign_issue.cjs @@ -3,6 +3,7 @@ const { getAgentName, getIssueDetails, findAgent, assignAgentToIssue } = require("./assign_agent_helpers.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { getGhEnv } = require("./git_helpers.cjs"); const { ERR_API, ERR_CONFIG, ERR_NOT_FOUND, ERR_PERMISSION } = require("./error_codes.cjs"); /** @@ -79,7 +80,7 @@ async function main() { } else { // Use gh CLI for regular user assignment await exec.exec("gh", ["issue", "edit", String(issueNum), "--add-assignee", trimmedAssignee], { - env: { ...process.env, GH_TOKEN: ghToken }, + env: getGhEnv({ GH_TOKEN: ghToken }), }); } diff --git a/actions/setup/js/checkout_pr_branch.cjs b/actions/setup/js/checkout_pr_branch.cjs index de39fbc3f80..ab749233066 100644 --- a/actions/setup/js/checkout_pr_branch.cjs +++ b/actions/setup/js/checkout_pr_branch.cjs @@ -26,6 +26,7 @@ */ const { getErrorMessage } = require("./error_helpers.cjs"); +const { getGhEnv } = require("./git_helpers.cjs"); const { renderTemplateFromFile } = require("./messages_core.cjs"); const { detectForkPR } = require("./pr_helpers.cjs"); const { ERR_API } = require("./error_codes.cjs"); @@ -177,15 +178,11 @@ async function main() { core.info(`Checking out PR #${prNumber} using gh CLI`); - // Derive the correct GH_HOST from GITHUB_SERVER_URL so that gh CLI targets the - // actual GitHub instance for remote matching. When the DIFC proxy is active, - // GH_HOST is overridden to localhost:18443 (the proxy address) in GITHUB_ENV, - // which doesn't match any git remote (origin points to github.com or a GHE host). - // We must use the real GitHub host so gh pr checkout can resolve the repository. - const serverUrl = process.env.GITHUB_SERVER_URL || "https://github.com"; - const ghHost = serverUrl.replace(/^https?:\/\/|\/+$/g, ""); + // Override GH_HOST with the real GitHub hostname so gh pr checkout can resolve + // the repository from git remotes. The DIFC proxy may have set GH_HOST to + // localhost:18443 which doesn't match any remote. await exec.exec("gh", ["pr", "checkout", prNumber.toString()], { - env: { ...process.env, GH_HOST: ghHost }, + env: getGhEnv(), }); // Log the resulting branch after checkout diff --git a/actions/setup/js/checkout_pr_branch.test.cjs b/actions/setup/js/checkout_pr_branch.test.cjs index 2cf2713c3f9..54b6a8119c4 100644 --- a/actions/setup/js/checkout_pr_branch.test.cjs +++ b/actions/setup/js/checkout_pr_branch.test.cjs @@ -153,6 +153,9 @@ If the pull request is still open, verify that: if (module === "./error_codes.cjs") { return require("./error_codes.cjs"); } + if (module === "./git_helpers.cjs") { + return require("./git_helpers.cjs"); + } throw new Error(`Module ${module} not mocked in test`); }; diff --git a/actions/setup/js/git_helpers.cjs b/actions/setup/js/git_helpers.cjs index 70becf63bb2..54ebd754ac7 100644 --- a/actions/setup/js/git_helpers.cjs +++ b/actions/setup/js/git_helpers.cjs @@ -114,7 +114,39 @@ function execGitSync(args, options = {}) { return result.stdout; } +/** + * Derive the real GitHub hostname from GITHUB_SERVER_URL. + * + * When the DIFC proxy is active, GH_HOST is overridden to localhost:18443 + * in GITHUB_ENV. This causes `gh` CLI commands that resolve the repository + * from git remotes (e.g. `gh pr checkout`) to fail because the proxy address + * doesn't match any remote. Use this helper to get the actual GitHub host + * (e.g. "github.com" or "myorg.ghe.com") for per-call GH_HOST overrides. + * + * @returns {string} The GitHub hostname (e.g. "github.com") + */ +function getGitHubHost() { + const serverUrl = process.env.GITHUB_SERVER_URL || "https://github.com"; + return serverUrl.replace(/^https?:\/\/|\/+$/g, ""); +} + +/** + * Build environment variables for a `gh` CLI exec call with the correct GH_HOST. + * + * Spreads process.env and overrides GH_HOST with the real GitHub hostname + * derived from GITHUB_SERVER_URL. Use this for any `gh` CLI call that needs + * to bypass a DIFC proxy GH_HOST override. + * + * @param {Object} [extraEnv] - Additional environment variables to set (e.g. { GH_TOKEN: token }) + * @returns {Object} Environment object suitable for exec.exec options + */ +function getGhEnv(extraEnv) { + return { ...process.env, GH_HOST: getGitHubHost(), ...extraEnv }; +} + module.exports = { execGitSync, + getGhEnv, getGitAuthEnv, + getGitHubHost, }; From 026b6662658a5da611a3399caac557dad53b9dad Mon Sep 17 00:00:00 2001 From: Don Syme Date: Tue, 14 Apr 2026 09:11:32 +1000 Subject: [PATCH 6/9] Update actions/setup/js/git_helpers.cjs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- actions/setup/js/git_helpers.cjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/git_helpers.cjs b/actions/setup/js/git_helpers.cjs index 54ebd754ac7..ce9d2558e52 100644 --- a/actions/setup/js/git_helpers.cjs +++ b/actions/setup/js/git_helpers.cjs @@ -133,15 +133,15 @@ function getGitHubHost() { /** * Build environment variables for a `gh` CLI exec call with the correct GH_HOST. * - * Spreads process.env and overrides GH_HOST with the real GitHub hostname - * derived from GITHUB_SERVER_URL. Use this for any `gh` CLI call that needs - * to bypass a DIFC proxy GH_HOST override. + * Spreads process.env and any additional environment variables, then enforces + * GH_HOST to the real GitHub hostname derived from GITHUB_SERVER_URL. Use this + * for any `gh` CLI call that needs to bypass a DIFC proxy GH_HOST override. * * @param {Object} [extraEnv] - Additional environment variables to set (e.g. { GH_TOKEN: token }) * @returns {Object} Environment object suitable for exec.exec options */ function getGhEnv(extraEnv) { - return { ...process.env, GH_HOST: getGitHubHost(), ...extraEnv }; + return { ...process.env, ...extraEnv, GH_HOST: getGitHubHost() }; } module.exports = { From 0dcc3f7e42c48b01256dc111b590688fd477d0f9 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Tue, 14 Apr 2026 09:27:38 +1000 Subject: [PATCH 7/9] fix: only use getGhEnvForGitOps for git operations that must bypass DIFC proxy Revert GH_HOST override for gh run download and gh issue edit, which should route through the DIFC proxy when active for integrity filtering. Only gh pr checkout needs the real-host override (git remote mismatch). --- actions/setup/js/apply_safe_outputs_replay.cjs | 3 +-- actions/setup/js/assign_issue.cjs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/apply_safe_outputs_replay.cjs b/actions/setup/js/apply_safe_outputs_replay.cjs index f73c289cffd..5583b0a1878 100644 --- a/actions/setup/js/apply_safe_outputs_replay.cjs +++ b/actions/setup/js/apply_safe_outputs_replay.cjs @@ -23,7 +23,6 @@ const fs = require("fs"); const path = require("path"); const { getErrorMessage } = require("./error_helpers.cjs"); -const { getGhEnv } = require("./git_helpers.cjs"); const { ERR_CONFIG, ERR_SYSTEM, ERR_VALIDATION } = require("./error_codes.cjs"); const { AGENT_OUTPUT_FILENAME, TMP_GH_AW_PATH } = require("./constants.cjs"); @@ -78,7 +77,7 @@ async function downloadAgentArtifact(runId, destDir, repoSlug) { args.push("--repo", repoSlug); } - const exitCode = await exec.exec("gh", args, { env: getGhEnv() }); + const exitCode = await exec.exec("gh", args); if (exitCode !== 0) { throw new Error(`${ERR_SYSTEM}: Failed to download agent artifact from run ${runId}`); } diff --git a/actions/setup/js/assign_issue.cjs b/actions/setup/js/assign_issue.cjs index d140059caa0..5bbf202b1f0 100644 --- a/actions/setup/js/assign_issue.cjs +++ b/actions/setup/js/assign_issue.cjs @@ -3,7 +3,6 @@ const { getAgentName, getIssueDetails, findAgent, assignAgentToIssue } = require("./assign_agent_helpers.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); -const { getGhEnv } = require("./git_helpers.cjs"); const { ERR_API, ERR_CONFIG, ERR_NOT_FOUND, ERR_PERMISSION } = require("./error_codes.cjs"); /** @@ -80,7 +79,7 @@ async function main() { } else { // Use gh CLI for regular user assignment await exec.exec("gh", ["issue", "edit", String(issueNum), "--add-assignee", trimmedAssignee], { - env: getGhEnv({ GH_TOKEN: ghToken }), + env: { ...process.env, GH_TOKEN: ghToken }, }); } From 4f8d12f5e1f996cc5dc929ef7f7dd9df49fabd1e Mon Sep 17 00:00:00 2001 From: Don Syme Date: Tue, 14 Apr 2026 09:30:28 +1000 Subject: [PATCH 8/9] chore: remove dead code assign_issue.cjs Superseded by assign_to_agent.cjs, assign_to_user.cjs, and assign_copilot_to_created_issues.cjs in the handler-manager architecture. --- actions/setup/js/assign_issue.cjs | 97 ------------- actions/setup/js/assign_issue.test.cjs | 188 ------------------------- 2 files changed, 285 deletions(-) delete mode 100644 actions/setup/js/assign_issue.cjs delete mode 100644 actions/setup/js/assign_issue.test.cjs diff --git a/actions/setup/js/assign_issue.cjs b/actions/setup/js/assign_issue.cjs deleted file mode 100644 index 5bbf202b1f0..00000000000 --- a/actions/setup/js/assign_issue.cjs +++ /dev/null @@ -1,97 +0,0 @@ -// @ts-check -/// - -const { getAgentName, getIssueDetails, findAgent, assignAgentToIssue } = require("./assign_agent_helpers.cjs"); -const { getErrorMessage } = require("./error_helpers.cjs"); -const { ERR_API, ERR_CONFIG, ERR_NOT_FOUND, ERR_PERMISSION } = require("./error_codes.cjs"); - -/** - * Assign an issue to a user or bot (including copilot) - * This script handles assigning issues after they are created - */ - -async function main() { - // Validate required environment variables - const ghToken = process.env.GH_TOKEN; - const assignee = process.env.ASSIGNEE; - const issueNumber = process.env.ISSUE_NUMBER; - - // Check if GH_TOKEN is present - if (!ghToken?.trim()) { - const docsUrl = "https://github.github.com/gh-aw/reference/safe-outputs/#assigning-issues-to-copilot"; - core.setFailed(`${ERR_CONFIG}: GH_TOKEN environment variable is required but not set. This token is needed to assign issues. For more information on configuring Copilot tokens, see: ${docsUrl}`); - return; - } - - // Validate assignee - if (!assignee?.trim()) { - core.setFailed(`${ERR_CONFIG}: ASSIGNEE environment variable is required but not set`); - return; - } - - // Validate issue number - if (!issueNumber?.trim()) { - core.setFailed(`${ERR_CONFIG}: ISSUE_NUMBER environment variable is required but not set`); - return; - } - - const trimmedAssignee = assignee.trim(); - const issueNum = parseInt(issueNumber.trim(), 10); - - core.info(`Assigning issue #${issueNum} to ${trimmedAssignee}`); - - // Check if the assignee is a known coding agent (e.g., copilot, @copilot) - const agentName = getAgentName(trimmedAssignee); - - try { - if (agentName) { - // Use GraphQL API for agent assignment - // The token is set at the step level via github-token parameter - core.info(`Detected coding agent: ${agentName}. Using GraphQL API for assignment.`); - - // Get repository owner and repo from context - const { owner, repo } = context.repo; - - // Find the agent in the repository - const agentId = await findAgent(owner, repo, agentName); - if (!agentId) { - throw new Error(`${ERR_PERMISSION}: ${agentName} coding agent is not available for this repository`); - } - core.info(`Found ${agentName} coding agent (ID: ${agentId})`); - - // Get issue details - const issueDetails = await getIssueDetails(owner, repo, issueNum); - if (!issueDetails) { - throw new Error(`${ERR_API}: Failed to get issue details`); - } - - // Check if agent is already assigned - if (issueDetails.currentAssignees.some(a => a.id === agentId)) { - core.info(`${agentName} is already assigned to issue #${issueNum}`); - } else { - // Assign agent using GraphQL mutation - uses built-in github object authenticated via github-token (no allowed list filtering) - const success = await assignAgentToIssue(issueDetails.issueId, agentId, issueDetails.currentAssignees, agentName, null); - - if (!success) { - throw new Error(`${ERR_API}: Failed to assign ${agentName} via GraphQL`); - } - } - } else { - // Use gh CLI for regular user assignment - await exec.exec("gh", ["issue", "edit", String(issueNum), "--add-assignee", trimmedAssignee], { - env: { ...process.env, GH_TOKEN: ghToken }, - }); - } - - core.info(`✅ Successfully assigned issue #${issueNum} to ${trimmedAssignee}`); - - // Write summary - await core.summary.addRaw(`## Issue Assignment\n\nSuccessfully assigned issue #${issueNum} to \`${trimmedAssignee}\`.\n`).write(); - } catch (error) { - const errorMessage = getErrorMessage(error); - core.error(`Failed to assign issue: ${errorMessage}`); - core.setFailed(`${ERR_NOT_FOUND}: Failed to assign issue #${issueNum} to ${trimmedAssignee}: ${errorMessage}`); - } -} - -module.exports = { main }; diff --git a/actions/setup/js/assign_issue.test.cjs b/actions/setup/js/assign_issue.test.cjs deleted file mode 100644 index 44fd507e432..00000000000 --- a/actions/setup/js/assign_issue.test.cjs +++ /dev/null @@ -1,188 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from "vitest"; -import fs from "fs"; -import path from "path"; -const { ERR_CONFIG, ERR_NOT_FOUND } = require("./error_codes.cjs"); -const mockCore = { - debug: vi.fn(), - info: vi.fn(), - notice: vi.fn(), - warning: vi.fn(), - error: vi.fn(), - setFailed: vi.fn(), - setOutput: vi.fn(), - exportVariable: vi.fn(), - setSecret: vi.fn(), - getInput: vi.fn(), - getBooleanInput: vi.fn(), - getMultilineInput: vi.fn(), - getState: vi.fn(), - saveState: vi.fn(), - startGroup: vi.fn(), - endGroup: vi.fn(), - group: vi.fn(), - addPath: vi.fn(), - setCommandEcho: vi.fn(), - isDebug: vi.fn().mockReturnValue(!1), - getIDToken: vi.fn(), - toPlatformPath: vi.fn(), - toPosixPath: vi.fn(), - toWin32Path: vi.fn(), - summary: { addRaw: vi.fn().mockReturnThis(), write: vi.fn().mockResolvedValue() }, - }, - mockExec = { exec: vi.fn() }, - mockGithub = { graphql: vi.fn() }, - mockContext = { repo: { owner: "testowner", repo: "testrepo" } }; -((global.core = mockCore), - (global.exec = mockExec), - (global.github = mockGithub), - (global.context = mockContext), - describe("assign_issue.cjs", () => { - let assignIssueScript; - (beforeEach(() => { - (vi.clearAllMocks(), delete process.env.GH_TOKEN, delete process.env.ASSIGNEE, delete process.env.ISSUE_NUMBER); - const scriptPath = path.join(process.cwd(), "assign_issue.cjs"); - assignIssueScript = fs.readFileSync(scriptPath, "utf8"); - }), - describe("Environment variable validation", () => { - (it("should fail when GH_TOKEN is not set", async () => { - ((process.env.ASSIGNEE = "test-user"), - (process.env.ISSUE_NUMBER = "123"), - delete process.env.GH_TOKEN, - await eval(`(async () => { ${assignIssueScript}; await main(); })()`), - expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("GH_TOKEN environment variable is required but not set")), - expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("https://github.github.com/gh-aw/reference/safe-outputs/#assigning-issues-to-copilot")), - expect(mockExec.exec).not.toHaveBeenCalled()); - }), - it("should fail when GH_TOKEN is empty string", async () => { - ((process.env.GH_TOKEN = " "), - (process.env.ASSIGNEE = "test-user"), - (process.env.ISSUE_NUMBER = "123"), - await eval(`(async () => { ${assignIssueScript}; await main(); })()`), - expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("GH_TOKEN environment variable is required but not set")), - expect(mockExec.exec).not.toHaveBeenCalled()); - }), - it("should fail when ASSIGNEE is not set", async () => { - ((process.env.GH_TOKEN = "ghp_test123"), - (process.env.ISSUE_NUMBER = "123"), - delete process.env.ASSIGNEE, - await eval(`(async () => { ${assignIssueScript}; await main(); })()`), - expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_CONFIG}: ASSIGNEE environment variable is required but not set`), - expect(mockExec.exec).not.toHaveBeenCalled()); - }), - it("should fail when ASSIGNEE is empty string", async () => { - ((process.env.GH_TOKEN = "ghp_test123"), - (process.env.ASSIGNEE = " "), - (process.env.ISSUE_NUMBER = "123"), - await eval(`(async () => { ${assignIssueScript}; await main(); })()`), - expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_CONFIG}: ASSIGNEE environment variable is required but not set`), - expect(mockExec.exec).not.toHaveBeenCalled()); - }), - it("should fail when ISSUE_NUMBER is not set", async () => { - ((process.env.GH_TOKEN = "ghp_test123"), - (process.env.ASSIGNEE = "test-user"), - delete process.env.ISSUE_NUMBER, - await eval(`(async () => { ${assignIssueScript}; await main(); })()`), - expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_CONFIG}: ISSUE_NUMBER environment variable is required but not set`), - expect(mockExec.exec).not.toHaveBeenCalled()); - }), - it("should fail when ISSUE_NUMBER is empty string", async () => { - ((process.env.GH_TOKEN = "ghp_test123"), - (process.env.ASSIGNEE = "test-user"), - (process.env.ISSUE_NUMBER = " "), - await eval(`(async () => { ${assignIssueScript}; await main(); })()`), - expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_CONFIG}: ISSUE_NUMBER environment variable is required but not set`), - expect(mockExec.exec).not.toHaveBeenCalled()); - })); - }), - describe("Successful assignment for regular users", () => { - (it("should successfully assign issue to a regular user", async () => { - ((process.env.GH_TOKEN = "ghp_test123"), - (process.env.ASSIGNEE = "test-user"), - (process.env.ISSUE_NUMBER = "456"), - mockExec.exec.mockResolvedValue(0), - await eval(`(async () => { ${assignIssueScript}; await main(); })()`), - expect(mockCore.info).toHaveBeenCalledWith("Assigning issue #456 to test-user"), - expect(mockExec.exec).toHaveBeenCalledWith("gh", ["issue", "edit", "456", "--add-assignee", "test-user"], expect.objectContaining({ env: expect.objectContaining({ GH_TOKEN: "ghp_test123" }) })), - expect(mockCore.info).toHaveBeenCalledWith("✅ Successfully assigned issue #456 to test-user"), - expect(mockCore.summary.addRaw).toHaveBeenCalledWith(expect.stringContaining("Successfully assigned issue #456")), - expect(mockCore.summary.write).toHaveBeenCalled(), - expect(mockCore.setFailed).not.toHaveBeenCalled()); - }), - it("should trim whitespace from environment variables", async () => { - ((process.env.GH_TOKEN = " ghp_test123 "), - (process.env.ASSIGNEE = " test-user "), - (process.env.ISSUE_NUMBER = " 123 "), - mockExec.exec.mockResolvedValue(0), - await eval(`(async () => { ${assignIssueScript}; await main(); })()`), - expect(mockCore.info).toHaveBeenCalledWith("Assigning issue #123 to test-user"), - expect(mockExec.exec).toHaveBeenCalledWith("gh", ["issue", "edit", "123", "--add-assignee", "test-user"], expect.any(Object)), - expect(mockCore.setFailed).not.toHaveBeenCalled()); - }), - it("should include summary in output", async () => { - ((process.env.GH_TOKEN = "ghp_test123"), - (process.env.ASSIGNEE = "test-user"), - (process.env.ISSUE_NUMBER = "123"), - mockExec.exec.mockResolvedValue(0), - await eval(`(async () => { ${assignIssueScript}; await main(); })()`), - expect(mockCore.summary.addRaw).toHaveBeenCalledWith(expect.stringContaining("## Issue Assignment")), - expect(mockCore.summary.addRaw).toHaveBeenCalledWith(expect.stringContaining("Successfully assigned issue #123 to `test-user`")), - expect(mockCore.summary.write).toHaveBeenCalled()); - })); - }), - describe("Error handling for regular users", () => { - (it("should handle gh CLI execution errors", async () => { - ((process.env.GH_TOKEN = "ghp_test123"), (process.env.ASSIGNEE = "test-user"), (process.env.ISSUE_NUMBER = "999")); - const testError = new Error("User not found"); - (mockExec.exec.mockRejectedValue(testError), - await eval(`(async () => { ${assignIssueScript}; await main(); })()`), - expect(mockCore.error).toHaveBeenCalledWith("Failed to assign issue: User not found"), - expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_NOT_FOUND}: Failed to assign issue #999 to test-user: User not found`)); - }), - it("should handle non-Error objects in catch block", async () => { - ((process.env.GH_TOKEN = "ghp_test123"), (process.env.ASSIGNEE = "test-user"), (process.env.ISSUE_NUMBER = "999")); - const stringError = "Command failed"; - (mockExec.exec.mockRejectedValue(stringError), - await eval(`(async () => { ${assignIssueScript}; await main(); })()`), - expect(mockCore.error).toHaveBeenCalledWith("Failed to assign issue: Command failed"), - expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_NOT_FOUND}: Failed to assign issue #999 to test-user: Command failed`)); - }), - it("should handle top-level errors with catch handler", async () => { - ((process.env.GH_TOKEN = "ghp_test123"), (process.env.ASSIGNEE = "test-user"), (process.env.ISSUE_NUMBER = "123")); - const uncaughtError = new Error("Uncaught error"); - (mockExec.exec.mockRejectedValue(uncaughtError), await eval(`(async () => { ${assignIssueScript}; await main(); })()`), expect(mockCore.setFailed).toHaveBeenCalled()); - })); - }), - describe("Edge cases for regular users", () => { - (it("should handle numeric issue number", async () => { - ((process.env.GH_TOKEN = "ghp_test123"), - (process.env.ASSIGNEE = "test-user"), - (process.env.ISSUE_NUMBER = "123"), - mockExec.exec.mockResolvedValue(0), - await eval(`(async () => { ${assignIssueScript}; await main(); })()`), - expect(mockExec.exec).toHaveBeenCalledWith("gh", ["issue", "edit", "123", "--add-assignee", "test-user"], expect.any(Object))); - }), - it("should pass through GH_TOKEN in exec environment", async () => { - ((process.env.GH_TOKEN = "ghp_test123"), - (process.env.ASSIGNEE = "test-user"), - (process.env.ISSUE_NUMBER = "123"), - (process.env.OTHER_VAR = "other_value"), - mockExec.exec.mockResolvedValue(0), - await eval(`(async () => { ${assignIssueScript}; await main(); })()`), - expect(mockExec.exec).toHaveBeenCalledWith("gh", ["issue", "edit", "123", "--add-assignee", "test-user"], { env: expect.objectContaining({ GH_TOKEN: "ghp_test123", OTHER_VAR: "other_value" }) })); - }), - it("should handle special characters in assignee name", async () => { - ((process.env.GH_TOKEN = "ghp_test123"), - (process.env.ASSIGNEE = "user-with-dash"), - (process.env.ISSUE_NUMBER = "123"), - mockExec.exec.mockResolvedValue(0), - await eval(`(async () => { ${assignIssueScript}; await main(); })()`), - expect(mockExec.exec).toHaveBeenCalledWith("gh", ["issue", "edit", "123", "--add-assignee", "user-with-dash"], expect.any(Object)), - expect(mockCore.setFailed).not.toHaveBeenCalled()); - }), - it("should include documentation link in error message", async () => { - (delete process.env.GH_TOKEN, (process.env.ASSIGNEE = "test-user"), (process.env.ISSUE_NUMBER = "123"), await eval(`(async () => { ${assignIssueScript}; await main(); })()`)); - const failedCall = mockCore.setFailed.mock.calls[0][0]; - expect(failedCall).toContain("https://github.github.com/gh-aw/reference/safe-outputs/#assigning-issues-to-copilot"); - })); - })); - })); From eedb29058ce050a5d56e25cbc47feac920f31cd9 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Tue, 14 Apr 2026 09:35:20 +1000 Subject: [PATCH 9/9] rename --- actions/setup/js/checkout_pr_branch.cjs | 4 ++-- actions/setup/js/git_helpers.cjs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/checkout_pr_branch.cjs b/actions/setup/js/checkout_pr_branch.cjs index ab749233066..2ffddab8411 100644 --- a/actions/setup/js/checkout_pr_branch.cjs +++ b/actions/setup/js/checkout_pr_branch.cjs @@ -26,7 +26,7 @@ */ const { getErrorMessage } = require("./error_helpers.cjs"); -const { getGhEnv } = require("./git_helpers.cjs"); +const { getGhEnvBypassingIntegrityFilteringForGitOps } = require("./git_helpers.cjs"); const { renderTemplateFromFile } = require("./messages_core.cjs"); const { detectForkPR } = require("./pr_helpers.cjs"); const { ERR_API } = require("./error_codes.cjs"); @@ -182,7 +182,7 @@ async function main() { // the repository from git remotes. The DIFC proxy may have set GH_HOST to // localhost:18443 which doesn't match any remote. await exec.exec("gh", ["pr", "checkout", prNumber.toString()], { - env: getGhEnv(), + env: getGhEnvBypassingIntegrityFilteringForGitOps(), }); // Log the resulting branch after checkout diff --git a/actions/setup/js/git_helpers.cjs b/actions/setup/js/git_helpers.cjs index ce9d2558e52..29feca1924b 100644 --- a/actions/setup/js/git_helpers.cjs +++ b/actions/setup/js/git_helpers.cjs @@ -140,13 +140,13 @@ function getGitHubHost() { * @param {Object} [extraEnv] - Additional environment variables to set (e.g. { GH_TOKEN: token }) * @returns {Object} Environment object suitable for exec.exec options */ -function getGhEnv(extraEnv) { +function getGhEnvBypassingIntegrityFilteringForGitOps(extraEnv) { return { ...process.env, ...extraEnv, GH_HOST: getGitHubHost() }; } module.exports = { execGitSync, - getGhEnv, + getGhEnvBypassingIntegrityFilteringForGitOps, getGitAuthEnv, getGitHubHost, };