Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions workspaces/scorecard/.changeset/quick-monkeys-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-dora': minor
'@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-github': minor
'@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-jira': minor
'@red-hat-developer-hub/backstage-plugin-scorecard-backend': minor
'@red-hat-developer-hub/backstage-plugin-scorecard-common': minor
'@red-hat-developer-hub/backstage-plugin-scorecard': minor
---

Add DORA metrics and a collectors framework for composing datasource data into metrics.

- New `@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-dora` with Deployment Frequency, Median Lead Time for Changes, Mean Time to Restore, and Change Failure Rate
- New data collectors used by DORA: GitHub deployments, deployment workflow runs, and deployment pull requests; Jira incidents
- Metric time-series API `/metrics/catalog/:kind/:namespace/:name/time-series`
- Adds `defaultVisualization` to Metric metadata for sparkline
36 changes: 23 additions & 13 deletions workspaces/scorecard/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@

All metric IDs use `lowerCamelCase` with a `<provider>.<metricName>` format:

- Provider prefix is lowercase: `github`, `jira`, `sonarqube`, `dependabot`, `openssf`, `filecheck`
- Provider prefix is lowercase: `github`, `jira`, `sonarqube`, `dependabot`, `openssf`, `filecheck`, `dora`
- Metric name is lowerCamelCase: `openPRs`, `qualityGate`, `ciiBestPractices`
- Full ID examples: `github.openPRs`, `sonarqube.qualityGate`, `openssf.ciiBestPractices`
- Full ID examples: `github.openPRs`, `sonarqube.qualityGate`, `openssf.ciiBestPractices`, `dora.deploymentFrequency`

Never use snake_case for metric IDs. SonarQube API keys (e.g.,
`security_rating`, `code_smells`) are external API field names and remain
Expand All @@ -35,6 +35,15 @@ snake_case in the API layer only -- they are not metric IDs.
| ----------------- | ------ | --------------------------- |
| `jira.openIssues` | number | `JiraOpenIssuesProvider.ts` |

### DORA (4 metrics)

| Metric ID | Type | Source |
| ------------------------------- | ------ | ----------------------------------------- |
| `dora.deploymentFrequency` | number | `DoraDeploymentFrequencyProvider.ts` |
| `dora.medianLeadTimeForChanges` | number | `DoraMedianLeadTimeForChangesProvider.ts` |
| `dora.meanTimeToRestore` | number | `DoraMeanTimeToRestoreProvider.ts` |
| `dora.changeFailureRate` | number | `DoraChangeFailureRateProvider.ts` |

### Dependabot (4 metrics)

| Metric ID | Type | Source |
Expand Down Expand Up @@ -192,14 +201,15 @@ When reviewing changes to `ThresholdResolver`, the `Metric` type,

### Key files

