Files
gemini-cli/scripts/close_duplicate_issues.js
Bryan Morgan d079b7a216 chore(scripts): add duplicate issue closer script and fix lint errors (#16997)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-19 09:23:08 -05:00

152 lines
3.9 KiB
JavaScript

/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { Octokit } from '@octokit/rest';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import prompts from 'prompts';
if (!process.env.GITHUB_TOKEN) {
console.error('Error: GITHUB_TOKEN environment variable is required.');
process.exit(1);
}
const argv = yargs(hideBin(process.argv))
.option('query', {
alias: 'q',
type: 'string',
description:
'Search query to find duplicate issues (e.g. "function response parts")',
demandOption: true,
})
.option('canonical', {
alias: 'c',
type: 'number',
description: 'The canonical issue number to duplicate others to',
demandOption: true,
})
.option('pr', {
type: 'string',
description:
'Optional Pull Request URL or ID to mention in the closing comment',
})
.option('owner', {
type: 'string',
default: 'google-gemini',
description: 'Repository owner',
})
.option('repo', {
type: 'string',
default: 'gemini-cli',
description: 'Repository name',
})
.option('dry-run', {
alias: 'd',
type: 'boolean',
default: false,
description: 'Run without making actual changes (read-only mode)',
})
.option('auto', {
type: 'boolean',
default: false,
description:
'Automatically close all duplicates without prompting (batch mode)',
})
.help()
.parse();
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
});
const { query, canonical, pr, owner, repo, dryRun, auto } = argv;
// Construct the full search query ensuring it targets the specific repo and open issues
const fullSearchQuery = `repo:${owner}/${repo} is:issue is:open ${query}`;
async function run() {
console.log(`Searching for issues matching: ${fullSearchQuery}`);
if (dryRun) {
console.log('--- DRY RUN MODE: No changes will be made ---');
}
try {
const issues = await octokit.paginate(
octokit.rest.search.issuesAndPullRequests,
{
q: fullSearchQuery,
},
);
console.log(`Found ${issues.length} issues.`);
for (const issue of issues) {
if (issue.number === canonical) {
console.log(`Skipping canonical issue #${issue.number}`);
continue;
}
console.log(
`Processing issue #${issue.number}: ${issue.title} (by @${issue.user?.login})`,
);
if (!auto && !dryRun) {
const response = await prompts({
type: 'confirm',
name: 'value',
message: `Close issue #${issue.number} "${issue.title}" created by @${issue.user?.login}?`,
initial: true,
});
if (!response.value) {
console.log(`Skipping issue #${issue.number}`);
continue;
}
}
let commentBody = `Closing this issue as a duplicate of #${canonical}.`;
if (pr) {
commentBody += ` Please note that this issue should be resolved by PR ${pr}.`;
}
try {
if (!dryRun) {
// Add comment
await octokit.rest.issues.createComment({
owner,
repo,
issue_number: issue.number,
body: commentBody,
});
console.log(` Added comment.`);
// Close issue
await octokit.rest.issues.update({
owner,
repo,
issue_number: issue.number,
state: 'closed',
state_reason: 'duplicate',
});
console.log(` Closed issue.`);
} else {
console.log(` [DRY RUN] Would add comment: "${commentBody}"`);
console.log(` [DRY RUN] Would close issue #${issue.number}`);
}
} catch (error) {
console.error(
` Failed to process issue #${issue.number}:`,
error.message,
);
}
}
} catch (error) {
console.error('Error searching for issues:', error.message);
process.exit(1);
}
}
run().catch(console.error);