UNPKG

github-pr-automation

Version:

MCP server and CLI for automated GitHub PR management, review resolution, and workflow optimization

264 lines • 12.5 kB
#!/usr/bin/env node import { Command } from "commander"; import { GitHubClient } from "./github/client.js"; import { handleGetFailingTests } from "./tools/get-failing-tests/handler.js"; import { handleFindUnresolvedComments } from "./tools/find-unresolved-comments/handler.js"; import { handleManageStackedPRs } from "./tools/manage-stacked-prs/handler.js"; import { GetFailingTestsSchema } from "./tools/get-failing-tests/schema.js"; import { FindUnresolvedCommentsSchema } from "./tools/find-unresolved-comments/schema.js"; import { ManageStackedPRsSchema } from "./tools/manage-stacked-prs/schema.js"; import { handleResolveReviewThread } from "./tools/resolve-review-thread/handler.js"; import { ResolveReviewThreadInputSchema } from "./tools/resolve-review-thread/schema.js"; import { getVersionString } from "./utils/version.js"; const program = new Command(); // Lazy initialization of GitHub client let clientInstance = null; /** * Get or create GitHub client instance * @returns GitHub client instance */ function getClient() { if (!clientInstance) { clientInstance = new GitHubClient(); } return clientInstance; } program .name("github-pr-automation") .description("MCP server and CLI for automated GitHub PR management, review resolution, and workflow optimization") .version(getVersionString()); program .command("get-failing-tests") .description("Analyze PR CI failures") .requiredOption("--pr <identifier>", "PR identifier (owner/repo#123)") .option("--wait", "Wait for CI completion") .option("--bail-on-first", "Bail on first failure") .option("--cursor <string>", "Pagination cursor (from previous response)") .option("--json", "Output as JSON") .action(async (options) => { try { const client = getClient(); // Build input and let Zod schema apply defaults const input = GetFailingTestsSchema.parse({ pr: options.pr, ...(options.wait !== undefined && { wait: options.wait }), ...(options.bailOnFirst !== undefined && { bail_on_first: options.bailOnFirst, }), ...(options.cursor && { cursor: options.cursor }), }); const result = await handleGetFailingTests(client, input); if (options.json) { // eslint-disable-next-line no-console console.log(JSON.stringify(result, null, 2)); } else { /* eslint-disable no-console */ console.log(`\nšŸ“Š CI Status for ${result.pr}`); console.log(`Status: ${result.status}`); console.log(`Failures: ${result.failures.length}\n`); if (result.failures.length > 0) { console.log("Failed Tests:"); result.failures.forEach((test, i) => { console.log(`\n${i + 1}. ${test.test_name} (${test.check_name})`); if (test.file_path) console.log(` File: ${test.file_path}${test.line_number ? `:${test.line_number}` : ""}`); if (test.error_message) console.log(` Error: ${test.error_message}`); }); } if (result.instructions) { console.log(`\nšŸ“ ${result.instructions.summary}`); } if (result.nextCursor) { console.log(`\nšŸ“„ More results available. Use --cursor "${result.nextCursor}"`); } /* eslint-enable no-console */ } process.exit(0); } catch (error) { console.error(`Error: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } }); program .command("find-unresolved-comments") .description("Find unresolved PR comments") .requiredOption("--pr <identifier>", "PR identifier (owner/repo#123)") .option("--include-bots", "Include bot comments") .option("--exclude-authors <authors>", "Comma-separated list of authors to exclude") .option("--cursor <string>", "Pagination cursor (from previous response)") .option("--sort <type>", "Sort order (chronological|by_file|by_author|priority)") .option("--include-status-indicators", "Include status indicators and priority scoring") .option("--priority-ordering", "Use priority-based ordering") .option("--json", "Output as JSON") .action(async (options) => { try { const client = getClient(); // Build input and let Zod schema apply defaults const input = FindUnresolvedCommentsSchema.parse({ pr: options.pr, ...(options.includeBots !== undefined && { include_bots: options.includeBots, }), ...(options.excludeAuthors && { exclude_authors: options.excludeAuthors.split(","), }), ...(options.sort && { sort: options.sort }), ...(options.cursor && { cursor: options.cursor }), ...(options.includeStatusIndicators !== undefined && { include_status_indicators: options.includeStatusIndicators, }), ...(options.priorityOrdering !== undefined && { priority_ordering: options.priorityOrdering, }), }); const result = await handleFindUnresolvedComments(client, input); if (options.json) { // eslint-disable-next-line no-console console.log(JSON.stringify(result, null, 2)); if (result.nextCursor) { console.error(`\nāš ļø Large output detected. Use --cursor "${result.nextCursor}" for next page.`); } } else { /* eslint-disable no-console */ console.log(`\nšŸ’¬ Comments for ${result.pr}`); console.log(`Unresolved in page: ${result.unresolved_in_page}`); console.log(`Showing: ${result.comments.length}\n`); result.comments.forEach((comment, i) => { const icon = comment.is_bot ? "šŸ¤–" : "šŸ‘¤"; console.log(`\n${i + 1}. ${icon} ${comment.author} (${comment.type})`); if (comment.file_path) console.log(` File: ${comment.file_path}${comment.line_number ? `:${comment.line_number}` : ""}`); console.log(` ${comment.body}`); console.log(` Created: ${comment.created_at}`); // Show action commands console.log(`\n šŸ“ Reply: ${comment.action_commands.reply_command}`); /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any */ const ac = comment.action_commands; // display-only if (ac.mcp_action) { console.log(` āœ… MCP: ${ac.mcp_action.tool} ${JSON.stringify(ac.mcp_action.args)}`); if (ac.resolve_condition) { console.log(` āš ļø ${ac.resolve_condition}`); } } else if (ac.resolve_command) { console.log(` āœ… Resolve: ${ac.resolve_command}`); if (ac.resolve_condition) { console.log(` āš ļø ${ac.resolve_condition}`); } } /* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any */ }); console.log(`\nšŸ“Š Summary:`); console.log(` Bots: ${result.summary.bot_comments}, Humans: ${result.summary.human_comments}`); if (result.nextCursor) { console.log(`\nšŸ“„ More results available. Use --cursor "${result.nextCursor}"`); } /* eslint-enable no-console */ } process.exit(0); } catch (error) { console.error(`Error: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } }); program .command("manage-stacked-prs") .description("Manage stacked PRs") .requiredOption("--base-pr <identifier>", "Base PR (owner/repo#123)") .requiredOption("--dependent-pr <identifier>", "Dependent PR (owner/repo#124)") .option("--auto-fix", "Auto-fix test failures") .option("--use-onto", "Use --onto rebase strategy") .option("--cursor <string>", "Pagination cursor (from previous response)") .option("--json", "Output as JSON") .action(async (options) => { try { const client = getClient(); // Build input and let Zod schema apply defaults const input = ManageStackedPRsSchema.parse({ base_pr: options.basePr, dependent_pr: options.dependentPr, ...(options.autoFix !== undefined && { auto_fix: options.autoFix }), ...(options.useOnto !== undefined && { use_onto: options.useOnto }), ...(options.cursor && { cursor: options.cursor }), }); const result = await handleManageStackedPRs(client, input); if (options.json) { // eslint-disable-next-line no-console console.log(JSON.stringify(result, null, 2)); } else { /* eslint-disable no-console */ console.log(`\nšŸ”— Stack Analysis`); console.log(`Base PR: ${result.base_pr}`); console.log(`Dependent PR: ${result.dependent_pr}`); console.log(`Is stacked: ${result.is_stacked ? "āœ…" : "āŒ"}\n`); if (result.change_summary) { console.log(`Changes: ${result.change_summary.new_commits_in_base} new commits in base`); console.log(`Files changed: ${result.change_summary.files_changed.length}`); } if (result.commands.length > 0) { console.log("\nšŸ“ Rebase Commands:"); result.commands.forEach((cmd) => { console.log(`\n${cmd.step}. ${cmd.description}`); console.log(` $ ${cmd.command}`); if (cmd.estimated_duration) console.log(` ā±ļø ${cmd.estimated_duration}`); }); } console.log(`\nā±ļø Estimated time: ${result.summary.estimated_total_time}`); console.log(`āš ļø Risk level: ${result.summary.risk_level}`); if (result.nextCursor) { console.log(`\nšŸ“„ More commands available. Use --cursor "${result.nextCursor}"`); } /* eslint-enable no-console */ } process.exit(0); } catch (error) { console.error(`Error: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } }); program .command("resolve-review-thread") .description("Resolve a specific review thread (or via comment id) immediately") .requiredOption("--pr <identifier>", "PR identifier (owner/repo#123)") .option("--thread-id <id>", "Review thread GraphQL node ID") .option("--comment-id <id>", "Comment GraphQL node ID (will map to thread)") .option("--prefer <choice>", 'Prefer "thread" or "comment" when both are provided') .option("--json", "Output as JSON") .action(async (options) => { try { const client = getClient(); const input = ResolveReviewThreadInputSchema.parse({ pr: options.pr, ...(options.threadId && { thread_id: options.threadId }), ...(options.commentId && { comment_id: options.commentId }), ...(options.prefer && { prefer: options.prefer }), }); const result = await handleResolveReviewThread(client, input); if (options.json) { // eslint-disable-next-line no-console console.log(JSON.stringify(result, null, 2)); } else { /* eslint-disable no-console */ console.log(`\nāœ… Resolved thread ${result.thread_id}${result.alreadyResolved ? " (already resolved)" : ""}`); if (result.message) console.log(result.message); /* eslint-enable no-console */ } process.exit(0); } catch (error) { console.error(`Error: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } }); await program.parseAsync(process.argv); //# sourceMappingURL=cli.js.map