@probelabs/visor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +1240 -0
  2. package/action.yml +142 -0
  3. package/defaults/.visor.yaml +184 -0
  4. package/dist/action-cli-bridge.d.ts +104 -0
  5. package/dist/action-cli-bridge.d.ts.map +1 -0
  6. package/dist/action-cli-bridge.js +372 -0
  7. package/dist/action-cli-bridge.js.map +1 -0
  8. package/dist/ai-review-service.d.ts +84 -0
  9. package/dist/ai-review-service.d.ts.map +1 -0
  10. package/dist/ai-review-service.js +674 -0
  11. package/dist/ai-review-service.js.map +1 -0
  12. package/dist/check-execution-engine.d.ts +165 -0
  13. package/dist/check-execution-engine.d.ts.map +1 -0
  14. package/dist/check-execution-engine.js +1172 -0
  15. package/dist/check-execution-engine.js.map +1 -0
  16. package/dist/cli-main.d.ts +6 -0
  17. package/dist/cli-main.d.ts.map +1 -0
  18. package/dist/cli-main.js +247 -0
  19. package/dist/cli-main.js.map +1 -0
  20. package/dist/cli.d.ts +47 -0
  21. package/dist/cli.d.ts.map +1 -0
  22. package/dist/cli.js +224 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/commands.d.ts +10 -0
  25. package/dist/commands.d.ts.map +1 -0
  26. package/dist/commands.js +53 -0
  27. package/dist/commands.js.map +1 -0
  28. package/dist/config.d.ts +63 -0
  29. package/dist/config.d.ts.map +1 -0
  30. package/dist/config.js +369 -0
  31. package/dist/config.js.map +1 -0
  32. package/dist/dependency-resolver.d.ts +54 -0
  33. package/dist/dependency-resolver.d.ts.map +1 -0
  34. package/dist/dependency-resolver.js +163 -0
  35. package/dist/dependency-resolver.js.map +1 -0
  36. package/dist/event-mapper.d.ts +125 -0
  37. package/dist/event-mapper.d.ts.map +1 -0
  38. package/dist/event-mapper.js +311 -0
  39. package/dist/event-mapper.js.map +1 -0
  40. package/dist/failure-condition-evaluator.d.ts +81 -0
  41. package/dist/failure-condition-evaluator.d.ts.map +1 -0
  42. package/dist/failure-condition-evaluator.js +445 -0
  43. package/dist/failure-condition-evaluator.js.map +1 -0
  44. package/dist/git-repository-analyzer.d.ts +45 -0
  45. package/dist/git-repository-analyzer.d.ts.map +1 -0
  46. package/dist/git-repository-analyzer.js +285 -0
  47. package/dist/git-repository-analyzer.js.map +1 -0
  48. package/dist/github-check-service.d.ts +104 -0
  49. package/dist/github-check-service.d.ts.map +1 -0
  50. package/dist/github-check-service.js +382 -0
  51. package/dist/github-check-service.js.map +1 -0
  52. package/dist/github-comments.d.ts +109 -0
  53. package/dist/github-comments.d.ts.map +1 -0
  54. package/dist/github-comments.js +289 -0
  55. package/dist/github-comments.js.map +1 -0
  56. package/dist/index.d.ts +2 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +1265 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/output-formatters.d.ts +66 -0
  61. package/dist/output-formatters.d.ts.map +1 -0
  62. package/dist/output-formatters.js +624 -0
  63. package/dist/output-formatters.js.map +1 -0
  64. package/dist/pr-analyzer.d.ts +47 -0
  65. package/dist/pr-analyzer.d.ts.map +1 -0
  66. package/dist/pr-analyzer.js +194 -0
  67. package/dist/pr-analyzer.js.map +1 -0
  68. package/dist/pr-detector.d.ts +78 -0
  69. package/dist/pr-detector.d.ts.map +1 -0
  70. package/dist/pr-detector.js +357 -0
  71. package/dist/pr-detector.js.map +1 -0
  72. package/dist/providers/ai-check-provider.d.ts +40 -0
  73. package/dist/providers/ai-check-provider.d.ts.map +1 -0
  74. package/dist/providers/ai-check-provider.js +416 -0
  75. package/dist/providers/ai-check-provider.js.map +1 -0
  76. package/dist/providers/check-provider-registry.d.ts +67 -0
  77. package/dist/providers/check-provider-registry.d.ts.map +1 -0
  78. package/dist/providers/check-provider-registry.js +138 -0
  79. package/dist/providers/check-provider-registry.js.map +1 -0
  80. package/dist/providers/check-provider.interface.d.ts +78 -0
  81. package/dist/providers/check-provider.interface.d.ts.map +1 -0
  82. package/dist/providers/check-provider.interface.js +11 -0
  83. package/dist/providers/check-provider.interface.js.map +1 -0
  84. package/dist/providers/index.d.ts +10 -0
  85. package/dist/providers/index.d.ts.map +1 -0
  86. package/dist/providers/index.js +19 -0
  87. package/dist/providers/index.js.map +1 -0
  88. package/dist/providers/script-check-provider.d.ts +20 -0
  89. package/dist/providers/script-check-provider.d.ts.map +1 -0
  90. package/dist/providers/script-check-provider.js +163 -0
  91. package/dist/providers/script-check-provider.js.map +1 -0
  92. package/dist/providers/tool-check-provider.d.ts +19 -0
  93. package/dist/providers/tool-check-provider.d.ts.map +1 -0
  94. package/dist/providers/tool-check-provider.js +125 -0
  95. package/dist/providers/tool-check-provider.js.map +1 -0
  96. package/dist/providers/webhook-check-provider.d.ts +21 -0
  97. package/dist/providers/webhook-check-provider.d.ts.map +1 -0
  98. package/dist/providers/webhook-check-provider.js +173 -0
  99. package/dist/providers/webhook-check-provider.js.map +1 -0
  100. package/dist/reviewer.d.ts +88 -0
  101. package/dist/reviewer.d.ts.map +1 -0
  102. package/dist/reviewer.js +760 -0
  103. package/dist/reviewer.js.map +1 -0
  104. package/dist/types/cli.d.ts +41 -0
  105. package/dist/types/cli.d.ts.map +1 -0
  106. package/dist/types/cli.js +3 -0
  107. package/dist/types/cli.js.map +1 -0
  108. package/dist/types/config.d.ts +315 -0
  109. package/dist/types/config.d.ts.map +1 -0
  110. package/dist/types/config.js +6 -0
  111. package/dist/types/config.js.map +1 -0
  112. package/dist/utils/env-resolver.d.ts +38 -0
  113. package/dist/utils/env-resolver.d.ts.map +1 -0
  114. package/dist/utils/env-resolver.js +130 -0
  115. package/dist/utils/env-resolver.js.map +1 -0
  116. package/package.json +116 -0
