Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
07c5289
feat: implement batch provider for file checks
djanickova Feb 24, 2026
6a60828
feat: display correct file check scorecards
djanickova Mar 23, 2026
f0385fd
Merge branch 'main' into scorecard-file-level-checks
djanickova Apr 13, 2026
155643b
ref: use resolveMetricTranslation helper
djanickova Apr 14, 2026
4c87647
chore: re-add changes not solved in conflicts
djanickova Apr 14, 2026
154747e
chore: fix yarn tsc
djanickova Apr 14, 2026
3956b85
fix: do not show icon for boolean metric type
djanickova Apr 14, 2026
b6b3404
fix: pass extra props to db in pullProviderMetrics
djanickova Apr 14, 2026
269ca6d
fix: aggregations endpoint for batch providers
djanickova Apr 14, 2026
31c2393
chore: fix prettier
djanickova Apr 14, 2026
5e21972
ref: fix sonarqube issues
djanickova Apr 14, 2026
768ad4d
fix: broken test
djanickova Apr 15, 2026
ae8c0a8
feat: show aggregated cards for file checks
djanickova Apr 15, 2026
554dd8a
ref: fix sonarqube issue
djanickova Apr 15, 2026
4c04759
chore: generate changeset, update readme
djanickova Apr 15, 2026
87d44b2
ref: use license and codeowners as examples
djanickova Apr 15, 2026
3383dfa
fix: implement qodo suggestions
djanickova Apr 15, 2026
56289e1
fix: return empty map for no files
djanickova Apr 15, 2026
4bbe474
ref: fix sonarqube issues
djanickova Apr 15, 2026
9b4f0d1
feat: add aggregation card with custom title
djanickova Apr 16, 2026
671e690
Merge branch 'main' into scorecard-file-level-checks
djanickova Apr 16, 2026
d680d66
Merge branch 'main' into scorecard-file-level-checks
djanickova Apr 20, 2026
7df0227
fix: use ScorecardHomepageCardWithProvider
djanickova Apr 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions workspaces/scorecard/.changeset/big-yaks-say.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-github': minor
'@red-hat-developer-hub/backstage-plugin-scorecard-backend': minor
'@red-hat-developer-hub/backstage-plugin-scorecard-node': minor
'@red-hat-developer-hub/backstage-plugin-scorecard': minor
---

Add support for batch metric providers, allowing a single provider to handle multiple metrics efficiently. Introduce configurable GitHub file existence checks (github.files_check.\*) that verify whether required files (like README, LICENSE, or CODEOWNERS) are present in a repository.
13 changes: 13 additions & 0 deletions workspaces/scorecard/app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,11 @@ scorecard:
type: statusGrouped
description: This KPI is provide information about Jira open issues grouped by status.
metricId: jira.open_issues
licenseFileExistsKpi:
title: License File Exists KPI
type: statusGrouped
description: This KPI is provide information about whether the license file exists in the repository.
metricId: github.files_check.license
plugins:
jira:
open_issues:
Expand All @@ -211,3 +216,11 @@ scorecard:
frequency: { minutes: 5 }
timeout: { minutes: 10 }
initialDelay: { seconds: 5 }
files_check:
files:
- license: 'LICENSE'
- codeowners: 'CODEOWNERS'
schedule:
frequency: { minutes: 5 }
timeout: { minutes: 10 }
initialDelay: { seconds: 5 }
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
jiraEntitiesDrillDownResponse,
jiraEntitiesDrillDownNoDataResponse,
jiraMetricMetadataResponse,
fileCheckScorecardResponse,
} from './utils/scorecardResponseUtils';
import {
ScorecardMessages,
Expand Down Expand Up @@ -254,6 +255,71 @@ test.describe('Scorecard Plugin Tests', () => {

await runAccessibilityTests(page, testInfo);
});

test('Verify file check metrics display correctly', async ({
browser,
}, testInfo) => {
await mockApiResponse(
page,
ScorecardRoutes.SCORECARD_API_ROUTE,
fileCheckScorecardResponse,
);

await catalogPage.openCatalog();
await catalogPage.openComponent('Red Hat Developer Hub');
await scorecardPage.openTab();

const existLabel = translations.thresholds.exist ?? 'Exist';
const missingLabel = translations.thresholds.missing ?? 'Missing';

const readmeTitle = evaluateMessage(
translations.metric['github.files_check'].title,
'readme',
);
const readmeDescription = evaluateMessage(
translations.metric['github.files_check'].description,
'readme',
);

const readmeCard = page
.locator('[role="article"]')
.filter({ hasText: readmeTitle })
.first();
await expect(readmeCard).toBeVisible();
await expect(readmeCard.getByText(readmeDescription)).toBeVisible();
await expect(
readmeCard.getByText(existLabel, { exact: true }),
).toBeVisible();
await expect(
readmeCard.getByText(missingLabel, { exact: true }),
).toBeVisible();

const codeownersTitle = evaluateMessage(
translations.metric['github.files_check'].title,
'codeowners',
);
const codeownersDescription = evaluateMessage(
translations.metric['github.files_check'].description,
'codeowners',
);

const codeownersCard = page
.locator('[role="article"]')
.filter({ hasText: codeownersTitle })
.first();
await expect(codeownersCard).toBeVisible();
await expect(
codeownersCard.getByText(codeownersDescription),
).toBeVisible();
await expect(
codeownersCard.getByText(existLabel, { exact: true }),
).toBeVisible();
await expect(
codeownersCard.getByText(missingLabel, { exact: true }),
).toBeVisible();

await runAccessibilityTests(page, testInfo);
});
});

