Skip to content

Commit 720732e

Browse files
github-actions[bot]Claude (Cloclo)claude
authored
fix: Handle locked issue/PR reactions gracefully (#15967)
This commit fixes the add_reaction handlers to silently ignore failures when attempting to add reactions to locked issues, PRs, or discussions. Changes: - Modified add_reaction.cjs to detect locked resource errors (403 with "locked" message) - Modified add_reaction_and_edit_comment.cjs with the same fix - Added tests for locked issue scenarios in both test files - Tests verify that locked errors are logged but don't fail the workflow Fixes #15966 Co-authored-by: Claude (Cloclo) <cloclo@github.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 1c4b3c7 commit 720732e

4 files changed

Lines changed: 122 additions & 0 deletions

File tree

actions/setup/js/add_reaction.cjs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,23 @@ async function main() {
9797
await addReaction(reactionEndpoint, reaction);
9898
} catch (error) {
9999
const errorMessage = getErrorMessage(error);
100+
101+
// Check if the error is due to a locked issue/PR/discussion
102+
// GitHub API returns 403 with specific messages for locked resources
103+
const is403Error = error && typeof error === "object" && "status" in error && error.status === 403;
104+
const hasLockedMessage = errorMessage && (
105+
errorMessage.includes("locked") ||
106+
errorMessage.includes("Lock conversation")
107+
);
108+
109+
// Only ignore the error if it's a 403 AND mentions locked, or if the message mentions locked
110+
if ((is403Error && hasLockedMessage) || (!is403Error && hasLockedMessage)) {
111+
// Silently ignore locked resource errors - just log for debugging
112+
core.info(`Cannot add reaction: resource is locked (this is expected and not an error)`);
113+
return;
114+
}
115+
116+
// For other errors, fail as before
100117
core.error(`Failed to add reaction: ${errorMessage}`);
101118
core.setFailed(`Failed to add reaction: ${errorMessage}`);
102119
}

actions/setup/js/add_reaction.test.cjs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,50 @@ describe("add_reaction", () => {
389389
expect(mockCore.error).toHaveBeenCalled();
390390
expect(mockCore.setFailed).toHaveBeenCalled();
391391
});
392+
393+
it("should silently ignore locked issue errors (status 403)", async () => {
394+
const lockedError = new Error("Issue is locked");
395+
lockedError.status = 403;
396+
mockGithub.request.mockRejectedValueOnce(lockedError);
397+
398+
await runScript();
399+
400+
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("resource is locked"));
401+
expect(mockCore.error).not.toHaveBeenCalled();
402+
expect(mockCore.setFailed).not.toHaveBeenCalled();
403+
});
404+
405+
it("should silently ignore locked issue errors (message contains 'locked')", async () => {
406+
mockGithub.request.mockRejectedValueOnce(new Error("Lock conversation is enabled"));
407+
408+
await runScript();
409+
410+
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("resource is locked"));
411+
expect(mockCore.error).not.toHaveBeenCalled();
412+
expect(mockCore.setFailed).not.toHaveBeenCalled();
413+
});
414+
415+
it("should fail for 403 errors that don't mention locked", async () => {
416+
const forbiddenError = new Error("Forbidden: insufficient permissions");
417+
forbiddenError.status = 403;
418+
mockGithub.request.mockRejectedValueOnce(forbiddenError);
419+
420+
await runScript();
421+
422+
expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Failed to add reaction"));
423+
expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("Failed to add reaction"));
424+
});
425+
426+
it("should fail for other non-403 errors", async () => {
427+
const serverError = new Error("Internal server error");
428+
serverError.status = 500;
429+
mockGithub.request.mockRejectedValueOnce(serverError);
430+
431+
await runScript();
432+
433+
expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Failed to add reaction"));
434+
expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("Failed to add reaction"));
435+
});
392436
});
393437

