archicore 0.3.0 → 0.3.2
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 +2267 -374
- package/dist/cli/commands/interactive.js +83 -23
- package/dist/cli/commands/projects.js +3 -3
- package/dist/cli/ui/prompt.d.ts +4 -0
- package/dist/cli/ui/prompt.js +22 -0
- package/dist/cli/utils/config.js +2 -2
- package/dist/cli/utils/upload-utils.js +65 -18
- package/dist/code-index/ast-parser.d.ts +4 -0
- package/dist/code-index/ast-parser.js +42 -0
- package/dist/code-index/index.d.ts +21 -1
- package/dist/code-index/index.js +45 -1
- package/dist/code-index/source-map-extractor.d.ts +71 -0
- package/dist/code-index/source-map-extractor.js +194 -0
- package/dist/gitlab/gitlab-service.d.ts +162 -0
- package/dist/gitlab/gitlab-service.js +652 -0
- package/dist/gitlab/index.d.ts +8 -0
- package/dist/gitlab/index.js +8 -0
- package/dist/server/config/passport.d.ts +14 -0
- package/dist/server/config/passport.js +86 -0
- package/dist/server/index.js +52 -10
- package/dist/server/middleware/api-auth.d.ts +2 -2
- package/dist/server/middleware/api-auth.js +21 -2
- package/dist/server/middleware/csrf.d.ts +23 -0
- package/dist/server/middleware/csrf.js +96 -0
- package/dist/server/routes/auth.d.ts +2 -2
- package/dist/server/routes/auth.js +204 -5
- package/dist/server/routes/device-auth.js +2 -2
- package/dist/server/routes/gitlab.d.ts +12 -0
- package/dist/server/routes/gitlab.js +528 -0
- package/dist/server/routes/oauth.d.ts +6 -0
- package/dist/server/routes/oauth.js +198 -0
- package/dist/server/services/audit-service.d.ts +1 -1
- package/dist/server/services/auth-service.d.ts +13 -1
- package/dist/server/services/auth-service.js +108 -7
- package/dist/server/services/email-service.d.ts +63 -0
- package/dist/server/services/email-service.js +586 -0
- package/dist/server/utils/disposable-email-domains.d.ts +14 -0
- package/dist/server/utils/disposable-email-domains.js +192 -0
- package/dist/types/api.d.ts +98 -0
- package/dist/types/gitlab.d.ts +245 -0
- package/dist/types/gitlab.js +11 -0
- package/package.json +12 -4
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitLab Integration Routes for ArchiCore
|
|
3
|
+
*
|
|
4
|
+
* Provides API endpoints for:
|
|
5
|
+
* - Managing GitLab instances (add, remove, list)
|
|
6
|
+
* - Listing and connecting repositories
|
|
7
|
+
* - Branch selection
|
|
8
|
+
* - Webhooks for auto-analysis
|
|
9
|
+
*/
|
|
10
|
+
import { Router } from 'express';
|
|
11
|
+
import { mkdir } from 'fs/promises';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import AdmZip from 'adm-zip';
|
|
14
|
+
import { GitLabService } from '../../gitlab/gitlab-service.js';
|
|
15
|
+
import { ProjectService } from '../services/project-service.js';
|
|
16
|
+
import { AuthService } from '../services/auth-service.js';
|
|
17
|
+
import { authMiddleware } from './auth.js';
|
|
18
|
+
import { Logger } from '../../utils/logger.js';
|
|
19
|
+
export const gitlabRouter = Router();
|
|
20
|
+
// Services
|
|
21
|
+
const gitlabService = new GitLabService();
|
|
22
|
+
const projectService = new ProjectService();
|
|
23
|
+
const authService = AuthService.getInstance();
|
|
24
|
+
// ===== INSTANCE MANAGEMENT =====
|
|
25
|
+
/**
|
|
26
|
+
* POST /api/gitlab/instances
|
|
27
|
+
* Add a new GitLab instance connection
|
|
28
|
+
*/
|
|
29
|
+
gitlabRouter.post('/instances', authMiddleware, async (req, res) => {
|
|
30
|
+
try {
|
|
31
|
+
if (!req.user) {
|
|
32
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const { instanceUrl, accessToken, name, rejectUnauthorizedSSL } = req.body;
|
|
36
|
+
if (!instanceUrl || !accessToken) {
|
|
37
|
+
res.status(400).json({ error: 'instanceUrl and accessToken are required' });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const instance = await gitlabService.addInstance(req.user.id, instanceUrl, accessToken, { name, rejectUnauthorizedSSL });
|
|
41
|
+
res.json({
|
|
42
|
+
success: true,
|
|
43
|
+
instance: {
|
|
44
|
+
id: instance.id,
|
|
45
|
+
name: instance.name,
|
|
46
|
+
instanceUrl: instance.instanceUrl,
|
|
47
|
+
gitlabUsername: instance.gitlabUsername,
|
|
48
|
+
tokenScopes: instance.tokenScopes,
|
|
49
|
+
createdAt: instance.createdAt
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
Logger.error('Add GitLab instance error:', error);
|
|
55
|
+
const message = error instanceof Error ? error.message : 'Failed to add GitLab instance';
|
|
56
|
+
res.status(500).json({ error: message });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
/**
|
|
60
|
+
* GET /api/gitlab/instances
|
|
61
|
+
* List all GitLab instances for current user
|
|
62
|
+
*/
|
|
63
|
+
gitlabRouter.get('/instances', authMiddleware, async (req, res) => {
|
|
64
|
+
try {
|
|
65
|
+
if (!req.user) {
|
|
66
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const instances = await gitlabService.getInstances(req.user.id);
|
|
70
|
+
res.json({
|
|
71
|
+
instances: instances.map(i => ({
|
|
72
|
+
id: i.id,
|
|
73
|
+
name: i.name,
|
|
74
|
+
instanceUrl: i.instanceUrl,
|
|
75
|
+
gitlabUsername: i.gitlabUsername,
|
|
76
|
+
tokenScopes: i.tokenScopes,
|
|
77
|
+
createdAt: i.createdAt,
|
|
78
|
+
lastUsedAt: i.lastUsedAt
|
|
79
|
+
}))
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
Logger.error('List GitLab instances error:', error);
|
|
84
|
+
res.status(500).json({ error: 'Failed to list GitLab instances' });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
/**
|
|
88
|
+
* DELETE /api/gitlab/instances/:id
|
|
89
|
+
* Remove a GitLab instance connection
|
|
90
|
+
*/
|
|
91
|
+
gitlabRouter.delete('/instances/:id', authMiddleware, async (req, res) => {
|
|
92
|
+
try {
|
|
93
|
+
if (!req.user) {
|
|
94
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const success = await gitlabService.removeInstance(req.user.id, req.params.id);
|
|
98
|
+
if (!success) {
|
|
99
|
+
res.status(404).json({ error: 'GitLab instance not found' });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
res.json({ success: true });
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
Logger.error('Remove GitLab instance error:', error);
|
|
106
|
+
res.status(500).json({ error: 'Failed to remove GitLab instance' });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
// ===== PROJECTS/REPOSITORIES =====
|
|
110
|
+
/**
|
|
111
|
+
* GET /api/gitlab/instances/:instanceId/projects
|
|
112
|
+
* List available projects from a GitLab instance
|
|
113
|
+
*/
|
|
114
|
+
gitlabRouter.get('/instances/:instanceId/projects', authMiddleware, async (req, res) => {
|
|
115
|
+
try {
|
|
116
|
+
if (!req.user) {
|
|
117
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const { search, owned, page, per_page } = req.query;
|
|
121
|
+
const projects = await gitlabService.listProjects(req.user.id, req.params.instanceId, {
|
|
122
|
+
search: search,
|
|
123
|
+
owned: owned === 'true',
|
|
124
|
+
page: page ? parseInt(page, 10) : undefined,
|
|
125
|
+
perPage: per_page ? parseInt(per_page, 10) : undefined
|
|
126
|
+
});
|
|
127
|
+
// Get connected repositories to mark them
|
|
128
|
+
const connectedRepos = await gitlabService.getConnectedRepositories(req.user.id);
|
|
129
|
+
const connectedProjectIds = new Set(connectedRepos.map(r => r.gitlabProjectId));
|
|
130
|
+
res.json({
|
|
131
|
+
projects: projects.map(p => ({
|
|
132
|
+
id: p.id,
|
|
133
|
+
name: p.name,
|
|
134
|
+
fullPath: p.path_with_namespace,
|
|
135
|
+
description: p.description,
|
|
136
|
+
visibility: p.visibility,
|
|
137
|
+
defaultBranch: p.default_branch,
|
|
138
|
+
webUrl: p.web_url,
|
|
139
|
+
stars: p.star_count,
|
|
140
|
+
forks: p.forks_count,
|
|
141
|
+
lastActivity: p.last_activity_at,
|
|
142
|
+
namespace: p.namespace.name,
|
|
143
|
+
archived: p.archived,
|
|
144
|
+
connected: connectedProjectIds.has(p.id),
|
|
145
|
+
projectId: connectedRepos.find(r => r.gitlabProjectId === p.id)?.projectId
|
|
146
|
+
}))
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
Logger.error('List GitLab projects error:', error);
|
|
151
|
+
const message = error instanceof Error ? error.message : 'Failed to list projects';
|
|
152
|
+
res.status(500).json({ error: message });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
/**
|
|
156
|
+
* GET /api/gitlab/instances/:instanceId/projects/:projectId/branches
|
|
157
|
+
* List branches for a GitLab project
|
|
158
|
+
*/
|
|
159
|
+
gitlabRouter.get('/instances/:instanceId/projects/:projectId/branches', authMiddleware, async (req, res) => {
|
|
160
|
+
try {
|
|
161
|
+
if (!req.user) {
|
|
162
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const projectId = parseInt(req.params.projectId, 10) || req.params.projectId;
|
|
166
|
+
// Get project info for default branch
|
|
167
|
+
const project = await gitlabService.getProject(req.user.id, req.params.instanceId, projectId);
|
|
168
|
+
const branches = await gitlabService.listBranches(req.user.id, req.params.instanceId, projectId);
|
|
169
|
+
res.json({
|
|
170
|
+
branches: branches.map(b => ({
|
|
171
|
+
name: b.name,
|
|
172
|
+
protected: b.protected,
|
|
173
|
+
isDefault: b.name === project.default_branch,
|
|
174
|
+
lastCommit: {
|
|
175
|
+
id: b.commit.short_id,
|
|
176
|
+
title: b.commit.title,
|
|
177
|
+
date: b.commit.created_at
|
|
178
|
+
}
|
|
179
|
+
}))
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
Logger.error('List branches error:', error);
|
|
184
|
+
const message = error instanceof Error ? error.message : 'Failed to list branches';
|
|
185
|
+
res.status(500).json({ error: message });
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
// ===== CONNECTED REPOSITORIES =====
|
|
189
|
+
/**
|
|
190
|
+
* GET /api/gitlab/repositories
|
|
191
|
+
* List all connected GitLab repositories
|
|
192
|
+
*/
|
|
193
|
+
gitlabRouter.get('/repositories', authMiddleware, async (req, res) => {
|
|
194
|
+
try {
|
|
195
|
+
if (!req.user) {
|
|
196
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const repositories = await gitlabService.getConnectedRepositories(req.user.id);
|
|
200
|
+
res.json({
|
|
201
|
+
repositories: repositories.map(r => ({
|
|
202
|
+
id: r.id,
|
|
203
|
+
instanceId: r.instanceId,
|
|
204
|
+
fullPath: r.fullPath,
|
|
205
|
+
name: r.name,
|
|
206
|
+
owner: r.owner,
|
|
207
|
+
visibility: r.visibility,
|
|
208
|
+
projectId: r.projectId,
|
|
209
|
+
autoAnalyze: r.autoAnalyze,
|
|
210
|
+
analyzeMRs: r.analyzeMRs,
|
|
211
|
+
status: r.status,
|
|
212
|
+
lastAnalyzedAt: r.lastAnalyzedAt,
|
|
213
|
+
createdAt: r.createdAt
|
|
214
|
+
}))
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
Logger.error('List connected repositories error:', error);
|
|
219
|
+
res.status(500).json({ error: 'Failed to list repositories' });
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
/**
|
|
223
|
+
* POST /api/gitlab/repositories/connect
|
|
224
|
+
* Connect a GitLab repository to ArchiCore
|
|
225
|
+
*/
|
|
226
|
+
gitlabRouter.post('/repositories/connect', authMiddleware, async (req, res) => {
|
|
227
|
+
try {
|
|
228
|
+
if (!req.user) {
|
|
229
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const { instanceId, projectId, autoAnalyze, analyzeMRs, createProject, projectName, branch, forceReconnect } = req.body;
|
|
233
|
+
if (!instanceId || !projectId) {
|
|
234
|
+
res.status(400).json({ error: 'instanceId and projectId are required' });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
// Connect repository
|
|
238
|
+
const connectedRepo = await gitlabService.connectRepository(req.user.id, instanceId, projectId, { autoAnalyze, analyzeMRs, forceReconnect });
|
|
239
|
+
// Optionally create ArchiCore project
|
|
240
|
+
let archiProjectId;
|
|
241
|
+
if (createProject !== false) {
|
|
242
|
+
try {
|
|
243
|
+
// Download repository
|
|
244
|
+
const project = await gitlabService.getProject(req.user.id, instanceId, projectId);
|
|
245
|
+
const targetBranch = branch || project.default_branch || 'main';
|
|
246
|
+
Logger.progress(`Downloading GitLab repository: ${project.path_with_namespace} (branch: ${targetBranch})`);
|
|
247
|
+
const zipBuffer = await gitlabService.downloadRepository(req.user.id, instanceId, projectId, targetBranch);
|
|
248
|
+
Logger.info(`Downloaded ZIP: ${zipBuffer.length} bytes`);
|
|
249
|
+
// Create projects directory
|
|
250
|
+
const projectsDir = process.env.PROJECTS_DIR || join('.archicore', 'projects');
|
|
251
|
+
await mkdir(projectsDir, { recursive: true });
|
|
252
|
+
// Extract to project directory
|
|
253
|
+
const projectPath = join(projectsDir, project.name);
|
|
254
|
+
await mkdir(projectPath, { recursive: true });
|
|
255
|
+
const zip = new AdmZip(zipBuffer);
|
|
256
|
+
const zipEntries = zip.getEntries();
|
|
257
|
+
Logger.info(`ZIP contains ${zipEntries.length} entries`);
|
|
258
|
+
// Extract files
|
|
259
|
+
let extractedCount = 0;
|
|
260
|
+
for (const entry of zipEntries) {
|
|
261
|
+
if (entry.isDirectory) {
|
|
262
|
+
const dirPath = join(projectPath, entry.entryName);
|
|
263
|
+
await mkdir(dirPath, { recursive: true });
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
const filePath = join(projectPath, entry.entryName);
|
|
267
|
+
const fileDir = join(projectPath, entry.entryName.split('/').slice(0, -1).join('/'));
|
|
268
|
+
await mkdir(fileDir, { recursive: true });
|
|
269
|
+
const content = entry.getData();
|
|
270
|
+
const { writeFile } = await import('fs/promises');
|
|
271
|
+
await writeFile(filePath, content);
|
|
272
|
+
extractedCount++;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
Logger.info(`Extraction complete: ${extractedCount} files extracted`);
|
|
276
|
+
// GitLab creates a folder like "project-branch" inside, find it
|
|
277
|
+
const { readdir, stat } = await import('fs/promises');
|
|
278
|
+
const contents = await readdir(projectPath);
|
|
279
|
+
let actualPath = projectPath;
|
|
280
|
+
if (contents.length === 1) {
|
|
281
|
+
const innerPath = join(projectPath, contents[0]);
|
|
282
|
+
const stats = await stat(innerPath);
|
|
283
|
+
if (stats.isDirectory()) {
|
|
284
|
+
actualPath = innerPath;
|
|
285
|
+
Logger.info(`Using inner path: ${actualPath}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
Logger.success(`Downloaded and extracted to: ${actualPath}`);
|
|
289
|
+
// Check project creation limit
|
|
290
|
+
const usageResult = await authService.checkAndUpdateUsage(req.user.id, 'project');
|
|
291
|
+
if (!usageResult.allowed) {
|
|
292
|
+
res.status(429).json({
|
|
293
|
+
error: 'Project limit reached',
|
|
294
|
+
message: `You have reached your daily project limit (${usageResult.limit})`,
|
|
295
|
+
usage: { used: usageResult.limit, limit: usageResult.limit, remaining: 0 }
|
|
296
|
+
});
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
// Create ArchiCore project
|
|
300
|
+
const archiProject = await projectService.createProject(projectName || project.name, actualPath, req.user.id);
|
|
301
|
+
archiProjectId = archiProject.id;
|
|
302
|
+
// Update connected repo with project ID
|
|
303
|
+
await gitlabService.updateRepositoryProjectId(connectedRepo.id, archiProjectId);
|
|
304
|
+
await gitlabService.updateRepositoryStatus(connectedRepo.id, 'active');
|
|
305
|
+
Logger.info(`Linked ArchiCore project ${archiProjectId} to GitLab repository ${project.path_with_namespace}`);
|
|
306
|
+
}
|
|
307
|
+
catch (e) {
|
|
308
|
+
Logger.warn(`Failed to create ArchiCore project for ${connectedRepo.fullPath}: ${e}`);
|
|
309
|
+
await gitlabService.updateRepositoryStatus(connectedRepo.id, 'error', String(e));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
res.json({
|
|
313
|
+
success: true,
|
|
314
|
+
repository: {
|
|
315
|
+
id: connectedRepo.id,
|
|
316
|
+
fullPath: connectedRepo.fullPath,
|
|
317
|
+
projectId: archiProjectId,
|
|
318
|
+
status: connectedRepo.status
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
Logger.error('Connect GitLab repository error:', error);
|
|
324
|
+
const message = error instanceof Error ? error.message : 'Failed to connect repository';
|
|
325
|
+
res.status(500).json({ error: message });
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
/**
|
|
329
|
+
* DELETE /api/gitlab/repositories/:id
|
|
330
|
+
* Disconnect a GitLab repository
|
|
331
|
+
*/
|
|
332
|
+
gitlabRouter.delete('/repositories/:id', authMiddleware, async (req, res) => {
|
|
333
|
+
try {
|
|
334
|
+
if (!req.user) {
|
|
335
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const success = await gitlabService.disconnectRepository(req.user.id, req.params.id);
|
|
339
|
+
if (!success) {
|
|
340
|
+
res.status(404).json({ error: 'Repository not found' });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
res.json({ success: true });
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
Logger.error('Disconnect GitLab repository error:', error);
|
|
347
|
+
res.status(500).json({ error: 'Failed to disconnect repository' });
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
/**
|
|
351
|
+
* POST /api/gitlab/repositories/:id/analyze
|
|
352
|
+
* Trigger analysis for a connected repository
|
|
353
|
+
*/
|
|
354
|
+
gitlabRouter.post('/repositories/:id/analyze', authMiddleware, async (req, res) => {
|
|
355
|
+
try {
|
|
356
|
+
if (!req.user) {
|
|
357
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const repo = await gitlabService.getConnectedRepository(req.params.id);
|
|
361
|
+
if (!repo) {
|
|
362
|
+
res.status(404).json({ error: 'Repository not found' });
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (!repo.projectId) {
|
|
366
|
+
res.status(400).json({ error: 'Repository not linked to an ArchiCore project' });
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
// Update status
|
|
370
|
+
await gitlabService.updateRepositoryStatus(repo.id, 'syncing');
|
|
371
|
+
try {
|
|
372
|
+
const result = await projectService.runFullAnalysis(repo.projectId);
|
|
373
|
+
await gitlabService.updateLastAnalyzed(repo.id);
|
|
374
|
+
await gitlabService.updateRepositoryStatus(repo.id, 'active');
|
|
375
|
+
res.json({ success: true, result });
|
|
376
|
+
}
|
|
377
|
+
catch (e) {
|
|
378
|
+
await gitlabService.updateRepositoryStatus(repo.id, 'error', String(e));
|
|
379
|
+
throw e;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
Logger.error('Analyze GitLab repository error:', error);
|
|
384
|
+
res.status(500).json({ error: 'Failed to analyze repository' });
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
// ===== WEBHOOKS =====
|
|
388
|
+
/**
|
|
389
|
+
* POST /api/gitlab/webhook
|
|
390
|
+
* Receive GitLab webhooks
|
|
391
|
+
*/
|
|
392
|
+
gitlabRouter.post('/webhook', async (req, res) => {
|
|
393
|
+
try {
|
|
394
|
+
const event = req.headers['x-gitlab-event'];
|
|
395
|
+
const token = req.headers['x-gitlab-token'];
|
|
396
|
+
const payload = req.body;
|
|
397
|
+
if (!event || !payload) {
|
|
398
|
+
res.status(400).json({ error: 'Invalid webhook' });
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
// Get repository from payload
|
|
402
|
+
const fullPath = payload.project?.path_with_namespace;
|
|
403
|
+
if (!fullPath) {
|
|
404
|
+
res.status(200).json({ message: 'No project in payload' });
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
// Find connected repository
|
|
408
|
+
const repo = await gitlabService.findRepositoryByWebhook(fullPath);
|
|
409
|
+
if (!repo) {
|
|
410
|
+
res.status(200).json({ message: 'Repository not connected' });
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
// Verify webhook token
|
|
414
|
+
if (repo.webhookSecret) {
|
|
415
|
+
const secret = gitlabService.getWebhookSecret(repo.webhookSecret);
|
|
416
|
+
if (!token || !gitlabService.verifyWebhookToken(token, secret)) {
|
|
417
|
+
Logger.warn(`Invalid webhook token for ${fullPath}`);
|
|
418
|
+
res.status(401).json({ error: 'Invalid token' });
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
Logger.info(`GitLab webhook received: ${event} for ${fullPath}`);
|
|
423
|
+
// Handle different events
|
|
424
|
+
switch (payload.object_kind) {
|
|
425
|
+
case 'push':
|
|
426
|
+
await handlePushEvent(repo, payload);
|
|
427
|
+
break;
|
|
428
|
+
case 'merge_request':
|
|
429
|
+
await handleMergeRequestEvent(repo, payload);
|
|
430
|
+
break;
|
|
431
|
+
default:
|
|
432
|
+
Logger.debug(`Unhandled GitLab webhook event: ${payload.object_kind}`);
|
|
433
|
+
}
|
|
434
|
+
res.status(200).json({ received: true });
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
Logger.error('GitLab webhook error:', error);
|
|
438
|
+
res.status(500).json({ error: 'Webhook processing failed' });
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
/**
|
|
442
|
+
* Handle push event
|
|
443
|
+
*/
|
|
444
|
+
async function handlePushEvent(repo, payload) {
|
|
445
|
+
if (!repo || !repo.autoAnalyze || !repo.projectId)
|
|
446
|
+
return;
|
|
447
|
+
const branch = payload.ref?.replace('refs/heads/', '');
|
|
448
|
+
if (branch !== repo.defaultBranch) {
|
|
449
|
+
Logger.debug(`Push to non-default branch ${branch}, skipping analysis`);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
Logger.info(`Auto-analyzing ${repo.fullPath} after push`);
|
|
453
|
+
try {
|
|
454
|
+
await gitlabService.updateRepositoryStatus(repo.id, 'syncing');
|
|
455
|
+
await projectService.runFullAnalysis(repo.projectId);
|
|
456
|
+
await gitlabService.updateLastAnalyzed(repo.id);
|
|
457
|
+
await gitlabService.updateRepositoryStatus(repo.id, 'active');
|
|
458
|
+
}
|
|
459
|
+
catch (e) {
|
|
460
|
+
Logger.error(`Analysis failed for ${repo.fullPath}: ${e}`);
|
|
461
|
+
await gitlabService.updateRepositoryStatus(repo.id, 'error', String(e));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Handle merge request event
|
|
466
|
+
*/
|
|
467
|
+
async function handleMergeRequestEvent(repo, payload) {
|
|
468
|
+
if (!repo || !repo.analyzeMRs || !repo.projectId)
|
|
469
|
+
return;
|
|
470
|
+
const action = payload.object_attributes?.action;
|
|
471
|
+
const mrIid = payload.object_attributes?.iid;
|
|
472
|
+
if (!mrIid || !['open', 'reopen', 'update'].includes(action || '')) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
Logger.info(`Analyzing MR !${mrIid} for ${repo.fullPath}`);
|
|
476
|
+
try {
|
|
477
|
+
const instance = await gitlabService.getInstanceForRepository(repo);
|
|
478
|
+
if (!instance)
|
|
479
|
+
return;
|
|
480
|
+
// Get MR changes
|
|
481
|
+
const changes = await gitlabService.getMergeRequestChanges(instance.userId, instance.id, repo.gitlabProjectId, mrIid);
|
|
482
|
+
// Analyze impact
|
|
483
|
+
const impact = await projectService.analyzeImpact(repo.projectId, {
|
|
484
|
+
description: payload.object_attributes?.title || 'Merge Request',
|
|
485
|
+
files: changes.map(c => c.new_path),
|
|
486
|
+
symbols: [],
|
|
487
|
+
type: 'modify'
|
|
488
|
+
});
|
|
489
|
+
// Generate comment
|
|
490
|
+
const riskEmoji = {
|
|
491
|
+
low: '✅',
|
|
492
|
+
medium: '⚠️',
|
|
493
|
+
high: '🔶',
|
|
494
|
+
critical: '🚨'
|
|
495
|
+
};
|
|
496
|
+
const riskLevel = impact.risks.length > 0
|
|
497
|
+
? impact.risks.reduce((max, r) => ['critical', 'high', 'medium', 'low'].indexOf(r.severity) <
|
|
498
|
+
['critical', 'high', 'medium', 'low'].indexOf(max) ? r.severity : max, 'low')
|
|
499
|
+
: 'low';
|
|
500
|
+
const comment = `## ArchiCore Analysis ${riskEmoji[riskLevel]}
|
|
501
|
+
|
|
502
|
+
**Risk Level:** ${riskLevel.toUpperCase()}
|
|
503
|
+
**Affected Components:** ${impact.affectedNodes.length}
|
|
504
|
+
**Files Changed:** ${changes.length}
|
|
505
|
+
|
|
506
|
+
### Impact Summary
|
|
507
|
+
|
|
508
|
+
${impact.affectedNodes.slice(0, 5).map(n => `- **${n.name}** (${n.type}) - ${n.reason}`).join('\n') || 'No significant impact detected.'}
|
|
509
|
+
|
|
510
|
+
${impact.risks.length > 0 ? `### Risks
|
|
511
|
+
|
|
512
|
+
${impact.risks.slice(0, 3).map(r => `- ${riskEmoji[r.severity]} **${r.category}**: ${r.description}`).join('\n')}` : ''}
|
|
513
|
+
|
|
514
|
+
${impact.recommendations.length > 0 ? `### Recommendations
|
|
515
|
+
|
|
516
|
+
${impact.recommendations.slice(0, 3).map(r => `- ${r.description}`).join('\n')}` : ''}
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
*Analyzed by [ArchiCore](https://archicore.io)*`;
|
|
520
|
+
// Post comment
|
|
521
|
+
await gitlabService.postMRComment(instance.userId, instance.id, repo.gitlabProjectId, mrIid, comment);
|
|
522
|
+
}
|
|
523
|
+
catch (e) {
|
|
524
|
+
Logger.error(`MR analysis failed for ${repo.fullPath}!${mrIid}: ${e}`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
export default gitlabRouter;
|
|
528
|
+
//# sourceMappingURL=gitlab.js.map
|