From c9b1a2bc5f6c1654afa3ae368ef80a32fe1bd3e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 02:53:10 +0000 Subject: [PATCH 1/3] Initial plan From c681cd20c85e40a2f971af2e9f46cec405ecdee1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 03:00:22 +0000 Subject: [PATCH 2/3] Add checkout PR failure detection to skip issue creation Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/handle_agent_failure.cjs | 73 ++++++- .../setup/js/handle_agent_failure.test.cjs | 178 ++++++++++++++++++ 2 files changed, 250 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index 31483ef4db..9c621e3e2f 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -236,6 +236,67 @@ async function linkSubIssue(parentNodeId, subIssueNodeId, parentNumber, subIssue } } +/** + * Check if the agent job failed due to PR checkout failure + * This typically happens when a PR is merged and the branch is deleted + * @returns {Promise} True if failure was due to checkout step + */ +async function isCheckoutPRFailure() { + try { + const runId = context.runId; + const { owner, repo } = context.repo; + + core.info(`Checking if failure was due to PR checkout (run ID: ${runId})`); + + // Get all jobs for this workflow run + const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({ + owner, + repo, + run_id: runId, + }); + + // Find the agent job + const agentJob = jobs.jobs.find(job => job.name === "agent"); + if (!agentJob) { + core.info("Agent job not found, cannot determine checkout failure"); + return false; + } + + core.info(`Agent job status: ${agentJob.status}, conclusion: ${agentJob.conclusion}`); + + // Check if agent job failed + if (agentJob.conclusion !== "failure") { + return false; + } + + // Check if steps array exists + if (!agentJob.steps || agentJob.steps.length === 0) { + core.info("Agent job has no steps"); + return false; + } + + // Find the "Checkout PR branch" step + const checkoutStep = agentJob.steps.find(step => step.name === "Checkout PR branch"); + if (!checkoutStep) { + core.info("Checkout PR branch step not found"); + return false; + } + + core.info(`Checkout PR branch step conclusion: ${checkoutStep.conclusion}`); + + // If checkout step failed, this is a checkout failure + if (checkoutStep.conclusion === "failure") { + core.info("✓ Detected checkout PR failure - this is expected when PR is merged"); + return true; + } + + return false; + } catch (error) { + core.warning(`Error checking for checkout failure: ${getErrorMessage(error)}`); + return false; + } +} + /** * Handle agent job failure by creating or updating a failure tracking issue * This script is called from the conclusion job when the agent job has failed @@ -279,6 +340,16 @@ async function main() { return; } + // Check if this is a checkout PR failure (e.g., PR was merged and branch deleted) + // If so, skip creating an issue as this is expected behavior + if (agentConclusion === "failure") { + const isCheckoutFailure = await isCheckoutPRFailure(); + if (isCheckoutFailure) { + core.info("Skipping failure handling - failure was due to PR checkout (likely PR merged)"); + return; + } + } + const { owner, repo } = context.repo; // Try to find a pull request for the current branch @@ -493,4 +564,4 @@ async function main() { } } -module.exports = { main }; +module.exports = { main, isCheckoutPRFailure }; diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs index 6fbd08aefe..5fadd2cc1c 100644 --- a/actions/setup/js/handle_agent_failure.test.cjs +++ b/actions/setup/js/handle_agent_failure.test.cjs @@ -1111,4 +1111,182 @@ When prompted, instruct the agent to debug this workflow failure.`; expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Found existing parent issue #5")); }); }); + + describe("checkout PR failure detection", () => { + let isCheckoutPRFailure; + + beforeEach(async () => { + // Import the function we want to test + const module = await import("./handle_agent_failure.cjs"); + isCheckoutPRFailure = module.isCheckoutPRFailure; + + // Set up context for checkout tests + mockContext.runId = 123456; + }); + + it("should detect checkout PR failure when step fails", async () => { + mockGithub.rest.actions = { + listJobsForWorkflowRun: vi.fn().mockResolvedValue({ + data: { + jobs: [ + { + name: "agent", + status: "completed", + conclusion: "failure", + steps: [ + { name: "Checkout actions folder", conclusion: "success" }, + { name: "Setup Scripts", conclusion: "success" }, + { name: "Checkout repository", conclusion: "success" }, + { name: "Create gh-aw temp directory", conclusion: "success" }, + { name: "Configure Git credentials", conclusion: "success" }, + { name: "Checkout PR branch", conclusion: "failure" }, + { name: "Validate COPILOT_GITHUB_TOKEN secret", conclusion: "skipped" }, + { name: "Install GitHub Copilot CLI", conclusion: "skipped" }, + ], + }, + ], + }, + }), + }; + + const result = await isCheckoutPRFailure(); + + expect(result).toBe(true); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Detected checkout PR failure")); + }); + + it("should return false when checkout step succeeds", async () => { + mockGithub.rest.actions = { + listJobsForWorkflowRun: vi.fn().mockResolvedValue({ + data: { + jobs: [ + { + name: "agent", + status: "completed", + conclusion: "failure", + steps: [ + { name: "Checkout actions folder", conclusion: "success" }, + { name: "Setup Scripts", conclusion: "success" }, + { name: "Checkout repository", conclusion: "success" }, + { name: "Checkout PR branch", conclusion: "success" }, + { name: "Install GitHub Copilot CLI", conclusion: "failure" }, + ], + }, + ], + }, + }), + }; + + const result = await isCheckoutPRFailure(); + + expect(result).toBe(false); + }); + + it("should return false when agent job succeeds", async () => { + mockGithub.rest.actions = { + listJobsForWorkflowRun: vi.fn().mockResolvedValue({ + data: { + jobs: [ + { + name: "agent", + status: "completed", + conclusion: "success", + steps: [ + { name: "Checkout actions folder", conclusion: "success" }, + { name: "Checkout PR branch", conclusion: "success" }, + ], + }, + ], + }, + }), + }; + + const result = await isCheckoutPRFailure(); + + expect(result).toBe(false); + }); + + it("should return false when agent job not found", async () => { + mockGithub.rest.actions = { + listJobsForWorkflowRun: vi.fn().mockResolvedValue({ + data: { + jobs: [ + { + name: "other-job", + status: "completed", + conclusion: "failure", + steps: [], + }, + ], + }, + }), + }; + + const result = await isCheckoutPRFailure(); + + expect(result).toBe(false); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Agent job not found")); + }); + + it("should return false when checkout step not found", async () => { + mockGithub.rest.actions = { + listJobsForWorkflowRun: vi.fn().mockResolvedValue({ + data: { + jobs: [ + { + name: "agent", + status: "completed", + conclusion: "failure", + steps: [ + { name: "Checkout actions folder", conclusion: "success" }, + { name: "Other step", conclusion: "failure" }, + ], + }, + ], + }, + }), + }; + + const result = await isCheckoutPRFailure(); + + expect(result).toBe(false); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Checkout PR branch step not found")); + }); + + it("should handle API errors gracefully", async () => { + mockGithub.rest.actions = { + listJobsForWorkflowRun: vi.fn().mockRejectedValue(new Error("API Error")), + }; + + const result = await isCheckoutPRFailure(); + + expect(result).toBe(false); + expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Error checking for checkout failure")); + }); + + it("should skip issue creation when checkout fails", async () => { + // Mock the checkout failure detection to return true + mockGithub.rest.actions = { + listJobsForWorkflowRun: vi.fn().mockResolvedValue({ + data: { + jobs: [ + { + name: "agent", + status: "completed", + conclusion: "failure", + steps: [{ name: "Checkout PR branch", conclusion: "failure" }], + }, + ], + }, + }), + }; + + await main(); + + // Verify that no issue was created + expect(mockGithub.rest.issues.create).not.toHaveBeenCalled(); + expect(mockGithub.rest.issues.createComment).not.toHaveBeenCalled(); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Skipping failure handling - failure was due to PR checkout")); + }); + }); }); From b658822d28c7311330f02c8d3e0f290bb4b659f8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 03:14:38 +0000 Subject: [PATCH 3/3] Use job outputs instead of Actions API for checkout failure detection Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/security-guard.lock.yml | 3 + actions/setup/js/checkout_pr_branch.cjs | 7 + actions/setup/js/checkout_pr_branch.test.cjs | 37 +++ actions/setup/js/handle_agent_failure.cjs | 78 +----- .../setup/js/handle_agent_failure.test.cjs | 230 ++++++------------ pkg/workflow/compiler_activation_jobs.go | 6 + pkg/workflow/notify_comment.go | 3 + pkg/workflow/pr.go | 1 + 8 files changed, 137 insertions(+), 228 deletions(-) diff --git a/.github/workflows/security-guard.lock.yml b/.github/workflows/security-guard.lock.yml index 1e9bde7812..cd518c78a7 100644 --- a/.github/workflows/security-guard.lock.yml +++ b/.github/workflows/security-guard.lock.yml @@ -90,6 +90,7 @@ jobs: GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json outputs: + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} model: ${{ steps.generate_aw_info.outputs.model }} output: ${{ steps.collect_output.outputs.output }} @@ -124,6 +125,7 @@ jobs: git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - name: Checkout PR branch + id: checkout-pr if: | github.event.pull_request uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 @@ -796,6 +798,7 @@ jobs: GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.agent.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e 🛡️ *Security posture analysis by [{workflow_name}]({run_url})*\",\"runStarted\":\"🔒 [{workflow_name}]({run_url}) is analyzing this pull request for security posture changes...\",\"runSuccess\":\"🛡️ [{workflow_name}]({run_url}) completed security posture analysis.\",\"runFailure\":\"⚠️ [{workflow_name}]({run_url}) {status} during security analysis.\"}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/actions/setup/js/checkout_pr_branch.cjs b/actions/setup/js/checkout_pr_branch.cjs index a3310b3568..45a09d806f 100644 --- a/actions/setup/js/checkout_pr_branch.cjs +++ b/actions/setup/js/checkout_pr_branch.cjs @@ -16,6 +16,7 @@ async function main() { if (!pullRequest) { core.info("No pull request context available, skipping checkout"); + core.setOutput("checkout_pr_success", "true"); return; } @@ -41,9 +42,15 @@ async function main() { core.info(`✅ Successfully checked out PR #${prNumber}`); } + + // Set output to indicate successful checkout + core.setOutput("checkout_pr_success", "true"); } catch (error) { const errorMsg = getErrorMessage(error); + // Set output to indicate checkout failure + core.setOutput("checkout_pr_success", "false"); + // Load and render step summary template const templatePath = "/opt/gh-aw/prompts/pr_checkout_failure.md"; const template = fs.readFileSync(templatePath, "utf8"); diff --git a/actions/setup/js/checkout_pr_branch.test.cjs b/actions/setup/js/checkout_pr_branch.test.cjs index c4deb67c05..5a27a90685 100644 --- a/actions/setup/js/checkout_pr_branch.test.cjs +++ b/actions/setup/js/checkout_pr_branch.test.cjs @@ -10,6 +10,7 @@ describe("checkout_pr_branch.cjs", () => { mockCore = { info: vi.fn(), setFailed: vi.fn(), + setOutput: vi.fn(), summary: { addRaw: vi.fn().mockReturnThis(), write: vi.fn().mockResolvedValue(undefined), @@ -327,4 +328,40 @@ If the pull request is still open, verify that: expect(mockExec.exec).toHaveBeenCalledWith("git", ["fetch", "origin", longBranchName]); }); }); + + describe("checkout output", () => { + it("should set output to true on successful checkout (pull_request event)", async () => { + await runScript(); + + expect(mockCore.setOutput).toHaveBeenCalledWith("checkout_pr_success", "true"); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + }); + + it("should set output to true on successful checkout (comment event)", async () => { + mockContext.eventName = "issue_comment"; + + await runScript(); + + expect(mockCore.setOutput).toHaveBeenCalledWith("checkout_pr_success", "true"); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + }); + + it("should set output to false on checkout failure", async () => { + mockExec.exec.mockRejectedValueOnce(new Error("checkout failed")); + + await runScript(); + + expect(mockCore.setOutput).toHaveBeenCalledWith("checkout_pr_success", "false"); + expect(mockCore.setFailed).toHaveBeenCalledWith("Failed to checkout PR branch: checkout failed"); + }); + + it("should set output to true when no PR context", async () => { + mockContext.payload.pull_request = null; + + await runScript(); + + expect(mockCore.setOutput).toHaveBeenCalledWith("checkout_pr_success", "true"); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + }); + }); }); diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index 9c621e3e2f..16162a21c4 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -236,67 +236,6 @@ async function linkSubIssue(parentNodeId, subIssueNodeId, parentNumber, subIssue } } -/** - * Check if the agent job failed due to PR checkout failure - * This typically happens when a PR is merged and the branch is deleted - * @returns {Promise} True if failure was due to checkout step - */ -async function isCheckoutPRFailure() { - try { - const runId = context.runId; - const { owner, repo } = context.repo; - - core.info(`Checking if failure was due to PR checkout (run ID: ${runId})`); - - // Get all jobs for this workflow run - const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({ - owner, - repo, - run_id: runId, - }); - - // Find the agent job - const agentJob = jobs.jobs.find(job => job.name === "agent"); - if (!agentJob) { - core.info("Agent job not found, cannot determine checkout failure"); - return false; - } - - core.info(`Agent job status: ${agentJob.status}, conclusion: ${agentJob.conclusion}`); - - // Check if agent job failed - if (agentJob.conclusion !== "failure") { - return false; - } - - // Check if steps array exists - if (!agentJob.steps || agentJob.steps.length === 0) { - core.info("Agent job has no steps"); - return false; - } - - // Find the "Checkout PR branch" step - const checkoutStep = agentJob.steps.find(step => step.name === "Checkout PR branch"); - if (!checkoutStep) { - core.info("Checkout PR branch step not found"); - return false; - } - - core.info(`Checkout PR branch step conclusion: ${checkoutStep.conclusion}`); - - // If checkout step failed, this is a checkout failure - if (checkoutStep.conclusion === "failure") { - core.info("✓ Detected checkout PR failure - this is expected when PR is merged"); - return true; - } - - return false; - } catch (error) { - core.warning(`Error checking for checkout failure: ${getErrorMessage(error)}`); - return false; - } -} - /** * Handle agent job failure by creating or updating a failure tracking issue * This script is called from the conclusion job when the agent job has failed @@ -313,11 +252,13 @@ async function main() { const secretVerificationResult = process.env.GH_AW_SECRET_VERIFICATION_RESULT || ""; const assignmentErrors = process.env.GH_AW_ASSIGNMENT_ERRORS || ""; const assignmentErrorCount = process.env.GH_AW_ASSIGNMENT_ERROR_COUNT || "0"; + const checkoutPRSuccess = process.env.GH_AW_CHECKOUT_PR_SUCCESS || ""; core.info(`Agent conclusion: ${agentConclusion}`); core.info(`Workflow name: ${workflowName}`); core.info(`Secret verification result: ${secretVerificationResult}`); core.info(`Assignment error count: ${assignmentErrorCount}`); + core.info(`Checkout PR success: ${checkoutPRSuccess}`); // Check if there are assignment errors (regardless of agent job status) const hasAssignmentErrors = parseInt(assignmentErrorCount, 10) > 0; @@ -340,14 +281,11 @@ async function main() { return; } - // Check if this is a checkout PR failure (e.g., PR was merged and branch deleted) - // If so, skip creating an issue as this is expected behavior - if (agentConclusion === "failure") { - const isCheckoutFailure = await isCheckoutPRFailure(); - if (isCheckoutFailure) { - core.info("Skipping failure handling - failure was due to PR checkout (likely PR merged)"); - return; - } + // Check if the failure was due to PR checkout (e.g., PR was merged and branch deleted) + // If checkout_pr_success is "false", skip creating an issue as this is expected behavior + if (agentConclusion === "failure" && checkoutPRSuccess === "false") { + core.info("Skipping failure handling - failure was due to PR checkout (likely PR merged)"); + return; } const { owner, repo } = context.repo; @@ -564,4 +502,4 @@ async function main() { } } -module.exports = { main, isCheckoutPRFailure }; +module.exports = { main }; diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs index 5fadd2cc1c..048c4ea278 100644 --- a/actions/setup/js/handle_agent_failure.test.cjs +++ b/actions/setup/js/handle_agent_failure.test.cjs @@ -1112,181 +1112,95 @@ When prompted, instruct the agent to debug this workflow failure.`; }); }); - describe("checkout PR failure detection", () => { - let isCheckoutPRFailure; + describe("checkout PR failure via output", () => { + it("should skip issue creation when checkout_pr_success is false", async () => { + // Set the checkout PR failure environment variable + process.env.GH_AW_CHECKOUT_PR_SUCCESS = "false"; - beforeEach(async () => { - // Import the function we want to test - const module = await import("./handle_agent_failure.cjs"); - isCheckoutPRFailure = module.isCheckoutPRFailure; - - // Set up context for checkout tests - mockContext.runId = 123456; - }); - - it("should detect checkout PR failure when step fails", async () => { - mockGithub.rest.actions = { - listJobsForWorkflowRun: vi.fn().mockResolvedValue({ - data: { - jobs: [ - { - name: "agent", - status: "completed", - conclusion: "failure", - steps: [ - { name: "Checkout actions folder", conclusion: "success" }, - { name: "Setup Scripts", conclusion: "success" }, - { name: "Checkout repository", conclusion: "success" }, - { name: "Create gh-aw temp directory", conclusion: "success" }, - { name: "Configure Git credentials", conclusion: "success" }, - { name: "Checkout PR branch", conclusion: "failure" }, - { name: "Validate COPILOT_GITHUB_TOKEN secret", conclusion: "skipped" }, - { name: "Install GitHub Copilot CLI", conclusion: "skipped" }, - ], - }, - ], - }, - }), - }; - - const result = await isCheckoutPRFailure(); - - expect(result).toBe(true); - expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Detected checkout PR failure")); - }); - - it("should return false when checkout step succeeds", async () => { - mockGithub.rest.actions = { - listJobsForWorkflowRun: vi.fn().mockResolvedValue({ - data: { - jobs: [ - { - name: "agent", - status: "completed", - conclusion: "failure", - steps: [ - { name: "Checkout actions folder", conclusion: "success" }, - { name: "Setup Scripts", conclusion: "success" }, - { name: "Checkout repository", conclusion: "success" }, - { name: "Checkout PR branch", conclusion: "success" }, - { name: "Install GitHub Copilot CLI", conclusion: "failure" }, - ], - }, - ], - }, - }), - }; - - const result = await isCheckoutPRFailure(); - - expect(result).toBe(false); - }); - - it("should return false when agent job succeeds", async () => { - mockGithub.rest.actions = { - listJobsForWorkflowRun: vi.fn().mockResolvedValue({ - data: { - jobs: [ - { - name: "agent", - status: "completed", - conclusion: "success", - steps: [ - { name: "Checkout actions folder", conclusion: "success" }, - { name: "Checkout PR branch", conclusion: "success" }, - ], - }, - ], - }, - }), - }; - - const result = await isCheckoutPRFailure(); + await main(); - expect(result).toBe(false); + // Verify that no issue was created + expect(mockGithub.rest.issues.create).not.toHaveBeenCalled(); + expect(mockGithub.rest.issues.createComment).not.toHaveBeenCalled(); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Skipping failure handling - failure was due to PR checkout")); }); - it("should return false when agent job not found", async () => { - mockGithub.rest.actions = { - listJobsForWorkflowRun: vi.fn().mockResolvedValue({ - data: { - jobs: [ - { - name: "other-job", - status: "completed", - conclusion: "failure", - steps: [], - }, - ], - }, - }), - }; + it("should create issue when checkout_pr_success is true", async () => { + // Set the checkout PR success environment variable + process.env.GH_AW_CHECKOUT_PR_SUCCESS = "true"; - const result = await isCheckoutPRFailure(); + mockGithub.rest.search.issuesAndPullRequests + .mockResolvedValueOnce({ + // First search: PR search (no PR found) + data: { total_count: 0, items: [] }, + }) + .mockResolvedValueOnce({ + // Second search: parent issue + data: { total_count: 0, items: [] }, + }) + .mockResolvedValueOnce({ + // Third search: failure issue + data: { total_count: 0, items: [] }, + }); - expect(result).toBe(false); - expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Agent job not found")); - }); + mockGithub.rest.issues.create + .mockResolvedValueOnce({ + data: { number: 1, html_url: "https://example.com/1", node_id: "I_1" }, + }) + .mockResolvedValueOnce({ + data: { number: 2, html_url: "https://example.com/2", node_id: "I_2" }, + }); - it("should return false when checkout step not found", async () => { - mockGithub.rest.actions = { - listJobsForWorkflowRun: vi.fn().mockResolvedValue({ - data: { - jobs: [ - { - name: "agent", - status: "completed", - conclusion: "failure", - steps: [ - { name: "Checkout actions folder", conclusion: "success" }, - { name: "Other step", conclusion: "failure" }, - ], - }, - ], - }, - }), - }; + mockGithub.graphql = vi.fn().mockResolvedValue({ + addSubIssue: { + issue: { id: "I_1", number: 1 }, + subIssue: { id: "I_2", number: 2 }, + }, + }); - const result = await isCheckoutPRFailure(); + await main(); - expect(result).toBe(false); - expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Checkout PR branch step not found")); + // Verify issue was created + expect(mockGithub.rest.issues.create).toHaveBeenCalled(); }); - it("should handle API errors gracefully", async () => { - mockGithub.rest.actions = { - listJobsForWorkflowRun: vi.fn().mockRejectedValue(new Error("API Error")), - }; + it("should create issue when checkout_pr_success is not set", async () => { + // Don't set GH_AW_CHECKOUT_PR_SUCCESS (workflow without PR checkout) + delete process.env.GH_AW_CHECKOUT_PR_SUCCESS; - const result = await isCheckoutPRFailure(); + mockGithub.rest.search.issuesAndPullRequests + .mockResolvedValueOnce({ + // First search: PR search (no PR found) + data: { total_count: 0, items: [] }, + }) + .mockResolvedValueOnce({ + // Second search: parent issue + data: { total_count: 0, items: [] }, + }) + .mockResolvedValueOnce({ + // Third search: failure issue + data: { total_count: 0, items: [] }, + }); - expect(result).toBe(false); - expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Error checking for checkout failure")); - }); + mockGithub.rest.issues.create + .mockResolvedValueOnce({ + data: { number: 1, html_url: "https://example.com/1", node_id: "I_1" }, + }) + .mockResolvedValueOnce({ + data: { number: 2, html_url: "https://example.com/2", node_id: "I_2" }, + }); - it("should skip issue creation when checkout fails", async () => { - // Mock the checkout failure detection to return true - mockGithub.rest.actions = { - listJobsForWorkflowRun: vi.fn().mockResolvedValue({ - data: { - jobs: [ - { - name: "agent", - status: "completed", - conclusion: "failure", - steps: [{ name: "Checkout PR branch", conclusion: "failure" }], - }, - ], - }, - }), - }; + mockGithub.graphql = vi.fn().mockResolvedValue({ + addSubIssue: { + issue: { id: "I_1", number: 1 }, + subIssue: { id: "I_2", number: 2 }, + }, + }); await main(); - // Verify that no issue was created - expect(mockGithub.rest.issues.create).not.toHaveBeenCalled(); - expect(mockGithub.rest.issues.createComment).not.toHaveBeenCalled(); - expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Skipping failure handling - failure was due to PR checkout")); + // Verify issue was created (normal failure handling) + expect(mockGithub.rest.issues.create).toHaveBeenCalled(); }); }); }); diff --git a/pkg/workflow/compiler_activation_jobs.go b/pkg/workflow/compiler_activation_jobs.go index bd4166bb7e..ccd80bc719 100644 --- a/pkg/workflow/compiler_activation_jobs.go +++ b/pkg/workflow/compiler_activation_jobs.go @@ -734,6 +734,12 @@ func (c *Compiler) buildMainJob(data *WorkflowData, activationJobCreated bool) ( outputs["has_patch"] = "${{ steps.collect_output.outputs.has_patch }}" } + // Add checkout_pr_success output to track PR checkout status + // This is used by the conclusion job to skip failure handling when checkout fails + // (e.g., when PR is merged and branch is deleted) + outputs["checkout_pr_success"] = "${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }}" + compilerActivationJobsLog.Print("Added checkout_pr_success output") + // Build job-level environment variables for safe outputs var env map[string]string if data.SafeOutputs != nil { diff --git a/pkg/workflow/notify_comment.go b/pkg/workflow/notify_comment.go index 19e23f3c97..17a7cc44f7 100644 --- a/pkg/workflow/notify_comment.go +++ b/pkg/workflow/notify_comment.go @@ -162,6 +162,9 @@ func (c *Compiler) buildConclusionJob(data *WorkflowData, mainJobName string, sa agentFailureEnvVars = append(agentFailureEnvVars, fmt.Sprintf(" GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.%s.outputs.secret_verification_result }}\n", mainJobName)) } + // Add checkout_pr_success to detect PR checkout failures (e.g., PR merged and branch deleted) + agentFailureEnvVars = append(agentFailureEnvVars, fmt.Sprintf(" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.%s.outputs.checkout_pr_success }}\n", mainJobName)) + // Pass assignment error outputs from safe_outputs job if assign-to-agent is configured if data.SafeOutputs != nil && data.SafeOutputs.AssignToAgent != nil { agentFailureEnvVars = append(agentFailureEnvVars, " GH_AW_ASSIGNMENT_ERRORS: ${{ needs.safe_outputs.outputs.assign_to_agent_assignment_errors }}\n") diff --git a/pkg/workflow/pr.go b/pkg/workflow/pr.go index 4d18e77ce9..63a094827c 100644 --- a/pkg/workflow/pr.go +++ b/pkg/workflow/pr.go @@ -25,6 +25,7 @@ func (c *Compiler) generatePRReadyForReviewCheckout(yaml *strings.Builder, data // Always add the step with a condition that checks if PR context is available yaml.WriteString(" - name: Checkout PR branch\n") + yaml.WriteString(" id: checkout-pr\n") // Build condition that checks if github.event.pull_request exists // This will be true for pull_request events and comment events on PRs