Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions actions/setup/js/handle_agent_failure.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
134 changes: 129 additions & 5 deletions actions/setup/js/handle_agent_failure.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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: [] },
});

Expand Down Expand Up @@ -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: [
Expand All @@ -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: [] },
});

Expand Down Expand Up @@ -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: [] },
});

Expand Down Expand Up @@ -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: [] },
});

Expand All @@ -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: [] },
});

Expand Down Expand Up @@ -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: [] },
});

Expand Down Expand Up @@ -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: [] },
});

Expand All @@ -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: [] },
});

Expand Down Expand Up @@ -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: [] },
});

Expand All @@ -527,5 +577,79 @@ describe("handle_agent_failure.cjs", () => {
expect(failureIssueCreateCall.body).toContain("<!-- gh-aw-expires:");
expect(failureIssueCreateCall.body).toMatch(/<!-- gh-aw-expires: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z -->/);
});

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:**");
});
});
});
3 changes: 1 addition & 2 deletions actions/setup/md/agent_failure_issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading