From 806e384eeb0b8cf779d0518effb064be0a1a035d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 18:35:12 +0000 Subject: [PATCH 1/5] Initial plan From 85ea6f8b7a18d951687b706ddc22777bf4a14c16 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 18:41:23 +0000 Subject: [PATCH 2/5] plan: fix pushSignedCommits for new branches Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/9a1cfa2e-dbc6-4899-8ea8-77ba1711969f --- .github/workflows/smoke-codex.lock.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index e93417720d..fc04bf871e 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -450,13 +450,20 @@ jobs: { "description": "Add the 'smoked' label to the current pull request (can only be called once)", "inputSchema": { - "additionalProperties": true, + "additionalProperties": false, "properties": { - "payload": { - "description": "JSON-encoded payload to pass to the action", + "labels": { + "description": "The labels' name to be added. Must be separated with line breaks if there're multiple labels.", + "type": "string" + }, + "number": { + "description": "The number of the issue or pull request.", "type": "string" } }, + "required": [ + "labels" + ], "type": "object" }, "name": "add_smoked_label" @@ -1561,7 +1568,8 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} with: - payload: ${{ steps.process_safe_outputs.outputs.action_add_smoked_label_payload }} + labels: ${{ fromJSON(steps.process_safe_outputs.outputs.action_add_smoked_label_payload).labels }} + number: ${{ fromJSON(steps.process_safe_outputs.outputs.action_add_smoked_label_payload).number }} - name: Upload safe output items if: always() uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 From 78efe80479241d7be077a6b8c47eed7987bafc82 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 18:47:24 +0000 Subject: [PATCH 3/5] fix: pushSignedCommits supports new branches not yet on remote When `git ls-remote` returns empty (branch doesn't exist on remote), use `git rev-parse sha^` to get the parent commit OID as `expectedHeadOid` for the `createCommitOnBranch` GraphQL mutation. This allows the mutation to create the branch automatically. After each successful mutation, reuse the returned OID for the next iteration instead of re-querying `git ls-remote`. Fixes: create-pull-request signed commits fail when branch does not yet exist on remote (GH013 rejection with signed commits ruleset). Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/9a1cfa2e-dbc6-4899-8ea8-77ba1711969f --- actions/setup/js/push_signed_commits.cjs | 31 ++++++-- actions/setup/js/push_signed_commits.test.cjs | 79 +++++++++++++++++++ 2 files changed, 103 insertions(+), 7 deletions(-) diff --git a/actions/setup/js/push_signed_commits.cjs b/actions/setup/js/push_signed_commits.cjs index 481de1bf24..0530392d9d 100644 --- a/actions/setup/js/push_signed_commits.cjs +++ b/actions/setup/js/push_signed_commits.cjs @@ -47,12 +47,29 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c core.info(`pushSignedCommits: replaying ${shas.length} commit(s) via GraphQL createCommitOnBranch`); try { + /** @type {string | undefined} */ + let lastOid; for (const sha of shas) { - // Get the current remote HEAD OID (updated each iteration) - const { stdout: oidOut } = await exec.getExecOutput("git", ["ls-remote", "origin", `refs/heads/${branch}`], { cwd }); - const expectedHeadOid = oidOut.trim().split(/\s+/)[0]; - if (!expectedHeadOid) { - throw new Error(`Could not resolve remote HEAD OID for branch ${branch}`); + // Determine the expected HEAD OID for this commit. + // After the first signed commit, reuse the OID returned by the previous GraphQL + // mutation instead of re-querying ls-remote (works even if the branch is new). + let expectedHeadOid; + if (lastOid) { + expectedHeadOid = lastOid; + } else { + // First commit: check whether the branch already exists on the remote. + const { stdout: oidOut } = await exec.getExecOutput("git", ["ls-remote", "origin", `refs/heads/${branch}`], { cwd }); + expectedHeadOid = oidOut.trim().split(/\s+/)[0]; + if (!expectedHeadOid) { + // Branch does not exist on the remote yet – createCommitOnBranch will create it. + // Use the local parent commit OID as the expected base. + core.info(`pushSignedCommits: branch ${branch} not yet on the remote, resolving parent OID for first commit`); + const { stdout: parentOut } = await exec.getExecOutput("git", ["rev-parse", `${sha}^`], { cwd }); + expectedHeadOid = parentOut.trim(); + if (!expectedHeadOid) { + throw new Error(`Could not resolve OID for new branch ${branch}`); + } + } } // Full commit message (subject + body) @@ -99,8 +116,8 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c }`, { input } ); - const oid = result?.createCommitOnBranch?.commit?.oid; - core.info(`pushSignedCommits: signed commit created: ${oid}`); + lastOid = result?.createCommitOnBranch?.commit?.oid; + core.info(`pushSignedCommits: signed commit created: ${lastOid}`); } core.info(`pushSignedCommits: all ${shas.length} commit(s) pushed as signed commits`); } catch (graphqlError) { diff --git a/actions/setup/js/push_signed_commits.test.cjs b/actions/setup/js/push_signed_commits.test.cjs index 67e933976c..dde736dec4 100644 --- a/actions/setup/js/push_signed_commits.test.cjs +++ b/actions/setup/js/push_signed_commits.test.cjs @@ -337,6 +337,85 @@ describe("push_signed_commits integration tests", () => { }); }); + // ────────────────────────────────────────────────────── + // New branch – branch does not yet exist on remote + // ────────────────────────────────────────────────────── + + describe("new branch (does not exist on remote)", () => { + it("should use parent OID when branch is not yet on remote (single commit)", async () => { + // Create a local branch with one commit but do NOT push it + execGit(["checkout", "-b", "new-unpushed-branch"], { cwd: workDir }); + fs.writeFileSync(path.join(workDir, "new-file.txt"), "New file content\n"); + execGit(["add", "new-file.txt"], { cwd: workDir }); + execGit(["commit", "-m", "Add new-file.txt"], { cwd: workDir }); + + // Capture the local parent OID (main HEAD before the new commit) + const expectedParentOid = execGit(["rev-parse", "HEAD^"], { cwd: workDir }).stdout.trim(); + + global.exec = makeRealExec(workDir); + const githubClient = makeMockGithubClient(); + + await pushSignedCommits({ + githubClient, + owner: "test-owner", + repo: "test-repo", + branch: "new-unpushed-branch", + baseRef: "origin/main", + cwd: workDir, + }); + + expect(githubClient.graphql).toHaveBeenCalledTimes(1); + const callArg = githubClient.graphql.mock.calls[0][1].input; + // expectedHeadOid must be the parent commit OID, not empty + expect(callArg.expectedHeadOid).toBe(expectedParentOid); + expect(callArg.branch.branchName).toBe("new-unpushed-branch"); + expect(callArg.message.headline).toBe("Add new-file.txt"); + // Verify the info log was emitted + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("not yet on the remote")); + }); + + it("should chain GraphQL OIDs for multiple commits on a new branch", async () => { + // Create a local branch with two commits but do NOT push it + execGit(["checkout", "-b", "new-multi-commit-branch"], { cwd: workDir }); + + fs.writeFileSync(path.join(workDir, "alpha.txt"), "Alpha\n"); + execGit(["add", "alpha.txt"], { cwd: workDir }); + execGit(["commit", "-m", "Add alpha.txt"], { cwd: workDir }); + + fs.writeFileSync(path.join(workDir, "beta.txt"), "Beta\n"); + execGit(["add", "beta.txt"], { cwd: workDir }); + execGit(["commit", "-m", "Add beta.txt"], { cwd: workDir }); + + // The parent OID of the first commit is main's HEAD (two commits back from current) + const expectedParentOid = execGit(["rev-parse", "HEAD^^"], { cwd: workDir }).stdout.trim(); + + global.exec = makeRealExec(workDir); + // Mock returns the same OID for all calls; second call must use that OID + const githubClient = makeMockGithubClient({ oid: "signed-oid-first" }); + + await pushSignedCommits({ + githubClient, + owner: "test-owner", + repo: "test-repo", + branch: "new-multi-commit-branch", + baseRef: "origin/main", + cwd: workDir, + }); + + expect(githubClient.graphql).toHaveBeenCalledTimes(2); + + // First call: expectedHeadOid is the parent commit OID (resolved via git rev-parse) + const firstCallArg = githubClient.graphql.mock.calls[0][1].input; + expect(firstCallArg.expectedHeadOid).toBe(expectedParentOid); + expect(firstCallArg.message.headline).toBe("Add alpha.txt"); + + // Second call: expectedHeadOid is the OID returned by the first GraphQL mutation + const secondCallArg = githubClient.graphql.mock.calls[1][1].input; + expect(secondCallArg.expectedHeadOid).toBe("signed-oid-first"); + expect(secondCallArg.message.headline).toBe("Add beta.txt"); + }); + }); + // ────────────────────────────────────────────────────── // Fallback path – GraphQL fails → git push // ────────────────────────────────────────────────────── From 93aa7922523725c7a52903550bddb90ba84511da Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:01:39 +0000 Subject: [PATCH 4/5] fix: validate GraphQL OID and require number in smoke-codex add_smoked_label - Validate createCommitOnBranch OID before chaining; throw if null/empty - Add number to required fields for add_smoked_label in smoke-codex.lock.yml - Update smoke-codex.md agent instructions to include number in the call Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/c7d3c322-e1bd-4ec2-bf01-fdf30ae9571b --- .github/workflows/smoke-codex.lock.yml | 3 ++- .github/workflows/smoke-codex.md | 2 +- actions/setup/js/push_signed_commits.cjs | 6 +++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index fc04bf871e..b456352634 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -462,7 +462,8 @@ jobs: } }, "required": [ - "labels" + "labels", + "number" ], "type": "object" }, diff --git a/.github/workflows/smoke-codex.md b/.github/workflows/smoke-codex.md index cd0316b115..45ec1e316c 100644 --- a/.github/workflows/smoke-codex.md +++ b/.github/workflows/smoke-codex.md @@ -105,7 +105,7 @@ If all tests pass: - Use the `add_labels` safe-output tool to add the label `smoke-codex` to the pull request - Use the `remove_labels` safe-output tool to remove the label `smoke` from the pull request - Use the `unassign_from_user` safe-output tool to unassign the user `githubactionagent` from the pull request (this is a fictitious user used for testing) -- Use the `add_smoked_label` safe-output action tool to add the label `smoked` to the pull request (call it with `{"labels": "smoked"}`) +- Use the `add_smoked_label` safe-output action tool to add the label `smoked` to the pull request (call it with `{"labels": "smoked", "number": "${{ github.event.pull_request.number }}"}`) **Important**: If no action is needed after completing your analysis, you **MUST** call the `noop` safe-output tool with a brief explanation. Failing to call any safe-output tool is the most common cause of safe-output workflow failures. diff --git a/actions/setup/js/push_signed_commits.cjs b/actions/setup/js/push_signed_commits.cjs index 0530392d9d..06875cb2ee 100644 --- a/actions/setup/js/push_signed_commits.cjs +++ b/actions/setup/js/push_signed_commits.cjs @@ -116,7 +116,11 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c }`, { input } ); - lastOid = result?.createCommitOnBranch?.commit?.oid; + const newOid = result && result.createCommitOnBranch && result.createCommitOnBranch.commit ? result.createCommitOnBranch.commit.oid : undefined; + if (typeof newOid !== "string" || newOid.length === 0) { + throw new Error("pushSignedCommits: GraphQL createCommitOnBranch did not return a valid commit OID"); + } + lastOid = newOid; core.info(`pushSignedCommits: signed commit created: ${lastOid}`); } core.info(`pushSignedCommits: all ${shas.length} commit(s) pushed as signed commits`); From a6456980da3327253726db9275f70dcc359e32e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:15:59 +0000 Subject: [PATCH 5/5] fix: add diagnostic logging to pushSignedCommits Log per-commit SHA and progress, OID source (chained/ls-remote/parent), commit headline, file change counts, and mutation invocation details Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/fe3acff2-47c1-4f2f-b977-d1b6b832cd69 --- .github/workflows/smoke-codex.lock.yml | 4 ++-- actions/setup/js/push_signed_commits.cjs | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index b456352634..01397d7bcd 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -245,6 +245,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} @@ -462,8 +463,7 @@ jobs: } }, "required": [ - "labels", - "number" + "labels" ], "type": "object" }, diff --git a/actions/setup/js/push_signed_commits.cjs b/actions/setup/js/push_signed_commits.cjs index 06875cb2ee..bbb8593c94 100644 --- a/actions/setup/js/push_signed_commits.cjs +++ b/actions/setup/js/push_signed_commits.cjs @@ -44,18 +44,22 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c return; } - core.info(`pushSignedCommits: replaying ${shas.length} commit(s) via GraphQL createCommitOnBranch`); + core.info(`pushSignedCommits: replaying ${shas.length} commit(s) via GraphQL createCommitOnBranch (branch: ${branch}, repo: ${owner}/${repo})`); try { /** @type {string | undefined} */ let lastOid; - for (const sha of shas) { + for (let i = 0; i < shas.length; i++) { + const sha = shas[i]; + core.info(`pushSignedCommits: processing commit ${i + 1}/${shas.length} sha=${sha}`); + // Determine the expected HEAD OID for this commit. // After the first signed commit, reuse the OID returned by the previous GraphQL // mutation instead of re-querying ls-remote (works even if the branch is new). let expectedHeadOid; if (lastOid) { expectedHeadOid = lastOid; + core.info(`pushSignedCommits: using chained OID from previous mutation: ${expectedHeadOid}`); } else { // First commit: check whether the branch already exists on the remote. const { stdout: oidOut } = await exec.getExecOutput("git", ["ls-remote", "origin", `refs/heads/${branch}`], { cwd }); @@ -69,6 +73,9 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c if (!expectedHeadOid) { throw new Error(`Could not resolve OID for new branch ${branch}`); } + core.info(`pushSignedCommits: using parent OID for new branch: ${expectedHeadOid}`); + } else { + core.info(`pushSignedCommits: using remote HEAD OID from ls-remote: ${expectedHeadOid}`); } } @@ -77,6 +84,7 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c const message = msgOut.trim(); const headline = message.split("\n")[0]; const body = message.split("\n").slice(1).join("\n").trim(); + core.info(`pushSignedCommits: commit message headline: "${headline}"`); // File changes for this commit (supports Add/Modify/Delete/Rename/Copy) const { stdout: nameStatusOut } = await exec.getExecOutput("git", ["diff", "--name-status", `${sha}^`, sha], { cwd }); @@ -102,6 +110,8 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c } } + core.info(`pushSignedCommits: file changes: ${additions.length} addition(s), ${deletions.length} deletion(s)`); + /** @type {any} */ const input = { branch: { repositoryNameWithOwner: `${owner}/${repo}`, branchName: branch }, @@ -110,6 +120,7 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c expectedHeadOid, }; + core.info(`pushSignedCommits: calling createCommitOnBranch mutation (expectedHeadOid=${expectedHeadOid})`); const result = await githubClient.graphql( `mutation($input: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $input) { commit { oid } }