| File | Package | Role |
| ----------------------------------- | ------------------------------------- | ----------------------------------------------------------- |
| `ThresholdResolver.ts` | `scorecard-backend` | Resolves thresholds using the three-tier chain |
| `Metric.ts` | `scorecard-common` | Defines `Metric`, `MetricType`, and `MetricValue` types |
| `MetricProvider.ts` | `scorecard-node` | Defines the `MetricProvider<T>` interface |
| `mergeEntityAndMetricThresholds.ts` | `scorecard-backend` | Merges entity annotation overrides with metric thresholds |
| `getThresholdsFromConfig.ts` | `scorecard-node` | Reads and validates threshold config from `app-config.yaml` |
| `DependabotConfig.ts` | `scorecard-backend-module-dependabot` | Dependabot provider metric and threshold definitions |
| `SonarQubeConfig.ts` | `scorecard-backend-module-sonarqube` | SonarQube provider metric and threshold definitions |
| `OpenSSFConfig.ts` | `scorecard-backend-module-openssf` | OpenSSF provider metric and threshold definitions |
| `FilecheckConfig.ts` | `scorecard-backend-module-filecheck` | Filecheck provider metric and threshold definitions |
| File | Package | Role |
| ----------------------------------- | ------------------------------------- | ----------------------------------------------------------------- |
| `ThresholdResolver.ts` | `scorecard-backend` | Resolves thresholds using the three-tier chain |
| `Metric.ts` | `scorecard-common` | Defines `Metric`, `MetricType`, and `MetricValue` types |
| `MetricProvider.ts` | `scorecard-node` | Defines the `MetricProvider<T>` interface |
| `mergeEntityAndMetricThresholds.ts` | `scorecard-backend` | Merges entity annotation overrides with metric thresholds |
| `getThresholdsFromConfig.ts` | `scorecard-node` | Reads and validates threshold config from `app-config.yaml` |
| `DependabotConfig.ts` | `scorecard-backend-module-dependabot` | Dependabot provider metric and threshold definitions |
| `SonarQubeConfig.ts` | `scorecard-backend-module-sonarqube` | SonarQube provider metric and threshold definitions |
| `OpenSSFConfig.ts` | `scorecard-backend-module-openssf` | OpenSSF provider metric and threshold definitions |
| `FilecheckConfig.ts` | `scorecard-backend-module-filecheck` | Filecheck provider metric and threshold definitions |
| `DoraConfig.ts` | `scorecard-backend-module-dora` | DORA provider config, collector wiring, and threshold definitions |
59 changes: 59 additions & 0 deletions workspaces/scorecard/app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -390,3 +390,62 @@ scorecard:
frequency: { minutes: 5 }
timeout: { minutes: 10 }
initialDelay: { seconds: 10 }
dora:
deploymentFrequency:
options:
collectors:
deployments:
id: github:deployments
# Uncomment the following to use workflow runs
# id: github:deploymentWorkflowRuns
# input:
# workflowName: Create Test Deployment on PR Merge
schedule:
frequency: { minutes: 5 }
timeout: { minutes: 10 }
initialDelay: { seconds: 10 }
medianLeadTimeForChanges:
options:
collectors:
deployments:
id: github:deployments
# Uncomment the following to use workflow runs
# id: github:deploymentWorkflowRuns
# input:
# workflowName: Create Test Deployment on PR Merge
deploymentPullRequests:
id: github:deploymentPullRequests
schedule:
frequency: { minutes: 5 }
timeout: { minutes: 10 }
initialDelay: { seconds: 10 }
changeFailureRate:
options:
collectors:
deployments:
id: github:deployments
# Uncomment the following to use workflow runs
# id: github:deploymentWorkflowRuns
# input:
# workflowName: Create Test Deployment on PR Merge
incidents:
id: jira:incidents
# Optional: override default Incident issue type
# input:
# issueType: ServiceIncident
schedule:
frequency: { minutes: 5 }
timeout: { minutes: 10 }
initialDelay: { seconds: 10 }
meanTimeToRestore:
options:
collectors:
incidents:
id: jira:incidents
# Optional: override default Incident issue type
# input:
# issueType: ServiceIncident
schedule:
frequency: { minutes: 5 }
timeout: { minutes: 10 }
initialDelay: { seconds: 10 }
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ spec:
- ./components/no-scorecards.yaml
- ./components/openssf-scorecard-only.yaml
- ./components/sonarqube-scorecard-only.yaml
- ./components/dora-scorecard.yaml
15 changes: 15 additions & 0 deletions workspaces/scorecard/examples/components/dora-scorecard.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
# Component with DORA Scorecard
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: dora-scorecard
annotations:
github.com/project-slug: dzemanov/test-scorecard-github-dora
backstage.io/source-location: url:https://github.com/dzemanov/test-scorecard-github-dora
Comment thread
dzemanov marked this conversation as resolved.
scorecard.io/dora: 'true'
jira/incident-project-key: RSPT
spec:
type: service
owner: group:development/guests
lifecycle: experimental
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ test.describe('Metric Group Cards', () => {
await catalogPage.openCatalog();
await catalogPage.openComponent('sonarqube-scorecard-only');
await page.getByText('Scorecard', { exact: true }).click();
await expect(page.getByLabel('Security Vulnerabilities')).toBeVisible({
await expect(
scorecardPage.getGroupCard('Security Vulnerabilities'),
).toBeVisible({
timeout: 15000,
});
});
Expand All @@ -62,7 +64,7 @@ test.describe('Metric Group Cards', () => {

test('Verify group cards render with titles, descriptions, and bucket tiles', async ({}, testInfo) => {
const securityCard = scorecardPage.getGroupCard('Security Vulnerabilities');
await expect(page.getByLabel('Security Vulnerabilities')).toBeVisible();
await expect(securityCard).toBeVisible();
await expect(
securityCard.getByText('Track security issues across your repositories'),
).toBeVisible();
Expand All @@ -71,7 +73,7 @@ test.describe('Metric Group Cards', () => {
).toContainText('1');

const codeQualityCard = scorecardPage.getGroupCard('Code Quality');
await expect(page.getByLabel('Code Quality')).toBeVisible();
await expect(codeQualityCard).toBeVisible();
await expect(
codeQualityCard.getByText('Code quality and maintainability metrics'),
).toBeVisible();
Expand All @@ -80,7 +82,7 @@ test.describe('Metric Group Cards', () => {
).toContainText('3');

const coverageCard = scorecardPage.getGroupCard('SonarQube Coverage');
await expect(page.getByLabel('SonarQube Coverage')).toBeVisible();
await expect(coverageCard).toBeVisible();
await expect(
coverageCard.getByText('SonarQube coverage metrics'),
).toBeVisible();
Expand Down Expand Up @@ -197,9 +199,13 @@ test.describe('Metric Group Cards', () => {
});
}

await expect(page.getByLabel('Security Vulnerabilities')).toBeVisible();
await expect(page.getByLabel('Code Quality')).toBeVisible();
await expect(page.getByLabel('SonarQube Coverage')).toBeVisible();
await expect(
scorecardPage.getGroupCard('Security Vulnerabilities'),
).toBeVisible();
await expect(scorecardPage.getGroupCard('Code Quality')).toBeVisible();
await expect(
scorecardPage.getGroupCard('SonarQube Coverage'),
).toBeVisible();

await runAccessibilityTests(page, testInfo);
});
Expand All @@ -210,7 +216,7 @@ test.describe('Metric Group Cards', () => {
await catalogPage.openComponent('sonarqube-scorecard-only');
await page.getByText('Scorecard', { exact: true }).click();

await expect(page.getByLabel('Code Quality')).toBeVisible({
await expect(scorecardPage.getGroupCard('Code Quality')).toBeVisible({
timeout: 10000,
});
});
Expand Down
1 change: 1 addition & 0 deletions workspaces/scorecard/packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@red-hat-developer-hub/backstage-plugin-scorecard-backend": "workspace:^",
"@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-code-coverage": "workspace:^",
"@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-dependabot": "workspace:^",
"@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-dora": "workspace:^",
"@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-filecheck": "workspace:^",
"@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-github": "workspace:^",
"@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-jira": "workspace:^",
Expand Down
5 changes: 5 additions & 0 deletions workspaces/scorecard/packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,10 @@ backend.add(
'@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-code-coverage'
),
);
backend.add(
import(
'@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-dora'
),
);
backend.add(import('@backstage/plugin-mcp-actions-backend'));
backend.start();
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
Loading
Loading