394438
describe("output handling", () => {

actions/setup/js/add_reaction_and_edit_comment.cjs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,23 @@ async function main() {
155155
}
156156
} catch (error) {
157157
const errorMessage = getErrorMessage(error);
158+
159+
// Check if the error is due to a locked issue/PR/discussion
160+
// GitHub API returns 403 with specific messages for locked resources
161+
const is403Error = error && typeof error === "object" && "status" in error && error.status === 403;
162+
const hasLockedMessage = errorMessage && (
163+
errorMessage.includes("locked") ||
164+
errorMessage.includes("Lock conversation")
165+
);
166+
167+
// Only ignore the error if it's a 403 AND mentions locked, or if the message mentions locked
168+
if ((is403Error && hasLockedMessage) || (!is403Error && hasLockedMessage)) {
169+
// Silently ignore locked resource errors - just log for debugging
170+
core.info(`Cannot add reaction: resource is locked (this is expected and not an error)`);
171+
return;
172+
}
173+
174+
// For other errors, fail as before
158175
core.error(`Failed to process reaction and comment creation: ${errorMessage}`);
159176
core.setFailed(`Failed to process reaction and comment creation: ${errorMessage}`);
160177
}

actions/setup/js/add_reaction_and_edit_comment.test.cjs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,50 @@ const mockCore = {
235235
(global.context.payload = { repository: { html_url: "https://github.com/testowner/testrepo" } }),
236236
await eval(`(async () => { ${reactionScript}; await main(); })()`),
237237
expect(mockCore.setFailed).toHaveBeenCalledWith("Unsupported event type: push"));
238+
}),
239+
it("should silently ignore locked issue errors (status 403)", async () => {
240+
const lockedError = new Error("Issue is locked");
241+
lockedError.status = 403;
242+
((process.env.GH_AW_REACTION = "eyes"),
243+
(global.context.eventName = "issues"),
244+
(global.context.payload = { issue: { number: 123 }, repository: { html_url: "https://github.com/testowner/testrepo" } }),
245+
mockGithub.request.mockRejectedValueOnce(lockedError),
246+
await eval(`(async () => { ${reactionScript}; await main(); })()`),
247+
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("resource is locked")),
248+
expect(mockCore.error).not.toHaveBeenCalled(),
249+
expect(mockCore.setFailed).not.toHaveBeenCalled());
250+
}),
251+
it("should silently ignore locked issue errors (message contains 'locked')", async () => {
252+
((process.env.GH_AW_REACTION = "eyes"),
253+
(global.context.eventName = "issues"),
254+
(global.context.payload = { issue: { number: 123 }, repository: { html_url: "https://github.com/testowner/testrepo" } }),
255+
mockGithub.request.mockRejectedValueOnce(new Error("Lock conversation is enabled")),
256+
await eval(`(async () => { ${reactionScript}; await main(); })()`),
257+
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("resource is locked")),
258+
expect(mockCore.error).not.toHaveBeenCalled(),
259+
expect(mockCore.setFailed).not.toHaveBeenCalled());
260+
}),
261+
it("should fail for 403 errors that don't mention locked", async () => {
262+
const forbiddenError = new Error("Forbidden: insufficient permissions");
263+
forbiddenError.status = 403;
264+
((process.env.GH_AW_REACTION = "eyes"),
265+
(global.context.eventName = "issues"),
266+
(global.context.payload = { issue: { number: 123 }, repository: { html_url: "https://github.com/testowner/testrepo" } }),
267+
mockGithub.request.mockRejectedValueOnce(forbiddenError),
268+
await eval(`(async () => { ${reactionScript}; await main(); })()`),
269+
expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Failed to process reaction")),
270+
expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("Failed to process reaction")));
271+
}),
272+
it("should fail for other non-403 errors", async () => {
273+
const serverError = new Error("Internal server error");
274+
serverError.status = 500;
275+
((process.env.GH_AW_REACTION = "eyes"),
276+
(global.context.eventName = "issues"),
277+
(global.context.payload = { issue: { number: 123 }, repository: { html_url: "https://github.com/testowner/testrepo" } }),
278+
mockGithub.request.mockRejectedValueOnce(serverError),
279+
await eval(`(async () => { ${reactionScript}; await main(); })()`),
280+
expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Failed to process reaction")),
281+
expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("Failed to process reaction")));
238282
}));
239283
}));
240284
}));

0 commit comments

Comments
 (0)