package/dist/index.js ADDED
@@ -0,0 +1,1265 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.run = run;
37
+ const rest_1 = require("@octokit/rest");
38
+ const auth_app_1 = require("@octokit/auth-app");
39
+ const core_1 = require("@actions/core");
40
+ const commands_1 = require("./commands");
41
+ const pr_analyzer_1 = require("./pr-analyzer");
42
+ const reviewer_1 = require("./reviewer");
43
+ const action_cli_bridge_1 = require("./action-cli-bridge");
44
+ const github_comments_1 = require("./github-comments");
45
+ const config_1 = require("./config");
46
+ const pr_detector_1 = require("./pr-detector");
47
+ const github_check_service_1 = require("./github-check-service");
48
+ const failure_condition_evaluator_1 = require("./failure-condition-evaluator");
49
+ /**
50
+ * Create an authenticated Octokit instance using either GitHub App or token authentication
51
+ */
52
+ async function createAuthenticatedOctokit() {
53
+ const token = (0, core_1.getInput)('github-token');
54
+ const appId = (0, core_1.getInput)('app-id');
55
+ const privateKey = (0, core_1.getInput)('private-key');
56
+ const installationId = (0, core_1.getInput)('installation-id');
57
+ // Prefer GitHub App authentication if app credentials are provided
58
+ if (appId && privateKey) {
59
+ console.log('🔐 Using GitHub App authentication');
60
+ try {
61
+ // Note: createAppAuth is used in the Octokit constructor below
62
+ // If no installation ID provided, try to get it for the current repository
63
+ let finalInstallationId;
64
+ // Validate and parse the installation ID if provided
65
+ if (installationId) {
66
+ finalInstallationId = parseInt(installationId, 10);
67
+ if (isNaN(finalInstallationId) || finalInstallationId <= 0) {
68
+ throw new Error('Invalid installation-id provided. It must be a positive integer.');
69
+ }
70
+ }
71
+ if (!finalInstallationId) {
72
+ const owner = (0, core_1.getInput)('owner') || process.env.GITHUB_REPOSITORY_OWNER;
73
+ const repo = (0, core_1.getInput)('repo') || process.env.GITHUB_REPOSITORY?.split('/')[1];
74
+ if (owner && repo) {
75
+ // Create a temporary JWT-authenticated client to find the installation
76
+ const appOctokit = new rest_1.Octokit({
77
+ authStrategy: auth_app_1.createAppAuth,
78
+ auth: {
79
+ appId,
80
+ privateKey,
81
+ },
82
+ });
83
+ try {
84
+ const { data: installation } = await appOctokit.rest.apps.getRepoInstallation({
85
+ owner,
86
+ repo,
87
+ });
88
+ finalInstallationId = installation.id;
89
+ console.log(`✅ Auto-detected installation ID: ${finalInstallationId}`);
90
+ }
91
+ catch {
92
+ console.warn('⚠️ Could not auto-detect installation ID. Please check app permissions and installation status.');
93
+ throw new Error('GitHub App installation ID is required but could not be auto-detected. Please ensure the app is installed on this repository or provide the `installation-id` manually.');
94
+ }
95
+ }
96
+ }
97
+ // Create the authenticated Octokit instance
98
+ const octokit = new rest_1.Octokit({
99
+ authStrategy: auth_app_1.createAppAuth,
100
+ auth: {
101
+ appId,
102
+ privateKey,
103
+ installationId: finalInstallationId,
104
+ },
105
+ });
106
+ return { octokit, authType: 'github-app' };
107
+ }
108
+ catch (error) {
109
+ console.error('❌ GitHub App authentication failed. Please check your App ID, Private Key, and installation permissions.');
110
+ throw new Error(`GitHub App authentication failed`, { cause: error });
111
+ }
112
+ }
113
+ // Fall back to token authentication
114
+ if (token) {
115
+ console.log('🔑 Using GitHub token authentication');
116
+ return {
117
+ octokit: new rest_1.Octokit({ auth: token }),
118
+ authType: 'token',
119
+ };
120
+ }
121
+ throw new Error('Either github-token or app-id/private-key must be provided for authentication');
122
+ }
123
+ async function run() {
124
+ try {
125
+ const { octokit, authType } = await createAuthenticatedOctokit();
126
+ console.log(`✅ Authenticated successfully using ${authType}`);
127
+ // Get token for passing to CLI bridge (might be undefined if using App auth)
128
+ const token = (0, core_1.getInput)('github-token') || '';
129
+ // Collect all GitHub Action inputs
130
+ const inputs = {
131
+ 'github-token': token,
132
+ owner: (0, core_1.getInput)('owner') || process.env.GITHUB_REPOSITORY_OWNER,
133
+ repo: (0, core_1.getInput)('repo') || process.env.GITHUB_REPOSITORY?.split('/')[1],
134
+ 'auto-review': (0, core_1.getInput)('auto-review'),
135
+ debug: (0, core_1.getInput)('debug'),
136
+ // GitHub App authentication inputs
137
+ 'app-id': (0, core_1.getInput)('app-id') || undefined,
138
+ 'private-key': (0, core_1.getInput)('private-key') || undefined,
139
+ 'installation-id': (0, core_1.getInput)('installation-id') || undefined,
140
+ // Only collect other inputs if they have values to avoid triggering CLI mode
141
+ checks: (0, core_1.getInput)('checks') || undefined,
142
+ 'output-format': (0, core_1.getInput)('output-format') || undefined,
143
+ 'config-path': (0, core_1.getInput)('config-path') || undefined,
144
+ 'comment-on-pr': (0, core_1.getInput)('comment-on-pr') || undefined,
145
+ 'create-check': (0, core_1.getInput)('create-check') || undefined,
146
+ 'add-labels': (0, core_1.getInput)('add-labels') || undefined,
147
+ 'fail-on-critical': (0, core_1.getInput)('fail-on-critical') || undefined,
148
+ 'fail-on-api-error': (0, core_1.getInput)('fail-on-api-error') || undefined,
149
+ 'min-score': (0, core_1.getInput)('min-score') || undefined,
150
+ // Legacy inputs for backward compatibility
151
+ 'visor-config-path': (0, core_1.getInput)('visor-config-path') || undefined,
152
+ 'visor-checks': (0, core_1.getInput)('visor-checks') || undefined,
153
+ };
154
+ const eventName = process.env.GITHUB_EVENT_NAME;
155
+ const autoReview = inputs['auto-review'] === 'true';
156
+ // Create GitHub context for CLI bridge
157
+ const context = {
158
+ event_name: eventName || 'unknown',
159
+ repository: process.env.GITHUB_REPOSITORY
160
+ ? {
161
+ owner: { login: process.env.GITHUB_REPOSITORY.split('/')[0] },
162
+ name: process.env.GITHUB_REPOSITORY.split('/')[1],
163
+ }
164
+ : undefined,
165
+ event: process.env.GITHUB_CONTEXT ? JSON.parse(process.env.GITHUB_CONTEXT).event : {},
166
+ payload: process.env.GITHUB_CONTEXT ? JSON.parse(process.env.GITHUB_CONTEXT) : {},
167
+ };
168
+ // Initialize CLI bridge
169
+ const cliBridge = new action_cli_bridge_1.ActionCliBridge(token, context);
170
+ // Check if we should use Visor CLI
171
+ console.log('Debug: inputs.debug =', inputs.debug);
172
+ console.log('Debug: inputs.checks =', inputs.checks);
173
+ console.log('Debug: inputs.config-path =', inputs['config-path']);
174
+ console.log('Debug: inputs.visor-checks =', inputs['visor-checks']);
175
+ console.log('Debug: inputs.visor-config-path =', inputs['visor-config-path']);
176
+ if (cliBridge.shouldUseVisor(inputs)) {
177
+ console.log('🔍 Using Visor CLI mode');
178
+ // ENHANCED FIX: For PR auto-reviews, detect PR context across all event types
179
+ const isAutoReview = inputs['auto-review'] === 'true';
180
+ if (isAutoReview) {
181
+ console.log('🔄 Auto-review enabled - attempting to detect PR context across all event types');
182
+ // Try to detect if we're in a PR context (works for push, pull_request, issue_comment, etc.)
183
+ const prDetected = await detectPRContext(inputs, context, octokit);
184
+ if (prDetected) {
185
+ console.log('✅ PR context detected - using GitHub API for PR analysis');
186
+ await handlePullRequestVisorMode(inputs, context, octokit, authType);
187
+ return;
188
+ }
189
+ else {
190
+ console.log('ℹ️ No PR context detected - proceeding with CLI mode for general analysis');
191
+ }
192
+ }
193
+ await handleVisorMode(cliBridge, inputs, context, octokit);
194
+ return;
195
+ }
196
+ console.log('🤖 Using legacy GitHub Action mode');
197
+ await handleLegacyMode(octokit, inputs, eventName, autoReview);
198
+ }
199
+ catch (error) {
200
+ (0, core_1.setFailed)(error instanceof Error ? error.message : 'Unknown error');
201
+ }
202
+ }
203
+ /**
204
+ * Handle Visor CLI mode
205
+ */
206
+ async function handleVisorMode(cliBridge, inputs, _context, _octokit) {
207
+ try {
208
+ // Note: PR auto-review cases are now handled upstream in the main run() function
209
+ // Execute CLI with the provided config file (no temp config creation)
210
+ const result = await cliBridge.executeCliWithContext(inputs);
211
+ if (result.success) {
212
+ console.log('✅ Visor CLI execution completed successfully');
213
+ // Parse JSON output for PR comment creation
214
+ let cliOutput;
215
+ try {
216
+ // Extract JSON from CLI output
217
+ const outputLines = result.output?.split('\n') || [];
218
+ const jsonLine = outputLines.find(line => line.trim().startsWith('{') && line.trim().endsWith('}'));
219
+ if (jsonLine) {
220
+ cliOutput = JSON.parse(jsonLine);
221
+ console.log('📊 CLI Review Results:', cliOutput);
222
+ // Note: PR comment posting is now handled by handlePullRequestVisorMode for PR events
223
+ // CLI mode output is intended for non-PR scenarios
224
+ }
225
+ else {
226
+ console.log('📄 CLI Output (non-JSON):', result.output);
227
+ }
228
+ }
229
+ catch (parseError) {
230
+ console.log('⚠️ Could not parse CLI output as JSON:', parseError);
231
+ console.log('📄 Raw CLI Output:', result.output);
232
+ }
233
+ // Set outputs based on CLI result
234
+ const outputs = cliBridge.mergeActionAndCliOutputs(inputs, result);
235
+ // Add additional outputs from parsed JSON
236
+ if (cliOutput) {
237
+ outputs['total-issues'] = cliOutput.totalIssues?.toString() || '0';
238
+ outputs['critical-issues'] = cliOutput.criticalIssues?.toString() || '0';
239
+ }
240
+ for (const [key, value] of Object.entries(outputs)) {
241
+ (0, core_1.setOutput)(key, value);
242
+ }
243
+ }
244
+ else {
245
+ console.error('❌ Visor CLI execution failed');
246
+ console.error(result.error || result.output);
247
+ (0, core_1.setFailed)(result.error || 'CLI execution failed');
248
+ }
249
+ }
250
+ catch (error) {
251
+ console.error('❌ Visor mode error:', error);
252
+ (0, core_1.setFailed)(error instanceof Error ? error.message : 'Visor mode failed');
253
+ }
254
+ }
255
+ /**
256
+ * Post CLI review results as PR comment with robust PR detection
257
+ */
258
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
259
+ async function postCliReviewComment(cliOutput, inputs, octokit) {
260
+ try {
261
+ const owner = inputs.owner || process.env.GITHUB_REPOSITORY_OWNER;
262
+ const repo = inputs.repo || process.env.GITHUB_REPOSITORY?.split('/')[1];
263
+ if (!owner || !repo) {
264
+ console.log('⚠️ Missing required parameters for PR comment creation');
265
+ return;
266
+ }
267
+ // Use the provided authenticated Octokit instance
268
+ const prDetector = new pr_detector_1.PRDetector(octokit, inputs.debug === 'true');
269
+ // Convert GitHub context to our format
270
+ const eventContext = {
271
+ event_name: process.env.GITHUB_EVENT_NAME || 'unknown',
272
+ repository: {
273
+ owner: { login: owner },
274
+ name: repo,
275
+ },
276
+ event: process.env.GITHUB_CONTEXT ? JSON.parse(process.env.GITHUB_CONTEXT).event : undefined,
277
+ payload: process.env.GITHUB_CONTEXT ? JSON.parse(process.env.GITHUB_CONTEXT) : {},
278
+ };
279
+ // Use robust PR detection
280
+ const prResult = await prDetector.detectPRNumber(eventContext, owner, repo);
281
+ if (!prResult.prNumber) {
282
+ console.log(`⚠️ No PR found using any detection strategy: ${prResult.details || 'Unknown reason'}`);
283
+ if (inputs.debug === 'true') {
284
+ console.log('Available detection strategies:');
285
+ prDetector.getDetectionStrategies().forEach(strategy => console.log(` ${strategy}`));
286
+ }
287
+ return;
288
+ }
289
+ console.log(`✅ Found PR #${prResult.prNumber} using ${prResult.source} (confidence: ${prResult.confidence})`);
290
+ if (prResult.details) {
291
+ console.log(` Details: ${prResult.details}`);
292
+ }
293
+ const commentManager = new github_comments_1.CommentManager(octokit);
294
+ // Create Visor-formatted comment from CLI output
295
+ let comment = `# 🔍 Visor Code Review Results\n\n`;
296
+ comment += `## 📊 Summary\n`;
297
+ comment += `- **Overall Score**: ${cliOutput.overallScore || 0}/100\n`;
298
+ comment += `- **Issues Found**: ${cliOutput.totalIssues || 0} (${cliOutput.criticalIssues || 0} Critical)\n`;
299
+ comment += `- **Files Analyzed**: ${cliOutput.filesAnalyzed || 'N/A'}\n\n`;
300
+ // Add category scores if available
301
+ if (cliOutput.securityScore ||
302
+ cliOutput.performanceScore ||
303
+ cliOutput.styleScore ||
304
+ cliOutput.architectureScore) {
305
+ comment += `## 📈 Category Scores\n`;
306
+ if (cliOutput.securityScore !== undefined)
307
+ comment += `- **Security**: ${cliOutput.securityScore}/100\n`;
308
+ if (cliOutput.performanceScore !== undefined)
309
+ comment += `- **Performance**: ${cliOutput.performanceScore}/100\n`;
310
+ if (cliOutput.styleScore !== undefined)
311
+ comment += `- **Style**: ${cliOutput.styleScore}/100\n`;
312
+ if (cliOutput.architectureScore !== undefined)
313
+ comment += `- **Architecture**: ${cliOutput.architectureScore}/100\n`;
314
+ comment += '\n';
315
+ }
316
+ // Load config to determine grouping method
317
+ const { ConfigManager } = await Promise.resolve().then(() => __importStar(require('./config')));
318
+ const configManager = new ConfigManager();
319
+ const config = await configManager.findAndLoadConfig();
320
+ // Add issues grouped by check or category based on config
321
+ if (cliOutput.issues && cliOutput.issues.length > 0) {
322
+ // Always use check-based grouping when configured
323
+ const useCheckGrouping = config.output?.pr_comment?.group_by === 'check';
324
+ const groupedIssues = useCheckGrouping
325
+ ? groupIssuesByCheck(cliOutput.issues)
326
+ : groupIssuesByCategory(cliOutput.issues);
327
+ // Get configured checks for filtering
328
+ const configuredChecks = config.checks ? Object.keys(config.checks) : [];
329
+ for (const [groupKey, issues] of Object.entries(groupedIssues)) {
330
+ if (issues.length === 0)
331
+ continue;
332
+ // When using check-based grouping, only show configured checks
333
+ if (useCheckGrouping && configuredChecks.length > 0) {
334
+ // Skip if not a configured check (unless it's uncategorized)
335
+ if (!configuredChecks.includes(groupKey) && groupKey !== 'uncategorized') {
336
+ continue;
337
+ }
338
+ }
339
+ const title = `${groupKey.charAt(0).toUpperCase() + groupKey.slice(1)} Issues (${issues.length})`;
340
+ let sectionContent = '';
341
+ for (const issue of issues.slice(0, 5)) {
342
+ // Limit to 5 issues per category
343
+ sectionContent += `- **${issue.severity?.toUpperCase() || 'UNKNOWN'}**: ${issue.message}\n`;
344
+ sectionContent += ` - **File**: \`${issue.file}:${issue.line}\`\n\n`;
345
+ }
346
+ if (issues.length > 5) {
347
+ sectionContent += `*...and ${issues.length - 5} more issues in this category.*\n\n`;
348
+ }
349
+ comment += commentManager.createCollapsibleSection(title, sectionContent, true);
350
+ comment += '\n\n';
351
+ }
352
+ }
353
+ // Add suggestions if any
354
+ if (cliOutput.suggestions && cliOutput.suggestions.length > 0) {
355
+ const suggestionsContent = cliOutput.suggestions.map((s) => `- ${s}`).join('\n') + '\n';
356
+ comment += commentManager.createCollapsibleSection('💡 Recommendations', suggestionsContent, true);
357
+ comment += '\n\n';
358
+ }
359
+ // Add debug information if available
360
+ if (cliOutput.debug) {
361
+ const debugContent = formatDebugInfo(cliOutput.debug);
362
+ comment +=
363
+ '\n\n' +
364
+ commentManager.createCollapsibleSection('🐛 Debug Information', debugContent, false);
365
+ comment += '\n\n';
366
+ }
367
+ // Fetch fresh PR data to get the latest commit SHA
368
+ let latestCommitSha;
369
+ try {
370
+ const { data: pullRequest } = await octokit.rest.pulls.get({
371
+ owner,
372
+ repo,
373
+ pull_number: prResult.prNumber,
374
+ });
375
+ latestCommitSha = pullRequest.head.sha;
376
+ console.log(`📝 Latest commit SHA: ${latestCommitSha.substring(0, 7)}`);
377
+ }
378
+ catch (error) {
379
+ console.warn('⚠️ Could not fetch latest PR data:', error);
380
+ // Fallback to environment or event data
381
+ latestCommitSha =
382
+ eventContext.event?.pull_request?.head?.sha ||
383
+ eventContext.payload?.event?.pull_request?.head?.sha ||
384
+ process.env.GITHUB_SHA;
385
+ }
386
+ // Use smart comment updating with unique ID
387
+ const commentId = `visor-cli-review-${prResult.prNumber}`;
388
+ await commentManager.updateOrCreateComment(owner, repo, prResult.prNumber, comment, {
389
+ commentId,
390
+ triggeredBy: 'visor-cli',
391
+ allowConcurrentUpdates: true,
392
+ commitSha: latestCommitSha,
393
+ });
394
+ console.log(`✅ Posted CLI review comment to PR #${prResult.prNumber}`);
395
+ }
396
+ catch (error) {
397
+ console.error('❌ Failed to post CLI review comment:', error);
398
+ }
399
+ }
400
+ function groupIssuesByCategory(issues) {
401
+ const grouped = {
402
+ security: [],
403
+ performance: [],
404
+ style: [],
405
+ logic: [],
406
+ documentation: [],
407
+ architecture: [],
408
+ };
409
+ for (const issue of issues) {
410
+ const category = issue.category || 'logic';
411
+ if (!grouped[category])
412
+ grouped[category] = [];
413
+ grouped[category].push(issue);
414
+ }
415
+ return grouped;
416
+ }
417
+ /**
418
+ * Group issues by the check that found them (extracted from ruleId prefix)
419
+ */
420
+ function groupIssuesByCheck(issues) {
421
+ const grouped = {};
422
+ for (const issue of issues) {
423
+ // Extract check name from ruleId prefix
424
+ // Format: "checkName/specific-rule" -> "checkName"
425
+ let checkName = 'uncategorized';
426
+ if (issue.ruleId && issue.ruleId.includes('/')) {
427
+ const parts = issue.ruleId.split('/');
428
+ checkName = parts[0];
429
+ }
430
+ // No fallback to category - only use ruleId prefix
431
+ if (!grouped[checkName]) {
432
+ grouped[checkName] = [];
433
+ }
434
+ grouped[checkName].push(issue);
435
+ }
436
+ return grouped;
437
+ }
438
+ function formatDebugInfo(debug) {
439
+ let content = '';
440
+ if (debug.provider)
441
+ content += `**Provider:** ${debug.provider}\n`;
442
+ if (debug.model)
443
+ content += `**Model:** ${debug.model}\n`;
444
+ if (debug.processingTime)
445
+ content += `**Processing Time:** ${debug.processingTime}ms\n`;
446
+ if (debug.parallelExecution !== undefined)
447
+ content += `**Parallel Execution:** ${debug.parallelExecution ? '✅' : '❌'}\n`;
448
+ if (debug.checksExecuted)
449
+ content += `**Checks Executed:** ${debug.checksExecuted.join(', ')}\n`;
450
+ content += '\n';
451
+ return content;
452
+ }
453
+ /**
454
+ * Handle legacy GitHub Action mode (backward compatibility)
455
+ */
456
+ async function handleLegacyMode(octokit, inputs, eventName, autoReview) {
457
+ const owner = inputs.owner;
458
+ const repo = inputs.repo;
459
+ if (!owner || !repo) {
460
+ throw new Error('Owner and repo are required');
461
+ }
462
+ console.log(`Event: ${eventName}, Owner: ${owner}, Repo: ${repo}`);
463
+ // Handle different GitHub events
464
+ switch (eventName) {
465
+ case 'issue_comment':
466
+ await handleIssueComment(octokit, owner, repo);
467
+ break;
468
+ case 'pull_request':
469
+ if (autoReview) {
470
+ await handlePullRequestEvent(octokit, owner, repo, inputs);
471
+ }
472
+ break;
473
+ default:
474
+ // Fallback to original repo info functionality
475
+ await handleRepoInfo(octokit, owner, repo);
476
+ break;
477
+ }
478
+ }
479
+ async function handleIssueComment(octokit, owner, repo) {
480
+ const context = JSON.parse(process.env.GITHUB_CONTEXT || '{}');
481
+ const comment = context.event?.comment;
482
+ const issue = context.event?.issue;
483
+ if (!comment || !issue) {
484
+ console.log('No comment or issue found in context');
485
+ return;
486
+ }
487
+ // Only process PR comments (issues with pull_request key are PRs)
488
+ if (!issue.pull_request) {
489
+ console.log('Comment is not on a pull request');
490
+ return;
491
+ }
492
+ // Load configuration to get available commands
493
+ const configManager = new config_1.ConfigManager();
494
+ let config;
495
+ const commandRegistry = {};
496
+ try {
497
+ config = await configManager.loadConfig('.visor.yaml');
498
+ // Build command registry from config
499
+ if (config.checks) {
500
+ // Add 'review' command that runs all checks
501
+ commandRegistry['review'] = Object.keys(config.checks);
502
+ // Also add individual check names as commands
503
+ for (const [checkId, checkConfig] of Object.entries(config.checks)) {
504
+ // Legacy: check if it has old 'command' property
505
+ if (checkConfig.command) {
506
+ if (!commandRegistry[checkConfig.command]) {
507
+ commandRegistry[checkConfig.command] = [];
508
+ }
509
+ commandRegistry[checkConfig.command].push(checkId);
510
+ }
511
+ // New: add check name as command
512
+ commandRegistry[checkId] = [checkId];
513
+ }
514
+ }
515
+ }
516
+ catch {
517
+ console.log('Could not load config, using defaults');
518
+ config = null;
519
+ // Default commands when no config is available
520
+ commandRegistry['review'] = ['security', 'performance', 'style', 'architecture'];
521
+ }
522
+ // Parse comment with available commands
523
+ const availableCommands = Object.keys(commandRegistry);
524
+ const command = (0, commands_1.parseComment)(comment.body, availableCommands);
525
+ if (!command) {
526
+ console.log('No valid command found in comment');
527
+ return;
528
+ }
529
+ console.log(`Processing command: ${command.type}`);
530
+ const prNumber = issue.number;
531
+ const analyzer = new pr_analyzer_1.PRAnalyzer(octokit);
532
+ const reviewer = new reviewer_1.PRReviewer(octokit);
533
+ switch (command.type) {
534
+ case 'status':
535
+ const statusPrInfo = await analyzer.fetchPRDiff(owner, repo, prNumber);
536
+ const statusComment = `## 📊 PR Status\n\n` +
537
+ `**Title:** ${statusPrInfo.title}\n` +
538
+ `**Author:** ${statusPrInfo.author}\n` +
539
+ `**Files Changed:** ${statusPrInfo.files.length}\n` +
540
+ `**Additions:** +${statusPrInfo.totalAdditions}\n` +
541
+ `**Deletions:** -${statusPrInfo.totalDeletions}\n` +
542
+ `**Base:** ${statusPrInfo.base} → **Head:** ${statusPrInfo.head}\n\n` +
543
+ `---\n` +
544
+ `*Powered by [Visor](https://probelabs.com/visor) from [Probelabs](https://probelabs.com)*`;
545
+ await octokit.rest.issues.createComment({
546
+ owner,
547
+ repo,
548
+ issue_number: prNumber,
549
+ body: statusComment,
550
+ });
551
+ break;
552
+ case 'help':
553
+ await octokit.rest.issues.createComment({
554
+ owner,
555
+ repo,
556
+ issue_number: prNumber,
557
+ body: (0, commands_1.getHelpText)(commandRegistry),
558
+ });
559
+ break;
560
+ default:
561
+ // Handle custom commands from config
562
+ if (commandRegistry[command.type]) {
563
+ const checkIds = commandRegistry[command.type];
564
+ console.log(`Running checks for command /${command.type}: ${checkIds.join(', ')}`);
565
+ const prInfo = await analyzer.fetchPRDiff(owner, repo, prNumber);
566
+ // Extract common arguments
567
+ const focus = command.args?.find(arg => arg.startsWith('--focus='))?.split('=')[1];
568
+ const format = command.args?.find(arg => arg.startsWith('--format='))?.split('=')[1];
569
+ // If focus is specified, update the checks' focus
570
+ if (focus && config?.checks) {
571
+ for (const checkId of checkIds) {
572
+ if (config.checks[checkId]) {
573
+ config.checks[checkId].focus = focus;
574
+ }
575
+ }
576
+ }
577
+ const review = await reviewer.reviewPR(owner, repo, prNumber, prInfo, {
578
+ focus,
579
+ format,
580
+ config: config,
581
+ checks: checkIds,
582
+ parallelExecution: false,
583
+ });
584
+ await reviewer.postReviewComment(owner, repo, prNumber, review, {
585
+ focus,
586
+ format,
587
+ });
588
+ (0, core_1.setOutput)('issues-found', (0, reviewer_1.calculateTotalIssues)(review.issues).toString());
589
+ }
590
+ break;
591
+ }
592
+ }
593
+ async function handlePullRequestEvent(octokit, owner, repo, inputs) {
594
+ const context = JSON.parse(process.env.GITHUB_CONTEXT || '{}');
595
+ const pullRequest = context.event?.pull_request;
596
+ const action = context.event?.action;
597
+ if (!pullRequest) {
598
+ console.log('No pull request found in context');
599
+ return;
600
+ }
601
+ // Handle multiple PR actions: opened, synchronize, edited
602
+ const supportedActions = ['opened', 'synchronize', 'edited'];
603
+ if (!supportedActions.includes(action)) {
604
+ console.log(`Unsupported PR action: ${action}. Supported actions: ${supportedActions.join(', ')}`);
605
+ return;
606
+ }
607
+ console.log(`Auto-reviewing PR #${pullRequest.number} (action: ${action})`);
608
+ const prNumber = pullRequest.number;
609
+ const analyzer = new pr_analyzer_1.PRAnalyzer(octokit);
610
+ const reviewer = new reviewer_1.PRReviewer(octokit);
611
+ const commentManager = new github_comments_1.CommentManager(octokit);
612
+ // Generate comment ID for this PR to enable smart updating
613
+ const commentId = `pr-review-${prNumber}`;
614
+ let prInfo;
615
+ let reviewContext = '';
616
+ // For synchronize (new commits), get the latest commit SHA for incremental analysis
617
+ if (action === 'synchronize') {
618
+ const latestCommitSha = pullRequest.head?.sha;
619
+ if (latestCommitSha) {
620
+ console.log(`Analyzing incremental changes from commit: ${latestCommitSha}`);
621
+ prInfo = await analyzer.fetchPRDiff(owner, repo, prNumber, latestCommitSha);
622
+ reviewContext =
623
+ '## 🔄 Updated PR Analysis\n\nThis review has been updated to include the latest changes.\n\n';
624
+ }
625
+ else {
626
+ // Fallback to full analysis if no commit SHA available
627
+ prInfo = await analyzer.fetchPRDiff(owner, repo, prNumber);
628
+ reviewContext = '## 🔄 Updated PR Analysis\n\nAnalyzing all changes in this PR.\n\n';
629
+ }
630
+ }
631
+ else {
632
+ // For opened and edited events, do full PR analysis
633
+ prInfo = await analyzer.fetchPRDiff(owner, repo, prNumber);
634
+ if (action === 'opened') {
635
+ reviewContext =
636
+ '## 🚀 Welcome to Automated PR Review!\n\nThis PR has been automatically analyzed. Use `/help` to see available commands.\n\n';
637
+ }
638
+ else {
639
+ reviewContext =
640
+ '## ✏️ PR Analysis Updated\n\nThis review has been updated based on PR changes.\n\n';
641
+ }
642
+ }
643
+ // Load config for the review
644
+ const configManager = new config_1.ConfigManager();
645
+ let config;
646
+ try {
647
+ config = await configManager.loadConfig('.visor.yaml');
648
+ }
649
+ catch {
650
+ // Fall back to a basic configuration for PR auto-review
651
+ config = {
652
+ version: '1.0',
653
+ output: {},
654
+ checks: {
655
+ 'auto-review': {
656
+ type: 'ai',
657
+ on: ['pr_opened', 'pr_updated'],
658
+ prompt: `Review this pull request comprehensively. Look for security issues, performance problems, code quality, bugs, and suggest improvements. Action: ${action}`,
659
+ },
660
+ },
661
+ };
662
+ }
663
+ // Create review options, including debug if enabled
664
+ const reviewOptions = {
665
+ debug: inputs?.debug === 'true',
666
+ config: config,
667
+ checks: ['auto-review'],
668
+ parallelExecution: false,
669
+ };
670
+ // Create GitHub check runs for legacy auto-review
671
+ let checkResults = null;
672
+ if (inputs && inputs['github-token']) {
673
+ checkResults = await createGitHubChecks(octokit, inputs, owner, repo, pullRequest.head?.sha || 'unknown', ['auto-review'], config);
674
+ }
675
+ // Update checks to in-progress status
676
+ if (checkResults) {
677
+ await updateChecksInProgress(octokit, owner, repo, checkResults.checkRunMap);
678
+ }
679
+ // Perform the review with debug options
680
+ const review = await reviewer.reviewPR(owner, repo, prNumber, prInfo, reviewOptions);
681
+ // Complete GitHub check runs with results
682
+ if (checkResults) {
683
+ await completeGitHubChecks(octokit, owner, repo, checkResults.checkRunMap, review, config);
684
+ }
685
+ // If debug mode is enabled, output debug information to console
686
+ if (reviewOptions.debug && review.debug) {
687
+ console.log('\n========================================');
688
+ console.log('🐛 DEBUG INFORMATION');
689
+ console.log('========================================');
690
+ console.log(`Provider: ${review.debug.provider}`);
691
+ console.log(`Model: ${review.debug.model}`);
692
+ console.log(`API Key Source: ${review.debug.apiKeySource}`);
693
+ console.log(`Processing Time: ${review.debug.processingTime}ms`);
694
+ console.log(`Prompt Length: ${review.debug.promptLength} characters`);
695
+ console.log(`Response Length: ${review.debug.responseLength} characters`);
696
+ console.log(`JSON Parse Success: ${review.debug.jsonParseSuccess ? '✅' : '❌'}`);
697
+ if (review.debug.errors && review.debug.errors.length > 0) {
698
+ console.log(`\n⚠️ Errors:`);
699
+ review.debug.errors.forEach(err => console.log(` - ${err}`));
700
+ }
701
+ console.log('\n--- AI PROMPT ---');
702
+ console.log(review.debug.prompt.substring(0, 500) + '...');
703
+ console.log('\n--- RAW RESPONSE ---');
704
+ console.log(review.debug.rawResponse.substring(0, 500) + '...');
705
+ console.log('========================================\n');
706
+ }
707
+ const reviewComment = await reviewer['formatReviewCommentWithVisorFormat'](review, reviewOptions);
708
+ const fullComment = reviewContext + reviewComment;
709
+ // Use smart comment updating - will update existing comment or create new one
710
+ try {
711
+ const comment = await commentManager.updateOrCreateComment(owner, repo, prNumber, fullComment, {
712
+ commentId,
713
+ triggeredBy: action,
714
+ allowConcurrentUpdates: true, // Allow updates even if comment was modified externally
715
+ commitSha: pullRequest.head?.sha,
716
+ });
717
+ console.log(`✅ ${action === 'opened' ? 'Created' : 'Updated'} PR review comment (ID: ${comment.id})`);
718
+ }
719
+ catch (error) {
720
+ console.error(`❌ Failed to ${action === 'opened' ? 'create' : 'update'} PR review comment:`, error);
721
+ // Fallback to creating a new comment without the smart updating
722
+ await octokit.rest.issues.createComment({
723
+ owner,
724
+ repo,
725
+ issue_number: prNumber,
726
+ body: fullComment,
727
+ });
728
+ console.log('✅ Created fallback PR review comment');
729
+ }
730
+ // Set outputs
731
+ (0, core_1.setOutput)('auto-review-completed', 'true');
732
+ (0, core_1.setOutput)('issues-found', (0, reviewer_1.calculateTotalIssues)(review.issues).toString());
733
+ (0, core_1.setOutput)('pr-action', action);
734
+ (0, core_1.setOutput)('incremental-analysis', action === 'synchronize' ? 'true' : 'false');
735
+ // Set GitHub check run outputs
736
+ (0, core_1.setOutput)('checks-api-available', checkResults.checksApiAvailable.toString());
737
+ (0, core_1.setOutput)('check-runs-created', checkResults.checkRunsCreated.toString());
738
+ (0, core_1.setOutput)('check-runs-urls', checkResults.checkRunUrls.join(','));
739
+ }
740
+ async function handleRepoInfo(octokit, owner, repo) {
741
+ const { data: repoData } = await octokit.rest.repos.get({
742
+ owner,
743
+ repo,
744
+ });
745
+ (0, core_1.setOutput)('repo-name', repoData.name);
746
+ (0, core_1.setOutput)('repo-description', repoData.description || '');
747
+ (0, core_1.setOutput)('repo-stars', repoData.stargazers_count.toString());
748
+ console.log(`Repository: ${repoData.full_name}`);
749
+ console.log(`Description: ${repoData.description || 'No description'}`);
750
+ console.log(`Stars: ${repoData.stargazers_count}`);
751
+ }
752
+ /**
753
+ * Create GitHub check runs for individual checks if enabled
754
+ */
755
+ async function createGitHubChecks(octokit, inputs, owner, repo, headSha, checksToRun, config) {
756
+ // Check if GitHub checks are enabled via input (default is true)
757
+ const createCheckInput = inputs['create-check'] !== 'false';
758
+ // Check if GitHub checks are enabled via config (default is true if not specified)
759
+ const createCheckConfig = config?.output?.github_checks?.enabled !== false;
760
+ if (!createCheckInput || !createCheckConfig) {
761
+ const reason = !createCheckInput ? 'create-check input' : 'configuration';
762
+ console.log(`🔧 GitHub check runs disabled via ${reason}`);
763
+ return {
764
+ checkRunMap: null,
765
+ checksApiAvailable: true,
766
+ checkRunsCreated: 0,
767
+ checkRunUrls: [],
768
+ };
769
+ }
770
+ // Check if per-check mode is enabled (default is true)
771
+ const perCheckMode = config?.output?.github_checks?.per_check !== false;
772
+ try {
773
+ const checkService = new github_check_service_1.GitHubCheckService(octokit);
774
+ const checkRunMap = new Map();
775
+ const checkRunUrls = [];
776
+ // Get custom name prefix if specified
777
+ const namePrefix = config?.output?.github_checks?.name_prefix || 'Visor';
778
+ if (perCheckMode) {
779
+ console.log(`🔍 Creating individual GitHub check runs for ${checksToRun.length} checks...`);
780
+ // Create individual check runs for each configured check
781
+ for (const checkName of checksToRun) {
782
+ try {
783
+ const checkRunOptions = {
784
+ owner,
785
+ repo,
786
+ head_sha: headSha,
787
+ name: `${namePrefix}: ${checkName}`,
788
+ external_id: `visor-${checkName}-${headSha.substring(0, 7)}`,
789
+ };
790
+ const checkRun = await checkService.createCheckRun(checkRunOptions, {
791
+ title: `${checkName} Analysis`,
792
+ summary: `Running ${checkName} check using AI-powered analysis...`,
793
+ });
794
+ checkRunMap.set(checkName, checkRun);
795
+ checkRunUrls.push(checkRun.url);
796
+ console.log(`✅ Created check run for ${checkName}: ${checkRun.url}`);
797
+ }
798
+ catch (error) {
799
+ console.error(`❌ Failed to create check run for ${checkName}:`, error);
800
+ // Continue with other checks even if one fails
801
+ }
802
+ }
803
+ }
804
+ else {
805
+ // Create a single check run for all checks
806
+ console.log(`🔍 Creating single GitHub check run for ${checksToRun.length} checks...`);
807
+ try {
808
+ const checkRunOptions = {
809
+ owner,
810
+ repo,
811
+ head_sha: headSha,
812
+ name: `${namePrefix}: Code Review`,
813
+ external_id: `visor-combined-${headSha.substring(0, 7)}`,
814
+ };
815
+ const checkRun = await checkService.createCheckRun(checkRunOptions, {
816
+ title: 'AI Code Review',
817
+ summary: `Running ${checksToRun.join(', ')} checks using AI-powered analysis...`,
818
+ });
819
+ // Use 'combined' as the key for all checks
820
+ checkRunMap.set('combined', checkRun);
821
+ checkRunUrls.push(checkRun.url);
822
+ console.log(`✅ Created combined check run: ${checkRun.url}`);
823
+ }
824
+ catch (error) {
825
+ console.error(`❌ Failed to create combined check run:`, error);
826
+ }
827
+ }
828
+ return {
829
+ checkRunMap,
830
+ checksApiAvailable: true,
831
+ checkRunsCreated: checkRunMap.size,
832
+ checkRunUrls,
833
+ };
834
+ }
835
+ catch (error) {
836
+ // Check if this is a permissions error
837
+ if (error instanceof Error &&
838
+ (error.message.includes('403') || error.message.includes('checks:write'))) {
839
+ console.warn('⚠️ GitHub checks API not available - insufficient permissions. Check runs will be skipped.');
840
+ console.warn('💡 To enable check runs, ensure your GitHub token has "checks:write" permission.');
841
+ return {
842
+ checkRunMap: null,
843
+ checksApiAvailable: false,
844
+ checkRunsCreated: 0,
845
+ checkRunUrls: [],
846
+ };
847
+ }
848
+ else {
849
+ console.error('❌ Failed to create GitHub check runs:', error);
850
+ return {
851
+ checkRunMap: null,
852
+ checksApiAvailable: false,
853
+ checkRunsCreated: 0,
854
+ checkRunUrls: [],
855
+ };
856
+ }
857
+ }
858
+ }
859
+ /**
860
+ * Update GitHub check runs to in-progress status
861
+ */
862
+ async function updateChecksInProgress(octokit, owner, repo, checkRunMap) {
863
+ if (!checkRunMap)
864
+ return;
865
+ const checkService = new github_check_service_1.GitHubCheckService(octokit);
866
+ for (const [checkName, checkRun] of checkRunMap) {
867
+ try {
868
+ await checkService.updateCheckRunInProgress(owner, repo, checkRun.id, {
869
+ title: `Analyzing with ${checkName}...`,
870
+ summary: `AI-powered analysis is in progress for ${checkName} check.`,
871
+ });
872
+ console.log(`🔄 Updated ${checkName} check to in-progress status`);
873
+ }
874
+ catch (error) {
875
+ console.error(`❌ Failed to update ${checkName} check to in-progress:`, error);
876
+ }
877
+ }
878
+ }
879
+ /**
880
+ * Complete GitHub check runs with results
881
+ */
882
+ async function completeGitHubChecks(octokit, owner, repo, checkRunMap, reviewSummary, config) {
883
+ if (!checkRunMap)
884
+ return;
885
+ const checkService = new github_check_service_1.GitHubCheckService(octokit);
886
+ const perCheckMode = config?.output?.github_checks?.per_check !== false;
887
+ console.log(`🏁 Completing ${checkRunMap.size} GitHub check runs...`);
888
+ if (perCheckMode && !checkRunMap.has('combined')) {
889
+ // Per-check mode: complete individual check runs
890
+ await completeIndividualChecks(checkService, owner, repo, checkRunMap, reviewSummary, config);
891
+ }
892
+ else {
893
+ // Combined mode: complete single check run with all results
894
+ await completeCombinedCheck(checkService, owner, repo, checkRunMap, reviewSummary, config);
895
+ }
896
+ }
897
+ /**
898
+ * Complete individual GitHub check runs
899
+ */
900
+ async function completeIndividualChecks(checkService, owner, repo, checkRunMap, reviewSummary, config) {
901
+ // Group issues by check name
902
+ const issuesByCheck = new Map();
903
+ // Initialize empty arrays for all checks
904
+ for (const checkName of checkRunMap.keys()) {
905
+ issuesByCheck.set(checkName, []);
906
+ }
907
+ // Group issues by their check name (extracted from ruleId prefix)
908
+ for (const issue of reviewSummary.issues || []) {
909
+ if (issue.ruleId && issue.ruleId.includes('/')) {
910
+ const checkName = issue.ruleId.split('/')[0];
911
+ if (issuesByCheck.has(checkName)) {
912
+ issuesByCheck.get(checkName).push(issue);
913
+ }
914
+ }
915
+ }
916
+ for (const [checkName, checkRun] of checkRunMap) {
917
+ try {
918
+ const checkIssues = issuesByCheck.get(checkName) || [];
919
+ const checkConfig = config.checks?.[checkName];
920
+ // Evaluate failure conditions for this specific check
921
+ const failureResults = await evaluateCheckFailureConditions(config, checkConfig, checkName, checkIssues);
922
+ await checkService.completeCheckRun(owner, repo, checkRun.id, checkName, failureResults, checkIssues);
923
+ console.log(`✅ Completed ${checkName} check with ${checkIssues.length} issues`);
924
+ }
925
+ catch (error) {
926
+ console.error(`❌ Failed to complete ${checkName} check:`, error);
927
+ await markCheckAsFailed(checkService, owner, repo, checkRun.id, checkName, error);
928
+ }
929
+ }
930
+ }
931
+ /**
932
+ * Complete combined GitHub check run
933
+ */
934
+ async function completeCombinedCheck(checkService, owner, repo, checkRunMap, reviewSummary, config) {
935
+ const combinedCheckRun = checkRunMap.get('combined');
936
+ if (!combinedCheckRun)
937
+ return;
938
+ try {
939
+ // Use all issues for the combined check
940
+ const allIssues = reviewSummary.issues || [];
941
+ // Evaluate global failure conditions
942
+ const failureResults = await evaluateGlobalFailureConditions(config, allIssues);
943
+ await checkService.completeCheckRun(owner, repo, combinedCheckRun.id, 'Code Review', failureResults, allIssues);
944
+ console.log(`✅ Completed combined check with ${allIssues.length} issues`);
945
+ }
946
+ catch (error) {
947
+ console.error(`❌ Failed to complete combined check:`, error);
948
+ await markCheckAsFailed(checkService, owner, repo, combinedCheckRun.id, 'Code Review', error);
949
+ }
950
+ }
951
+ /**
952
+ * Evaluate failure conditions for a specific check
953
+ */
954
+ async function evaluateCheckFailureConditions(config, checkConfig, checkName, checkIssues) {
955
+ const failureResults = [];
956
+ const criticalIssues = checkIssues.filter(issue => issue.severity === 'critical').length;
957
+ const errorIssues = checkIssues.filter(issue => issue.severity === 'error').length;
958
+ // Check global fail_if condition
959
+ if (config.fail_if) {
960
+ try {
961
+ const evaluator = new failure_condition_evaluator_1.FailureConditionEvaluator();
962
+ const reviewSummary = {
963
+ issues: [],
964
+ suggestions: [],
965
+ metadata: {
966
+ totalIssues: checkIssues.length,
967
+ criticalIssues,
968
+ errorIssues,
969
+ warningIssues: 0,
970
+ infoIssues: 0,
971
+ },
972
+ };
973
+ const shouldFail = await evaluator.evaluateSimpleCondition(checkName, 'legacy', 'legacy', reviewSummary, config.fail_if);
974
+ if (shouldFail) {
975
+ failureResults.push({
976
+ conditionName: 'global_fail_if',
977
+ failed: true,
978
+ severity: 'error',
979
+ expression: config.fail_if,
980
+ message: 'Global failure condition met',
981
+ });
982
+ }
983
+ }
984
+ catch (error) {
985
+ console.error('❌ Failed to evaluate global fail_if condition:', config.fail_if, error);
986
+ }
987
+ }
988
+ // Check check-specific fail_if condition
989
+ if (checkConfig?.fail_if) {
990
+ try {
991
+ const evaluator = new failure_condition_evaluator_1.FailureConditionEvaluator();
992
+ const reviewSummary = {
993
+ issues: [],
994
+ suggestions: [],
995
+ metadata: {
996
+ totalIssues: checkIssues.length,
997
+ criticalIssues,
998
+ errorIssues,
999
+ warningIssues: 0,
1000
+ infoIssues: 0,
1001
+ },
1002
+ };
1003
+ const shouldFail = await evaluator.evaluateSimpleCondition(checkName, 'legacy', 'legacy', reviewSummary, checkConfig.fail_if);
1004
+ if (shouldFail) {
1005
+ failureResults.push({
1006
+ conditionName: `${checkName}_fail_if`,
1007
+ failed: true,
1008
+ severity: 'error',
1009
+ expression: checkConfig.fail_if,
1010
+ message: `Check ${checkName} failure condition met`,
1011
+ });
1012
+ }
1013
+ }
1014
+ catch (error) {
1015
+ console.error('❌ Failed to evaluate check-specific fail_if condition:', checkConfig.fail_if, error);
1016
+ }
1017
+ }
1018
+ return failureResults;
1019
+ }
1020
+ /**
1021
+ * Evaluate global failure conditions for combined check
1022
+ */
1023
+ async function evaluateGlobalFailureConditions(config, allIssues) {
1024
+ const failureResults = [];
1025
+ const criticalIssues = allIssues.filter(issue => issue.severity === 'critical').length;
1026
+ const errorIssues = allIssues.filter(issue => issue.severity === 'error').length;
1027
+ // Check global fail_if condition
1028
+ if (config.fail_if) {
1029
+ try {
1030
+ const evaluator = new failure_condition_evaluator_1.FailureConditionEvaluator();
1031
+ const reviewSummary = {
1032
+ issues: [],
1033
+ suggestions: [],
1034
+ metadata: {
1035
+ totalIssues: allIssues.length,
1036
+ criticalIssues,
1037
+ errorIssues,
1038
+ warningIssues: 0,
1039
+ infoIssues: 0,
1040
+ },
1041
+ };
1042
+ const shouldFail = await evaluator.evaluateSimpleCondition('combined', 'legacy', 'legacy', reviewSummary, config.fail_if);
1043
+ if (shouldFail) {
1044
+ failureResults.push({
1045
+ conditionName: 'global_fail_if',
1046
+ failed: true,
1047
+ severity: 'error',
1048
+ expression: config.fail_if,
1049
+ message: 'Global failure condition met',
1050
+ });
1051
+ }
1052
+ }
1053
+ catch (error) {
1054
+ console.error('❌ Failed to evaluate global fail_if condition:', config.fail_if, error);
1055
+ }
1056
+ }
1057
+ return failureResults;
1058
+ }
1059
+ /**
1060
+ * Mark a check as failed due to execution error
1061
+ */
1062
+ async function markCheckAsFailed(checkService, owner, repo, checkRunId, checkName, error) {
1063
+ try {
1064
+ await checkService.completeCheckRun(owner, repo, checkRunId, checkName, [], [], error instanceof Error ? error.message : 'Unknown error occurred');
1065
+ }
1066
+ catch (finalError) {
1067
+ console.error(`❌ Failed to mark ${checkName} check as failed:`, finalError);
1068
+ }
1069
+ }
1070
+ /**
1071
+ * Handle PR review using Visor config but with proper GitHub API PR diff analysis
1072
+ */
1073
+ async function handlePullRequestVisorMode(inputs, _context, octokit, _authType) {
1074
+ const owner = inputs.owner || process.env.GITHUB_REPOSITORY_OWNER;
1075
+ const repo = inputs.repo || process.env.GITHUB_REPOSITORY?.split('/')[1];
1076
+ if (!owner || !repo) {
1077
+ console.error('❌ Missing required GitHub parameters for PR analysis');
1078
+ (0, core_1.setFailed)('Missing required GitHub parameters');
1079
+ return;
1080
+ }
1081
+ // Use the provided authenticated Octokit instance
1082
+ const prDetector = new pr_detector_1.PRDetector(octokit, inputs.debug === 'true');
1083
+ // Convert GitHub context to our format
1084
+ const eventContext = {
1085
+ event_name: process.env.GITHUB_EVENT_NAME || 'unknown',
1086
+ repository: {
1087
+ owner: { login: owner },
1088
+ name: repo,
1089
+ },
1090
+ event: process.env.GITHUB_CONTEXT ? JSON.parse(process.env.GITHUB_CONTEXT).event : undefined,
1091
+ payload: process.env.GITHUB_CONTEXT ? JSON.parse(process.env.GITHUB_CONTEXT) : {},
1092
+ };
1093
+ // Use robust PR detection
1094
+ const prResult = await prDetector.detectPRNumber(eventContext, owner, repo);
1095
+ const action = eventContext.event?.action;
1096
+ if (!prResult.prNumber) {
1097
+ console.error(`❌ No PR found using any detection strategy: ${prResult.details || 'Unknown reason'}`);
1098
+ if (inputs.debug === 'true') {
1099
+ console.error('Available detection strategies:');
1100
+ prDetector.getDetectionStrategies().forEach(strategy => console.error(` ${strategy}`));
1101
+ }
1102
+ (0, core_1.setFailed)('No PR number found');
1103
+ return;
1104
+ }
1105
+ const prNumber = prResult.prNumber;
1106
+ console.log(`✅ Found PR #${prNumber} using ${prResult.source} (confidence: ${prResult.confidence})`);
1107
+ if (prResult.details) {
1108
+ console.log(` Details: ${prResult.details}`);
1109
+ }
1110
+ console.log(`🔍 Analyzing PR #${prNumber} using Visor config (action: ${action})`);
1111
+ try {
1112
+ // Use the existing PR analysis infrastructure but with Visor config
1113
+ const analyzer = new pr_analyzer_1.PRAnalyzer(octokit);
1114
+ const reviewer = new reviewer_1.PRReviewer(octokit);
1115
+ // Load Visor config
1116
+ const configManager = new config_1.ConfigManager();
1117
+ let config;
1118
+ const configPath = inputs['config-path'];
1119
+ if (configPath) {
1120
+ try {
1121
+ config = await configManager.loadConfig(configPath);
1122
+ console.log(`📋 Loaded Visor config from: ${configPath}`);
1123
+ }
1124
+ catch (error) {
1125
+ console.error(`⚠️ Could not load config from ${configPath}:`, error);
1126
+ config = await configManager.findAndLoadConfig();
1127
+ }
1128
+ }
1129
+ else {
1130
+ // Try to find and load config from default locations (.visor.yaml)
1131
+ config = await configManager.findAndLoadConfig();
1132
+ const hasCustomConfig = config.checks && Object.keys(config.checks).length > 0;
1133
+ if (hasCustomConfig) {
1134
+ console.log(`📋 Loaded Visor config from default location (.visor.yaml)`);
1135
+ }
1136
+ else {
1137
+ console.log(`📋 Using default Visor configuration`);
1138
+ }
1139
+ }
1140
+ // Extract checks from config
1141
+ const configChecks = Object.keys(config.checks || {});
1142
+ const checksToRun = configChecks.length > 0 ? configChecks : ['security', 'performance', 'style', 'architecture'];
1143
+ console.log(`🔧 Running checks: ${checksToRun.join(', ')}`);
1144
+ // Fetch PR diff using GitHub API
1145
+ const prInfo = await analyzer.fetchPRDiff(owner, repo, prNumber);
1146
+ console.log(`📄 Found ${prInfo.files.length} changed files`);
1147
+ if (prInfo.files.length === 0) {
1148
+ console.log('⚠️ No files changed in this PR - skipping review');
1149
+ // Set basic outputs
1150
+ (0, core_1.setOutput)('auto-review-completed', 'true');
1151
+ (0, core_1.setOutput)('issues-found', '0');
1152
+ (0, core_1.setOutput)('pr-action', action || 'unknown');
1153
+ (0, core_1.setOutput)('incremental-analysis', action === 'synchronize' ? 'true' : 'false');
1154
+ return;
1155
+ }
1156
+ // Create a custom review options with Visor config
1157
+ const reviewOptions = {
1158
+ debug: inputs.debug === 'true',
1159
+ config: config,
1160
+ checks: checksToRun,
1161
+ parallelExecution: true,
1162
+ };
1163
+ // Fetch PR info to get commit SHA for metadata
1164
+ const { data: pullRequest } = await octokit.rest.pulls.get({
1165
+ owner,
1166
+ repo,
1167
+ pull_number: prNumber,
1168
+ });
1169
+ // Create GitHub check runs for each configured check
1170
+ const checkResults = await createGitHubChecks(octokit, inputs, owner, repo, pullRequest.head.sha, checksToRun, config);
1171
+ // Update checks to in-progress status
1172
+ await updateChecksInProgress(octokit, owner, repo, checkResults.checkRunMap);
1173
+ // Perform the review
1174
+ console.log('🤖 Starting parallel AI review with Visor config...');
1175
+ const review = await reviewer.reviewPR(owner, repo, prNumber, prInfo, reviewOptions);
1176
+ // Update the review summary to show correct checks executed
1177
+ if (review.debug) {
1178
+ review.debug.checksExecuted = checksToRun;
1179
+ review.debug.parallelExecution = true;
1180
+ }
1181
+ // Complete GitHub check runs with results
1182
+ if (checkResults) {
1183
+ await completeGitHubChecks(octokit, owner, repo, checkResults.checkRunMap, review, config);
1184
+ }
1185
+ // Post comment using group-based comment separation
1186
+ const commentId = `visor-config-review-${prNumber}`;
1187
+ await reviewer.postReviewComment(owner, repo, prNumber, review, {
1188
+ ...reviewOptions,
1189
+ commentId,
1190
+ triggeredBy: `visor-config-${action}`,
1191
+ commitSha: pullRequest.head?.sha,
1192
+ });
1193
+ console.log('✅ Posted Visor config-based review comment');
1194
+ // Check for API errors in the review issues
1195
+ const apiErrors = review.issues.filter(issue => issue.file === 'system' &&
1196
+ issue.severity === 'critical' &&
1197
+ (issue.message.includes('API rate limit') ||
1198
+ issue.message.includes('403') ||
1199
+ issue.message.includes('401') ||
1200
+ issue.message.includes('authentication') ||
1201
+ issue.message.includes('API key')));
1202
+ if (apiErrors.length > 0) {
1203
+ console.error('🚨 Critical API errors detected in review:');
1204
+ apiErrors.forEach(error => {
1205
+ console.error(` - ${error.message}`);
1206
+ });
1207
+ // Check if we should fail on API errors
1208
+ const failOnApiError = inputs['fail-on-api-error'] === 'true';
1209
+ if (failOnApiError) {
1210
+ (0, core_1.setFailed)(`Critical API errors detected: ${apiErrors.length} authentication/rate limit issues found. Please check your API credentials.`);
1211
+ return;
1212
+ }
1213
+ }
1214
+ // Set outputs
1215
+ (0, core_1.setOutput)('auto-review-completed', 'true');
1216
+ (0, core_1.setOutput)('issues-found', (0, reviewer_1.calculateTotalIssues)(review.issues).toString());
1217
+ (0, core_1.setOutput)('pr-action', action || 'unknown');
1218
+ (0, core_1.setOutput)('incremental-analysis', action === 'synchronize' ? 'true' : 'false');
1219
+ (0, core_1.setOutput)('visor-config-used', 'true');
1220
+ (0, core_1.setOutput)('checks-executed', checksToRun.join(','));
1221
+ (0, core_1.setOutput)('api-errors-found', apiErrors.length.toString());
1222
+ // Set GitHub check run outputs
1223
+ (0, core_1.setOutput)('checks-api-available', checkResults.checksApiAvailable.toString());
1224
+ (0, core_1.setOutput)('check-runs-created', checkResults.checkRunsCreated.toString());
1225
+ (0, core_1.setOutput)('check-runs-urls', checkResults.checkRunUrls.join(','));
1226
+ }
1227
+ catch (error) {
1228
+ console.error('❌ Error in Visor PR analysis:', error);
1229
+ (0, core_1.setFailed)(error instanceof Error ? error.message : 'Visor PR analysis failed');
1230
+ }
1231
+ }
1232
+ /**
1233
+ * Detect if we're in a PR context for any GitHub event type
1234
+ */
1235
+ async function detectPRContext(inputs, context, octokit) {
1236
+ try {
1237
+ const owner = inputs.owner || process.env.GITHUB_REPOSITORY_OWNER;
1238
+ const repo = inputs.repo || process.env.GITHUB_REPOSITORY?.split('/')[1];
1239
+ if (!owner || !repo) {
1240
+ return false;
1241
+ }
1242
+ // Use the provided authenticated Octokit instance
1243
+ const prDetector = new pr_detector_1.PRDetector(octokit, inputs.debug === 'true');
1244
+ // Convert GitHub context to our format
1245
+ const eventContext = {
1246
+ event_name: context.event_name,
1247
+ repository: {
1248
+ owner: { login: owner },
1249
+ name: repo,
1250
+ },
1251
+ event: context.event,
1252
+ payload: context.payload || {},
1253
+ };
1254
+ const prResult = await prDetector.detectPRNumber(eventContext, owner, repo);
1255
+ return prResult.prNumber !== null;
1256
+ }
1257
+ catch (error) {
1258
+ console.error('Error detecting PR context:', error);
1259
+ return false;
1260
+ }
1261
+ }
1262
+ if (require.main === module) {
1263
+ run();
1264
+ }
1265
+ //# sourceMappingURL=index.js.map