diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index b82062853ba..da0c183bcfe 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -1084,7 +1084,14 @@ 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 envTargetSlug = (process.env.GH_AW_TARGET_REPO_SLUG || "").trim(); + const currentRepo = (process.env.GITHUB_REPOSITORY || "").toLowerCase(); + 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); 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() 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