@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.
- package/README.md +1240 -0
- package/action.yml +142 -0
- package/defaults/.visor.yaml +184 -0
- package/dist/action-cli-bridge.d.ts +104 -0
- package/dist/action-cli-bridge.d.ts.map +1 -0
- package/dist/action-cli-bridge.js +372 -0
- package/dist/action-cli-bridge.js.map +1 -0
- package/dist/ai-review-service.d.ts +84 -0
- package/dist/ai-review-service.d.ts.map +1 -0
- package/dist/ai-review-service.js +674 -0
- package/dist/ai-review-service.js.map +1 -0
- package/dist/check-execution-engine.d.ts +165 -0
- package/dist/check-execution-engine.d.ts.map +1 -0
- package/dist/check-execution-engine.js +1172 -0
- package/dist/check-execution-engine.js.map +1 -0
- package/dist/cli-main.d.ts +6 -0
- package/dist/cli-main.d.ts.map +1 -0
- package/dist/cli-main.js +247 -0
- package/dist/cli-main.js.map +1 -0
- package/dist/cli.d.ts +47 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +224 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +10 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +53 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +63 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +369 -0
- package/dist/config.js.map +1 -0
- package/dist/dependency-resolver.d.ts +54 -0
- package/dist/dependency-resolver.d.ts.map +1 -0
- package/dist/dependency-resolver.js +163 -0
- package/dist/dependency-resolver.js.map +1 -0
- package/dist/event-mapper.d.ts +125 -0
- package/dist/event-mapper.d.ts.map +1 -0
- package/dist/event-mapper.js +311 -0
- package/dist/event-mapper.js.map +1 -0
- package/dist/failure-condition-evaluator.d.ts +81 -0
- package/dist/failure-condition-evaluator.d.ts.map +1 -0
- package/dist/failure-condition-evaluator.js +445 -0
- package/dist/failure-condition-evaluator.js.map +1 -0
- package/dist/git-repository-analyzer.d.ts +45 -0
- package/dist/git-repository-analyzer.d.ts.map +1 -0
- package/dist/git-repository-analyzer.js +285 -0
- package/dist/git-repository-analyzer.js.map +1 -0
- package/dist/github-check-service.d.ts +104 -0
- package/dist/github-check-service.d.ts.map +1 -0
- package/dist/github-check-service.js +382 -0
- package/dist/github-check-service.js.map +1 -0
- package/dist/github-comments.d.ts +109 -0
- package/dist/github-comments.d.ts.map +1 -0
- package/dist/github-comments.js +289 -0
- package/dist/github-comments.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1265 -0
- package/dist/index.js.map +1 -0
- package/dist/output-formatters.d.ts +66 -0
- package/dist/output-formatters.d.ts.map +1 -0
- package/dist/output-formatters.js +624 -0
- package/dist/output-formatters.js.map +1 -0
- package/dist/pr-analyzer.d.ts +47 -0
- package/dist/pr-analyzer.d.ts.map +1 -0
- package/dist/pr-analyzer.js +194 -0
- package/dist/pr-analyzer.js.map +1 -0
- package/dist/pr-detector.d.ts +78 -0
- package/dist/pr-detector.d.ts.map +1 -0
- package/dist/pr-detector.js +357 -0
- package/dist/pr-detector.js.map +1 -0
- package/dist/providers/ai-check-provider.d.ts +40 -0
- package/dist/providers/ai-check-provider.d.ts.map +1 -0
- package/dist/providers/ai-check-provider.js +416 -0
- package/dist/providers/ai-check-provider.js.map +1 -0
- package/dist/providers/check-provider-registry.d.ts +67 -0
- package/dist/providers/check-provider-registry.d.ts.map +1 -0
- package/dist/providers/check-provider-registry.js +138 -0
- package/dist/providers/check-provider-registry.js.map +1 -0
- package/dist/providers/check-provider.interface.d.ts +78 -0
- package/dist/providers/check-provider.interface.d.ts.map +1 -0
- package/dist/providers/check-provider.interface.js +11 -0
- package/dist/providers/check-provider.interface.js.map +1 -0
- package/dist/providers/index.d.ts +10 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +19 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/script-check-provider.d.ts +20 -0
- package/dist/providers/script-check-provider.d.ts.map +1 -0
- package/dist/providers/script-check-provider.js +163 -0
- package/dist/providers/script-check-provider.js.map +1 -0
- package/dist/providers/tool-check-provider.d.ts +19 -0
- package/dist/providers/tool-check-provider.d.ts.map +1 -0
- package/dist/providers/tool-check-provider.js +125 -0
- package/dist/providers/tool-check-provider.js.map +1 -0
- package/dist/providers/webhook-check-provider.d.ts +21 -0
- package/dist/providers/webhook-check-provider.d.ts.map +1 -0
- package/dist/providers/webhook-check-provider.js +173 -0
- package/dist/providers/webhook-check-provider.js.map +1 -0
- package/dist/reviewer.d.ts +88 -0
- package/dist/reviewer.d.ts.map +1 -0
- package/dist/reviewer.js +760 -0
- package/dist/reviewer.js.map +1 -0
- package/dist/types/cli.d.ts +41 -0
- package/dist/types/cli.d.ts.map +1 -0
- package/dist/types/cli.js +3 -0
- package/dist/types/cli.js.map +1 -0
- package/dist/types/config.d.ts +315 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +6 -0
- package/dist/types/config.js.map +1 -0
- package/dist/utils/env-resolver.d.ts +38 -0
- package/dist/utils/env-resolver.d.ts.map +1 -0
- package/dist/utils/env-resolver.js +130 -0
- package/dist/utils/env-resolver.js.map +1 -0
- 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
|