Skip to content

Commit 1532dbc

Browse files
authored
fix(scorecard): forward entity threshold annotation errors (#1428)
* Throw error on invalid annotation Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> * Update default thresholds order Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> * Fix schema change Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> * Introduce supportsEntity Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> --------- Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
1 parent d62c3af commit 1532dbc

17 files changed

Lines changed: 195 additions & 81 deletions

File tree

workspaces/scorecard/plugins/scorecard-backend-module-github/src/metricProviders/GithubOpenPRsProvider.test.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ConfigReader } from '@backstage/config';
1818
import type { Entity } from '@backstage/catalog-model';
1919
import { GithubOpenPRsProvider } from './GithubOpenPRsProvider';
2020
import { GithubClient } from '../github/GithubClient';
21+
import { DEFAULT_NUMBER_THRESHOLDS } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';
2122

2223
jest.mock('@backstage/catalog-model', () => ({
2324
...jest.requireActual('@backstage/catalog-model'),
@@ -29,17 +30,47 @@ jest.mock('@backstage/catalog-model', () => ({
2930
jest.mock('../github/GithubClient');
3031

3132
describe('GithubOpenPRsProvider', () => {
33+
describe('supportsEntity', () => {
34+
let provider: GithubOpenPRsProvider;
35+
36+
beforeEach(() => {
37+
provider = GithubOpenPRsProvider.fromConfig(new ConfigReader({}));
38+
});
39+
40+
it.each([
41+
[
42+
'should return true for entity with github.com/project-slug annotation',
43+
{
44+
'github.com/project-slug': 'org/repo',
45+
},
46+
true,
47+
],
48+
[
49+
'should return false for entity without github.com/project-slug annotation',
50+
{
51+
'some.other/annotation': 'value',
52+
},
53+
false,
54+
],
55+
['should return false for entity with no annotations', undefined, false],
56+
])('%s', (_, annotations, expected) => {
57+
const entity: Entity = {
58+
apiVersion: 'backstage.io/v1alpha1',
59+
kind: 'Component',
60+
metadata: {
61+
name: 'test-component',
62+
annotations,
63+
},
64+
};
65+
expect(provider.supportsEntity(entity)).toBe(expected);
66+
});
67+
});
68+
3269
describe('fromConfig', () => {
3370
it('should create provider with default thresholds when no thresholds are configured', () => {
3471
const provider = GithubOpenPRsProvider.fromConfig(new ConfigReader({}));
3572

36-
expect(provider.getMetricThresholds()).toEqual({
37-
rules: [
38-
{ key: 'error', expression: '>50' },
39-
{ key: 'warning', expression: '10-50' },
40-
{ key: 'success', expression: '<10' },
41-
],
42-
});
73+
expect(provider.getMetricThresholds()).toEqual(DEFAULT_NUMBER_THRESHOLDS);
4374
});
4475

4576
it('should create provider with custom thresholds when configured', () => {

workspaces/scorecard/plugins/scorecard-backend-module-github/src/metricProviders/GithubOpenPRsProvider.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
getHostnameFromEntity,
3131
getRepositoryInformationFromEntity,
3232
} from '../github/utils';
33+
import { GITHUB_PROJECT_ANNOTATION } from '../github/constants';
3334

3435
export class GithubOpenPRsProvider implements MetricProvider<'number'> {
3536
private readonly thresholds: ThresholdConfig;
@@ -63,6 +64,12 @@ export class GithubOpenPRsProvider implements MetricProvider<'number'> {
6364
return this.thresholds;
6465
}
6566

67+
supportsEntity(entity: Entity): boolean {
68+
return (
69+
entity.metadata.annotations?.[GITHUB_PROJECT_ANNOTATION] !== undefined
70+
);
71+
}
72+
6673
static fromConfig(config: Config): GithubOpenPRsProvider {
6774
const configPath = 'scorecard.plugins.github.open_prs.thresholds';
6875
const configuredThresholds = config.getOptional(configPath);

workspaces/scorecard/plugins/scorecard-backend-module-jira/src/metricProviders/JiraOpenIssuesProvider.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ export class JiraOpenIssuesProvider implements MetricProvider<'number'> {
5656
return this.thresholds;
5757
}
5858

59+
supportsEntity(entity: Entity): boolean {
60+
return entity.metadata.annotations?.['jira/project-key'] !== undefined;
61+
}
62+
5963
static fromConfig(config: Config): JiraOpenIssuesProvider {
6064
const configPath = 'scorecard.plugins.jira.open_issues.thresholds';
6165
const configuredThresholds = config.getOptional(configPath);

workspaces/scorecard/plugins/scorecard-backend/__fixtures__/mockProviders.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ abstract class MockMetricProvider<T extends MetricType>
4545
return this.providerId;
4646
}
4747

48+
supportsEntity(_: Entity): boolean {
49+
return true;
50+
}
51+
4852
getMetric(): Metric<T> {
4953
const metric: Metric<T> = {
5054
id: this.providerId,

workspaces/scorecard/plugins/scorecard-backend/docs/providers.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ export class MyMetricProvider implements MetricProvider<'number'> {
6767
};
6868
}
6969

70+
// Determines whether this metric can be calculated for a specific entity (e.g., based on annotations)
71+
supportsEntity(_: Entity): boolean {
72+
return true;
73+
}
74+
7075
// Calculates and returns the metric value
7176
async calculateMetric(): Promise<number> {
7277
// TODO: Implement your metric calculation logic here

workspaces/scorecard/plugins/scorecard-backend/docs/thresholds.md

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Thresholds Configuration
22

3-
Thresholds define conditions that determine which category a metric value belongs to ( `error`, `warning`, or `success`). When a metric value matches a threshold condition, it gets assigned to that threshold's category. The Scorecard plugin provides multiple ways to configure thresholds with a flexible priority system.
3+
Thresholds define conditions that determine which category a metric value belongs to (`success`, `warning`, or `error`). When a metric value matches a threshold condition, it gets assigned to that threshold's category. The Scorecard plugin provides multiple ways to configure thresholds with a flexible priority system.
44

55
## Overview
66

77
Thresholds are evaluated in order and the **first matching** threshold rule is applied. Each threshold rule consists of:
88

9-
- **`key`**: The threshold category (only allowed keys are `error`, `warning`, `success`)
9+
- **`key`**: The threshold category (only allowed keys are `success`, `warning`, `error`)
1010
- **`expression`**: The condition that determines if a metric value matches this threshold
1111

1212
## Threshold Configuration Options
@@ -24,9 +24,9 @@ export class MyMetricProvider implements MetricProvider<'number'> {
2424
getMetricThresholds(): ThresholdConfig {
2525
return {
2626
rules: [
27-
{ key: 'error', expression: '>50' },
28-
{ key: 'warning', expression: '10-50' },
2927
{ key: 'success', expression: '<10' },
28+
{ key: 'warning', expression: '10-50' },
29+
{ key: 'error', expression: '>50' },
3030
],
3131
};
3232
}
@@ -81,12 +81,12 @@ scorecard:
8181
myMetric:
8282
thresholds:
8383
rules:
84+
- key: success
85+
expression: '<10'
86+
- key: warning
87+
expression: '<=20'
8488
- key: error
8589
expression: '>20'
86-
- key: warning
87-
expression: '>10'
88-
- key: success
89-
expression: '<=10'
9090
myOtherDatasource:
9191
myOtherMetric: ...
9292
```
@@ -102,9 +102,9 @@ metadata:
102102
name: my-service
103103
annotations:
104104
# Override specific threshold rules for this entity
105+
scorecard.io/myDatasource.myMetric.thresholds.rules.warning: '10-15'
105106
scorecard.io/myDatasource.myMetric.thresholds.rules.error: '>15'
106-
scorecard.io/myDatasource.myMetric.thresholds.rules.warning: '>8'
107-
# success threshold will use the default/config value
107+
# success threshold will use the default config value
108108
spec:
109109
type: service
110110
```
@@ -120,7 +120,7 @@ scorecard.io/{providerId}.thresholds.rules.{thresholdKey}: '{expression}'
120120
Where:
121121
122122
- `{providerId}`: The metric provider ID (e.g., `github.open-prs`)
123-
- `{thresholdKey}`: The threshold category (e.g., `error`, `warning`, `success`)
123+
- `{thresholdKey}`: The threshold category (e.g., `success`, `warning`, `error`)
124124
- `{expression}`: The threshold expression (e.g., `>10`, `==true`, `5-15`)
125125

126126
## Threshold Priority Order
@@ -143,28 +143,29 @@ Thresholds are applied with the following priority (highest to lowest):
143143
```typescript
144144
// 1. Provider defaults
145145
providerDefaults: [
146-
{ key: 'error', expression: '>10' },
147-
{ key: 'warning', expression: '>5' },
148-
{ key: 'success', expression: '<=5' }
146+
{ key: 'success', expression: '<10' },
147+
{ key: 'warning', expression: '10-50' },
148+
{ key: 'error', expression: '>50' }
149149
]
150150
151151
// 2. App configuration (completely replaces defaults)
152152
appConfig: [
153-
{ key: 'error', expression: '>20' },
154-
{ key: 'warning', expression: '>10' },
155-
{ key: 'success', expression: '<=10' }
153+
{ key: 'success', expression: '<10' },
154+
{ key: 'warning', expression: '10-30' },
155+
{ key: 'error', expression: '>30' },
156156
]
157157
158158
// 3. Entity annotation overrides (merged with app-config)
159159
annotations: {
160-
'scorecard.io/myProvider.thresholds.rules.error': '>25',
160+
'scorecard.io/myProviderId.thresholds.rules.warning': '10-25',
161+
'scorecard.io/myProviderId.thresholds.rules.error': '>25',
161162
}
162163
163164
// Final result (annotations merged with app-config)
164165
finalRules: [
166+
{ key: 'success', expression: '<10' }, // from app-config
167+
{ key: 'warning', expression: '10-25' }, // from annotation (overrides app-config)
165168
{ key: 'error', expression: '>25' }, // from annotation (overrides app-config)
166-
{ key: 'warning', expression: '>10' }, // from app-config
167-
{ key: 'success', expression: '<=10' } // from app-config
168169
]
169170
```
170171

@@ -225,27 +226,27 @@ The `ThresholdEvaluator` service processes threshold rules and determines which
225226

226227
### Key Features
227228

228-
1. **Order-dependent evaluation**: Rules are evaluated in the order they appear
229+
1. **Order-dependent evaluation**: Rules are evaluated in the order they appear. If provider supports overriding defaults through [app configuration](#App-Configuration-Thresholds), you can change the evaluation order by specifying threshold keys in a different order. Entity annotations cannot alter the evaluation order, which is determined by either the [app configuration](#Provider-Default-Thresholds) or, if not specified, the [default provider configuration](#Provider-Default-Thresholds).
229230
2. **First-match wins**: Returns the first threshold rule whose condition the value satisfies
230231
3. **Type-safe**: Validates expressions against metric types
231-
4. **Error handling**: Graceful handling of invalid expressions. You should validate your expressions loaded from config in your custom providers using `validateThresholds` from `backstage-plugin-scorecard-node`. Invalid annotation expressions are logged and skipped (do not override)
232+
4. **Error handling**: You should validate your expressions loaded from config in your custom providers using `validateThresholds` from `backstage-plugin-scorecard-node`. Invalid expressions will result in evaluation error.
232233

233234
### Best Practices
234235

235236
### 1. Logical Ordering
236237

237238
Order rules from most restrictive to least restrictive.
238239

239-
In this example, the `error` rule would never be triggered because any value greater than 20 would already match the `warning` rule (since it is evaluated first). As a result, all values above 50 would be classified as `warning` instead of `error`:
240+
In this example, the `success` rule would never be triggered because any value smaller than 15 would already match the `warning` rule (since it is evaluated first). As a result, all values under 15 would be classified as `warning` instead of `success`:
240241

241242
```yaml
242243
rules:
243244
- key: warning
244-
expression: '>20'
245-
- key: error
246-
expression: '>50'
245+
expression: '<30'
247246
- key: success
248-
expression: '<=20'
247+
expression: '<15'
248+
- key: error
249+
expression: '>30'
249250
```
250251

251252
### 2. Avoid Overlapping Ranges

workspaces/scorecard/plugins/scorecard-backend/src/plugin.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ export const scorecardPlugin = createBackendPlugin({
5757
auth: coreServices.auth,
5858
httpRouter: coreServices.httpRouter,
5959
catalog: catalogServiceRef,
60-
logger: coreServices.logger,
6160
httpAuth: coreServices.httpAuth,
6261
permissions: coreServices.permissions,
6362
permissionsRegistry: coreServices.permissionsRegistry,
@@ -66,7 +65,6 @@ export const scorecardPlugin = createBackendPlugin({
6665
discovery,
6766
auth,
6867
httpRouter,
69-
logger,
7068
httpAuth,
7169
permissions,
7270
permissionsRegistry,
@@ -82,7 +80,6 @@ export const scorecardPlugin = createBackendPlugin({
8280
catalogApi: catalogClient,
8381
registry: metricProvidersRegistry,
8482
thresholdEvaluator: new ThresholdEvaluator(),
85-
logger,
8683
auth,
8784
});
8885

0 commit comments

Comments
 (0)