From dab6034713c80b93619f34ca793d698dc432e051 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Jul 2026 06:31:16 +0000 Subject: [PATCH 1/5] Initial plan From 96df080b6e7d0ba9dcf078a66aa83607bd9f01f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Jul 2026 06:43:25 +0000 Subject: [PATCH 2/5] Fix side-repo push branch and manifest fallback handling Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/build_checkout_manifest.cjs | 18 +++++++++++ .../setup/js/build_checkout_manifest.test.cjs | 27 ++++++++++++++++ actions/setup/js/safe_outputs_handlers.cjs | 3 +- .../setup/js/safe_outputs_handlers.test.cjs | 31 +++++++++++++++++++ 4 files changed, 78 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/build_checkout_manifest.cjs b/actions/setup/js/build_checkout_manifest.cjs index dd7c26b9198..464d587a9ed 100644 --- a/actions/setup/js/build_checkout_manifest.cjs +++ b/actions/setup/js/build_checkout_manifest.cjs @@ -78,6 +78,24 @@ function resolveDefaultBranch(repository, checkoutPath, options = {}) { } } + if (defaultBranch === "" && repoPath && fs.existsSync(path.join(repoPath, ".git"))) { + try { + const output = runGit(["-C", repoPath, "remote", "show", "origin"], { + stdio: ["ignore", "pipe", "pipe"], + }); + const match = output.match(/^\s*HEAD branch:\s*(.+)\s*$/m); + const remoteHeadBranch = match && match[1] ? match[1].trim() : ""; + if (remoteHeadBranch && remoteHeadBranch !== "(unknown)") { + defaultBranch = remoteHeadBranch; + core.debug(`build_checkout_manifest: git remote show resolved default branch for ${repository}: ${defaultBranch}`); + } else { + core.debug(`build_checkout_manifest: git remote show did not expose HEAD branch for ${repository}`); + } + } catch (error) { + core.debug(`build_checkout_manifest: git remote show lookup failed for ${repository}: ${getErrorMessage(error)}`); + } + } + if (defaultBranch === "") { try { const checkoutToken = options.checkoutToken || ""; diff --git a/actions/setup/js/build_checkout_manifest.test.cjs b/actions/setup/js/build_checkout_manifest.test.cjs index 83ce6f58c59..030ba2c5084 100644 --- a/actions/setup/js/build_checkout_manifest.test.cjs +++ b/actions/setup/js/build_checkout_manifest.test.cjs @@ -105,6 +105,33 @@ describe("build_checkout_manifest.cjs", () => { expect(ghOptions?.env?.GH_TOKEN).toBe("${{ secrets.CROSS_REPO_PAT }}"); }); + it("falls back to git remote show origin when origin/HEAD is unavailable", () => { + const workspace = createTempDir("checkout-manifest-workspace-"); + tempDirs.push(workspace); + const checkoutPath = "target"; + const repoDir = path.join(workspace, checkoutPath); + fs.mkdirSync(repoDir, { recursive: true }); + execGit(["init", "-q"], { cwd: repoDir }); + + const defaultBranch = resolveDefaultBranch("owner/repo", checkoutPath, { + workspace, + runGit: args => { + if (args.includes("symbolic-ref")) { + throw new Error("origin/HEAD not set"); + } + if (args.includes("remote") && args.includes("show") && args.includes("origin")) { + return " HEAD branch: release/v2\n"; + } + throw new Error(`Unexpected git args: ${args.join(" ")}`); + }, + runGH: () => { + throw new Error("gh api should not be called"); + }, + }); + + expect(defaultBranch).toBe("release/v2"); + }); + it("writes manifest with lowercase keys", () => { const workspace = createTempDir("checkout-manifest-workspace-"); const runnerTemp = createTempDir("checkout-manifest-runner-temp-"); diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index b82062853ba..97a80cea048 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -1084,7 +1084,8 @@ function createHandlers(server, appendSafeOutput, config = {}) { server.debug(`Using configured patch_workspace_path for push_to_pull_request_branch: ${pushPatchWorkspacePath} -> ${repoCwd}`); } - if (((entry.repo && entry.repo.trim()) || pushConfig["target-repo"]) && !repoCwd) { + const hasExplicitTargetRepoHint = (entry.repo && entry.repo.trim()) || pushConfig["target-repo"] || process.env.GH_AW_TARGET_REPO_SLUG; + if (hasExplicitTargetRepoHint && !repoCwd) { server.debug(`Looking for checkout of target repo: ${itemRepo}`); const checkoutResult = findRepoCheckout(itemRepo); if (!checkoutResult.success) { diff --git a/actions/setup/js/safe_outputs_handlers.test.cjs b/actions/setup/js/safe_outputs_handlers.test.cjs index cb987cd142d..a444d4d71c4 100644 --- a/actions/setup/js/safe_outputs_handlers.test.cjs +++ b/actions/setup/js/safe_outputs_handlers.test.cjs @@ -1551,6 +1551,37 @@ describe("safe_outputs_handlers", () => { } }); + it("should detect branch from GH_AW_TARGET_REPO_SLUG checkout when target-repo is not configured", async () => { + const { targetRepoDir } = createSideRepoWithTrackedAndLocalCommits(); + process.env.GH_AW_TARGET_REPO_SLUG = "test-owner/test-repo"; + process.env.GITHUB_BASE_REF = "main"; + execSync("git init -b main", { cwd: testWorkspaceDir, stdio: "pipe" }); + execSync("git config user.email 'test@example.com'", { cwd: testWorkspaceDir, stdio: "pipe" }); + execSync("git config user.name 'Test User'", { cwd: testWorkspaceDir, stdio: "pipe" }); + fs.writeFileSync(path.join(testWorkspaceDir, "HOST.md"), "host\n"); + execSync("git add HOST.md", { cwd: testWorkspaceDir, stdio: "pipe" }); + execSync("git commit -m 'host base commit'", { cwd: testWorkspaceDir, stdio: "pipe" }); + execSync("git remote add origin https://github.com/owner/repo.git", { cwd: testWorkspaceDir, stdio: "pipe" }); + + try { + const result = await handlers.pushToPullRequestBranchHandler({}); + + expect(result.isError).toBeFalsy(); + expect(mockServer.debug).toHaveBeenCalledWith(expect.stringContaining(`Selected checkout folder for test-owner/test-repo: ${targetRepoDir}`)); + expect(mockServer.debug).toHaveBeenCalledWith(expect.stringContaining("Using current branch for push_to_pull_request_branch: feature/test-change")); + expect(mockAppendSafeOutput).toHaveBeenCalledWith( + expect.objectContaining({ + type: "push_to_pull_request_branch", + branch: "feature/test-change", + repo_cwd: targetRepoDir, + }) + ); + } finally { + delete process.env.GH_AW_TARGET_REPO_SLUG; + delete process.env.GITHUB_BASE_REF; + } + }); + it("should reject push_to_pull_request_branch when branch still equals base_branch after detection", async () => { // Simulates the scenario where getBaseBranch() incorrectly resolves to the // feature branch itself. Detection cannot recover when getCurrentBranch() From 14da7f82cd0dccf116c872920ed321961696e242 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Jul 2026 09:50:21 +0000 Subject: [PATCH 3/5] Remove git remote show origin fallback; go directly to gh api after symbolic-ref The git remote show origin call performs a network request that can block indefinitely in persist-credentials: false environments. Skip it and fall through directly to the existing credential-aware gh api fallback. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/build_checkout_manifest.cjs | 18 ------------- .../setup/js/build_checkout_manifest.test.cjs | 27 ------------------- 2 files changed, 45 deletions(-) diff --git a/actions/setup/js/build_checkout_manifest.cjs b/actions/setup/js/build_checkout_manifest.cjs index 464d587a9ed..dd7c26b9198 100644 --- a/actions/setup/js/build_checkout_manifest.cjs +++ b/actions/setup/js/build_checkout_manifest.cjs @@ -78,24 +78,6 @@ function resolveDefaultBranch(repository, checkoutPath, options = {}) { } } - if (defaultBranch === "" && repoPath && fs.existsSync(path.join(repoPath, ".git"))) { - try { - const output = runGit(["-C", repoPath, "remote", "show", "origin"], { - stdio: ["ignore", "pipe", "pipe"], - }); - const match = output.match(/^\s*HEAD branch:\s*(.+)\s*$/m); - const remoteHeadBranch = match && match[1] ? match[1].trim() : ""; - if (remoteHeadBranch && remoteHeadBranch !== "(unknown)") { - defaultBranch = remoteHeadBranch; - core.debug(`build_checkout_manifest: git remote show resolved default branch for ${repository}: ${defaultBranch}`); - } else { - core.debug(`build_checkout_manifest: git remote show did not expose HEAD branch for ${repository}`); - } - } catch (error) { - core.debug(`build_checkout_manifest: git remote show lookup failed for ${repository}: ${getErrorMessage(error)}`); - } - } - if (defaultBranch === "") { try { const checkoutToken = options.checkoutToken || ""; diff --git a/actions/setup/js/build_checkout_manifest.test.cjs b/actions/setup/js/build_checkout_manifest.test.cjs index 030ba2c5084..83ce6f58c59 100644 --- a/actions/setup/js/build_checkout_manifest.test.cjs +++ b/actions/setup/js/build_checkout_manifest.test.cjs @@ -105,33 +105,6 @@ describe("build_checkout_manifest.cjs", () => { expect(ghOptions?.env?.GH_TOKEN).toBe("${{ secrets.CROSS_REPO_PAT }}"); }); - it("falls back to git remote show origin when origin/HEAD is unavailable", () => { - const workspace = createTempDir("checkout-manifest-workspace-"); - tempDirs.push(workspace); - const checkoutPath = "target"; - const repoDir = path.join(workspace, checkoutPath); - fs.mkdirSync(repoDir, { recursive: true }); - execGit(["init", "-q"], { cwd: repoDir }); - - const defaultBranch = resolveDefaultBranch("owner/repo", checkoutPath, { - workspace, - runGit: args => { - if (args.includes("symbolic-ref")) { - throw new Error("origin/HEAD not set"); - } - if (args.includes("remote") && args.includes("show") && args.includes("origin")) { - return " HEAD branch: release/v2\n"; - } - throw new Error(`Unexpected git args: ${args.join(" ")}`); - }, - runGH: () => { - throw new Error("gh api should not be called"); - }, - }); - - expect(defaultBranch).toBe("release/v2"); - }); - it("writes manifest with lowercase keys", () => { const workspace = createTempDir("checkout-manifest-workspace-"); const runnerTemp = createTempDir("checkout-manifest-runner-temp-"); From fe07c5865eeb742cbc9d2c15d79ce6773f4f165c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:50:07 +0000 Subject: [PATCH 4/5] fix: trim GH_AW_TARGET_REPO_SLUG and only use as side-repo hint when target differs from current repo Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- actions/setup/js/safe_outputs_handlers.cjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index 97a80cea048..65a583c0128 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -1084,7 +1084,9 @@ function createHandlers(server, appendSafeOutput, config = {}) { server.debug(`Using configured patch_workspace_path for push_to_pull_request_branch: ${pushPatchWorkspacePath} -> ${repoCwd}`); } - const hasExplicitTargetRepoHint = (entry.repo && entry.repo.trim()) || pushConfig["target-repo"] || process.env.GH_AW_TARGET_REPO_SLUG; + const envTargetSlug = (process.env.GH_AW_TARGET_REPO_SLUG || "").trim(); + const currentRepo = (process.env.GITHUB_REPOSITORY || "").toLowerCase(); + const hasExplicitTargetRepoHint = (entry.repo && entry.repo.trim()) || pushConfig["target-repo"] || (envTargetSlug && envTargetSlug.toLowerCase() !== currentRepo); if (hasExplicitTargetRepoHint && !repoCwd) { server.debug(`Looking for checkout of target repo: ${itemRepo}`); const checkoutResult = findRepoCheckout(itemRepo); From 7a86af32af6ca05998d925a7cd46bdd8578918e3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Jul 2026 16:18:33 +0000 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20add=20logging=20for=20GH=5FAW=5FTAR?= =?UTF-8?q?GET=5FREPO=5FSLUG=20passthrough;=20update=20spec=20=C2=A76.3=20?= =?UTF-8?q?and=20T-CHK-015?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add debug log in push_to_pull_request_branch handler when GH_AW_TARGET_REPO_SLUG is set but matches GITHUB_REPOSITORY (same-repo passthrough case) - Refactor envSlugIsSideRepo for clarity - Add §6.3 to checkout-behavior-specification.md covering push_to_pull_request_branch side-repo checkout resolution - Add T-CHK-015 to §7.1 and §7.2 compliance checklist - Bump spec version to 1.1.0 Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- actions/setup/js/safe_outputs_handlers.cjs | 6 ++++- .../specs/checkout-behavior-specification.md | 25 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index 65a583c0128..da0c183bcfe 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -1086,7 +1086,11 @@ function createHandlers(server, appendSafeOutput, config = {}) { const envTargetSlug = (process.env.GH_AW_TARGET_REPO_SLUG || "").trim(); const currentRepo = (process.env.GITHUB_REPOSITORY || "").toLowerCase(); - const hasExplicitTargetRepoHint = (entry.repo && entry.repo.trim()) || pushConfig["target-repo"] || (envTargetSlug && envTargetSlug.toLowerCase() !== currentRepo); + const envSlugIsSideRepo = envTargetSlug && envTargetSlug.toLowerCase() !== currentRepo; + if (envTargetSlug && !envSlugIsSideRepo) { + server.debug(`GH_AW_TARGET_REPO_SLUG (${envTargetSlug}) matches current repo; not using as side-repo checkout hint for push_to_pull_request_branch`); + } + const hasExplicitTargetRepoHint = (entry.repo && entry.repo.trim()) || pushConfig["target-repo"] || envSlugIsSideRepo; if (hasExplicitTargetRepoHint && !repoCwd) { server.debug(`Looking for checkout of target repo: ${itemRepo}`); const checkoutResult = findRepoCheckout(itemRepo); diff --git a/docs/src/content/docs/specs/checkout-behavior-specification.md b/docs/src/content/docs/specs/checkout-behavior-specification.md index 7566c24dddb..abc0c41c560 100644 --- a/docs/src/content/docs/specs/checkout-behavior-specification.md +++ b/docs/src/content/docs/specs/checkout-behavior-specification.md @@ -7,7 +7,7 @@ sidebar: # Checkout Behavior Specification -**Version**: 1.0.0 +**Version**: 1.1.0 **Status**: Working Draft **Publication Date**: 2026-07-08 **Editor**: GitHub Agentic Workflows Team @@ -230,6 +230,22 @@ Effective side-repo token precedence MUST be: 2. side-repo target checkout `github-app` minted token reference 3. `${{ secrets.GH_AW_GITHUB_TOKEN }}` +### 6.3 `push_to_pull_request_branch` Side-Repo Checkout Resolution + +The `push_to_pull_request_branch` safe-output handler MUST resolve the target checkout directory from one of three sources, evaluated in priority order: + +1. `patch_workspace_path` configuration (explicit path; resolved via `resolvePatchWorkspacePath`). +2. `entry.repo` field or `target-repo` workflow config entry — triggers `findRepoCheckout` lookup. +3. `GH_AW_TARGET_REPO_SLUG` environment variable — used as a side-repo checkout hint **only when** its value (case-insensitively) differs from `GITHUB_REPOSITORY`. When the env var is set but matches `GITHUB_REPOSITORY`, it MUST be ignored and the handler MUST log a debug message explaining the passthrough. + +When source 2 or 3 triggers a `findRepoCheckout` lookup, all subsequent git operations for that handler invocation (branch detection, patch generation, bundle generation) MUST operate under the resolved checkout directory (`repo_cwd`) rather than the workspace root. + +When `GH_AW_TARGET_REPO_SLUG` is used as a side-repo hint (source 3), the implementation SHOULD emit an informational debug log identifying the resolved `owner/repo`. + +When `GH_AW_TARGET_REPO_SLUG` is set but equals `GITHUB_REPOSITORY`, the implementation MUST emit a debug log stating that the variable is not being used as a side-repo hint and explaining why. + +**Rationale**: `GH_AW_TARGET_REPO_SLUG` may be globally set in side-repo maintenance workflows for cross-repo operations other than `push_to_pull_request_branch`. Using it unconditionally as a checkout hint would misdirect branch and patch detection when the push target is the current (workflow-host) repository. + --- ## 7. Compliance Testing @@ -250,6 +266,7 @@ Effective side-repo token precedence MUST be: - **T-CHK-012**: safe_outputs checkout token MUST NOT use `safe-outputs.github-app` or `safe-outputs.github-token`; only `safe-outputs-github-app` (per entry) or `GITHUB_TOKEN` are permitted - **T-CHK-013**: Checkout-manifest generation includes safe_outputs auth metadata without persisting resolved tokens - **T-CHK-014**: Checkout-manifest path resolution MUST reject paths that are absolute (e.g., `/etc/passwd`) or escape the workspace root (e.g., `../../sensitive`); rejected paths MUST produce an error and MUST NOT be used for checkout or file lookup +- **T-CHK-015**: `push_to_pull_request_branch` uses side-repo checkout from `GH_AW_TARGET_REPO_SLUG` only when it differs from `GITHUB_REPOSITORY`; emits debug log and ignores it when they match ### 7.2 Compliance Checklist @@ -264,6 +281,7 @@ Effective side-repo token precedence MUST be: | Checkout-level safe_outputs auth field | T-CHK-011, T-CHK-012 | C1/C2 | Required | | Checkout-manifest generation requirements | T-CHK-013 | C1/C2 | Required | | Checkout-manifest path-escape rejection | T-CHK-014 | C2 | Required | +| `push_to_pull_request_branch` side-repo cwd resolution | T-CHK-015 | C2 | Required | ### 7.3 Safeguards @@ -305,6 +323,11 @@ The following MUST-level norms govern credential and token safety during checkou ## 9. Change Log +### Version 1.1.0 (Working Draft) + +- Added §6.3: `push_to_pull_request_branch` side-repo checkout resolution requirements, covering `GH_AW_TARGET_REPO_SLUG` passthrough guard, debug-logging obligation, and `repo_cwd` scoping of all git operations. +- Added T-CHK-015 to §7.1 and §7.2 compliance checklist. + ### Version 1.0.0 (Working Draft) - Initial specification for checkout behavior in activation, agent, and safe_outputs jobs