diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index 1700e9c0f1..82dee15aab 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -5,8 +5,45 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); const { getFooterAgentFailureIssueMessage, getFooterAgentFailureCommentMessage, generateXMLMarker } = require("./messages.cjs"); const { renderTemplate } = require("./messages_core.cjs"); +const { getCurrentBranch } = require("./get_current_branch.cjs"); const fs = require("fs"); +/** + * Attempt to find a pull request for the current branch + * @returns {Promise<{number: number, html_url: string} | null>} PR info or null if not found + */ +async function findPullRequestForCurrentBranch() { + try { + const { owner, repo } = context.repo; + const currentBranch = getCurrentBranch(); + + core.info(`Searching for pull request from branch: ${currentBranch}`); + + // Search for open PRs with the current branch as head + const searchQuery = `repo:${owner}/${repo} is:pr is:open head:${currentBranch}`; + + const searchResult = await github.rest.search.issuesAndPullRequests({ + q: searchQuery, + per_page: 1, + }); + + if (searchResult.data.total_count > 0) { + const pr = searchResult.data.items[0]; + core.info(`Found pull request #${pr.number}: ${pr.html_url}`); + return { + number: pr.number, + html_url: pr.html_url, + }; + } + + core.info(`No pull request found for branch: ${currentBranch}`); + return null; + } catch (error) { + core.warning(`Failed to find pull request for current branch: ${getErrorMessage(error)}`); + return null; + } +} + /** * Search for or create the parent issue for all agentic workflow failures * @returns {Promise<{number: number, node_id: string}>} Parent issue number and node ID @@ -186,6 +223,9 @@ async function main() { const { owner, repo } = context.repo; + // Try to find a pull request for the current branch + const pullRequest = await findPullRequestForCurrentBranch(); + // Ensure parent issue exists first let parentIssue; try { @@ -284,8 +324,7 @@ The agentic workflow **{workflow_name}** has failed. This typically indicates a ## Failed Run - **Workflow:** [{workflow_name}]({workflow_source_url}) -- **Failed Run:** {run_url} -- **Source:** {workflow_source} +- **Failed Run:** {run_url}{pull_request_info} ## How to investigate @@ -305,8 +344,8 @@ The debug agent will help you: const templateContext = { workflow_name: sanitizedWorkflowName, run_url: runUrl, - workflow_source: sanitizeContent(workflowSource, { maxLength: 500 }), workflow_source_url: workflowSourceURL || "#", + pull_request_info: pullRequest ? `\n- **Pull Request:** [#${pullRequest.number}](${pullRequest.html_url})` : "", }; // Render the issue template diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs index 58096ad56a..8bbfaca764 100644 --- a/actions/setup/js/handle_agent_failure.test.cjs +++ b/actions/setup/js/handle_agent_failure.test.cjs @@ -72,11 +72,15 @@ describe("handle_agent_failure.cjs", () => { // Mock no existing parent issue - will create it mockGithub.rest.search.issuesAndPullRequests .mockResolvedValueOnce({ - // First search: parent issue + // First search: PR search (no PR found) data: { total_count: 0, items: [] }, }) .mockResolvedValueOnce({ - // Second search: failure issue + // Second search: parent issue + data: { total_count: 0, items: [] }, + }) + .mockResolvedValueOnce({ + // Third search: failure issue data: { total_count: 0, items: [] }, }); @@ -157,7 +161,11 @@ describe("handle_agent_failure.cjs", () => { // Mock existing parent issue mockGithub.rest.search.issuesAndPullRequests .mockResolvedValueOnce({ - // First search: existing parent issue + // First search: PR search (no PR found) + data: { total_count: 0, items: [] }, + }) + .mockResolvedValueOnce({ + // Second search: existing parent issue data: { total_count: 1, items: [ @@ -170,7 +178,7 @@ describe("handle_agent_failure.cjs", () => { }, }) .mockResolvedValueOnce({ - // Second search: no failure issue + // Third search: no failure issue data: { total_count: 0, items: [] }, }); @@ -217,9 +225,15 @@ describe("handle_agent_failure.cjs", () => { // Mock searches 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: [] }, }); @@ -248,9 +262,15 @@ describe("handle_agent_failure.cjs", () => { // Mock searches 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: [] }, }); @@ -275,12 +295,18 @@ describe("handle_agent_failure.cjs", () => { }); it("should create a new issue when no existing issue is found", async () => { - // Mock no existing issues (parent search + failure issue search) + // Mock no existing issues (PR search + parent search + failure issue search) 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: [] }, }); @@ -372,9 +398,15 @@ describe("handle_agent_failure.cjs", () => { 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: [] }, }); @@ -444,9 +476,15 @@ describe("handle_agent_failure.cjs", () => { 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: [] }, }); @@ -473,9 +511,15 @@ describe("handle_agent_failure.cjs", () => { 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: [] }, }); @@ -505,9 +549,15 @@ describe("handle_agent_failure.cjs", () => { it("should add expiration comment to new issues", async () => { 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: [] }, }); @@ -527,5 +577,79 @@ describe("handle_agent_failure.cjs", () => { expect(failureIssueCreateCall.body).toContain("/); }); + + it("should include pull request information when PR is found", async () => { + mockGithub.rest.search.issuesAndPullRequests + .mockResolvedValueOnce({ + // First search: PR search (PR found!) + data: { + total_count: 1, + items: [ + { + number: 99, + html_url: "https://github.com/test-owner/test-repo/pull/99", + }, + ], + }, + }) + .mockResolvedValueOnce({ + // Second search: parent issue + data: { total_count: 0, items: [] }, + }) + .mockResolvedValueOnce({ + // Third search: failure issue + data: { total_count: 0, items: [] }, + }); + + 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" }, + }); + + mockGithub.graphql = vi.fn().mockResolvedValue({}); + + await main(); + + const failureIssueCreateCall = mockGithub.rest.issues.create.mock.calls[1][0]; + // Verify PR information is included in the issue body + expect(failureIssueCreateCall.body).toContain("**Pull Request:**"); + expect(failureIssueCreateCall.body).toContain("#99"); + expect(failureIssueCreateCall.body).toContain("https://github.com/test-owner/test-repo/pull/99"); + }); + + it("should not include pull request information when no PR is found", async () => { + 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: [] }, + }); + + 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" }, + }); + + mockGithub.graphql = vi.fn().mockResolvedValue({}); + + await main(); + + const failureIssueCreateCall = mockGithub.rest.issues.create.mock.calls[1][0]; + // Verify PR information is NOT included in the issue body + expect(failureIssueCreateCall.body).not.toContain("**Pull Request:**"); + }); }); }); diff --git a/actions/setup/md/agent_failure_issue.md b/actions/setup/md/agent_failure_issue.md index e4022e562c..15d8b0e7e9 100644 --- a/actions/setup/md/agent_failure_issue.md +++ b/actions/setup/md/agent_failure_issue.md @@ -5,8 +5,7 @@ The agentic workflow **{workflow_name}** has failed. This typically indicates a ## Failed Run - **Workflow:** [{workflow_name}]({workflow_source_url}) -- **Failed Run:** {run_url} -- **Source:** {workflow_source} +- **Failed Run:** {run_url}{pull_request_info} ## How to investigate