test.describe('Homepage aggregated scorecards', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -506,3 +506,74 @@ export const jiraEntitiesDrillDownNoDataResponse = {
isCapped: false,
},
};

export const fileCheckScorecardResponse = [
{
id: 'github.files_check.readme',
status: 'success',
metadata: {
title: 'GitHub File: README.md',
description: 'Checks if README.md exists in the repository.',
type: 'boolean',
history: true,
},
result: {
value: true,
timestamp: '2025-09-08T09:08:55.629Z',
thresholdResult: {
definition: {
rules: [
{
key: 'exist',
expression: '==true',
color: 'success.main',
icon: 'scorecardSuccessStatusIcon',
},
{
key: 'missing',
expression: '==false',
color: 'error.main',
icon: 'scorecardErrorStatusIcon',
},
],
},
status: 'success',
evaluation: 'exist',
},
},
},
{
id: 'github.files_check.codeowners',
status: 'success',
metadata: {
title: 'GitHub File: CODEOWNERS',
description: 'Checks if CODEOWNERS exists in the repository.',
type: 'boolean',
history: true,
},
result: {
value: false,
timestamp: '2025-09-08T09:08:55.629Z',
thresholdResult: {
definition: {
rules: [
{
key: 'exist',
expression: '==true',
color: 'success.main',
icon: 'scorecardSuccessStatusIcon',
},
{
key: 'missing',
expression: '==false',
color: 'error.main',
icon: 'scorecardErrorStatusIcon',
},
],
},
status: 'success',
evaluation: 'missing',
},
},
},
];
74 changes: 71 additions & 3 deletions workspaces/scorecard/packages/app-legacy/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,66 @@ const mountPoints: HomePageCardMountPoint[] = [
},
},
},
{
Component: ScorecardHomepageCard as ComponentType,
config: {
id: 'scorecard-github.files_check.license',
title: 'Scorecard: LICENSE file exists',
cardLayout: {
width: {
minColumns: 3,
maxColumns: 12,
defaultColumns: 4,
},
height: {
minRows: 5,
maxRows: 12,
defaultRows: 6,
},
},
layouts: {
xl: { w: 4, h: 6 },
lg: { w: 4, h: 6 },
md: { w: 4, h: 6 },
sm: { w: 4, h: 6 },
xs: { w: 4, h: 6 },
xxs: { w: 4, h: 6 },
},
props: {
aggregationId: 'github.files_check.license',
},
},
},
{
Component: ScorecardHomepageCard as ComponentType,
config: {
id: 'scorecard-github.files_check.codeowners',
title: 'Scorecard: CODEOWNERS file exists',
cardLayout: {
width: {
minColumns: 3,
maxColumns: 12,
defaultColumns: 4,
},
height: {
minRows: 5,
maxRows: 12,
defaultRows: 6,
},
},
layouts: {
xl: { w: 4, h: 6, x: 4 },
lg: { w: 4, h: 6, x: 4 },
md: { w: 4, h: 6, x: 4 },
sm: { w: 4, h: 6, x: 4 },
xs: { w: 4, h: 6, x: 4 },
xxs: { w: 4, h: 6, x: 4 },
},
props: {
aggregationId: 'github.files_check.codeowners',
},
},
},
Comment thread
djanickova marked this conversation as resolved.
{
Component: ScorecardHomepageCard as ComponentType,
config: {
Expand Down Expand Up @@ -250,15 +310,23 @@ const mountPoints: HomePageCardMountPoint[] = [
metricId: {
title: 'Metric (Needs currently a page reload after change!)',
type: 'string',
default: 'jira.open_issues',
enum: ['jira.open_issues', 'github.open_prs'],
default: 'github.open_prs',
enum: [
'github.open_prs',
'github.files_check.license',
'github.files_check.codeowners',
],
},
},
},
uiSchema: {
metricId: {
'ui:widget': 'RadioWidget',
'ui:enumNames': ['Jira Open Issues', 'GitHub Open PRs'],
'ui:enumNames': [
'GitHub Open PRs',
'LICENSE file exists',
'CODEOWNERS file exists',
],
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,51 @@ This metric counts all pull requests that are currently in an "open" state for t
expression: '<10'
```

### GitHub File Checks (`github.files_check.*`)

This metric provider checks whether specific files exist in a repository. It's useful for enforcing best practices like having a `README.md`, `LICENSE`, `CODEOWNERS`, or other required files.

- **Metric ID**: `github.files_check.<file_id>` (e.g., `github.files_check.readme`)
- **Type**: Boolean
- **Datasource**: `github`
- **Default thresholds**:
- `success`: File exists (`==true`)
- `error`: File is missing (`==false`)

#### Configuration

To enable file checks, add a `files_check` configuration in your `app-config.yaml`:

```yaml
# app-config.yaml
scorecard:
plugins:
github:
files_check:
files:
- readme: 'README.md'
- license: 'LICENSE'
- codeowners: 'CODEOWNERS'
- dockerfile: 'Dockerfile'
```

Each entry in the `files` array creates a separate metric:

- The **key** (e.g., `readme`) becomes the metric identifier suffix (`github.files_check.readme`)
- The **value** (e.g., `README.md`) is the file path to check in the repository

#### File Path Format

File paths must be relative to the repository root:

| Format | Example | Valid |
| ---------------- | ---------------------- | ----- |
| Root file | `README.md` | ✅ |
| Subdirectory | `docs/CONTRIBUTING.md` | ✅ |
| Hidden file | `.gitignore` | ✅ |
| With `./` prefix | `./README.md` | ❌ |
| Absolute path | `/home/file.txt` | ❌ |

## Configuration

### Threshold Configuration
Expand All @@ -107,6 +152,16 @@ scorecard:
minutes: 5
initialDelay:
seconds: 5
files_check:
files:
- readme: 'README.md'
schedule:
frequency:
cron: '0 6 * * *'
timeout:
minutes: 5
initialDelay:
seconds: 5
```

The schedule configuration follows Backstage's `SchedulerServiceTaskScheduleDefinitionConfig` [schema](https://github.com/backstage/backstage/blob/master/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts#L157).
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ export interface Config {
};
schedule?: SchedulerServiceTaskScheduleDefinitionConfig;
};
files_check?: {
/** File existence checks configuration */
files?: Array<{
/** Key is the metric identifier, value is the file path */
[metricId: string]: string;
}>;
schedule?: SchedulerServiceTaskScheduleDefinitionConfig;
};
};
};
};
Expand Down
Loading
Loading