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
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'@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
---

Aggregated scorecards now use **aggregation IDs** and dedicated HTTP routes. The old catalog-aggregations URL still works but is **deprecated** (not removed).

**Backend (`@red-hat-developer-hub/backstage-plugin-scorecard-backend`)**

- **Deprecated:** `GET /metrics/:metricId/catalog/aggregations` — responses are unchanged, but the handler emits [RFC 8594](https://datatracker.ietf.org/doc/html/rfc8594) `Deprecation` and `Link` headers (alternate successor: `GET .../aggregations/:aggregationId`) and logs a warning. Prefer **`GET /aggregations/:aggregationId`** for new integrations.
- **Added:** `GET /aggregations/:aggregationId` for aggregated results using configured aggregation.
- **Added:** `GET /aggregations/:aggregationId/metadata` for KPI titles, descriptions, and aggregation metadata consumed by the UI.

**Common (`@red-hat-developer-hub/backstage-plugin-scorecard-common`)**

- Types and constants aligned with the aggregation config and new API shapes.

**Frontend (`@red-hat-developer-hub/backstage-plugin-scorecard`)**

- Homepage and aggregated flows resolve cards via **`aggregationId`**, fetch metadata from the new endpoint, and keep localized threshold and error strings where translation keys exist.

**Action for adopters:** Configure aggregated scorecards with `aggregationId` values that match backend aggregation config, replace direct calls to `GET /metrics/:metricId/catalog/aggregations` with `GET /aggregations/:aggregationId` (and metadata if you need the same labels as the plugin UI).
72 changes: 52 additions & 20 deletions workspaces/scorecard/app-config.local.EXAMPLE.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,81 @@ permission:
enabled: false

auth:
environment: production
environment: development
providers:
github:
production:
clientId: TODO
clientSecret: TODO
development:
clientId: ${GITHUB_CLIENT_ID}
clientSecret: ${GITHUB_CLIENT_SECRET}
signIn:
resolvers:
- resolver: usernameMatchingUserEntityName

catalog:
locations:
- type: file
target: ../../examples/all-scorecards.yaml
target: ../../examples/all-scorecards-location.yaml
rules:
- allow:
[Component, System, API, Resource, Location, Template, User, Group]
- allow: [Component]
- type: file
target: ../../examples/orgs/guest.yaml
rules:
- allow: [User, Group]
# TODO: Additional catalog entities for your user

proxy:
'/jira/api':
target: ${JIRA_URL}
headers:
Authorization: ${JIRA_TOKEN}
Accept: 'application/json'
Content-Type: 'application/json'
X-Atlassian-Token: 'nocheck'
User-Agent: 'MY-UA-STRING'
# ToDo: uncomment this when Jira Proxy is needed
# proxy:
# '/jira/api':
# target: ${JIRA_URL}
# headers:
# Authorization: ${JIRA_TOKEN}
# Accept: 'application/json'
# Content-Type: 'application/json'
# X-Atlassian-Token: 'nocheck'
# User-Agent: 'MY-UA-STRING'

integrations:
github:
- host: github.com
token: TODO
token: ${GITHUB_TOKEN}

jira:
proxyPath: /jira/api
# ToDo: uncomment this when Jira Proxy is needed
# proxyPath: /jira/api

product: cloud
baseUrl: ${JIRA_URL}
token: ${JIRA_TOKEN}

# ToDo: uncomment this when Jira Data Center is needed
# product: datacenter
# baseUrl: ${JIRA_DATA_CENTER_URL}
# token: ${JIRA_DATA_CENTER_TOKEN}

# Optional Scorecard overrides (aggregationKPIs defaults are in app-config.yaml).
scorecard:
plugins:
jira:
open_issues:
options:
mandatoryFilter: Resolution = Unresolved
schedule:
frequency: { days: 1 }
timeout: { minutes: 2 }
initialDelay: { minutes: 1 }
# Optional — uncomment to narrow which Jira issues are counted (otherwise all open issues match the provider rules).
# options:
# mandatoryFilter: type = Story
# customFilter: priority = High
github:
open_prs:
# Example threshold overrides; remove to use provider defaults.
thresholds:
rules:
- key: success
expression: '<=20'
color: 'success.main'
- key: warning
expression: '20-50'
color: '#FFFF00'
- key: error
expression: '>50'
color: 'rgb(255, 0, 0)'
15 changes: 15 additions & 0 deletions workspaces/scorecard/app-config.production.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,18 @@ catalog:
target: ./examples/org.yaml
rules:
- allow: [User, Group]
# Scorecard (uncomment and adjust for production — requires scorecard backend + metric modules)
# scorecard:
# aggregationKPIs:
# openPrsKpi:
# title: GitHub open PRs
# description: Open PRs across owned entities, grouped by status.
# type: statusGrouped
# metricId: github.open_prs
# plugins:
# github:
# open_prs:
# schedule:
# frequency: { hours: 1 }
# timeout: { minutes: 15 }
# initialDelay: { minutes: 1 }
15 changes: 15 additions & 0 deletions workspaces/scorecard/app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ catalog:
target: ../../examples/all-scorecards-location.yaml
rules:
- allow: [Component]
- type: file
target: ../../examples/orgs/guest.yaml
rules:
- allow: [User, Group]

- type: url
target: https://github.com/redhat-developer/rhdh/blob/main/catalog-entities/components/showcase.yaml
Expand All @@ -161,6 +165,17 @@ permission:

# Scorecard development configuration
scorecard:
aggregationKPIs:
openPrsKpi:
title: GitHub Open PRs KPI
type: statusGrouped
description: This KPI is provide information about GitHub open PRs grouped by status.
metricId: github.open_prs
openIssuesKpi:
title: Jira Open Issues KPI
type: statusGrouped
description: This KPI is provide information about Jira open issues grouped by status.
metricId: jira.open_issues
plugins:
jira:
open_issues:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export const AGGREGATED_CARDS_METRIC_IDS = {
withDeprecatedMetricId: 'jira.open_issues',
withDefaultAggregation: 'github.open_prs',
withGithubOpenPrs: 'openPrsKpi',
withJiraOpenIssuesKpi: 'openIssuesKpi',
} as const;

export const AGGREGATED_CARDS_WIDGET_TITLES = {
/** Must match `title` in App.tsx homepage widget config (Add widget picker). */
withDeprecatedMetricId: 'Scorecard: With deprecated metricId property (Jira)',
withDefaultAggregation: 'Scorecard: With default aggregation config (GitHub)',
withGithubOpenPrs: 'Scorecard: GitHub open PRs',
withJiraOpenIssuesKpi: 'Scorecard: Jira open blocking tickets',
} as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export const ScorecardRoutes = {
SCORECARD_API_ROUTE:
'**/api/scorecard/metrics/catalog/Component/default/red-hat-developer-hub*',
OPEN_PRS_KPI_METADATA_ROUTE:
'**/api/scorecard/aggregations/openPrsKpi/metadata',
OPEN_ISSUES_KPI_METADATA_ROUTE:
'**/api/scorecard/aggregations/openIssuesKpi/metadata',
OPEN_PRS_KPI_AGGREGATION_ROUTE: '**/api/scorecard/aggregations/openPrsKpi',
OPEN_ISSUES_KPI_AGGREGATION_ROUTE:
'**/api/scorecard/aggregations/openIssuesKpi',
/** Default aggregation when aggregationId is the metric id (no KPI entry). */
JIRA_OPEN_ISSUES_METRIC_AGGREGATION_ROUTE:
'**/api/scorecard/aggregations/jira.open_issues',
GITHUB_OPEN_PRS_METRIC_AGGREGATION_ROUTE:
'**/api/scorecard/aggregations/github.open_prs',
} as const;
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class CatalogPage {
}

async openCatalog() {
await this.page.getByRole('link', { name: 'Catalog', exact: true }).click();
await this.page.goto('/catalog'); // Resolves the issue when "My Groups" sidebar covers the catalog toolbar
await this.page.getByTestId('user-picker-all').getByText('All').click();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
getEntitiesLabel,
getEntityCount,
getLastUpdatedLabel,
getMetricTitleEn,
} from '../utils/translationUtils';

type ThresholdState = 'success' | 'warning' | 'error';
Expand Down Expand Up @@ -74,26 +73,16 @@ export class HomePage {
await this.page.getByRole('button', { name: 'Save' }).click();
}

async expectCardVisible(metricId: 'github.open_prs' | 'jira.open_issues') {
await expect(
this.page.getByText(this.translations.metric[metricId].title),
).toBeVisible();
async expectCardVisible(instanceId: string) {
await expect(this.getCard(instanceId)).toBeVisible();
}

async expectCardNotVisible(metricId: 'github.open_prs' | 'jira.open_issues') {
await expect(
this.page.getByText(this.translations.metric[metricId].title),
).not.toBeVisible();
async expectCardNotVisible(instanceId: string) {
await expect(this.getCard(instanceId)).not.toBeVisible();
}

getCard(metricId: 'github.open_prs' | 'jira.open_issues'): Locator {
const translatedTitle = this.translations.metric[metricId].title;
const enTitle = getMetricTitleEn(metricId);
const pattern =
translatedTitle === enTitle
? translatedTitle
: new RegExp(`${escapeRegex(translatedTitle)}|${escapeRegex(enTitle)}`);
return this.page.locator('article').filter({ hasText: pattern });
getCard(instanceId: string): Locator {
return this.page.getByTestId(`scorecard-homepage-card-${instanceId}`);
}

async verifyThresholdTooltip(
Expand All @@ -103,7 +92,7 @@ export class HomePage {
percentage: string,
) {
const stateLabel = this.translations.thresholds[state];
await card.getByText(stateLabel, { exact: true }).hover();
await card.getByText(stateLabel, { exact: true }).first().hover();
await expect(
this.page.getByText(
getEntityCount(this.translations, this.locale, entityCount),
Expand All @@ -115,25 +104,21 @@ export class HomePage {
).toBeVisible();
}

async expectCardHasMissingPermission(
metricId: 'github.open_prs' | 'jira.open_issues',
) {
const card = this.getCard(metricId);
async expectCardHasMissingPermission(instanceId: string) {
const card = this.getCard(instanceId);
await expect(card).toContainText(
this.translations.errors.missingPermission,
);
}

async expectCardHasNoDataFound(
metricId: 'github.open_prs' | 'jira.open_issues',
) {
const card = this.getCard(metricId);
async expectCardHasNoDataFound(instanceId: string) {
const card = this.getCard(instanceId);
await expect(card).toContainText(this.translations.errors.noDataFound);
}

async verifyLastUpdatedTooltip(card: Locator, formattedTimestamp: string) {
const label = getLastUpdatedLabel(this.translations, formattedTimestamp);
const infoIcon = card.locator('[data-testid="InfoOutlinedIcon"]');
const infoIcon = card.getByTestId('scorecard-homepage-card-info');
await expect(infoIcon).toBeVisible();
await infoIcon.hover();
await expect(this.page.getByText(label)).toBeVisible();
Expand Down
Loading
Loading