|
|
|
|
@@ -10,6 +10,7 @@ interface GitHubIssue {
|
|
|
|
|
number: number;
|
|
|
|
|
title: string;
|
|
|
|
|
user: { id: number };
|
|
|
|
|
created_at: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface GitHubComment {
|
|
|
|
|
@@ -24,13 +25,16 @@ interface GitHubReaction {
|
|
|
|
|
content: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function githubRequest<T>(endpoint: string, token: string): Promise<T> {
|
|
|
|
|
async function githubRequest<T>(endpoint: string, token: string, method: string = 'GET', body?: any): Promise<T> {
|
|
|
|
|
const response = await fetch(`https://api.github.com${endpoint}`, {
|
|
|
|
|
method,
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${token}`,
|
|
|
|
|
Accept: "application/vnd.github.v3+json",
|
|
|
|
|
"User-Agent": "auto-close-duplicates-script",
|
|
|
|
|
...(body && { "Content-Type": "application/json" }),
|
|
|
|
|
},
|
|
|
|
|
...(body && { body: JSON.stringify(body) }),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
@@ -42,6 +46,42 @@ async function githubRequest<T>(endpoint: string, token: string): Promise<T> {
|
|
|
|
|
return response.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function extractDuplicateIssueNumber(commentBody: string): number | null {
|
|
|
|
|
const match = commentBody.match(/#(\d+)/);
|
|
|
|
|
return match ? parseInt(match[1], 10) : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function closeIssueAsDuplicate(
|
|
|
|
|
owner: string,
|
|
|
|
|
repo: string,
|
|
|
|
|
issueNumber: number,
|
|
|
|
|
duplicateOfNumber: number,
|
|
|
|
|
token: string
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
await githubRequest(
|
|
|
|
|
`/repos/${owner}/${repo}/issues/${issueNumber}`,
|
|
|
|
|
token,
|
|
|
|
|
'PATCH',
|
|
|
|
|
{
|
|
|
|
|
state: 'closed',
|
|
|
|
|
state_reason: 'not_planned'
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await githubRequest(
|
|
|
|
|
`/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
|
|
|
|
|
token,
|
|
|
|
|
'POST',
|
|
|
|
|
{
|
|
|
|
|
body: `This issue has been automatically closed as a duplicate of #${duplicateOfNumber}.
|
|
|
|
|
|
|
|
|
|
If this is incorrect, please re-open this issue or create a new one.
|
|
|
|
|
|
|
|
|
|
🤖 Generated with [Claude Code](https://claude.ai/code)`
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function autoCloseDuplicates(): Promise<void> {
|
|
|
|
|
console.log("[DEBUG] Starting auto-close duplicates script");
|
|
|
|
|
|
|
|
|
|
@@ -61,11 +101,32 @@ async function autoCloseDuplicates(): Promise<void> {
|
|
|
|
|
`[DEBUG] Checking for duplicate comments older than: ${threeDaysAgo.toISOString()}`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
console.log("[DEBUG] Fetching open issues...");
|
|
|
|
|
const issues: GitHubIssue[] = await githubRequest(
|
|
|
|
|
`/repos/${owner}/${repo}/issues?state=open&per_page=100`,
|
|
|
|
|
token
|
|
|
|
|
);
|
|
|
|
|
console.log("[DEBUG] Fetching open issues created more than 3 days ago...");
|
|
|
|
|
const allIssues: GitHubIssue[] = [];
|
|
|
|
|
let page = 1;
|
|
|
|
|
const perPage = 100;
|
|
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
|
const pageIssues: GitHubIssue[] = await githubRequest(
|
|
|
|
|
`/repos/${owner}/${repo}/issues?state=open&per_page=${perPage}&page=${page}`,
|
|
|
|
|
token
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (pageIssues.length === 0) break;
|
|
|
|
|
|
|
|
|
|
// Filter for issues created more than 3 days ago
|
|
|
|
|
const oldEnoughIssues = pageIssues.filter(issue =>
|
|
|
|
|
new Date(issue.created_at) <= threeDaysAgo
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
allIssues.push(...oldEnoughIssues);
|
|
|
|
|
page++;
|
|
|
|
|
|
|
|
|
|
// Safety limit to avoid infinite loops
|
|
|
|
|
if (page > 20) break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const issues = allIssues;
|
|
|
|
|
console.log(`[DEBUG] Found ${issues.length} open issues`);
|
|
|
|
|
|
|
|
|
|
let processedCount = 0;
|
|
|
|
|
@@ -165,11 +226,30 @@ async function autoCloseDuplicates(): Promise<void> {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const duplicateIssueNumber = extractDuplicateIssueNumber(lastDupeComment.body);
|
|
|
|
|
if (!duplicateIssueNumber) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[DEBUG] Issue #${issue.number} - could not extract duplicate issue number from comment, skipping`
|
|
|
|
|
);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
candidateCount++;
|
|
|
|
|
const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`;
|
|
|
|
|
console.log(
|
|
|
|
|
`[DRY RUN] Would auto-close issue #${issue.number} as duplicate: ${issueUrl}`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
console.log(
|
|
|
|
|
`[INFO] Auto-closing issue #${issue.number} as duplicate of #${duplicateIssueNumber}: ${issueUrl}`
|
|
|
|
|
);
|
|
|
|
|
await closeIssueAsDuplicate(owner, repo, issue.number, duplicateIssueNumber, token);
|
|
|
|
|
console.log(
|
|
|
|
|
`[SUCCESS] Successfully closed issue #${issue.number} as duplicate of #${duplicateIssueNumber}`
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(
|
|
|
|
|
`[ERROR] Failed to close issue #${issue.number} as duplicate: ${error}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
|
|