Skip to content

Commit 04e95fe

Browse files
christoph-jerolimovclaudeimykhno
authored
feat(scorecard): Add SonarQube metric providers (#2576)
* chore(scorecard): create a new module for sonarqube Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com> * feat(scorecard): add SonarQube metric providers for quality gate, issues, and security Add four metric providers to the scorecard-backend-module-sonarqube plugin: - Quality gate status (boolean) - Open issues count (number) - Security rating (number, A=1 to E=5) - Security issues/vulnerabilities count (number) Includes SonarQubeClient, config, factory, example catalog entity, and unit tests. SonarQube baseUrl defaults to https://sonarcloud.io; token is optional for public projects. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com> * feat(scorecard): support multiple SonarQube instances and align with config schema - Add config.d.ts with typed config schema supporting default + named instances - Refactor SonarQubeClient to resolve instance by name from sonarqube.instances[] - Parse sonarqube.org/project-key annotation for optional instance prefix (instance/project-key) - Use apiKey + authType (Basic/Bearer) from config.d.ts instead of token - Falls back to default instance when no instance prefix in annotation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com> * docs(scorecard): add README for sonarqube module Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com> * fix(scorecard): base64-encode Basic auth header for SonarQube API SonarQube expects Basic auth as base64(apiKey:) with an appended colon. Bearer auth passes the apiKey directly without encoding. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com> * chore(scorecard): add api report for ci check Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com> * chore(scorecard): fix publish check ci checks and make the package public Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com> * feat(scorecard): add 8 additional SonarQube metric providers Add metrics for code coverage, code duplications, security review rating, security hotspots, reliability rating/issues, and maintainability rating/issues. Refactors calculateMetric to use a data-driven API key mapping table instead of a switch statement, and deduplicates rating thresholds into a shared constant. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com> * fix(scorecard): add missing mockSonarqubeScorecardResponse to e2e apiUtils Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com> * chore(scorecard): try to fix the scorecard e2e tests Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com> * chore(scorecard): update descriptions and readme Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com> * chore(scorecard): remove unused externalBaseUrl from sonarqube config Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com> * chore(scorecard): yarn dedupe Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com> * chore(scorecard): refactore code to fromConfig pattern Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com> * chore(scorecard): use five thresholds for A-E ratings, use success/error thresholds for security rating Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com> * feat(scorecard): add sonarqube translations Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com> * feat(scorecard): add tooltip to Scorecard header to show titles that are longer then one line Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com> * chore(scorecard): fix e2e tests Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com> * fix(scorecard): e2e tests Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * refactor(scorecard): SonarQube providers Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * fix(scorecard): update fetch reference and correct expected result in tests Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * fix(scorecard): type imports Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * fix(scorecard): increase initial delay and refactor logger mocks in tests Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * fix(scorecard): make apiKey optional for SonarQube configuration and update README Signed-off-by: Ihor Mykhno <imykhno@redhat.com> --------- Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com> Signed-off-by: Ihor Mykhno <imykhno@redhat.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Ihor Mykhno <imykhno@redhat.com>
1 parent 384ade8 commit 04e95fe

45 files changed

Lines changed: 6261 additions & 681 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-scorecard': patch
3+
---
4+
5+
Add translations for new sonarqube module.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-scorecard': patch
3+
---
4+
5+
Add tooltip to Scorecard header to show titles that are longer then one line
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-sonarqube': minor
3+
---
4+
5+
Add metric providers for code coverage, code duplications, security review rating, security hotspots, reliability rating, reliability issues, maintainability rating, and maintainability issues
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-sonarqube': patch
3+
---
4+
5+
Fix Basic auth to base64-encode apiKey with appended colon, matching the SonarQube API expectation
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-sonarqube': minor
3+
---
4+
5+
Add SonarQube metric providers for quality gate status, open issues, security rating, and security issues
Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
.eslintrc.js
2+
.vscode
3+
coverage
14
dist
25
dist-types
3-
coverage
4-
.vscode
5-
.eslintrc.js
6+
e2e-test-report
7+
e2e-test-report-legacy
8+
knip-report.md
69
report-alpha.api.md
710
report.api.md

workspaces/scorecard/app-config.yaml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,18 @@ permission:
204204
users:
205205
- name: user:development/guest
206206

207+
# SonarQube configuration (optional - defaults to https://sonarcloud.io, no auth for public projects)
208+
sonarqube:
209+
baseUrl: https://sonarcloud.io
210+
apiKey: ${SONARQUBE_TOKEN}
211+
instances:
212+
- name: internal
213+
baseUrl: https://sonarcloud.io
214+
apiKey: ${SONARQUBE_INTERNAL_KEY}
215+
authType: Basic # optional, defaults to Basic
216+
- name: cloud
217+
baseUrl: https://sonarcloud.io
218+
207219
# Scorecard development configuration
208220
scorecard:
209221
aggregationKPIs:
@@ -249,18 +261,18 @@ scorecard:
249261
schedule:
250262
frequency: { minutes: 5 }
251263
timeout: { minutes: 10 }
252-
initialDelay: { seconds: 5 }
264+
initialDelay: { seconds: 10 }
253265
github:
254266
open_prs:
255267
schedule:
256268
frequency: { minutes: 5 }
257269
timeout: { minutes: 10 }
258-
initialDelay: { seconds: 5 }
270+
initialDelay: { seconds: 10 }
259271
filecheck:
260272
files:
261273
license: 'LICENSE'
262274
codeowners: 'CODEOWNERS'
263275
schedule:
264276
frequency: { minutes: 5 }
265277
timeout: { minutes: 10 }
266-
initialDelay: { seconds: 5 }
278+
initialDelay: { seconds: 10 }

workspaces/scorecard/examples/all-scorecards-location.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ spec:
1010
- ./components/github-scorecard-only.yaml
1111
- ./components/jira-scorecard-only.yaml
1212
- ./components/openssf-scorecard-only.yaml
13+
- ./components/sonarqube-scorecard-only.yaml
1314
- ./components/all-scorecards.yaml
1415
- ./components/no-scorecards.yaml
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
# Component with SonarQube Scorecard only
3+
apiVersion: backstage.io/v1alpha1
4+
kind: Component
5+
metadata:
6+
name: sonarqube-scorecard-only
7+
annotations:
8+
sonarqube.org/project-key: redhat-developer_rhdh-plugins
9+
spec:
10+
type: service
11+
owner: group:development/guests
12+
lifecycle: experimental

workspaces/scorecard/packages/app-legacy/e2e-tests/scorecard.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
mockJiraDrillDownMissingPermission,
2323
mockMetricsApi,
2424
mockApiResponse,
25+
mockSonarqubeScorecardResponse,
2526
} from './utils/apiUtils';
2627
import { CatalogPage } from './pages/CatalogPage';
2728
import { ScorecardPage } from './pages/ScorecardPage';
@@ -46,6 +47,8 @@ import {
4647
jiraEntitiesDrillDownResponse,
4748
jiraEntitiesDrillDownNoDataResponse,
4849
jiraMetricMetadataResponse,
50+
sonarqubeScorecardResponse,
51+
sonarqubeFailedQualityGateResponse,
4952
fileCheckScorecardResponse,
5053
} from './utils/scorecardResponseUtils';
5154
import {
@@ -333,6 +336,113 @@ test.describe('Scorecard Plugin Tests', () => {
333336
});
334337
});
335338

339+
test.describe('SonarQube Entity Scorecards', () => {
340+
test('Verify all SonarQube metrics display correctly', async ({
341+
browser,
342+
}, testInfo) => {
343+
const sonarqubeMetrics = Object.entries(translations.metric)
344+
.filter(([key]) => key.startsWith('sonarqube.'))
345+
.map(
346+
([_key, value]) => value as { title: string; description: string },
347+
);
348+
349+
await mockSonarqubeScorecardResponse(page, sonarqubeScorecardResponse);
350+
351+
await catalogPage.openCatalog();
352+
await catalogPage.openComponent('sonarqube-scorecard-only');
353+
await page.getByText('Scorecard', { exact: true }).click();
354+
355+
for (const sonarqubeMetric of sonarqubeMetrics) {
356+
await expect(
357+
page.getByText(sonarqubeMetric.title, { exact: true }),
358+
).toBeVisible({
359+
timeout: 10000,
360+
});
361+
}
362+
363+
await runAccessibilityTests(page, testInfo);
364+
});
365+
366+
test('Verify SonarQube metric values', async () => {
367+
await mockSonarqubeScorecardResponse(page, sonarqubeScorecardResponse);
368+
369+
await catalogPage.openCatalog();
370+
await catalogPage.openComponent('sonarqube-scorecard-only');
371+
await page.getByText('Scorecard', { exact: true }).click();
372+
373+
await expect(
374+
page.getByText(translations.metric['sonarqube.quality_gate'].title),
375+
).toBeVisible({ timeout: 10000 });
376+
377+
const expectedValues: Record<string, string> = {
378+
[translations.metric['sonarqube.open_issues'].title]: '3',
379+
[translations.metric['sonarqube.security_rating'].title]: '1',
380+
[translations.metric['sonarqube.security_issues'].title]: '0',
381+
[translations.metric['sonarqube.security_review_rating'].title]: '1',
382+
[translations.metric['sonarqube.security_hotspots'].title]: '2',
383+
[translations.metric['sonarqube.reliability_rating'].title]: '1',
384+
[translations.metric['sonarqube.reliability_issues'].title]: '0',
385+
[translations.metric['sonarqube.maintainability_rating'].title]: '1',
386+
[translations.metric['sonarqube.maintainability_issues'].title]: '12',
387+
[translations.metric['sonarqube.code_coverage'].title]: '82.5',
388+
[translations.metric['sonarqube.code_duplications'].title]: '3.2',
389+
};
390+
391+
for (const [title, value] of Object.entries(expectedValues)) {
392+
const card = page
393+
.locator('[role="article"]')
394+
.filter({ hasText: title })
395+
.first();
396+
await expect(card).toContainText(value);
397+
}
398+
399+
const qualityGateCard = page
400+
.locator('[role="article"]')
401+
.filter({
402+
hasText: translations.metric['sonarqube.quality_gate'].title,
403+
})
404+
.first();
405+
await expect(
406+
qualityGateCard.getByTestId('CheckCircleOutlineIcon'),
407+
).toBeVisible();
408+
});
409+
410+
test('Verify SonarQube quality gate failure state', async () => {
411+
await mockSonarqubeScorecardResponse(
412+
page,
413+
sonarqubeFailedQualityGateResponse,
414+
);
415+
416+
await catalogPage.openCatalog();
417+
await catalogPage.openComponent('sonarqube-scorecard-only');
418+
await page.getByText('Scorecard', { exact: true }).click();
419+
420+
await expect(
421+
page.getByText(translations.metric['sonarqube.quality_gate'].title),
422+
).toBeVisible({ timeout: 10000 });
423+
424+
const qualityGateCard = page
425+
.locator('[role="article"]')
426+
.filter({
427+
hasText: translations.metric['sonarqube.quality_gate'].description,
428+
})
429+
.first();
430+
await expect(
431+
qualityGateCard.getByTestId('DangerousOutlinedIcon'),
432+
).toBeVisible();
433+
});
434+
435+
test('Verify empty state for sonarqube entity with no metrics', async () => {
436+
await mockSonarqubeScorecardResponse(page, emptyScorecardResponse);
437+
438+
await catalogPage.openCatalog();
439+
await catalogPage.openComponent('sonarqube-scorecard-only');
440+
await page.getByText('Scorecard', { exact: true }).click();
441+
442+
await expect(page.getByText(translations.emptyState.title)).toBeVisible();
443+
});
444+
});
445+
336446
test.describe('Homepage aggregated scorecards', () => {
337447
test('Verify missing permission on all default homepage scorecard widgets', async () => {
338448
await mockHomepageAggregationsPermissionDenied(page);

0 commit comments

Comments
 (0)