-
Notifications
You must be signed in to change notification settings - Fork 46
Closed
Description
🔍 Duplicate Code Detected: Expired Entity Cleanup Logic
Analysis of commit 1659ea6
Assignee: @copilot
Summary
The expired-entity cleanup scripts for issues, pull requests, and discussions duplicate the same search/filter loop, expiration matching, and pagination logic. The three files are structurally identical in large sections (>70 lines each) with only entity-specific fields and API endpoints differing.
Duplication Details
Pattern: Expired entity GraphQL search + filter loop
- Severity: Medium
- Occurrences: 3
- Locations:
actions/setup/js/close_expired_issues.cjs(lines 22-123)actions/setup/js/close_expired_pull_requests.cjs(lines 22-123)actions/setup/js/close_expired_discussions.cjs(lines 22-139)
- Code Sample:
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
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, repo, 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;
let agenticCount = 0;
let withExpirationCount = 0;
for (const issue of nodes) {
const agenticPattern = /^> AI generated by/m;
const isAgenticWorkflow = issue.body && agenticPattern.test(issue.body);
if (!isAgenticWorkflow) continue;
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 } };
}Impact Analysis
- Maintainability: Changes to pagination, filtering rules, or logging must be replicated across three files.
- Bug Risk: Divergence is already visible (discussions adds duplicate ID handling). Future changes could accidentally drift.
- Code Bloat: ~70+ lines repeated per file, increasing review surface area.
Refactoring Recommendations
-
Extract shared pagination + filtering helper
- Extract a common helper in
actions/setup/js/expiration_helpers.cjs(or a newexpired_entity_search.cjs) that accepts entity type metadata (query fragment, node mapping, label/ID fields). - Estimated effort: medium (2-4 hours)
- Benefits: single place for pagination and agentic + expiration matching.
- Extract a common helper in
-
Normalize logging + stats structure
- Return a normalized
{items, stats}shape, with optional dedupe callback for discussions. - Estimated effort: low (1-2 hours)
- Benefits: reduces drift and makes future additions (e.g., projects) straightforward.
- Return a normalized
Implementation Checklist
- Review duplication findings
- Prioritize refactoring tasks
- Create refactoring plan
- Implement changes
- Update tests
- Verify no functionality broken
Analysis Metadata
- Analyzed Files: 3
- Detection Method: Serena semantic code analysis + targeted diff review
- Commit: 1659ea6
- Analysis Date: 2026-01-30
AI generated by Duplicate Code Detector
Copilot