From 929c358c37c598473db4d0280ae75575e4ac7cb9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 17:22:27 +0000 Subject: [PATCH 1/2] Initial plan From c432ff8bd53fd248da008fb0782459607b543b90 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 17:32:04 +0000 Subject: [PATCH 2/2] Fix: prevent adding comments to already-closed discussions Modified hasExpirationComment to also fetch and return the discussion's closed state. Added logic to skip processing discussions that are already closed before attempting to close them. Added test case to verify closed discussions are properly skipped. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../setup/js/close_expired_discussions.cjs | 32 +++++++--- .../js/close_expired_discussions.test.cjs | 58 ++++++++++++++++++- 2 files changed, 78 insertions(+), 12 deletions(-) diff --git a/actions/setup/js/close_expired_discussions.cjs b/actions/setup/js/close_expired_discussions.cjs index 4e77937ed4..f41d62a0c8 100644 --- a/actions/setup/js/close_expired_discussions.cjs +++ b/actions/setup/js/close_expired_discussions.cjs @@ -180,10 +180,10 @@ async function closeDiscussionAsOutdated(github, discussionId) { } /** - * Check if a discussion already has an expiration comment + * Check if a discussion already has an expiration comment and fetch its closed state * @param {any} github - GitHub GraphQL instance * @param {string} discussionId - Discussion node ID - * @returns {Promise} True if expiration comment exists + * @returns {Promise<{hasComment: boolean, isClosed: boolean}>} Object with comment existence and closed state */ async function hasExpirationComment(github, discussionId) { const result = await github.graphql( @@ -191,6 +191,7 @@ async function hasExpirationComment(github, discussionId) { query($dId: ID!) { node(id: $dId) { ... on Discussion { + closed comments(first: 100) { nodes { body @@ -202,14 +203,16 @@ async function hasExpirationComment(github, discussionId) { { dId: discussionId } ); - if (!result || !result.node || !result.node.comments) { - return false; + if (!result || !result.node) { + return { hasComment: false, isClosed: false }; } - const comments = result.node.comments.nodes || []; + const isClosed = result.node.closed || false; + const comments = result.node.comments?.nodes || []; const expirationCommentPattern = //; + const hasComment = comments.some(comment => comment.body && expirationCommentPattern.test(comment.body)); - return comments.some(comment => comment.body && expirationCommentPattern.test(comment.body)); + return { hasComment, isClosed }; } async function main() { @@ -323,9 +326,20 @@ async function main() { core.info(`[${i + 1}/${discussionsToClose.length}] Processing discussion #${discussion.number}: ${discussion.url}`); try { - // Check if an expiration comment already exists - core.info(` Checking for existing expiration comment on discussion #${discussion.number}`); - const hasComment = await hasExpirationComment(github, discussion.id); + // Check if an expiration comment already exists and if discussion is closed + core.info(` Checking for existing expiration comment and closed state on discussion #${discussion.number}`); + const { hasComment, isClosed } = await hasExpirationComment(github, discussion.id); + + if (isClosed) { + core.warning(` Discussion #${discussion.number} is already closed, skipping`); + skippedDiscussions.push({ + number: discussion.number, + url: discussion.url, + title: discussion.title, + }); + skippedCount++; + continue; + } if (hasComment) { core.warning(` Discussion #${discussion.number} already has an expiration comment, skipping to avoid duplicate`); diff --git a/actions/setup/js/close_expired_discussions.test.cjs b/actions/setup/js/close_expired_discussions.test.cjs index 8d2d65beb0..3534b0f5a9 100644 --- a/actions/setup/js/close_expired_discussions.test.cjs +++ b/actions/setup/js/close_expired_discussions.test.cjs @@ -93,9 +93,10 @@ describe("close_expired_discussions", () => { }, }, }) - // Second call: hasExpirationComment - returns true (comment exists) + // Second call: hasExpirationComment - returns true (comment exists) and not closed .mockResolvedValueOnce({ node: { + closed: false, comments: { nodes: [{ body: "This discussion was automatically closed because it expired on 2020-01-20T09:20:00.000Z.\n\n" }], }, @@ -123,6 +124,55 @@ describe("close_expired_discussions", () => { expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Attempting to close discussion")); }); + it("should skip already closed discussions entirely", async () => { + const module = await import("./close_expired_discussions.cjs"); + + // Mock the search query to return an open discussion with expiration marker + mockGithub.graphql + // First call: searchDiscussionsWithExpiration + .mockResolvedValueOnce({ + repository: { + discussions: { + pageInfo: { + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + id: "D_closed123", + number: 11060, + title: "Already Closed Discussion", + url: "https://github.com/testowner/testrepo/discussions/11060", + body: "> AI generated by Test Workflow\n>\n> - [x] expires on Jan 20, 2020, 9:20 AM UTC", + createdAt: "2020-01-19T09:20:00.000Z", + }, + ], + }, + }, + }) + // Second call: hasExpirationComment - returns discussion is closed + .mockResolvedValueOnce({ + node: { + closed: true, + comments: { + nodes: [{ body: "This discussion was automatically closed because it expired on 2020-01-20T09:20:00.000Z.\n\n" }], + }, + }, + }); + + await module.main(); + + // Verify that we only made 2 graphql calls (search and check closed state) + // Should NOT attempt to close or add comment + expect(mockGithub.graphql).toHaveBeenCalledTimes(2); + + // Verify that we detected the discussion was already closed + expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("is already closed")); + + // Verify that we did NOT try to close the discussion + expect(mockCore.info).not.toHaveBeenCalledWith(expect.stringContaining("Attempting to close discussion")); + }); + it("should add comment if none exists", async () => { const module = await import("./close_expired_discussions.cjs"); @@ -149,9 +199,10 @@ describe("close_expired_discussions", () => { }, }, }) - // Second call: hasExpirationComment - returns false (no comment exists) + // Second call: hasExpirationComment - returns false (no comment exists) and not closed .mockResolvedValueOnce({ node: { + closed: false, comments: { nodes: [{ body: "Some unrelated comment" }], }, @@ -213,9 +264,10 @@ describe("close_expired_discussions", () => { }, }, }) - // Second call: hasExpirationComment - empty comments + // Second call: hasExpirationComment - empty comments and not closed .mockResolvedValueOnce({ node: { + closed: false, comments: { nodes: [], },