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
16 changes: 16 additions & 0 deletions actions/setup/js/add_comment.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
217 changes: 217 additions & 0 deletions actions/setup/js/add_comment.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
10 changes: 10 additions & 0 deletions pkg/cli/templates/github-agentic-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading