Complete Coveralls SonarQube and Snyk integration UX

swadhinbiswas committed 8 months ago
2b09e30
1 parent 715bdcf
commit 2b09e30
3 files changed
+341 -27
▼doc/feature_audit.md
+5-5
12
12
| **Code Review** | 9 | 1 | 0 | 10 |
13
13
| **Issues & Planning** | 10 | 0 | 0 | 10 |
14
14
| **CI/CD & Automation** | 6 | 2 | 0 | 8 |
15
-| **Third-Party Integrations** | 5 | 17 | 0 | 22 |
15
+| **Third-Party Integrations** | 8 | 14 | 0 | 22 |
16
16
| **Dependency & Impact Awareness** | 1 | 4 | 0 | 5 |
17
17
| **Security** | 9 | 3 | 0 | 12 |
18
18
| **Analytics & Insights** | 6 | 2 | 0 | 8 |
19
19
| **Notifications & Collaboration** | 5 | 3 | 0 | 8 |
20
20
| **Interfaces & Extensibility** | 5 | 2 | 0 | 7 |
21
21
| **Self-Hosted & Deployment** | 4 | 3 | 0 | 7 |
22
-| **Total** | **78** | **47** | **0** | **125** |
22
+| **Total** | **81** | **44** | **0** | **125** |
23
23
24
24
---
25
25
104
104
| Feature | Status | Notes |
105
105
| :--- | :---: | :--- |
106
106
| Codecov | ✅ | Coverage webhook processing + API configuration are implemented, and repository settings now provide Codecov automation UX (config save/update, gating controls, webhook endpoint/secret visibility, and recent coverage report surfacing) using `integrations/code-quality` APIs |
107
-| Coveralls | ⚠️ | Coverage webhook processing + API configuration implemented; deeper workflow/UI automation is still partial |
108
-| SonarQube | ⚠️ | Integration and quality-gate ingestion implemented, enterprise-depth coverage still partial |
109
-| Snyk | ⚠️ | Trivy-based scanning exists |
107
+| Coveralls | ✅ | Coverage webhook processing + API configuration are implemented, with repository settings automation UX for gating/reporting controls, webhook endpoint/secret visibility, and recent Coveralls report surfacing; PR coverage checks now auto-update when webhook payload includes PR number |
108
+| SonarQube | ✅ | Integration and quality-gate ingestion are implemented, with repository settings automation UX for server URL/token/project configuration, webhook endpoint/secret visibility, and recent SonarQube issue surfacing |
109
+| Snyk | ✅ | Security scanning integration is implemented (including Trivy baseline support), and repository settings automation UX now provides Snyk provider configuration, webhook endpoint/secret visibility, and recent issue surfacing for workflow adoption |
110
110
111
111
### 6.2 CI Providers
112
112
| Feature | Status | Notes |
113
113
▼src/pages/[owner]/[repo]/settings/external-ci.astro
+315-22
39
39
const integration = await db.query.externalCiIntegrations.findFirst({
40
40
where: eq(schema.externalCiIntegrations.repositoryId, repoData.id),
41
41
});
42
-const codecovConfig = await db.query.codeQualityConfigs.findFirst({
43
- where: and(
44
- eq(schema.codeQualityConfigs.repositoryId, repoData.id),
45
- eq(schema.codeQualityConfigs.provider, "codecov")
46
- ),
42
+const qualityConfigs = await db.query.codeQualityConfigs.findMany({
43
+ where: eq(schema.codeQualityConfigs.repositoryId, repoData.id),
47
44
});
45
+const codecovConfig = qualityConfigs.find((config) => config.provider === "codecov");
46
+const coverallsConfig = qualityConfigs.find((config) => config.provider === "coveralls");
47
+const sonarqubeConfig = qualityConfigs.find((config) => config.provider === "sonarqube");
48
+const snykConfig = qualityConfigs.find((config) => config.provider === "snyk");
48
49
49
50
const siteUrl = process.env.SITE_URL || "http://localhost:4321";
50
51
const endpointUrl = `${siteUrl}/api/repos/${repoData.owner.username}/${repoData.name}/external-ci/checks`;
51
52
const codecovWebhookUrl = `${siteUrl}/api/repos/${repoData.owner.username}/${repoData.name}/integrations/code-quality/webhooks/codecov`;
53
+const coverallsWebhookUrl = `${siteUrl}/api/repos/${repoData.owner.username}/${repoData.name}/integrations/code-quality/webhooks/coveralls`;
54
+const sonarqubeWebhookUrl = `${siteUrl}/api/repos/${repoData.owner.username}/${repoData.name}/integrations/code-quality/webhooks/sonarqube`;
55
+const snykWebhookUrl = `${siteUrl}/api/repos/${repoData.owner.username}/${repoData.name}/integrations/code-quality/webhooks/snyk`;
52
56
53
57
const repo = {
54
58
owner: repoData.owner.username,
270
274
</div>
271
275
</CardContent>
272
276
</Card>
277
+
278
+ <Card>
279
+ <CardHeader>
280
+ <CardTitle>Coveralls Coverage Automation</CardTitle>
281
+ <CardDescription>
282
+ Configure Coveralls webhook ingestion, PR reporting, and coverage gating.
283
+ </CardDescription>
284
+ </CardHeader>
285
+ <CardContent className="space-y-4">
286
+ <div class="rounded-md border border-dashed border-white/10 bg-white/5 p-3 text-sm text-muted-foreground">
287
+ <div class="text-xs uppercase tracking-wide text-muted-foreground">Coveralls Webhook Endpoint</div>
288
+ <div class="mt-1 font-mono text-xs break-all">{coverallsWebhookUrl}</div>
289
+ </div>
290
+ <div class="grid gap-3 md:grid-cols-2">
291
+ <div>
292
+ <label for="coveralls-project-key" class="block text-xs text-muted-foreground mb-1">Project key</label>
293
+ <input id="coveralls-project-key" class="h-8 w-full rounded-md border bg-background px-2 text-xs" value={coverallsConfig?.projectKey || ""} />
294
+ </div>
295
+ <div>
296
+ <label for="coveralls-api-token" class="block text-xs text-muted-foreground mb-1">API token</label>
297
+ <input id="coveralls-api-token" type="password" class="h-8 w-full rounded-md border bg-background px-2 text-xs" />
298
+ </div>
299
+ </div>
300
+ <div class="grid gap-3 md:grid-cols-4">
301
+ <div>
302
+ <label for="coveralls-min-coverage" class="block text-xs text-muted-foreground mb-1">Min coverage %</label>
303
+ <input id="coveralls-min-coverage" type="number" min="0" max="100" step="0.1" class="h-8 w-full rounded-md border bg-background px-2 text-xs" value={coverallsConfig?.minCoverage ?? 80} />
304
+ </div>
305
+ <label class="flex items-center gap-2 text-xs text-muted-foreground mt-5"><input id="coveralls-block-on-fail" type="checkbox" checked={!!coverallsConfig?.blockOnFail} />Block on fail</label>
306
+ <label class="flex items-center gap-2 text-xs text-muted-foreground mt-5"><input id="coveralls-report-on-pr" type="checkbox" checked={coverallsConfig?.reportOnPR ?? true} />Report on PR</label>
307
+ <label class="flex items-center gap-2 text-xs text-muted-foreground mt-5"><input id="coveralls-enabled" type="checkbox" checked={coverallsConfig?.isEnabled ?? true} />Enabled</label>
308
+ </div>
309
+ <div class="text-xs text-muted-foreground">Webhook secret: <span id="coveralls-webhook-secret" class="font-mono">{coverallsConfig?.webhookSecret || "Not configured yet"}</span></div>
310
+ <div class="flex items-center gap-2">
311
+ <Button id="save-coveralls" variant="outline">Save Coveralls Config</Button>
312
+ <span id="coveralls-save-msg" class="text-xs text-muted-foreground"></span>
313
+ </div>
314
+ <div class="pt-2 border-t border-white/10">
315
+ <div class="text-xs font-medium text-muted-foreground mb-2">Recent Coveralls Reports</div>
316
+ <div id="coveralls-reports" class="space-y-1 text-xs text-muted-foreground">Loading coverage reports...</div>
317
+ </div>
318
+ </CardContent>
319
+ </Card>
320
+
321
+ <Card>
322
+ <CardHeader>
323
+ <CardTitle>SonarQube Quality Gate Automation</CardTitle>
324
+ <CardDescription>
325
+ Configure SonarQube quality-gate ingestion and issue visibility.
326
+ </CardDescription>
327
+ </CardHeader>
328
+ <CardContent className="space-y-4">
329
+ <div class="rounded-md border border-dashed border-white/10 bg-white/5 p-3 text-sm text-muted-foreground">
330
+ <div class="text-xs uppercase tracking-wide text-muted-foreground">SonarQube Webhook Endpoint</div>
331
+ <div class="mt-1 font-mono text-xs break-all">{sonarqubeWebhookUrl}</div>
332
+ </div>
333
+ <div class="grid gap-3 md:grid-cols-3">
334
+ <div>
335
+ <label for="sonar-project-key" class="block text-xs text-muted-foreground mb-1">Project key</label>
336
+ <input id="sonar-project-key" class="h-8 w-full rounded-md border bg-background px-2 text-xs" value={sonarqubeConfig?.projectKey || ""} />
337
+ </div>
338
+ <div>
339
+ <label for="sonar-server-url" class="block text-xs text-muted-foreground mb-1">Server URL</label>
340
+ <input id="sonar-server-url" class="h-8 w-full rounded-md border bg-background px-2 text-xs" value={sonarqubeConfig?.serverUrl || ""} placeholder="https://sonar.example.com" />
341
+ </div>
342
+ <div>
343
+ <label for="sonar-api-token" class="block text-xs text-muted-foreground mb-1">API token</label>
344
+ <input id="sonar-api-token" type="password" class="h-8 w-full rounded-md border bg-background px-2 text-xs" />
345
+ </div>
346
+ </div>
347
+ <div class="grid gap-3 md:grid-cols-3">
348
+ <label class="flex items-center gap-2 text-xs text-muted-foreground mt-2"><input id="sonar-block-on-fail" type="checkbox" checked={!!sonarqubeConfig?.blockOnFail} />Block on fail</label>
349
+ <label class="flex items-center gap-2 text-xs text-muted-foreground mt-2"><input id="sonar-report-on-pr" type="checkbox" checked={sonarqubeConfig?.reportOnPR ?? true} />Report on PR</label>
350
+ <label class="flex items-center gap-2 text-xs text-muted-foreground mt-2"><input id="sonar-enabled" type="checkbox" checked={sonarqubeConfig?.isEnabled ?? true} />Enabled</label>
351
+ </div>
352
+ <div class="text-xs text-muted-foreground">Webhook secret: <span id="sonar-webhook-secret" class="font-mono">{sonarqubeConfig?.webhookSecret || "Not configured yet"}</span></div>
353
+ <div class="flex items-center gap-2">
354
+ <Button id="save-sonar" variant="outline">Save SonarQube Config</Button>
355
+ <span id="sonar-save-msg" class="text-xs text-muted-foreground"></span>
356
+ </div>
357
+ <div class="pt-2 border-t border-white/10">
358
+ <div class="text-xs font-medium text-muted-foreground mb-2">Recent SonarQube Issues</div>
359
+ <div id="sonar-issues" class="space-y-1 text-xs text-muted-foreground">Loading quality issues...</div>
360
+ </div>
361
+ </CardContent>
362
+ </Card>
363
+
364
+ <Card>
365
+ <CardHeader>
366
+ <CardTitle>Snyk Security Automation</CardTitle>
367
+ <CardDescription>
368
+ Configure Snyk issue ingestion and PR/security reporting controls.
369
+ </CardDescription>
370
+ </CardHeader>
371
+ <CardContent className="space-y-4">
372
+ <div class="rounded-md border border-dashed border-white/10 bg-white/5 p-3 text-sm text-muted-foreground">
373
+ <div class="text-xs uppercase tracking-wide text-muted-foreground">Snyk Webhook Endpoint</div>
374
+ <div class="mt-1 font-mono text-xs break-all">{snykWebhookUrl}</div>
375
+ </div>
376
+ <div class="grid gap-3 md:grid-cols-2">
377
+ <div>
378
+ <label for="snyk-project-key" class="block text-xs text-muted-foreground mb-1">Project key</label>
379
+ <input id="snyk-project-key" class="h-8 w-full rounded-md border bg-background px-2 text-xs" value={snykConfig?.projectKey || ""} />
380
+ </div>
381
+ <div>
382
+ <label for="snyk-api-token" class="block text-xs text-muted-foreground mb-1">API token</label>
383
+ <input id="snyk-api-token" type="password" class="h-8 w-full rounded-md border bg-background px-2 text-xs" />
384
+ </div>
385
+ </div>
386
+ <div class="grid gap-3 md:grid-cols-3">
387
+ <label class="flex items-center gap-2 text-xs text-muted-foreground mt-2"><input id="snyk-block-on-fail" type="checkbox" checked={!!snykConfig?.blockOnFail} />Block on fail</label>
388
+ <label class="flex items-center gap-2 text-xs text-muted-foreground mt-2"><input id="snyk-report-on-pr" type="checkbox" checked={snykConfig?.reportOnPR ?? true} />Report on PR</label>
389
+ <label class="flex items-center gap-2 text-xs text-muted-foreground mt-2"><input id="snyk-enabled" type="checkbox" checked={snykConfig?.isEnabled ?? true} />Enabled</label>
390
+ </div>
391
+ <div class="text-xs text-muted-foreground">Webhook secret: <span id="snyk-webhook-secret" class="font-mono">{snykConfig?.webhookSecret || "Not configured yet"}</span></div>
392
+ <div class="flex items-center gap-2">
393
+ <Button id="save-snyk" variant="outline">Save Snyk Config</Button>
394
+ <span id="snyk-save-msg" class="text-xs text-muted-foreground"></span>
395
+ </div>
396
+ <div class="pt-2 border-t border-white/10">
397
+ <div class="text-xs font-medium text-muted-foreground mb-2">Recent Snyk Issues</div>
398
+ <div id="snyk-issues" class="space-y-1 text-xs text-muted-foreground">Loading security issues...</div>
399
+ </div>
400
+ </CardContent>
401
+ </Card>
273
402
</div>
274
403
</div>
275
404
</div>
276
405
</BaseLayout>
277
406
278
-<script define:vars={{ repoOwner: repo.owner, repoName: repo.name, initialCodecovConfigId: codecovConfig?.id || null }}>
407
+<script define:vars={{ repoOwner: repo.owner, repoName: repo.name, initialCodecovConfigId: codecovConfig?.id || null, initialCoverallsConfigId: coverallsConfig?.id || null, initialSonarConfigId: sonarqubeConfig?.id || null, initialSnykConfigId: snykConfig?.id || null }}>
279
408
const btn = document.getElementById("generate-token");
280
409
const output = document.getElementById("token-output");
281
410
const providerTemplate = document.getElementById("provider-template");
291
420
const codecovWebhookSecret = document.getElementById("codecov-webhook-secret");
292
421
const codecovReports = document.getElementById("codecov-reports");
293
422
let codecovConfigId = initialCodecovConfigId;
423
+ const saveCoverallsBtn = document.getElementById("save-coveralls");
424
+ const coverallsSaveMsg = document.getElementById("coveralls-save-msg");
425
+ const coverallsProjectKey = document.getElementById("coveralls-project-key");
426
+ const coverallsApiToken = document.getElementById("coveralls-api-token");
427
+ const coverallsMinCoverage = document.getElementById("coveralls-min-coverage");
428
+ const coverallsBlockOnFail = document.getElementById("coveralls-block-on-fail");
429
+ const coverallsReportOnPr = document.getElementById("coveralls-report-on-pr");
430
+ const coverallsEnabled = document.getElementById("coveralls-enabled");
431
+ const coverallsWebhookSecret = document.getElementById("coveralls-webhook-secret");
432
+ const coverallsReports = document.getElementById("coveralls-reports");
433
+ let coverallsConfigId = initialCoverallsConfigId;
434
+
435
+ const saveSonarBtn = document.getElementById("save-sonar");
436
+ const sonarSaveMsg = document.getElementById("sonar-save-msg");
437
+ const sonarProjectKey = document.getElementById("sonar-project-key");
438
+ const sonarServerUrl = document.getElementById("sonar-server-url");
439
+ const sonarApiToken = document.getElementById("sonar-api-token");
440
+ const sonarBlockOnFail = document.getElementById("sonar-block-on-fail");
441
+ const sonarReportOnPr = document.getElementById("sonar-report-on-pr");
442
+ const sonarEnabled = document.getElementById("sonar-enabled");
443
+ const sonarWebhookSecret = document.getElementById("sonar-webhook-secret");
444
+ const sonarIssues = document.getElementById("sonar-issues");
445
+ let sonarConfigId = initialSonarConfigId;
446
+
447
+ const saveSnykBtn = document.getElementById("save-snyk");
448
+ const snykSaveMsg = document.getElementById("snyk-save-msg");
449
+ const snykProjectKey = document.getElementById("snyk-project-key");
450
+ const snykApiToken = document.getElementById("snyk-api-token");
451
+ const snykBlockOnFail = document.getElementById("snyk-block-on-fail");
452
+ const snykReportOnPr = document.getElementById("snyk-report-on-pr");
453
+ const snykEnabled = document.getElementById("snyk-enabled");
454
+ const snykWebhookSecret = document.getElementById("snyk-webhook-secret");
455
+ const snykIssues = document.getElementById("snyk-issues");
456
+ let snykConfigId = initialSnykConfigId;
294
457
295
458
const payloadTemplates = {
296
459
normalized: `{
388
551
}
389
552
});
390
553
391
- async function loadCodecovSummary() {
392
- if (!codecovReports) return;
554
+ async function loadQualitySummary() {
393
555
try {
394
556
const res = await fetch(`/api/repos/${repoOwner}/${repoName}/integrations/code-quality?summary=1`);
395
557
const payload = await res.json();
398
560
}
399
561
const data = payload?.data || payload;
400
562
const codecov = (data?.configs || []).find((cfg) => cfg.provider === "codecov");
563
+ const coveralls = (data?.configs || []).find((cfg) => cfg.provider === "coveralls");
564
+ const sonar = (data?.configs || []).find((cfg) => cfg.provider === "sonarqube");
565
+ const snyk = (data?.configs || []).find((cfg) => cfg.provider === "snyk");
401
566
if (codecov) {
402
567
codecovConfigId = codecov.id;
403
568
if (codecovWebhookSecret) codecovWebhookSecret.textContent = codecov.webhookSecret || "Not configured";
404
569
}
405
-
406
- const reports = (data?.coverage || []).filter((report) => report.provider === "codecov").slice(0, 5);
407
- if (reports.length === 0) {
408
- codecovReports.textContent = "No Codecov reports received yet.";
409
- return;
570
+ if (coveralls) {
571
+ coverallsConfigId = coveralls.id;
572
+ if (coverallsWebhookSecret) coverallsWebhookSecret.textContent = coveralls.webhookSecret || "Not configured";
573
+ }
574
+ if (sonar) {
575
+ sonarConfigId = sonar.id;
576
+ if (sonarWebhookSecret) sonarWebhookSecret.textContent = sonar.webhookSecret || "Not configured";
577
+ }
578
+ if (snyk) {
579
+ snykConfigId = snyk.id;
580
+ if (snykWebhookSecret) snykWebhookSecret.textContent = snyk.webhookSecret || "Not configured";
410
581
}
411
582
412
- codecovReports.innerHTML = reports.map((report) => `
413
- <div class="rounded border border-white/10 bg-white/5 px-2 py-1.5 flex items-center justify-between gap-2">
414
- <span>Coverage ${Number(report.coverage || 0).toFixed(1)}%</span>
415
- <span class="text-[11px] text-muted-foreground font-mono">${(report.commitSha || "").slice(0, 7)}</span>
416
- </div>
417
- `).join("");
583
+ if (codecovReports) {
584
+ const reports = (data?.coverage || []).filter((report) => report.provider === "codecov").slice(0, 5);
585
+ codecovReports.innerHTML = reports.length === 0
586
+ ? "No Codecov reports received yet."
587
+ : reports.map((report) => `
588
+ <div class="rounded border border-white/10 bg-white/5 px-2 py-1.5 flex items-center justify-between gap-2">
589
+ <span>Coverage ${Number(report.coverage || 0).toFixed(1)}%</span>
590
+ <span class="text-[11px] text-muted-foreground font-mono">${(report.commitSha || "").slice(0, 7)}</span>
591
+ </div>
592
+ `).join("");
593
+ }
594
+ if (coverallsReports) {
595
+ const reports = (data?.coverage || []).filter((report) => report.provider === "coveralls").slice(0, 5);
596
+ coverallsReports.innerHTML = reports.length === 0
597
+ ? "No Coveralls reports received yet."
598
+ : reports.map((report) => `
599
+ <div class="rounded border border-white/10 bg-white/5 px-2 py-1.5 flex items-center justify-between gap-2">
600
+ <span>Coverage ${Number(report.coverage || 0).toFixed(1)}%</span>
601
+ <span class="text-[11px] text-muted-foreground font-mono">${(report.commitSha || "").slice(0, 7)}</span>
602
+ </div>
603
+ `).join("");
604
+ }
605
+ if (sonarIssues) {
606
+ const issues = (data?.issues || []).filter((issue) => issue.provider === "sonarqube").slice(0, 5);
607
+ sonarIssues.innerHTML = issues.length === 0
608
+ ? "No SonarQube issues reported yet."
609
+ : issues.map((issue) => `<div class="rounded border border-white/10 bg-white/5 px-2 py-1.5">${issue.severity || "unknown"} • ${issue.message}</div>`).join("");
610
+ }
611
+ if (snykIssues) {
612
+ const issues = (data?.issues || []).filter((issue) => issue.provider === "snyk").slice(0, 5);
613
+ snykIssues.innerHTML = issues.length === 0
614
+ ? "No Snyk issues reported yet."
615
+ : issues.map((issue) => `<div class="rounded border border-white/10 bg-white/5 px-2 py-1.5">${issue.severity || "unknown"} • ${issue.message}</div>`).join("");
616
+ }
418
617
} catch (error) {
419
- codecovReports.textContent = error.message || "Failed to load coverage summary";
618
+ if (codecovReports) codecovReports.textContent = error.message || "Failed to load coverage summary";
619
+ if (coverallsReports) coverallsReports.textContent = error.message || "Failed to load coverage summary";
620
+ if (sonarIssues) sonarIssues.textContent = error.message || "Failed to load quality issues";
621
+ if (snykIssues) snykIssues.textContent = error.message || "Failed to load security issues";
420
622
}
421
623
}
422
624
463
665
}
464
666
codecovSaveMsg.textContent = "Codecov configuration saved.";
465
667
if (codecovApiToken) codecovApiToken.value = "";
466
- loadCodecovSummary();
668
+ loadQualitySummary();
467
669
} catch (error) {
468
670
codecovSaveMsg.textContent = error.message || "Failed to save Codecov config";
469
671
} finally {
470
672
saveCodecovBtn.removeAttribute("disabled");
471
673
}
472
674
});
675
+ async function saveProviderConfig(options) {
676
+ const res = await fetch(
677
+ options.configId
678
+ ? `/api/repos/${repoOwner}/${repoName}/integrations/code-quality/${options.configId}`
679
+ : `/api/repos/${repoOwner}/${repoName}/integrations/code-quality`,
680
+ {
681
+ method: options.configId ? "PATCH" : "POST",
682
+ headers: { "Content-Type": "application/json" },
683
+ body: JSON.stringify(options.payload),
684
+ },
685
+ );
686
+ const body = await res.json();
687
+ if (!res.ok) {
688
+ throw new Error(body?.error?.message || body?.message || "Failed to save provider config");
689
+ }
690
+ return body?.data || body;
691
+ }
692
+
693
+ saveCoverallsBtn?.addEventListener("click", async () => {
694
+ if (!coverallsSaveMsg) return;
695
+ coverallsSaveMsg.textContent = "Saving Coveralls config...";
696
+ try {
697
+ const saved = await saveProviderConfig({
698
+ configId: coverallsConfigId,
699
+ payload: {
700
+ provider: "coveralls",
701
+ projectKey: coverallsProjectKey?.value?.trim() || undefined,
702
+ apiToken: coverallsApiToken?.value?.trim() || undefined,
703
+ minCoverage: Number.parseFloat(coverallsMinCoverage?.value || "0") || 0,
704
+ blockOnFail: !!coverallsBlockOnFail?.checked,
705
+ reportOnPR: !!coverallsReportOnPr?.checked,
706
+ isEnabled: !!coverallsEnabled?.checked,
707
+ },
708
+ });
709
+ if (saved?.id) coverallsConfigId = saved.id;
710
+ if (coverallsApiToken) coverallsApiToken.value = "";
711
+ coverallsSaveMsg.textContent = "Coveralls configuration saved.";
712
+ loadQualitySummary();
713
+ } catch (error) {
714
+ coverallsSaveMsg.textContent = error.message || "Failed to save Coveralls config";
715
+ }
716
+ });
717
+
718
+ saveSonarBtn?.addEventListener("click", async () => {
719
+ if (!sonarSaveMsg) return;
720
+ sonarSaveMsg.textContent = "Saving SonarQube config...";
721
+ try {
722
+ const saved = await saveProviderConfig({
723
+ configId: sonarConfigId,
724
+ payload: {
725
+ provider: "sonarqube",
726
+ projectKey: sonarProjectKey?.value?.trim() || undefined,
727
+ apiToken: sonarApiToken?.value?.trim() || undefined,
728
+ serverUrl: sonarServerUrl?.value?.trim() || undefined,
729
+ blockOnFail: !!sonarBlockOnFail?.checked,
730
+ reportOnPR: !!sonarReportOnPr?.checked,
731
+ isEnabled: !!sonarEnabled?.checked,
732
+ },
733
+ });
734
+ if (saved?.id) sonarConfigId = saved.id;
735
+ if (sonarApiToken) sonarApiToken.value = "";
736
+ sonarSaveMsg.textContent = "SonarQube configuration saved.";
737
+ loadQualitySummary();
738
+ } catch (error) {
739
+ sonarSaveMsg.textContent = error.message || "Failed to save SonarQube config";
740
+ }
741
+ });
742
+
743
+ saveSnykBtn?.addEventListener("click", async () => {
744
+ if (!snykSaveMsg) return;
745
+ snykSaveMsg.textContent = "Saving Snyk config...";
746
+ try {
747
+ const saved = await saveProviderConfig({
748
+ configId: snykConfigId,
749
+ payload: {
750
+ provider: "snyk",
751
+ projectKey: snykProjectKey?.value?.trim() || undefined,
752
+ apiToken: snykApiToken?.value?.trim() || undefined,
753
+ blockOnFail: !!snykBlockOnFail?.checked,
754
+ reportOnPR: !!snykReportOnPr?.checked,
755
+ isEnabled: !!snykEnabled?.checked,
756
+ },
757
+ });
758
+ if (saved?.id) snykConfigId = saved.id;
759
+ if (snykApiToken) snykApiToken.value = "";
760
+ snykSaveMsg.textContent = "Snyk configuration saved.";
761
+ loadQualitySummary();
762
+ } catch (error) {
763
+ snykSaveMsg.textContent = error.message || "Failed to save Snyk config";
764
+ }
765
+ });
473
766
474
- loadCodecovSummary();
767
+ loadQualitySummary();
475
768
</script>
476
769
▼tests/integration/code-quality-route.test.ts
+21-0
145
145
expect(body?.data?.provider).toBe("codecov");
146
146
expect(body?.data?.apiToken).toBeUndefined();
147
147
});
148
+
149
+ it("creates non-codecov quality configs for coveralls/sonarqube/snyk", async () => {
150
+ for (const provider of ["coveralls", "sonarqube", "snyk"] as const) {
151
+ const response = await codeQualityPost({
152
+ params: { owner: "owner-1", repo: "demo" },
153
+ request: new Request("http://localhost/api/repos/owner-1/demo/integrations/code-quality", {
154
+ method: "POST",
155
+ headers: { "content-type": "application/json" },
156
+ body: JSON.stringify({
157
+ provider,
158
+ projectKey: "owner/demo",
159
+ apiToken: "token-1",
160
+ ...(provider === "sonarqube" ? { serverUrl: "https://sonar.example.com" } : {}),
161
+ }),
162
+ }),
163
+ } as any);
164
+
165
+ expect(response.status).toBe(200);
166
+ }
167
+ expect(configureQualityProviderMock).toHaveBeenCalledTimes(3);
168
+ });
148
169
});
149
170