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
127 changes: 9 additions & 118 deletions actions/setup/js/close_expired_discussions.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
// <reference types="@actions/github-script" />

const { getErrorMessage } = require("./error_helpers.cjs");
const { EXPIRATION_PATTERN, extractExpirationDate } = require("./ephemerals.cjs");
const { extractExpirationDate } = require("./ephemerals.cjs");
const { searchEntitiesWithExpiration } = require("./expired_entity_search_helpers.cjs");

/**
* Maximum number of discussions to update per run
Expand All @@ -23,121 +24,6 @@ function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

/**
* Search for open discussions with expiration markers
* @param {any} github - GitHub GraphQL instance
* @param {string} owner - Repository owner
* @param {string} repo - Repository name
* @returns {Promise<{discussions: Array<{id: string, number: number, title: string, url: string, body: string, createdAt: string}>, stats: {pageCount: number, totalScanned: number, duplicateCount: number}}>}
*/
async function searchDiscussionsWithExpiration(github, owner, repo) {
const discussions = [];
const seenDiscussionIds = new Set(); // Track IDs to avoid duplicates
let hasNextPage = true;
let cursor = null;
let pageCount = 0;
let totalScanned = 0;
let duplicateCount = 0;

core.info(`Starting GraphQL search for open discussions in ${owner}/${repo}`);

while (hasNextPage) {
pageCount++;
core.info(`Fetching page ${pageCount} of open discussions (cursor: ${cursor || "initial"})`);

const query = `
query($owner: String!, $repo: String!, $cursor: String) {
repository(owner: $owner, name: $repo) {
discussions(first: 100, after: $cursor, states: [OPEN]) {
pageInfo {
hasNextPage
endCursor
}
nodes {
id
number
title
url
body
createdAt
}
}
}
}
`;

const result = await github.graphql(query, {
owner: owner,
repo: repo,
cursor: cursor,
});

if (!result || !result.repository || !result.repository.discussions) {
core.warning(`GraphQL query returned no data at page ${pageCount}`);
break;
}

const nodes = result.repository.discussions.nodes || [];
totalScanned += nodes.length;
core.info(`Page ${pageCount}: Retrieved ${nodes.length} open discussions (total scanned: ${totalScanned})`);

let agenticCount = 0;
let withExpirationCount = 0;

// Filter for discussions with agentic workflow markers and expiration comments
for (const discussion of nodes) {
// Check if created by an agentic workflow (body contains "> AI generated by" at start of line)
const agenticPattern = /^> AI generated by/m;
const isAgenticWorkflow = discussion.body && agenticPattern.test(discussion.body);

if (isAgenticWorkflow) {
agenticCount++;
}

if (!isAgenticWorkflow) {
continue;
}

// Check if has expiration marker with checked checkbox
const match = discussion.body ? discussion.body.match(EXPIRATION_PATTERN) : null;

if (match) {
withExpirationCount++;

// Deduplicate: check if we've already seen this discussion
if (seenDiscussionIds.has(discussion.id)) {
core.warning(` Skipping duplicate discussion #${discussion.number} (ID: ${discussion.id}) - already seen in previous page`);
duplicateCount++;
continue;
}

seenDiscussionIds.add(discussion.id);
core.info(` Found discussion #${discussion.number} with expiration marker: "${match[1]}" - ${discussion.title}`);
discussions.push(discussion);
}
}

core.info(`Page ${pageCount} summary: ${agenticCount} agentic discussions, ${withExpirationCount} with expiration markers`);

hasNextPage = result.repository.discussions.pageInfo.hasNextPage;
cursor = result.repository.discussions.pageInfo.endCursor;
}

if (duplicateCount > 0) {
core.warning(`Found and skipped ${duplicateCount} duplicate discussion(s) across pages`);
}

core.info(`Search complete: Scanned ${totalScanned} discussions across ${pageCount} pages, found ${discussions.length} unique with expiration markers`);
return {
discussions,
stats: {
pageCount,
totalScanned,
duplicateCount,
},
};
}

