From 16ec63a2ee1e7d3cc1cf4b53542b710a151c2dc5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 01:47:59 +0000 Subject: [PATCH 1/6] Initial plan From 760f1d06394bc4d899f467e629c6316f69ccf943 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 01:56:00 +0000 Subject: [PATCH 2/6] Add reports-parent-issues configuration flag - Added ReportsParentIssues field to SafeOutputsConfig - Updated parser to extract reports-parent-issues flag from frontmatter - Pass flag as GH_AW_REPORTS_PARENT_ISSUES env var to failure handler - Modified handle_agent_failure.cjs to check flag before creating parent issues - Added initial tests for the new flag - Updated existing tests that require parent issues to enable the flag Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/handle_agent_failure.cjs | 19 +- .../setup/js/handle_agent_failure.test.cjs | 215 +++++++++++++++--- pkg/workflow/compiler_types.go | 1 + pkg/workflow/notify_comment.go | 7 + pkg/workflow/safe_outputs_config.go | 8 + 5 files changed, 214 insertions(+), 36 deletions(-) diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index 75e7459ceb..9d91a51baf 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -416,13 +416,20 @@ async function main() { // Try to find a pull request for the current branch const pullRequest = await findPullRequestForCurrentBranch(); - // Ensure parent issue exists first + // Check if parent issue creation is enabled (defaults to false) + const reportsParentIssues = process.env.GH_AW_REPORTS_PARENT_ISSUES === "true"; + + // Ensure parent issue exists first (only if enabled) let parentIssue; - try { - parentIssue = await ensureParentIssue(); - } catch (error) { - core.warning(`Could not create parent issue, proceeding without parent: ${getErrorMessage(error)}`); - // Continue without parent issue + if (reportsParentIssues) { + try { + parentIssue = await ensureParentIssue(); + } catch (error) { + core.warning(`Could not create parent issue, proceeding without parent: ${getErrorMessage(error)}`); + // Continue without parent issue + } + } else { + core.info("Parent issue creation is disabled (reports-parent-issues: false)"); } // Sanitize workflow name for title diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs index 0c7784a72d..47bac0bc07 100644 --- a/actions/setup/js/handle_agent_failure.test.cjs +++ b/actions/setup/js/handle_agent_failure.test.cjs @@ -104,6 +104,9 @@ Debug this workflow failure using the \`agentic-workflows\` agent: describe("when agent job failed", () => { it("should create parent issue and link sub-issue when creating new failure issue", async () => { + // Enable parent issue creation for this test + process.env.GH_AW_REPORTS_PARENT_ISSUES = "true"; + // Mock no existing parent issue - will create it mockGithub.rest.search.issuesAndPullRequests .mockResolvedValueOnce({ @@ -197,6 +200,9 @@ Debug this workflow failure using the \`agentic-workflows\` agent: }); it("should reuse existing parent issue when it exists", async () => { + // Enable parent issue creation for this test + process.env.GH_AW_REPORTS_PARENT_ISSUES = "true"; + // Mock existing parent issue mockGithub.rest.search.issuesAndPullRequests .mockResolvedValueOnce({ @@ -274,6 +280,9 @@ Debug this workflow failure using the \`agentic-workflows\` agent: }); it("should handle sub-issue API not available gracefully", async () => { + // Enable parent issue creation for this test + process.env.GH_AW_REPORTS_PARENT_ISSUES = "true"; + // Mock searches mockGithub.rest.search.issuesAndPullRequests .mockResolvedValueOnce({ @@ -312,6 +321,9 @@ Debug this workflow failure using the \`agentic-workflows\` agent: }); it("should continue if parent issue creation fails", async () => { + // Enable parent issue creation for this test + process.env.GH_AW_REPORTS_PARENT_ISSUES = "true"; + // Mock searches mockGithub.rest.search.issuesAndPullRequests .mockResolvedValueOnce({ @@ -356,40 +368,24 @@ Debug this workflow failure using the \`agentic-workflows\` agent: }); it("should create a new issue when no existing issue is found", async () => { - // Mock no existing issues (PR search + parent search + failure issue search) + // Don't enable parent issues - test focuses on failure issue creation + // Mock no existing issues (PR 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 + // Second search: failure issue data: { total_count: 0, items: [] }, }); - mockGithub.rest.issues.create - .mockResolvedValueOnce({ - // Parent issue - data: { number: 1, html_url: "https://example.com/1", node_id: "I_1" }, - }) - .mockResolvedValueOnce({ - // Failure issue - data: { - number: 42, - html_url: "https://github.com/test-owner/test-repo/issues/42", - node_id: "I_42", - }, - }); - - // Mock GraphQL - new parent created, so just addSubIssue - mockGithub.graphql = vi.fn().mockResolvedValue({ - addSubIssue: { - issue: { id: "I_1", number: 1 }, - subIssue: { id: "I_42", number: 42 }, + mockGithub.rest.issues.create.mockResolvedValueOnce({ + // Failure issue + data: { + number: 42, + html_url: "https://github.com/test-owner/test-repo/issues/42", + node_id: "I_42", }, }); @@ -401,9 +397,8 @@ Debug this workflow failure using the \`agentic-workflows\` agent: per_page: 1, }); - // Verify failure issue was created (second call, after parent issue) - expect(mockGithub.rest.issues.create).toHaveBeenNthCalledWith( - 2, + // Verify failure issue was created + expect(mockGithub.rest.issues.create).toHaveBeenCalledWith( expect.objectContaining({ owner: "test-owner", repo: "test-repo", @@ -413,8 +408,8 @@ Debug this workflow failure using the \`agentic-workflows\` agent: }) ); - // Verify body contains required sections (check second call - failure issue) - const failureIssueCreateCall = mockGithub.rest.issues.create.mock.calls[1][0]; + // Verify body contains required sections + const failureIssueCreateCall = mockGithub.rest.issues.create.mock.calls[0][0]; expect(failureIssueCreateCall.body).toContain("### Workflow Failure"); expect(failureIssueCreateCall.body).toContain("### Action Required"); expect(failureIssueCreateCall.body).toContain("agentic-workflows"); @@ -877,6 +872,9 @@ Debug this workflow failure using the \`agentic-workflows\` agent: describe("parent issue sub-issue limit", () => { it("should create new parent issue when existing parent reaches 64 sub-issues", async () => { + // Enable parent issue creation for this test + process.env.GH_AW_REPORTS_PARENT_ISSUES = "true"; + // Mock searches: PR search, parent issue search, failure issue search mockGithub.rest.search.issuesAndPullRequests .mockResolvedValueOnce({ @@ -981,6 +979,9 @@ Debug this workflow failure using the \`agentic-workflows\` agent: }); it("should reuse parent issue when sub-issue count is below 64", async () => { + // Enable parent issue creation for this test + process.env.GH_AW_REPORTS_PARENT_ISSUES = "true"; + // Mock searches mockGithub.rest.search.issuesAndPullRequests .mockResolvedValueOnce({ @@ -1056,6 +1057,9 @@ Debug this workflow failure using the \`agentic-workflows\` agent: }); it("should continue if sub-issue count check fails", async () => { + // Enable parent issue creation for this test + process.env.GH_AW_REPORTS_PARENT_ISSUES = "true"; + // Mock searches mockGithub.rest.search.issuesAndPullRequests .mockResolvedValueOnce({ @@ -1374,4 +1378,155 @@ Debug this workflow failure using the \`agentic-workflows\` agent: expect(mockGithub.rest.issues.create).toHaveBeenCalled(); }); }); + + describe("reports-parent-issues flag", () => { + it("should not create parent issue when GH_AW_REPORTS_PARENT_ISSUES is false", async () => { + process.env.GH_AW_REPORTS_PARENT_ISSUES = "false"; + + // Initialize graphql mock (even though it shouldn't be called) + mockGithub.graphql = vi.fn(); + + // Mock PR search (no PR found) + mockGithub.rest.search.issuesAndPullRequests + .mockResolvedValueOnce({ + data: { total_count: 0, items: [] }, + }) + // Mock failure issue search (no existing issue) + .mockResolvedValueOnce({ + data: { total_count: 0, items: [] }, + }); + + // Mock failure issue creation + mockGithub.rest.issues.create.mockResolvedValueOnce({ + data: { + number: 42, + html_url: "https://github.com/test-owner/test-repo/issues/42", + node_id: "I_sub_42", + }, + }); + + await main(); + + // Verify parent issue search was NOT performed (only 2 searches: PR and failure issue) + expect(mockGithub.rest.search.issuesAndPullRequests).toHaveBeenCalledTimes(2); + + // Verify parent issue was NOT created + expect(mockGithub.rest.issues.create).toHaveBeenCalledTimes(1); + expect(mockGithub.rest.issues.create).toHaveBeenCalledWith( + expect.objectContaining({ + title: "[agentics] Test Workflow failed", + }) + ); + + // Verify GraphQL was NOT called (no parent to link) + expect(mockGithub.graphql).not.toHaveBeenCalled(); + + // Verify info message was logged + expect(mockCore.info).toHaveBeenCalledWith("Parent issue creation is disabled (reports-parent-issues: false)"); + }); + + it("should create parent issue when GH_AW_REPORTS_PARENT_ISSUES is true", async () => { + process.env.GH_AW_REPORTS_PARENT_ISSUES = "true"; + + // Mock PR search (no PR found) + mockGithub.rest.search.issuesAndPullRequests + .mockResolvedValueOnce({ + data: { total_count: 0, items: [] }, + }) + // Mock parent issue search (not found) + .mockResolvedValueOnce({ + data: { total_count: 0, items: [] }, + }) + // Mock failure issue search (not found) + .mockResolvedValueOnce({ + data: { total_count: 0, items: [] }, + }); + + // Mock parent issue creation then failure issue creation + mockGithub.rest.issues.create + .mockResolvedValueOnce({ + data: { + number: 1, + html_url: "https://github.com/test-owner/test-repo/issues/1", + node_id: "I_parent_1", + }, + }) + .mockResolvedValueOnce({ + data: { + number: 42, + html_url: "https://github.com/test-owner/test-repo/issues/42", + node_id: "I_sub_42", + }, + }); + + // Mock GraphQL sub-issue linking + mockGithub.graphql = vi.fn().mockResolvedValue({ + addSubIssue: { + issue: { id: "I_parent_1", number: 1 }, + subIssue: { id: "I_sub_42", number: 42 }, + }, + }); + + await main(); + + // Verify parent issue search was performed (3 searches: PR, parent, failure) + expect(mockGithub.rest.search.issuesAndPullRequests).toHaveBeenCalledTimes(3); + + // Verify parent issue was created + expect(mockGithub.rest.issues.create).toHaveBeenCalledWith({ + owner: "test-owner", + repo: "test-repo", + title: "[agentics] Failed runs", + body: expect.stringContaining("This issue tracks all failures from agentic workflows"), + labels: ["agentic-workflows"], + }); + + // Verify failure issue was created + expect(mockGithub.rest.issues.create).toHaveBeenCalledWith( + expect.objectContaining({ + title: "[agentics] Test Workflow failed", + }) + ); + + // Verify GraphQL was called to link sub-issue + expect(mockGithub.graphql).toHaveBeenCalledWith(expect.stringContaining("addSubIssue"), { + parentId: "I_parent_1", + subIssueId: "I_sub_42", + }); + }); + + it("should default to false when GH_AW_REPORTS_PARENT_ISSUES is not set", async () => { + // Don't set the env var - let it default + delete process.env.GH_AW_REPORTS_PARENT_ISSUES; + + // Initialize graphql mock (even though it shouldn't be called) + mockGithub.graphql = vi.fn(); + + // Mock PR search (no PR found) + mockGithub.rest.search.issuesAndPullRequests + .mockResolvedValueOnce({ + data: { total_count: 0, items: [] }, + }) + // Mock failure issue search (no existing issue) + .mockResolvedValueOnce({ + data: { total_count: 0, items: [] }, + }); + + // Mock failure issue creation + mockGithub.rest.issues.create.mockResolvedValueOnce({ + data: { + number: 42, + html_url: "https://github.com/test-owner/test-repo/issues/42", + node_id: "I_sub_42", + }, + }); + + await main(); + + // Verify parent issue was NOT created (default is false) + expect(mockGithub.rest.search.issuesAndPullRequests).toHaveBeenCalledTimes(2); + expect(mockGithub.graphql).not.toHaveBeenCalled(); + expect(mockCore.info).toHaveBeenCalledWith("Parent issue creation is disabled (reports-parent-issues: false)"); + }); + }); }); diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go index a771ca446e..54a2b84fa3 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -521,6 +521,7 @@ type SafeOutputsConfig struct { Messages *SafeOutputMessagesConfig `yaml:"messages,omitempty"` // Custom message templates for footer and notifications Mentions *MentionsConfig `yaml:"mentions,omitempty"` // Configuration for @mention filtering in safe outputs Footer *bool `yaml:"footer,omitempty"` // Global footer control - when false, omits visible footer from all safe outputs (XML markers still included) + ReportsParentIssues bool `yaml:"reports-parent-issues,omitempty"` // If true, create parent "Failed runs" issue for agent failures (default: false) } // SafeOutputMessagesConfig holds custom message templates for safe-output footer and notification messages diff --git a/pkg/workflow/notify_comment.go b/pkg/workflow/notify_comment.go index c36fad134f..8033f881f8 100644 --- a/pkg/workflow/notify_comment.go +++ b/pkg/workflow/notify_comment.go @@ -177,6 +177,13 @@ func (c *Compiler) buildConclusionJob(data *WorkflowData, mainJobName string, sa } } + // Pass reports-parent-issues configuration flag (defaults to false if not specified) + if data.SafeOutputs != nil && data.SafeOutputs.ReportsParentIssues { + agentFailureEnvVars = append(agentFailureEnvVars, " GH_AW_REPORTS_PARENT_ISSUES: \"true\"\n") + } else { + agentFailureEnvVars = append(agentFailureEnvVars, " GH_AW_REPORTS_PARENT_ISSUES: \"false\"\n") + } + // Build the agent failure handling step agentFailureSteps := c.buildGitHubScriptStepWithoutDownload(data, GitHubScriptStepConfig{ StepName: "Handle Agent Failure", diff --git a/pkg/workflow/safe_outputs_config.go b/pkg/workflow/safe_outputs_config.go index 805679f3d0..b9cdf6d23d 100644 --- a/pkg/workflow/safe_outputs_config.go +++ b/pkg/workflow/safe_outputs_config.go @@ -428,6 +428,14 @@ func (c *Compiler) extractSafeOutputsConfig(frontmatter map[string]any) *SafeOut } } + // Handle reports-parent-issues flag + if reportsParentIssues, exists := outputMap["reports-parent-issues"]; exists { + if reportsParentIssuesBool, ok := reportsParentIssues.(bool); ok { + config.ReportsParentIssues = reportsParentIssuesBool + safeOutputsConfigLog.Printf("Reports parent issues control: %t", reportsParentIssuesBool) + } + } + // Handle jobs (safe-jobs must be under safe-outputs) if jobs, exists := outputMap["jobs"]; exists { if jobsMap, ok := jobs.(map[string]any); ok { From 6aa1c8227b666c3613e3e246e84d8741db48fac3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 02:02:17 +0000 Subject: [PATCH 3/6] Add reports-parent-issues schema validation and fix test workflows - Added reports-parent-issues field to JSON schema - Updated test files to work with new default behavior - Verified compilation works with both true/false values - Schema correctly validates the new boolean field Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../setup/js/handle_agent_failure.test.cjs | 54 +++---------------- pkg/parser/schemas/main_workflow_schema.json | 6 +++ 2 files changed, 13 insertions(+), 47 deletions(-) diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs index 47bac0bc07..0c3555743d 100644 --- a/actions/setup/js/handle_agent_failure.test.cjs +++ b/actions/setup/js/handle_agent_failure.test.cjs @@ -426,27 +426,15 @@ Debug this workflow failure using the \`agentic-workflows\` agent: }); it("should add a comment to existing issue when found", async () => { - // Mock searches: PR search, parent search, and existing failure issue search + // Don't enable parent issues - test focuses on comment creation + // Mock searches: PR search and existing 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 (exists) - data: { - total_count: 1, - items: [ - { - number: 1, - html_url: "https://github.com/test-owner/test-repo/issues/1", - node_id: "I_parent_1", - }, - ], - }, - }) - .mockResolvedValueOnce({ - // Third search: existing failure issue + // Second search: existing failure issue data: { total_count: 1, items: [ @@ -458,18 +446,6 @@ Debug this workflow failure using the \`agentic-workflows\` agent: }, }); - // Mock GraphQL sub-issue count check (parent exists with < 64 sub-issues) - // When an existing failure issue is found, only the count check happens (no new issue created/linked) - mockGithub.graphql = vi.fn().mockResolvedValue({ - repository: { - issue: { - subIssues: { - totalCount: 20, - }, - }, - }, - }); - mockGithub.rest.issues.createComment.mockResolvedValue({}); await main(); @@ -501,33 +477,17 @@ Debug this workflow failure using the \`agentic-workflows\` agent: data: { total_count: 0, items: [] }, }) .mockResolvedValueOnce({ - // Second search: parent issue - data: { total_count: 0, items: [] }, - }) - .mockResolvedValueOnce({ - // Third search: failure issue + // Second 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" }, - }); - - // Mock GraphQL - new parent created, so just addSubIssue - mockGithub.graphql = vi.fn().mockResolvedValue({ - addSubIssue: { - issue: { id: "I_1", number: 1 }, - subIssue: { id: "I_2", number: 2 }, - }, + mockGithub.rest.issues.create.mockResolvedValueOnce({ + data: { number: 2, html_url: "https://example.com/2", node_id: "I_2" }, }); await main(); - const failureIssueCreateCall = mockGithub.rest.issues.create.mock.calls[1][0]; + const failureIssueCreateCall = mockGithub.rest.issues.create.mock.calls[0][0]; // Verify sanitization occurred - script tags are removed/escaped expect(failureIssueCreateCall.title).not.toContain("