Complete Coveralls SonarQube and Snyk integration UX
swadhinbiswas committed 8 months ago
2b09e30
1 parent 715bdcf
commit 2b09e30
3 files changed
+341 -27
Showing 3 changed files+341-27
▼doc/feature_audit.md
+5-5
| @@ -12,14 +12,14 @@ | ||
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,9 +104,9 @@ | ||
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,16 +39,20 @@ if (!hasAccess) { | ||
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,12 +274,137 @@ const repo = { | ||
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,6 +420,40 @@ const repo = { | ||
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,8 +551,7 @@ const repo = { | ||
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,25 +560,65 @@ const repo = { | ||
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,13 +665,104 @@ const repo = { | ||
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,4 +145,25 @@ describe("code quality integrations route", () => { | ||
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 | |