-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
201 lines (172 loc) · 7.35 KB
/
Copy pathserver.js
File metadata and controls
201 lines (172 loc) · 7.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
require('dotenv').config();
const express = require('express');
const { createNodeMiddleware, createProbot } = require('probot');
const {
getInstallation,
saveInstallation,
deleteInstallation,
recordUsage,
getPRMetadata,
updatePRMetadata
} = require('./lib/db');
const { decrypt, encrypt } = require('./lib/crypto');
const { performReview } = require('./lib/reviewer');
const { sendReviewEmail, sendApprovalEmail } = require('./lib/email-service');
const helmet = require('helmet');
const path = require('path');
const axios = require('axios');
const queryString = require('query-string');
const app = express();
app.use(helmet({
contentSecurityPolicy: false // Allow styles/scripts for our UI
}));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
const probot = createProbot();
// GitHub App Webhook logic
probot.on(['pull_request.opened', 'pull_request.synchronize'], async (context) => {
const installationId = context.payload.installation.id;
const pr = context.payload.pull_request;
const repo = context.payload.repository.full_name;
console.log(`Received PR event for ${repo} (Installation: ${installationId})`);
try {
const config = await getInstallation(installationId.toString());
if (!config || !config.gemini_key) {
console.log("No stored key found.");
return context.octokit.issues.createComment(context.issue({
body: `⚠️ **Gemini API Key not configured.** Please configure it [here](${process.env.APP_URL}/login?installation_id=${installationId}).`
}));
}
// Noise Suppression Check: If PR is already approved, only run for high/critical findings
const metadata = await getPRMetadata(repo, pr.number);
const isApproved = metadata && metadata.approval_status === 'approved';
if (isApproved) {
console.log(`PR ${repo} #${pr.number} is already approved. Running in Noise-Suppression mode.`);
}
const geminiKey = decrypt(config.gemini_key);
const severity = isApproved ? 'High' : (config.severity || 'Medium'); // Auto-escalate threshold post-approval
// Get the Diff
const { data: diff } = await context.octokit.pulls.get({
owner: context.payload.repository.owner.login,
repo: context.payload.repository.name,
pull_number: pr.number,
mediaType: { format: 'diff' }
});
// Perform Review
const reviewResult = await performReview({
patch: diff,
geminiKey,
severity,
octokit: context.octokit,
repo,
prNumber: pr.number
});
// Update Metadata
await updatePRMetadata(repo, pr.number, {
lastSha: pr.head.sha,
findingHashes: JSON.stringify((reviewResult.findings || []).map(f => `${f.file}:${f.issue}`))
});
// Send Email Notification
if (config.email) {
await sendReviewEmail({
to: config.email,
repo,
prNumber: pr.number,
summary: reviewResult.summary,
findingsCount: (reviewResult.findings || []).length,
isUpdate: context.name === 'pull_request_synchronize'
});
}
// Record usage
if (reviewResult && reviewResult.totalTokens) {
await recordUsage(installationId.toString(), reviewResult.totalTokens);
console.log(`Recorded usage for ${installationId}: ${reviewResult.totalTokens} tokens`);
}
} catch (error) {
console.error("Review process failed:", error);
}
});
// Approval Tracking
probot.on('pull_request_review', async (context) => {
if (context.payload.review.state === 'approved') {
const repo = context.payload.repository.full_name;
const prNumber = context.payload.pull_request.number;
const installationId = context.payload.installation.id;
console.log(`PR ${repo} #${prNumber} approved! Updating metadata.`);
await updatePRMetadata(repo, prNumber, { approvalStatus: 'approved' });
// Trigger Closure Email
const config = await getInstallation(installationId.toString());
if (config && config.email) {
await sendApprovalEmail({
to: config.email,
repo,
prNumber,
summary: "AI review has been verified and PR is marked as safe to merge."
});
}
}
});
// Purge data on uninstall
probot.on('installation.deleted', async (context) => {
const installationId = context.payload.installation.id;
console.log(`Purging data for uninstalled installation: ${installationId}`);
try {
await deleteInstallation(installationId.toString());
} catch (error) {
console.error("Failed to purge installation data:", error);
}
});
app.use(createNodeMiddleware(probot, { probot }));
// --- Premium Flow Routes ---
// 1. Initial login point after installation
app.get('/login', (req, res) => {
const { installation_id } = req.query;
const authorizeUrl = `https://github.com/login/oauth/authorize?${queryString.stringify({
client_id: process.env.GITHUB_CLIENT_ID,
redirect_uri: process.env.OAUTH_CALLBACK_URL || `${process.env.APP_URL}/oauth/callback`,
state: installation_id,
scope: 'repo' // Optional, depending on if you need user-level access
})}`;
res.redirect(authorizeUrl);
});
// 2. OAuth Callback
app.get('/oauth/callback', async (req, res) => {
const { code, state: installation_id } = req.query;
try {
const response = await axios.post('https://github.com/login/oauth/access_token', {
client_id: process.env.GITHUB_CLIENT_ID,
client_secret: process.env.GITHUB_CLIENT_SECRET,
code
}, {
headers: { Accept: 'application/json' }
});
const accessToken = response.data.access_token;
// In a real app, you'd verify the user here and set a session.
// For now, we'll redirect to config with the installation_id.
const configUrl = process.env.INSTALLATION_CALLBACK_URL || '/config.html';
res.redirect(`${configUrl}?installation_id=${installation_id}`);
} catch (error) {
console.error("OAuth failed:", error);
res.status(500).send("Authentication failed");
}
});
// 3. Config API
app.post('/api/config', async (req, res) => {
const { installation_id, gemini_key, severity, email } = req.body;
if (!installation_id || !gemini_key) {
return res.status(400).json({ error: "Missing required fields" });
}
try {
const encryptedKey = encrypt(gemini_key);
await saveInstallation(installation_id, encryptedKey, severity, email);
res.json({ message: "Configuration saved successfully! Gemini is now enabled with professional notifications." });
} catch (e) {
console.error(e);
res.status(500).json({ error: "Failed to save configuration" });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});