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
32 changes: 23 additions & 9 deletions actions/setup/js/close_expired_discussions.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -180,17 +180,18 @@ 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<boolean>} 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(
`
query($dId: ID!) {
node(id: $dId) {
... on Discussion {
closed
comments(first: 100) {
nodes {
body
Expand All @@ -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 = /<!--\s*gh-aw-closed\s*-->/;
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() {
Expand Down Expand Up @@ -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`);
Expand Down
58 changes: 55 additions & 3 deletions actions/setup/js/close_expired_discussions.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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<!-- gh-aw-closed -->" }],
},
Expand Down Expand Up @@ -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 <!-- gh-aw-expires: 2020-01-20T09:20:00.000Z --> 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<!-- gh-aw-closed -->" }],
},
},
});

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");

Expand All @@ -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" }],
},
Expand Down Expand Up @@ -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: [],
},
Expand Down