/**
* Validate discussion creation date
* @param {string} createdAt - ISO 8601 creation date
Expand Down Expand Up @@ -237,8 +123,13 @@ async function main() {

core.info(`Searching for expired discussions in ${owner}/${repo}`);

// Search for discussions with expiration markers
const { discussions: discussionsWithExpiration, stats: searchStats } = await searchDiscussionsWithExpiration(github, owner, repo);
// Search for discussions with expiration markers (enable dedupe for discussions)
const { items: discussionsWithExpiration, stats: searchStats } = await searchEntitiesWithExpiration(github, owner, repo, {
entityType: "discussions",
graphqlField: "discussions",
resultKey: "discussions",
enableDedupe: true, // Discussions may have duplicates across pages
});

if (discussionsWithExpiration.length === 0) {
core.info("No discussions with expiration markers found");
Expand Down
108 changes: 7 additions & 101 deletions actions/setup/js/close_expired_issues.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
// <reference types="@actions/github-script" />

const { getErrorMessage } = require("./error_helpers.cjs");
const { EXPIRATION_PATTERN, extractExpirationDate } = require("./ephemerals.cjs");
const { extractExpirationDate } = require("./ephemerals.cjs");
const { searchEntitiesWithExpiration } = require("./expired_entity_search_helpers.cjs");

/**
* Maximum number of issues to update per run
Expand All @@ -23,105 +24,6 @@ function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

/**
* Search for open issues with expiration markers
* @param {any} github - GitHub GraphQL instance
* @param {string} owner - Repository owner
* @param {string} repo - Repository name
* @returns {Promise<{issues: Array<{id: string, number: number, title: string, url: string, body: string, createdAt: string}>, stats: {pageCount: number, totalScanned: number}}>}
*/
async function searchIssuesWithExpiration(github, owner, repo) {
const issues = [];
let hasNextPage = true;
let cursor = null;
let pageCount = 0;
let totalScanned = 0;

core.info(`Starting GraphQL search for open issues in ${owner}/${repo}`);

while (hasNextPage) {
pageCount++;
core.info(`Fetching page ${pageCount} of open issues (cursor: ${cursor || "initial"})`);

const query = `
query($owner: String!, $repo: String!, $cursor: String) {
repository(owner: $owner, name: $repo) {
issues(first: 100, after: $cursor, states: [OPEN]) {
pageInfo {
hasNextPage
endCursor
}
nodes {
id
number
title
url
body
createdAt
}
}
}
}
`;

const result = await github.graphql(query, {
owner: owner,
repo: repo,
cursor: cursor,
});

if (!result || !result.repository || !result.repository.issues) {
core.warning(`GraphQL query returned no data at page ${pageCount}`);
break;
}

const nodes = result.repository.issues.nodes || [];
totalScanned += nodes.length;
core.info(`Page ${pageCount}: Retrieved ${nodes.length} open issues (total scanned: ${totalScanned})`);

let agenticCount = 0;
let withExpirationCount = 0;

// Filter for issues with agentic workflow markers and expiration comments
for (const issue of nodes) {
// Check if created by an agentic workflow (body contains "> AI generated by" at start of line)
const agenticPattern = /^> AI generated by/m;
const isAgenticWorkflow = issue.body && agenticPattern.test(issue.body);

if (isAgenticWorkflow) {
agenticCount++;
}

if (!isAgenticWorkflow) {
continue;
}

// Check if has expiration marker with checked checkbox
const match = issue.body ? issue.body.match(EXPIRATION_PATTERN) : null;

if (match) {
withExpirationCount++;
core.info(` Found issue #${issue.number} with expiration marker: "${match[1]}" - ${issue.title}`);
issues.push(issue);
}
}

core.info(`Page ${pageCount} summary: ${agenticCount} agentic issues, ${withExpirationCount} with expiration markers`);

hasNextPage = result.repository.issues.pageInfo.hasNextPage;
cursor = result.repository.issues.pageInfo.endCursor;
}

core.info(`Search complete: Scanned ${totalScanned} issues across ${pageCount} pages, found ${issues.length} with expiration markers`);
return {
issues,
stats: {
pageCount,
totalScanned,
},
};
}

/**
* Validate issue creation date
* @param {string} createdAt - ISO 8601 creation date
Expand Down Expand Up @@ -179,7 +81,11 @@ async function main() {
core.info(`Searching for expired issues in ${owner}/${repo}`);

// Search for issues with expiration markers
const { issues: issuesWithExpiration, stats: searchStats } = await searchIssuesWithExpiration(github, owner, repo);
const { items: issuesWithExpiration, stats: searchStats } = await searchEntitiesWithExpiration(github, owner, repo, {
entityType: "issues",
graphqlField: "issues",
resultKey: "issues",
});

if (issuesWithExpiration.length === 0) {
core.info("No issues with expiration markers found");
Expand Down
Loading
Loading