From 427b6f044c1ef869cf985761db359d734a243404 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 20:47:33 +0000 Subject: [PATCH 01/10] Initial plan From f9c3d7b9923021cb7439878eee38bd2b1b913d1b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 20:54:46 +0000 Subject: [PATCH 02/10] Plan: support repo cwd in push_to_pull_request_branch Agent-Logs-Url: https://github.com/github/gh-aw/sessions/b820e7b5-cce6-4be8-8217-691358bf8f5c Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-project.lock.yml | 4 ++-- .github/workflows/test-project-url-default.lock.yml | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smoke-project.lock.yml b/.github/workflows/smoke-project.lock.yml index 228c765f09..3bd1d6c005 100644 --- a/.github/workflows/smoke-project.lock.yml +++ b/.github/workflows/smoke-project.lock.yml @@ -1130,7 +1130,7 @@ jobs: permissions: contents: write discussions: write - issues: write + issues: read pull-requests: write concurrency: group: "gh-aw-conclusion-smoke-project" @@ -1499,7 +1499,7 @@ jobs: permissions: contents: write discussions: write - issues: write + issues: read pull-requests: write timeout-minutes: 15 env: diff --git a/.github/workflows/test-project-url-default.lock.yml b/.github/workflows/test-project-url-default.lock.yml index 98b8937c41..c4b7f8d3f7 100644 --- a/.github/workflows/test-project-url-default.lock.yml +++ b/.github/workflows/test-project-url-default.lock.yml @@ -892,6 +892,7 @@ jobs: runs-on: ubuntu-slim permissions: contents: read + issues: read concurrency: group: "gh-aw-conclusion-test-project-url-default" cancel-in-progress: false @@ -1197,6 +1198,7 @@ jobs: runs-on: ubuntu-slim permissions: contents: read + issues: read timeout-minutes: 15 env: GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/test-project-url-default" From 0abeb955f05793f22219a11b53e0f106ef48e1a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 20:58:16 +0000 Subject: [PATCH 03/10] Fix push_to_pull_request_branch repo cwd handling Agent-Logs-Url: https://github.com/github/gh-aw/sessions/b820e7b5-cce6-4be8-8217-691358bf8f5c Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-project.lock.yml | 4 +-- .../test-project-url-default.lock.yml | 2 -- actions/setup/js/safe_outputs_handlers.cjs | 17 +++++++++- .../setup/js/safe_outputs_handlers.test.cjs | 31 +++++++++++++++++++ 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/.github/workflows/smoke-project.lock.yml b/.github/workflows/smoke-project.lock.yml index 3bd1d6c005..228c765f09 100644 --- a/.github/workflows/smoke-project.lock.yml +++ b/.github/workflows/smoke-project.lock.yml @@ -1130,7 +1130,7 @@ jobs: permissions: contents: write discussions: write - issues: read + issues: write pull-requests: write concurrency: group: "gh-aw-conclusion-smoke-project" @@ -1499,7 +1499,7 @@ jobs: permissions: contents: write discussions: write - issues: read + issues: write pull-requests: write timeout-minutes: 15 env: diff --git a/.github/workflows/test-project-url-default.lock.yml b/.github/workflows/test-project-url-default.lock.yml index c4b7f8d3f7..98b8937c41 100644 --- a/.github/workflows/test-project-url-default.lock.yml +++ b/.github/workflows/test-project-url-default.lock.yml @@ -892,7 +892,6 @@ jobs: runs-on: ubuntu-slim permissions: contents: read - issues: read concurrency: group: "gh-aw-conclusion-test-project-url-default" cancel-in-progress: false @@ -1198,7 +1197,6 @@ jobs: runs-on: ubuntu-slim permissions: contents: read - issues: read timeout-minutes: 15 env: GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/test-project-url-default" diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index 032909d62c..11723d52fb 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -486,10 +486,22 @@ function createHandlers(server, appendSafeOutput, config = {}) { // Get base branch for the resolved target repository const baseBranch = await getBaseBranch(repoParts); + // Determine the working directory for git operations + // If repo is specified, find where it's checked out + let repoCwd = null; + if (entry.repo && entry.repo.trim()) { + const repoSlug = repoResult.repo; + const checkoutResult = findRepoCheckout(repoSlug); + if (checkoutResult.success) { + repoCwd = checkoutResult.path; + entry.repo_cwd = repoCwd; + } + } + // If branch is not provided, is empty, or equals the base branch, use the current branch from git // This handles cases where the agent incorrectly passes the base branch instead of the working branch if (!entry.branch || entry.branch.trim() === "" || entry.branch === baseBranch) { - const detectedBranch = getCurrentBranch(); + const detectedBranch = getCurrentBranch(repoCwd); if (entry.branch === baseBranch) { server.debug(`Branch equals base branch (${baseBranch}), detecting actual working branch: ${detectedBranch}`); @@ -507,6 +519,9 @@ function createHandlers(server, appendSafeOutput, config = {}) { // Build common options for both patch and bundle generation const pushTransportOptions = { mode: "incremental" }; + if (repoCwd) { + pushTransportOptions.cwd = repoCwd; + } // Pass per-handler token so cross-repo PATs are used for git fetch when configured. // Falls back to GITHUB_TOKEN if not set. if (pushConfig["github-token"]) { diff --git a/actions/setup/js/safe_outputs_handlers.test.cjs b/actions/setup/js/safe_outputs_handlers.test.cjs index 95ab380f7d..fd54a64b48 100644 --- a/actions/setup/js/safe_outputs_handlers.test.cjs +++ b/actions/setup/js/safe_outputs_handlers.test.cjs @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import fs from "fs"; import path from "path"; +import { execSync } from "child_process"; import { createHandlers } from "./safe_outputs_handlers.cjs"; // Mock the global objects that GitHub Actions provides @@ -636,6 +637,36 @@ describe("safe_outputs_handlers", () => { expect(responseData.details).toContain("git commit"); expect(responseData.details).toContain("push_to_pull_request_branch"); }); + + it("should detect branch from the checked out target repo when repo is provided", async () => { + const targetRepoDir = path.join(testWorkspaceDir, "target-repo"); + fs.mkdirSync(targetRepoDir, { recursive: true }); + + execSync("git init -b main", { cwd: targetRepoDir, stdio: "pipe" }); + execSync("git config user.email 'test@example.com'", { cwd: targetRepoDir, stdio: "pipe" }); + execSync("git config user.name 'Test User'", { cwd: targetRepoDir, stdio: "pipe" }); + fs.writeFileSync(path.join(targetRepoDir, "README.md"), "base\n"); + execSync("git add README.md", { cwd: targetRepoDir, stdio: "pipe" }); + execSync("git commit -m 'base commit'", { cwd: targetRepoDir, stdio: "pipe" }); + execSync("git checkout -b feature/test-change", { cwd: targetRepoDir, stdio: "pipe" }); + fs.writeFileSync(path.join(targetRepoDir, "README.md"), "feature\n"); + execSync("git add README.md", { cwd: targetRepoDir, stdio: "pipe" }); + execSync("git commit -m 'feature commit'", { cwd: targetRepoDir, stdio: "pipe" }); + execSync("git remote add origin https://github.com/test-owner/test-repo.git", { cwd: targetRepoDir, stdio: "pipe" }); + + process.env.GITHUB_BASE_REF = "main"; + try { + const result = await handlers.pushToPullRequestBranchHandler({ + branch: "main", + repo: "test-owner/test-repo", + }); + + expect(result.isError).toBe(true); + expect(mockServer.debug).toHaveBeenCalledWith(expect.stringContaining("detecting actual working branch: feature/test-change")); + } finally { + delete process.env.GITHUB_BASE_REF; + } + }); }); describe("handler structure", () => { From b7a93af5a39e721d6b3ac6b3a7d2ce6fbe424b68 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 23:00:31 +0000 Subject: [PATCH 04/10] Handle missing side-repo checkout and set repoSlug in push handler Agent-Logs-Url: https://github.com/github/gh-aw/sessions/3d4592c5-001e-4e65-a7c5-bc9e7e7779e3 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/safe_outputs_handlers.cjs | 21 ++++++++++++++++--- .../setup/js/safe_outputs_handlers.test.cjs | 14 +++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index 11723d52fb..9bcec7a9e7 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -492,10 +492,24 @@ function createHandlers(server, appendSafeOutput, config = {}) { if (entry.repo && entry.repo.trim()) { const repoSlug = repoResult.repo; const checkoutResult = findRepoCheckout(repoSlug); - if (checkoutResult.success) { - repoCwd = checkoutResult.path; - entry.repo_cwd = repoCwd; + if (!checkoutResult.success) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "error", + error: + `Repository checkout not found for ${repoSlug}. Ensure the repository is checked out in this workflow using actions/checkout. ` + + "If checking out multiple repositories, use the 'path' input so the checkout can be located.", + }), + }, + ], + isError: true, + }; } + repoCwd = checkoutResult.path; + entry.repo_cwd = repoCwd; } // If branch is not provided, is empty, or equals the base branch, use the current branch from git @@ -521,6 +535,7 @@ function createHandlers(server, appendSafeOutput, config = {}) { const pushTransportOptions = { mode: "incremental" }; if (repoCwd) { pushTransportOptions.cwd = repoCwd; + pushTransportOptions.repoSlug = repoResult.repo; } // Pass per-handler token so cross-repo PATs are used for git fetch when configured. // Falls back to GITHUB_TOKEN if not set. diff --git a/actions/setup/js/safe_outputs_handlers.test.cjs b/actions/setup/js/safe_outputs_handlers.test.cjs index fd54a64b48..c9a97ea118 100644 --- a/actions/setup/js/safe_outputs_handlers.test.cjs +++ b/actions/setup/js/safe_outputs_handlers.test.cjs @@ -638,6 +638,20 @@ describe("safe_outputs_handlers", () => { expect(responseData.details).toContain("push_to_pull_request_branch"); }); + it("should return error when repo checkout is not found for explicit repo", async () => { + const result = await handlers.pushToPullRequestBranchHandler({ + branch: "main", + repo: "test-owner/test-repo", + }); + + expect(result.isError).toBe(true); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("error"); + expect(responseData.error).toContain("Repository checkout not found for test-owner/test-repo"); + expect(responseData.error).toContain("actions/checkout"); + expect(responseData.error).toContain("'path' input"); + }); + it("should detect branch from the checked out target repo when repo is provided", async () => { const targetRepoDir = path.join(testWorkspaceDir, "target-repo"); fs.mkdirSync(targetRepoDir, { recursive: true }); From 67491cd0cc869e820380788d7cf295586d9a157c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 01:55:20 +0000 Subject: [PATCH 05/10] Plan: deepen tests for push_to_pull_request_branch Agent-Logs-Url: https://github.com/github/gh-aw/sessions/9cbfebfe-a9d7-42d9-9e47-accf291cc24b Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-project.lock.yml | 4 +- .../test-project-url-default.lock.yml | 2 + pkg/workflow/schemas/github-workflow.json | 90 ++++++++++++++++--- 3 files changed, 84 insertions(+), 12 deletions(-) diff --git a/.github/workflows/smoke-project.lock.yml b/.github/workflows/smoke-project.lock.yml index 228c765f09..3bd1d6c005 100644 --- a/.github/workflows/smoke-project.lock.yml +++ b/.github/workflows/smoke-project.lock.yml @@ -1130,7 +1130,7 @@ jobs: permissions: contents: write discussions: write - issues: write + issues: read pull-requests: write concurrency: group: "gh-aw-conclusion-smoke-project" @@ -1499,7 +1499,7 @@ jobs: permissions: contents: write discussions: write - issues: write + issues: read pull-requests: write timeout-minutes: 15 env: diff --git a/.github/workflows/test-project-url-default.lock.yml b/.github/workflows/test-project-url-default.lock.yml index 98b8937c41..c4b7f8d3f7 100644 --- a/.github/workflows/test-project-url-default.lock.yml +++ b/.github/workflows/test-project-url-default.lock.yml @@ -892,6 +892,7 @@ jobs: runs-on: ubuntu-slim permissions: contents: read + issues: read concurrency: group: "gh-aw-conclusion-test-project-url-default" cancel-in-progress: false @@ -1197,6 +1198,7 @@ jobs: runs-on: ubuntu-slim permissions: contents: read + issues: read timeout-minutes: 15 env: GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/test-project-url-default" diff --git a/pkg/workflow/schemas/github-workflow.json b/pkg/workflow/schemas/github-workflow.json index afa39b9014..e3437f83ba 100644 --- a/pkg/workflow/schemas/github-workflow.json +++ b/pkg/workflow/schemas/github-workflow.json @@ -62,16 +62,16 @@ } ] }, - "container": { + "jobContainer": { "type": "object", "properties": { "image": { - "$comment": "https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idcontainerimage", + "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idcontainerimage", "description": "The Docker image to use as the container to run the action. The value can be the Docker Hub image name or a registry name.", "type": "string" }, "credentials": { - "$comment": "https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#jobsjob_idcontainercredentials", + "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idcontainercredentials", "description": "If the image's container registry requires authentication to pull the image, you can use credentials to set a map of the username and password. The credentials are the same values that you would provide to the `docker login` command.", "type": "object", "properties": { @@ -84,12 +84,12 @@ } }, "env": { - "$comment": "https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idcontainerenv", + "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idcontainerenv", "$ref": "#/definitions/env", - "description": "Sets an array of environment variables in the container." + "description": "Sets a map of environment variables in the container." }, "ports": { - "$comment": "https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idcontainerports", + "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idcontainerports", "description": "Sets an array of ports to expose on the container.", "type": "array", "items": { @@ -105,7 +105,7 @@ "minItems": 1 }, "volumes": { - "$comment": "https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idcontainervolumes", + "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idcontainervolumes", "description": "Sets an array of volumes for the container to use. You can use volumes to share data between services or other steps in a job. You can specify named Docker volumes, anonymous Docker volumes, or bind mounts on the host.\nTo specify a volume, you specify the source and destination path: :\nThe is a volume name or an absolute path on the host machine, and is an absolute path in the container.", "type": "array", "items": { @@ -114,7 +114,7 @@ "minItems": 1 }, "options": { - "$comment": "https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idcontaineroptions", + "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idcontaineroptions", "description": "Additional Docker container resource options. For a list of options, see https://docs.docker.com/engine/reference/commandline/create/#options.", "type": "string" } @@ -122,6 +122,76 @@ "required": ["image"], "additionalProperties": false }, + "serviceContainer": { + "type": "object", + "properties": { + "image": { + "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idservicesservice_idimage", + "description": "The Docker image to use as the service container to run the action. The value can be the Docker Hub image name or a registry name.", + "type": "string" + }, + "credentials": { + "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idservicesservice_idcredentials", + "description": "If the image's container registry requires authentication to pull the image, you can use credentials to set a map of the username and password. The credentials are the same values that you would provide to the `docker login` command.", + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "env": { + "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idservicesservice_idenv", + "$ref": "#/definitions/env", + "description": "Sets a map of environment variables in the service container." + }, + "ports": { + "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idservicesservice_idports", + "description": "Sets an array of ports to expose on the service container.", + "type": "array", + "items": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + }, + "minItems": 1 + }, + "volumes": { + "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idservicesservice_idvolumes", + "description": "Sets an array of volumes for the service container to use. You can use volumes to share data between services or other steps in a job. You can specify named Docker volumes, anonymous Docker volumes, or bind mounts on the host.\nTo specify a volume, you specify the source and destination path: :\nThe is a volume name or an absolute path on the host machine, and is an absolute path in the container.", + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "options": { + "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idservicesservice_idoptions", + "description": "Additional Docker container resource options. For a list of options, see https://docs.docker.com/engine/reference/commandline/create/#options.", + "type": "string" + }, + "command": { + "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idservicesservice_idcommand", + "description": "Overrides the Docker image's default command (`CMD`). The value is passed as arguments after the image name in the `docker create` command. If you also specify `entrypoint`, `command` provides the arguments to that entrypoint.", + "type": "string" + }, + "entrypoint": { + "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idservicesservice_identrypoint", + "description": "Overrides the Docker image's default `ENTRYPOINT`. The value is a single string defining the executable to run. Use this when you need to replace the image's entrypoint entirely. You can combine `entrypoint` with `command` to pass arguments to the custom entrypoint.", + "type": "string" + } + }, + "required": ["image"], + "additionalProperties": false + }, "defaults": { "type": "object", "properties": { @@ -833,7 +903,7 @@ "type": "string" }, { - "$ref": "#/definitions/container" + "$ref": "#/definitions/jobContainer" } ] }, @@ -842,7 +912,7 @@ "description": "Additional containers to host services for a job in a workflow. These are useful for creating databases or cache services like redis. The runner on the virtual machine will automatically create a network and manage the life cycle of the service containers.\nWhen you use a service container for a job or your step uses container actions, you don't need to set port information to access the service. Docker automatically exposes all ports between containers on the same network.\nWhen both the job and the action run in a container, you can directly reference the container by its hostname. The hostname is automatically mapped to the service name.\nWhen a step does not use a container action, you must access the service using localhost and bind the ports.", "type": "object", "additionalProperties": { - "$ref": "#/definitions/container" + "$ref": "#/definitions/serviceContainer" } }, "concurrency": { From eba5f7a55ca39f8a4e35b2c77e500ae3c08a217c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 02:00:02 +0000 Subject: [PATCH 06/10] Add deep multi-repo push handler tests and clean unintended workflow artifacts Agent-Logs-Url: https://github.com/github/gh-aw/sessions/9cbfebfe-a9d7-42d9-9e47-accf291cc24b Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-project.lock.yml | 4 +- .../test-project-url-default.lock.yml | 2 - .../setup/js/safe_outputs_handlers.test.cjs | 79 ++++++++++++---- pkg/workflow/schemas/github-workflow.json | 90 +++---------------- 4 files changed, 76 insertions(+), 99 deletions(-) diff --git a/.github/workflows/smoke-project.lock.yml b/.github/workflows/smoke-project.lock.yml index 3bd1d6c005..228c765f09 100644 --- a/.github/workflows/smoke-project.lock.yml +++ b/.github/workflows/smoke-project.lock.yml @@ -1130,7 +1130,7 @@ jobs: permissions: contents: write discussions: write - issues: read + issues: write pull-requests: write concurrency: group: "gh-aw-conclusion-smoke-project" @@ -1499,7 +1499,7 @@ jobs: permissions: contents: write discussions: write - issues: read + issues: write pull-requests: write timeout-minutes: 15 env: diff --git a/.github/workflows/test-project-url-default.lock.yml b/.github/workflows/test-project-url-default.lock.yml index c4b7f8d3f7..98b8937c41 100644 --- a/.github/workflows/test-project-url-default.lock.yml +++ b/.github/workflows/test-project-url-default.lock.yml @@ -892,7 +892,6 @@ jobs: runs-on: ubuntu-slim permissions: contents: read - issues: read concurrency: group: "gh-aw-conclusion-test-project-url-default" cancel-in-progress: false @@ -1198,7 +1197,6 @@ jobs: runs-on: ubuntu-slim permissions: contents: read - issues: read timeout-minutes: 15 env: GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/test-project-url-default" diff --git a/actions/setup/js/safe_outputs_handlers.test.cjs b/actions/setup/js/safe_outputs_handlers.test.cjs index c9a97ea118..b5af54a4a2 100644 --- a/actions/setup/js/safe_outputs_handlers.test.cjs +++ b/actions/setup/js/safe_outputs_handlers.test.cjs @@ -588,6 +588,34 @@ describe("safe_outputs_handlers", () => { }); describe("pushToPullRequestBranchHandler", () => { + function setupSideRepoWithIncrementalCommit() { + const targetRepoDir = path.join(testWorkspaceDir, "target-repo"); + fs.mkdirSync(targetRepoDir, { recursive: true }); + + execSync("git init -b main", { cwd: targetRepoDir, stdio: "pipe" }); + execSync("git config user.email 'test@example.com'", { cwd: targetRepoDir, stdio: "pipe" }); + execSync("git config user.name 'Test User'", { cwd: targetRepoDir, stdio: "pipe" }); + + fs.writeFileSync(path.join(targetRepoDir, "README.md"), "base\n"); + execSync("git add README.md", { cwd: targetRepoDir, stdio: "pipe" }); + execSync("git commit -m 'base commit'", { cwd: targetRepoDir, stdio: "pipe" }); + + execSync("git checkout -b feature/test-change", { cwd: targetRepoDir, stdio: "pipe" }); + fs.writeFileSync(path.join(targetRepoDir, "README.md"), "tracked\n"); + execSync("git add README.md", { cwd: targetRepoDir, stdio: "pipe" }); + execSync("git commit -m 'tracked commit'", { cwd: targetRepoDir, stdio: "pipe" }); + const trackedCommit = execSync("git rev-parse HEAD", { cwd: targetRepoDir, stdio: "pipe" }).toString().trim(); + + execSync("git remote add origin https://github.com/test-owner/test-repo.git", { cwd: targetRepoDir, stdio: "pipe" }); + execSync(`git update-ref refs/remotes/origin/feature/test-change ${trackedCommit}`, { cwd: targetRepoDir, stdio: "pipe" }); + + fs.writeFileSync(path.join(targetRepoDir, "README.md"), "local-only\n"); + execSync("git add README.md", { cwd: targetRepoDir, stdio: "pipe" }); + execSync("git commit -m 'local only commit'", { cwd: targetRepoDir, stdio: "pipe" }); + + return { targetRepoDir }; + } + it("should be defined", () => { expect(handlers.pushToPullRequestBranchHandler).toBeDefined(); }); @@ -653,20 +681,7 @@ describe("safe_outputs_handlers", () => { }); it("should detect branch from the checked out target repo when repo is provided", async () => { - const targetRepoDir = path.join(testWorkspaceDir, "target-repo"); - fs.mkdirSync(targetRepoDir, { recursive: true }); - - execSync("git init -b main", { cwd: targetRepoDir, stdio: "pipe" }); - execSync("git config user.email 'test@example.com'", { cwd: targetRepoDir, stdio: "pipe" }); - execSync("git config user.name 'Test User'", { cwd: targetRepoDir, stdio: "pipe" }); - fs.writeFileSync(path.join(targetRepoDir, "README.md"), "base\n"); - execSync("git add README.md", { cwd: targetRepoDir, stdio: "pipe" }); - execSync("git commit -m 'base commit'", { cwd: targetRepoDir, stdio: "pipe" }); - execSync("git checkout -b feature/test-change", { cwd: targetRepoDir, stdio: "pipe" }); - fs.writeFileSync(path.join(targetRepoDir, "README.md"), "feature\n"); - execSync("git add README.md", { cwd: targetRepoDir, stdio: "pipe" }); - execSync("git commit -m 'feature commit'", { cwd: targetRepoDir, stdio: "pipe" }); - execSync("git remote add origin https://github.com/test-owner/test-repo.git", { cwd: targetRepoDir, stdio: "pipe" }); + const { targetRepoDir } = setupSideRepoWithIncrementalCommit(); process.env.GITHUB_BASE_REF = "main"; try { @@ -675,8 +690,42 @@ describe("safe_outputs_handlers", () => { repo: "test-owner/test-repo", }); - expect(result.isError).toBe(true); expect(mockServer.debug).toHaveBeenCalledWith(expect.stringContaining("detecting actual working branch: feature/test-change")); + if (!result.isError) { + expect(mockAppendSafeOutput).toHaveBeenCalledWith( + expect.objectContaining({ + type: "push_to_pull_request_branch", + branch: "feature/test-change", + }) + ); + } + } finally { + delete process.env.GITHUB_BASE_REF; + } + }); + + it("should include repo slug in incremental patch filename for side-repo checkout", async () => { + const { targetRepoDir } = setupSideRepoWithIncrementalCommit(); + + process.env.GITHUB_BASE_REF = "main"; + try { + const result = await handlers.pushToPullRequestBranchHandler({ + branch: "feature/test-change", + repo: "test-owner/test-repo", + }); + + expect(result.isError).toBeUndefined(); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("success"); + expect(responseData.patch.path).toContain("/tmp/gh-aw/aw-test-owner-test-repo-feature-test-change.patch"); + + expect(mockAppendSafeOutput).toHaveBeenCalledWith( + expect.objectContaining({ + type: "push_to_pull_request_branch", + repo_cwd: targetRepoDir, + patch_path: expect.stringContaining("aw-test-owner-test-repo-feature-test-change.patch"), + }) + ); } finally { delete process.env.GITHUB_BASE_REF; } diff --git a/pkg/workflow/schemas/github-workflow.json b/pkg/workflow/schemas/github-workflow.json index e3437f83ba..afa39b9014 100644 --- a/pkg/workflow/schemas/github-workflow.json +++ b/pkg/workflow/schemas/github-workflow.json @@ -62,16 +62,16 @@ } ] }, - "jobContainer": { + "container": { "type": "object", "properties": { "image": { - "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idcontainerimage", + "$comment": "https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idcontainerimage", "description": "The Docker image to use as the container to run the action. The value can be the Docker Hub image name or a registry name.", "type": "string" }, "credentials": { - "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idcontainercredentials", + "$comment": "https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#jobsjob_idcontainercredentials", "description": "If the image's container registry requires authentication to pull the image, you can use credentials to set a map of the username and password. The credentials are the same values that you would provide to the `docker login` command.", "type": "object", "properties": { @@ -84,12 +84,12 @@ } }, "env": { - "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idcontainerenv", + "$comment": "https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idcontainerenv", "$ref": "#/definitions/env", - "description": "Sets a map of environment variables in the container." + "description": "Sets an array of environment variables in the container." }, "ports": { - "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idcontainerports", + "$comment": "https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idcontainerports", "description": "Sets an array of ports to expose on the container.", "type": "array", "items": { @@ -105,7 +105,7 @@ "minItems": 1 }, "volumes": { - "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idcontainervolumes", + "$comment": "https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idcontainervolumes", "description": "Sets an array of volumes for the container to use. You can use volumes to share data between services or other steps in a job. You can specify named Docker volumes, anonymous Docker volumes, or bind mounts on the host.\nTo specify a volume, you specify the source and destination path: :\nThe is a volume name or an absolute path on the host machine, and is an absolute path in the container.", "type": "array", "items": { @@ -114,7 +114,7 @@ "minItems": 1 }, "options": { - "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idcontaineroptions", + "$comment": "https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idcontaineroptions", "description": "Additional Docker container resource options. For a list of options, see https://docs.docker.com/engine/reference/commandline/create/#options.", "type": "string" } @@ -122,76 +122,6 @@ "required": ["image"], "additionalProperties": false }, - "serviceContainer": { - "type": "object", - "properties": { - "image": { - "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idservicesservice_idimage", - "description": "The Docker image to use as the service container to run the action. The value can be the Docker Hub image name or a registry name.", - "type": "string" - }, - "credentials": { - "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idservicesservice_idcredentials", - "description": "If the image's container registry requires authentication to pull the image, you can use credentials to set a map of the username and password. The credentials are the same values that you would provide to the `docker login` command.", - "type": "object", - "properties": { - "username": { - "type": "string" - }, - "password": { - "type": "string" - } - } - }, - "env": { - "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idservicesservice_idenv", - "$ref": "#/definitions/env", - "description": "Sets a map of environment variables in the service container." - }, - "ports": { - "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idservicesservice_idports", - "description": "Sets an array of ports to expose on the service container.", - "type": "array", - "items": { - "oneOf": [ - { - "type": "number" - }, - { - "type": "string" - } - ] - }, - "minItems": 1 - }, - "volumes": { - "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idservicesservice_idvolumes", - "description": "Sets an array of volumes for the service container to use. You can use volumes to share data between services or other steps in a job. You can specify named Docker volumes, anonymous Docker volumes, or bind mounts on the host.\nTo specify a volume, you specify the source and destination path: :\nThe is a volume name or an absolute path on the host machine, and is an absolute path in the container.", - "type": "array", - "items": { - "type": "string" - }, - "minItems": 1 - }, - "options": { - "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idservicesservice_idoptions", - "description": "Additional Docker container resource options. For a list of options, see https://docs.docker.com/engine/reference/commandline/create/#options.", - "type": "string" - }, - "command": { - "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idservicesservice_idcommand", - "description": "Overrides the Docker image's default command (`CMD`). The value is passed as arguments after the image name in the `docker create` command. If you also specify `entrypoint`, `command` provides the arguments to that entrypoint.", - "type": "string" - }, - "entrypoint": { - "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idservicesservice_identrypoint", - "description": "Overrides the Docker image's default `ENTRYPOINT`. The value is a single string defining the executable to run. Use this when you need to replace the image's entrypoint entirely. You can combine `entrypoint` with `command` to pass arguments to the custom entrypoint.", - "type": "string" - } - }, - "required": ["image"], - "additionalProperties": false - }, "defaults": { "type": "object", "properties": { @@ -903,7 +833,7 @@ "type": "string" }, { - "$ref": "#/definitions/jobContainer" + "$ref": "#/definitions/container" } ] }, @@ -912,7 +842,7 @@ "description": "Additional containers to host services for a job in a workflow. These are useful for creating databases or cache services like redis. The runner on the virtual machine will automatically create a network and manage the life cycle of the service containers.\nWhen you use a service container for a job or your step uses container actions, you don't need to set port information to access the service. Docker automatically exposes all ports between containers on the same network.\nWhen both the job and the action run in a container, you can directly reference the container by its hostname. The hostname is automatically mapped to the service name.\nWhen a step does not use a container action, you must access the service using localhost and bind the ports.", "type": "object", "additionalProperties": { - "$ref": "#/definitions/serviceContainer" + "$ref": "#/definitions/container" } }, "concurrency": { From b89a7a1a7e1e631128d7303ff1019e50c22b446f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 02:01:42 +0000 Subject: [PATCH 07/10] Tighten deep-review push handler tests assertions Agent-Logs-Url: https://github.com/github/gh-aw/sessions/9cbfebfe-a9d7-42d9-9e47-accf291cc24b Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/safe_outputs_handlers.test.cjs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/actions/setup/js/safe_outputs_handlers.test.cjs b/actions/setup/js/safe_outputs_handlers.test.cjs index b5af54a4a2..35bb556eaa 100644 --- a/actions/setup/js/safe_outputs_handlers.test.cjs +++ b/actions/setup/js/safe_outputs_handlers.test.cjs @@ -690,15 +690,14 @@ describe("safe_outputs_handlers", () => { repo: "test-owner/test-repo", }); + expect(result.isError).toBeFalsy(); expect(mockServer.debug).toHaveBeenCalledWith(expect.stringContaining("detecting actual working branch: feature/test-change")); - if (!result.isError) { - expect(mockAppendSafeOutput).toHaveBeenCalledWith( - expect.objectContaining({ - type: "push_to_pull_request_branch", - branch: "feature/test-change", - }) - ); - } + expect(mockAppendSafeOutput).toHaveBeenCalledWith( + expect.objectContaining({ + type: "push_to_pull_request_branch", + branch: "feature/test-change", + }) + ); } finally { delete process.env.GITHUB_BASE_REF; } @@ -714,7 +713,7 @@ describe("safe_outputs_handlers", () => { repo: "test-owner/test-repo", }); - expect(result.isError).toBeUndefined(); + expect(result.isError).toBeFalsy(); const responseData = JSON.parse(result.content[0].text); expect(responseData.result).toBe("success"); expect(responseData.patch.path).toContain("/tmp/gh-aw/aw-test-owner-test-repo-feature-test-change.patch"); From 9ef173354c8142120a4a0bebd36cfae43d5e4100 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 02:03:25 +0000 Subject: [PATCH 08/10] Refine deep-review tests for robustness and readability Agent-Logs-Url: https://github.com/github/gh-aw/sessions/9cbfebfe-a9d7-42d9-9e47-accf291cc24b Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/safe_outputs_handlers.test.cjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/safe_outputs_handlers.test.cjs b/actions/setup/js/safe_outputs_handlers.test.cjs index 35bb556eaa..9ba471fe48 100644 --- a/actions/setup/js/safe_outputs_handlers.test.cjs +++ b/actions/setup/js/safe_outputs_handlers.test.cjs @@ -588,7 +588,7 @@ describe("safe_outputs_handlers", () => { }); describe("pushToPullRequestBranchHandler", () => { - function setupSideRepoWithIncrementalCommit() { + function createSideRepoWithIncrementalCommit() { const targetRepoDir = path.join(testWorkspaceDir, "target-repo"); fs.mkdirSync(targetRepoDir, { recursive: true }); @@ -681,7 +681,7 @@ describe("safe_outputs_handlers", () => { }); it("should detect branch from the checked out target repo when repo is provided", async () => { - const { targetRepoDir } = setupSideRepoWithIncrementalCommit(); + const { targetRepoDir } = createSideRepoWithIncrementalCommit(); process.env.GITHUB_BASE_REF = "main"; try { @@ -704,7 +704,7 @@ describe("safe_outputs_handlers", () => { }); it("should include repo slug in incremental patch filename for side-repo checkout", async () => { - const { targetRepoDir } = setupSideRepoWithIncrementalCommit(); + const { targetRepoDir } = createSideRepoWithIncrementalCommit(); process.env.GITHUB_BASE_REF = "main"; try { @@ -716,7 +716,7 @@ describe("safe_outputs_handlers", () => { expect(result.isError).toBeFalsy(); const responseData = JSON.parse(result.content[0].text); expect(responseData.result).toBe("success"); - expect(responseData.patch.path).toContain("/tmp/gh-aw/aw-test-owner-test-repo-feature-test-change.patch"); + expect(responseData.patch.path).toEqual(expect.stringMatching(/aw-test-owner-test-repo-feature-test-change\.patch$/)); expect(mockAppendSafeOutput).toHaveBeenCalledWith( expect.objectContaining({ From 9dde06d4a4edc0481eb6d46bf0cd6eb5c056418d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 02:05:12 +0000 Subject: [PATCH 09/10] Polish deep-review test naming and path assertions Agent-Logs-Url: https://github.com/github/gh-aw/sessions/9cbfebfe-a9d7-42d9-9e47-accf291cc24b Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/safe_outputs_handlers.test.cjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/safe_outputs_handlers.test.cjs b/actions/setup/js/safe_outputs_handlers.test.cjs index 9ba471fe48..0ba7cc6eea 100644 --- a/actions/setup/js/safe_outputs_handlers.test.cjs +++ b/actions/setup/js/safe_outputs_handlers.test.cjs @@ -588,7 +588,7 @@ describe("safe_outputs_handlers", () => { }); describe("pushToPullRequestBranchHandler", () => { - function createSideRepoWithIncrementalCommit() { + function createSideRepoWithTrackedAndLocalCommits() { const targetRepoDir = path.join(testWorkspaceDir, "target-repo"); fs.mkdirSync(targetRepoDir, { recursive: true }); @@ -681,7 +681,7 @@ describe("safe_outputs_handlers", () => { }); it("should detect branch from the checked out target repo when repo is provided", async () => { - const { targetRepoDir } = createSideRepoWithIncrementalCommit(); + const { targetRepoDir } = createSideRepoWithTrackedAndLocalCommits(); process.env.GITHUB_BASE_REF = "main"; try { @@ -704,7 +704,7 @@ describe("safe_outputs_handlers", () => { }); it("should include repo slug in incremental patch filename for side-repo checkout", async () => { - const { targetRepoDir } = createSideRepoWithIncrementalCommit(); + const { targetRepoDir } = createSideRepoWithTrackedAndLocalCommits(); process.env.GITHUB_BASE_REF = "main"; try { @@ -716,7 +716,7 @@ describe("safe_outputs_handlers", () => { expect(result.isError).toBeFalsy(); const responseData = JSON.parse(result.content[0].text); expect(responseData.result).toBe("success"); - expect(responseData.patch.path).toEqual(expect.stringMatching(/aw-test-owner-test-repo-feature-test-change\.patch$/)); + expect(path.basename(responseData.patch.path)).toBe("aw-test-owner-test-repo-feature-test-change.patch"); expect(mockAppendSafeOutput).toHaveBeenCalledWith( expect.objectContaining({ From 2c81766193436203ccba9b03d8a3c5b210dcb6ea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 02:29:14 +0000 Subject: [PATCH 10/10] Log selected side-repo checkout folder in push handler Agent-Logs-Url: https://github.com/github/gh-aw/sessions/65de9894-1f7e-4563-8ebd-4b7c9dc64854 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/safe_outputs_handlers.cjs | 1 + actions/setup/js/safe_outputs_handlers.test.cjs | 1 + 2 files changed, 2 insertions(+) diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index 9bcec7a9e7..ac92aab4d8 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -510,6 +510,7 @@ function createHandlers(server, appendSafeOutput, config = {}) { } repoCwd = checkoutResult.path; entry.repo_cwd = repoCwd; + server.debug(`Selected checkout folder for ${repoSlug}: ${repoCwd}`); } // If branch is not provided, is empty, or equals the base branch, use the current branch from git diff --git a/actions/setup/js/safe_outputs_handlers.test.cjs b/actions/setup/js/safe_outputs_handlers.test.cjs index 0ba7cc6eea..8a05502336 100644 --- a/actions/setup/js/safe_outputs_handlers.test.cjs +++ b/actions/setup/js/safe_outputs_handlers.test.cjs @@ -691,6 +691,7 @@ describe("safe_outputs_handlers", () => { }); 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("detecting actual working branch: feature/test-change")); expect(mockAppendSafeOutput).toHaveBeenCalledWith( expect.objectContaining({