From d9699c5da4ea0e6ec00c5531c5f698d74c471ac4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 22:55:11 +0000 Subject: [PATCH 1/3] Initial plan From 7b55fd8f71519572050590f697148107ef4265af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 23:00:07 +0000 Subject: [PATCH 2/3] Initial planning for add_comment 404 handling Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/templates/github-agentic-workflows.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pkg/cli/templates/github-agentic-workflows.md b/pkg/cli/templates/github-agentic-workflows.md index 9250716025..0724608609 100644 --- a/pkg/cli/templates/github-agentic-workflows.md +++ b/pkg/cli/templates/github-agentic-workflows.md @@ -465,6 +465,16 @@ The YAML frontmatter supports these fields: target-repo: "owner/repo" # Optional: cross-repository ``` When using `safe-outputs.add-labels`, the main job does **not** need `issues: write` or `pull-requests: write` permission since label addition is handled by a separate job with appropriate permissions. + - `remove-labels:` - Safe label removal from issues or PRs + ```yaml + safe-outputs: + remove-labels: + allowed: [automated, stale] # Optional: restrict to specific labels + max: 3 # Optional: maximum number of operations (default: 3) + target: "*" # Optional: "triggering" (default), "*" (any issue/PR), or number + target-repo: "owner/repo" # Optional: cross-repository + ``` + When `allowed` is omitted, any labels can be removed. Use `allowed` to restrict removal to specific labels. When using `safe-outputs.remove-labels`, the main job does **not** need `issues: write` or `pull-requests: write` permission since label removal is handled by a separate job with appropriate permissions. - `add-reviewer:` - Add reviewers to pull requests ```yaml safe-outputs: From 9fd9ac80b5893f4155e8a3256e31adb856cad7f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 23:02:33 +0000 Subject: [PATCH 3/3] Treat add_comment 404s as warnings in safe output handler Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/add_comment.cjs | 16 ++ actions/setup/js/add_comment.test.cjs | 217 ++++++++++++++++++++++++++ 2 files changed, 233 insertions(+) diff --git a/actions/setup/js/add_comment.cjs b/actions/setup/js/add_comment.cjs index 1ed4a3c194..d3a22f51d3 100644 --- a/actions/setup/js/add_comment.cjs +++ b/actions/setup/js/add_comment.cjs @@ -498,6 +498,22 @@ async function main(config = {}) { }; } catch (error) { const errorMessage = getErrorMessage(error); + + // Check if this is a 404 error (discussion/issue was deleted) + // @ts-expect-error - Error handling with optional chaining + const is404 = error?.status === 404 || errorMessage.includes("404") || errorMessage.toLowerCase().includes("not found"); + + if (is404) { + // Treat 404s as warnings - the target was deleted between execution and safe output processing + core.warning(`Target was not found (may have been deleted): ${errorMessage}`); + return { + success: true, + warning: `Target not found: ${errorMessage}`, + skipped: true, + }; + } + + // For non-404 errors, fail as before core.error(`Failed to add comment: ${errorMessage}`); return { success: false, diff --git a/actions/setup/js/add_comment.test.cjs b/actions/setup/js/add_comment.test.cjs index f5a9b7beb6..360dba456e 100644 --- a/actions/setup/js/add_comment.test.cjs +++ b/actions/setup/js/add_comment.test.cjs @@ -528,4 +528,221 @@ describe("add_comment", () => { delete process.env.GITHUB_WORKFLOW; }); }); + + describe("404 error handling", () => { + it("should treat 404 errors as warnings for issue comments", async () => { + const addCommentScript = fs.readFileSync(path.join(__dirname, "add_comment.cjs"), "utf8"); + + let warningCalls = []; + mockCore.warning = msg => { + warningCalls.push(msg); + }; + + let errorCalls = []; + mockCore.error = msg => { + errorCalls.push(msg); + }; + + // Mock API to throw 404 error + mockGithub.rest.issues.createComment = async () => { + const error = new Error("Not Found"); + // @ts-ignore + error.status = 404; + throw error; + }; + + const handler = await eval(`(async () => { ${addCommentScript}; return await main({}); })()`); + + const message = { + type: "add_comment", + body: "Test comment", + }; + + const result = await handler(message, {}); + + expect(result.success).toBe(true); + expect(result.warning).toBeTruthy(); + expect(result.warning).toContain("not found"); + expect(result.skipped).toBe(true); + expect(warningCalls.length).toBeGreaterThan(0); + expect(warningCalls[0]).toContain("not found"); + expect(errorCalls.length).toBe(0); + }); + + it("should treat 404 errors as warnings for discussion comments", async () => { + const addCommentScript = fs.readFileSync(path.join(__dirname, "add_comment.cjs"), "utf8"); + + let warningCalls = []; + mockCore.warning = msg => { + warningCalls.push(msg); + }; + + let errorCalls = []; + mockCore.error = msg => { + errorCalls.push(msg); + }; + + // Change context to discussion + mockContext.eventName = "discussion"; + mockContext.payload = { + discussion: { + number: 10, + }, + }; + + // Mock API to throw 404 error when querying discussion + mockGithub.graphql = async (query, variables) => { + if (query.includes("discussion(number")) { + // Return null to trigger the "not found" error + return { + repository: { + discussion: null, // Discussion not found + }, + }; + } + throw new Error("Unexpected query"); + }; + + const handler = await eval(`(async () => { ${addCommentScript}; return await main({}); })()`); + + const message = { + type: "add_comment", + body: "Test comment on deleted discussion", + }; + + const result = await handler(message, {}); + + // The error message contains "not found" so it should be treated as a warning + expect(result.success).toBe(true); + expect(result.warning).toBeTruthy(); + expect(result.warning).toContain("not found"); + expect(result.skipped).toBe(true); + expect(warningCalls.length).toBeGreaterThan(0); + expect(errorCalls.length).toBe(0); + }); + + it("should detect 404 from error message containing '404'", async () => { + const addCommentScript = fs.readFileSync(path.join(__dirname, "add_comment.cjs"), "utf8"); + + let warningCalls = []; + mockCore.warning = msg => { + warningCalls.push(msg); + }; + + // Mock API to throw error with 404 in message + mockGithub.rest.issues.createComment = async () => { + throw new Error("API request failed with status 404"); + }; + + const handler = await eval(`(async () => { ${addCommentScript}; return await main({}); })()`); + + const message = { + type: "add_comment", + body: "Test comment", + }; + + const result = await handler(message, {}); + + expect(result.success).toBe(true); + expect(result.warning).toBeTruthy(); + expect(result.skipped).toBe(true); + expect(warningCalls.length).toBeGreaterThan(0); + }); + + it("should detect 404 from error message containing 'Not Found'", async () => { + const addCommentScript = fs.readFileSync(path.join(__dirname, "add_comment.cjs"), "utf8"); + + let warningCalls = []; + mockCore.warning = msg => { + warningCalls.push(msg); + }; + + // Mock API to throw error with "Not Found" in message + mockGithub.rest.issues.createComment = async () => { + throw new Error("Resource Not Found"); + }; + + const handler = await eval(`(async () => { ${addCommentScript}; return await main({}); })()`); + + const message = { + type: "add_comment", + body: "Test comment", + }; + + const result = await handler(message, {}); + + expect(result.success).toBe(true); + expect(result.warning).toBeTruthy(); + expect(result.skipped).toBe(true); + expect(warningCalls.length).toBeGreaterThan(0); + }); + + it("should still fail for non-404 errors", async () => { + const addCommentScript = fs.readFileSync(path.join(__dirname, "add_comment.cjs"), "utf8"); + + let warningCalls = []; + mockCore.warning = msg => { + warningCalls.push(msg); + }; + + let errorCalls = []; + mockCore.error = msg => { + errorCalls.push(msg); + }; + + // Mock API to throw non-404 error + mockGithub.rest.issues.createComment = async () => { + const error = new Error("Forbidden"); + // @ts-ignore + error.status = 403; + throw error; + }; + + const handler = await eval(`(async () => { ${addCommentScript}; return await main({}); })()`); + + const message = { + type: "add_comment", + body: "Test comment", + }; + + const result = await handler(message, {}); + + expect(result.success).toBe(false); + expect(result.error).toBeTruthy(); + expect(result.error).toContain("Forbidden"); + expect(errorCalls.length).toBeGreaterThan(0); + expect(errorCalls[0]).toContain("Failed to add comment"); + }); + + it("should still fail for validation errors", async () => { + const addCommentScript = fs.readFileSync(path.join(__dirname, "add_comment.cjs"), "utf8"); + + let errorCalls = []; + mockCore.error = msg => { + errorCalls.push(msg); + }; + + // Mock API to throw validation error + mockGithub.rest.issues.createComment = async () => { + const error = new Error("Validation Failed"); + // @ts-ignore + error.status = 422; + throw error; + }; + + const handler = await eval(`(async () => { ${addCommentScript}; return await main({}); })()`); + + const message = { + type: "add_comment", + body: "Test comment", + }; + + const result = await handler(message, {}); + + expect(result.success).toBe(false); + expect(result.error).toBeTruthy(); + expect(result.error).toContain("Validation Failed"); + expect(errorCalls.length).toBeGreaterThan(0); + }); + }); });