| |
| |
| |
| |
| |
| 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; |
|
|
| |
| 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) { |
| |
| await octokit.rest.issues.createComment({ |
| owner, |
| repo, |
| issue_number: issue.number, |
| body: commentBody, |
| }); |
| console.log(` Added comment.`); |
|
|
| |
| 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); |
|
|