diff --git a/workspaces/scorecard/.changeset/add-codecov-module.md b/workspaces/scorecard/.changeset/add-codecov-module.md new file mode 100644 index 0000000000..fb8df083d7 --- /dev/null +++ b/workspaces/scorecard/.changeset/add-codecov-module.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-codecov': minor +--- + +Add new codecov backend module for the scorecard plugin with 7 code coverage metrics. diff --git a/workspaces/scorecard/examples/all-scorecards-location.yaml b/workspaces/scorecard/examples/all-scorecards-location.yaml index 2101c94600..d71d491738 100644 --- a/workspaces/scorecard/examples/all-scorecards-location.yaml +++ b/workspaces/scorecard/examples/all-scorecards-location.yaml @@ -6,6 +6,7 @@ metadata: description: A collection of all the scorecards spec: targets: + - ./components/codecov-scorecard-only.yaml - ./components/dependabot-scorecard-only.yaml - ./components/github-scorecard-only.yaml - ./components/jira-scorecard-only.yaml diff --git a/workspaces/scorecard/examples/components/codecov-scorecard-only.yaml b/workspaces/scorecard/examples/components/codecov-scorecard-only.yaml new file mode 100644 index 0000000000..edd9bcc660 --- /dev/null +++ b/workspaces/scorecard/examples/components/codecov-scorecard-only.yaml @@ -0,0 +1,13 @@ +--- +# Component with Codecov Scorecard only +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: codecov-scorecard-only + annotations: + codecov.io/repo: redhat-developer/rhdh-plugins + github.com/project-slug: redhat-developer/rhdh-plugins +spec: + type: service + owner: group:development/guests + lifecycle: experimental diff --git a/workspaces/scorecard/packages/backend/package.json b/workspaces/scorecard/packages/backend/package.json index a7d7b0e902..17478a298b 100644 --- a/workspaces/scorecard/packages/backend/package.json +++ b/workspaces/scorecard/packages/backend/package.json @@ -47,6 +47,7 @@ "@backstage/plugin-search-backend-node": "^1.4.2", "@backstage/plugin-techdocs-backend": "^2.1.6", "@red-hat-developer-hub/backstage-plugin-scorecard-backend": "workspace:^", + "@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-codecov": "workspace:^", "@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-dependabot": "workspace:^", "@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-filecheck": "workspace:^", "@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-github": "workspace:^", diff --git a/workspaces/scorecard/packages/backend/src/index.ts b/workspaces/scorecard/packages/backend/src/index.ts index 828b1f1538..be810918dc 100644 --- a/workspaces/scorecard/packages/backend/src/index.ts +++ b/workspaces/scorecard/packages/backend/src/index.ts @@ -63,34 +63,25 @@ backend.add( import('@red-hat-developer-hub/backstage-plugin-scorecard-backend'), ); backend.add( - import( - '@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-github' - ), + import('@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-github'), ); backend.add( - import( - '@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-jira' - ), + import('@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-jira'), ); backend.add( - import( - '@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-filecheck' - ), + import('@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-filecheck'), ); backend.add( - import( - '@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-openssf' - ), + import('@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-openssf'), ); backend.add( - import( - '@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-dependabot' - ), + import('@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-codecov'), ); backend.add( - import( - '@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-sonarqube' - ), + import('@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-dependabot'), +); +backend.add( + import('@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-sonarqube'), ); backend.add(import('@backstage/plugin-mcp-actions-backend')); backend.start(); diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-codecov/.eslintrc.js b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-codecov/README.md b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/README.md new file mode 100644 index 0000000000..014edeb435 --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/README.md @@ -0,0 +1,92 @@ +# Scorecard Backend Module: Codecov + +Adds [Codecov](https://about.codecov.io/) code coverage metrics to the scorecard plugin. All 7 metrics are fetched from a single Codecov API call per entity. + +## Metrics + +| Metric ID | Type | Description | +| ------------------------ | ------ | --------------------------------------- | +| `codecov.coverage` | number | Current code coverage percentage | +| `codecov.coverage_trend` | number | Code coverage trend for the last 7 days | +| `codecov.tracked_files` | number | Number of files tracked by Codecov | +| `codecov.tracked_lines` | number | Total lines of code tracked by Codecov | +| `codecov.covered_lines` | number | Number of lines covered by tests | +| `codecov.partial_lines` | number | Number of partially covered lines | +| `codecov.missed_lines` | number | Number of lines not covered by tests | + +## Installation + +```bash +yarn workspace backend add @red-hat-developer-hub/backstage-plugin-scorecard-backend-module-codecov +``` + +Then register the module in your backend: + +```ts +// packages/backend/src/index.ts +backend.add( + import('@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-codecov'), +); +``` + +## Entity annotation + +Entities need the `codecov.io/repo` annotation to opt in: + +```yaml +metadata: + annotations: + codecov.io/repo: owner/repo +``` + +The module resolves the Codecov service from the entity's annotations. If the entity also has a `github.com/project-slug` annotation, the service defaults to `github`. Otherwise, set the service explicitly: + +```yaml +metadata: + annotations: + codecov.io/repo: owner/repo + codecov.io/service: github +``` + +### Optional annotations + +| Annotation | Description | +| -------------------- | ---------------------------------------------------------------------------------------------------------- | +| `codecov.io/repo` | **Required.** The Codecov repository in `owner/repo` or `repo` format. | +| `codecov.io/service` | Git hosting service (`github`, `gitlab`, `bitbucket`). Inferred from `github.com/project-slug` if present. | +| `codecov.io/owner` | Override the repository owner (if not using `owner/repo` format). | +| `codecov.io/account` | Codecov account name for multi-account setups (maps to config accounts). | + +## Configuration + +Configuration is optional for public repositories. For private repositories, configure an auth token: + +```yaml +# app-config.yaml +codecov: + accounts: + - name: default + authToken: ${CODECOV_API_TOKEN} +``` + +### Multiple accounts + +```yaml +# app-config.yaml +codecov: + defaultAccount: primary + accounts: + - name: primary + authToken: ${CODECOV_PRIMARY_TOKEN} + - name: oss + # authToken optional for public repos +``` + +Then set the `codecov.io/account` annotation on entities to route them to the correct account: + +```yaml +metadata: + annotations: + codecov.io/repo: owner/repo + codecov.io/account: oss +``` diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-codecov/config.d.ts b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/config.d.ts new file mode 100644 index 0000000000..8e1552161e --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/config.d.ts @@ -0,0 +1,44 @@ +/* + * 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 interface Config { + /** Optional configurations for the Codecov plugin */ + codecov?: { + /** + * The default account name to use when the codecov.io/account annotation is not set. + * Defaults to "default". + * @visibility frontend + */ + defaultAccount?: string; + + /** + * The list of codecov accounts. + */ + accounts?: Array<{ + /** + * The name of the codecov account. + * @visibility frontend + */ + name: string; + + /** + * The auth token for the codecov account. Optional for public repositories. + * @visibility secret + */ + authToken?: string; + }>; + }; +} diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-codecov/package.json b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/package.json new file mode 100644 index 0000000000..729f85a9c3 --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/package.json @@ -0,0 +1,49 @@ +{ + "name": "@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-codecov", + "version": "0.0.0", + "license": "Apache-2.0", + "description": "The codecov backend module for the scorecard plugin.", + "main": "src/index.ts", + "types": "src/index.ts", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/redhat-developer/rhdh-plugins", + "directory": "workspaces/scorecard/plugins/scorecard-backend-module-codecov" + }, + "backstage": { + "role": "backend-plugin-module", + "pluginId": "scorecard", + "pluginPackage": "@red-hat-developer-hub/backstage-plugin-scorecard-backend" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-plugin-api": "^1.8.0", + "@backstage/catalog-client": "^1.14.0", + "@backstage/catalog-model": "^1.7.7", + "@backstage/config": "^1.3.6", + "@red-hat-developer-hub/backstage-plugin-scorecard-common": "workspace:^", + "@red-hat-developer-hub/backstage-plugin-scorecard-node": "workspace:^" + }, + "devDependencies": { + "@backstage/backend-test-utils": "^1.11.1", + "@backstage/cli": "^0.36.0" + }, + "configSchema": "config.d.ts", + "files": [ + "config.d.ts", + "dist" + ] +} diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-codecov/report.api.md b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/report.api.md new file mode 100644 index 0000000000..2472db7cb5 --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/report.api.md @@ -0,0 +1,11 @@ +## API Report File for "@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-codecov" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @public (undocumented) +const scorecardModuleCodecov: BackendFeature; +export default scorecardModuleCodecov; +``` diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/clients/CodecovClient.test.ts b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/clients/CodecovClient.test.ts new file mode 100644 index 0000000000..31c7d8594a --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/clients/CodecovClient.test.ts @@ -0,0 +1,228 @@ +/* + * 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. + */ + +import { ConfigReader } from '@backstage/config'; +import { CodecovClient } from './CodecovClient'; +import { mockServices } from '@backstage/backend-test-utils'; + +const mockFetch = jest.fn(); +globalThis.fetch = mockFetch; + +const SAMPLE_RESPONSE = { + name: 'rhdh-plugins', + private: false, + updatestamp: '2026-06-19T10:29:51.283089Z', + author: { + service: 'github', + username: 'redhat-developer', + name: 'redhat-developer', + }, + language: 'typescript', + branch: 'main', + active: true, + activated: true, + totals: { + files: 2252, + lines: 85789, + hits: 45982, + misses: 38246, + partials: 1561, + coverage: 53.59, + branches: 24121, + methods: 13480, + sessions: 23, + complexity: 0.0, + complexity_total: 0.0, + complexity_ratio: 0, + diff: 0, + }, +}; + +describe('CodecovClient', () => { + const logger = mockServices.logger.mock(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('fetches repo info with correct URL', async () => { + const config = new ConfigReader({}); + const client = new CodecovClient(config, logger); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => SAMPLE_RESPONSE, + }); + + const result = await client.getRepoInfo( + 'github', + 'redhat-developer', + 'rhdh-plugins', + ); + + expect(result).toEqual(SAMPLE_RESPONSE); + expect(mockFetch).toHaveBeenCalledWith( + 'https://api.codecov.io/api/v2/github/redhat-developer/repos/rhdh-plugins/', + expect.objectContaining({ + headers: { accept: 'application/json' }, + }), + ); + }); + + it('sends Authorization header when auth token is configured', async () => { + const config = new ConfigReader({ + codecov: { + accounts: [{ name: 'default', authToken: 'my-token' }], + }, + }); + const client = new CodecovClient(config, logger); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => SAMPLE_RESPONSE, + }); + + await client.getRepoInfo('github', 'redhat-developer', 'rhdh-plugins'); + + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + headers: { + accept: 'application/json', + Authorization: 'bearer my-token', + }, + }), + ); + }); + + it('sends no Authorization header when no token is configured', async () => { + const config = new ConfigReader({}); + const client = new CodecovClient(config, logger); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => SAMPLE_RESPONSE, + }); + + await client.getRepoInfo('github', 'redhat-developer', 'rhdh-plugins'); + + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + headers: { accept: 'application/json' }, + }), + ); + }); + + it('uses the specified account name for auth token lookup', async () => { + const config = new ConfigReader({ + codecov: { + accounts: [ + { name: 'default', authToken: 'default-token' }, + { name: 'custom', authToken: 'custom-token' }, + ], + }, + }); + const client = new CodecovClient(config, logger); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => SAMPLE_RESPONSE, + }); + + await client.getRepoInfo( + 'github', + 'redhat-developer', + 'rhdh-plugins', + 'custom', + ); + + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + headers: { + accept: 'application/json', + Authorization: 'bearer custom-token', + }, + }), + ); + }); + + it('uses custom defaultAccount name from config', async () => { + const config = new ConfigReader({ + codecov: { + defaultAccount: 'myorg', + accounts: [{ name: 'myorg', authToken: 'org-token' }], + }, + }); + const client = new CodecovClient(config, logger); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => SAMPLE_RESPONSE, + }); + + await client.getRepoInfo('github', 'redhat-developer', 'rhdh-plugins'); + + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + headers: { + accept: 'application/json', + Authorization: 'bearer org-token', + }, + }), + ); + }); + + it('throws when API returns non-OK response', async () => { + const config = new ConfigReader({}); + const client = new CodecovClient(config, logger); + + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + }); + + await expect( + client.getRepoInfo('github', 'redhat-developer', 'unknown-repo'), + ).rejects.toThrow(/Codecov API error: 404 Not Found/); + }); + + it('sends no auth header for account without authToken', async () => { + const config = new ConfigReader({ + codecov: { + accounts: [{ name: 'default' }], + }, + }); + const client = new CodecovClient(config, logger); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => SAMPLE_RESPONSE, + }); + + await client.getRepoInfo('github', 'redhat-developer', 'rhdh-plugins'); + + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + headers: { accept: 'application/json' }, + }), + ); + }); +}); diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/clients/CodecovClient.ts b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/clients/CodecovClient.ts new file mode 100644 index 0000000000..f8af89817b --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/clients/CodecovClient.ts @@ -0,0 +1,89 @@ +/* + * 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. + */ + +import type { LoggerService } from '@backstage/backend-plugin-api'; +import type { Config } from '@backstage/config'; +import type { CodecovRepoResponse } from './types'; + +const CODECOV_API_BASE_URL = 'https://api.codecov.io/api/v2'; +const DEFAULT_ACCOUNT_NAME = 'default'; + +export class CodecovClient { + private readonly config: Config; + private readonly logger: LoggerService; + + constructor(config: Config, logger: LoggerService) { + this.config = config; + this.logger = logger.child({ component: 'CodecovClient' }); + } + + private resolveAuthToken(accountName?: string): string | undefined { + const codecovConfig = this.config.getOptionalConfig('codecov'); + if (!codecovConfig) { + return undefined; + } + + const accounts = codecovConfig.getOptionalConfigArray('accounts') ?? []; + const defaultAccountName = + codecovConfig.getOptionalString('defaultAccount') ?? DEFAULT_ACCOUNT_NAME; + const resolvedName = accountName ?? defaultAccountName; + + const account = accounts.find(a => a.getString('name') === resolvedName); + if (account) { + return account.getOptionalString('authToken'); + } + + // If a specific account was requested but not found, log a warning + if (accountName) { + this.logger.warn( + `Codecov account '${accountName}' not found in configuration`, + ); + } + + return undefined; + } + + async getRepoInfo( + service: string, + owner: string, + repo: string, + accountName?: string, + ): Promise { + this.logger.debug( + `Fetching Codecov repo info for ${service}/${owner}/${repo}`, + ); + + const url = `${CODECOV_API_BASE_URL}/${encodeURIComponent( + service, + )}/${encodeURIComponent(owner)}/repos/${encodeURIComponent(repo)}/`; + const headers: Record = { + accept: 'application/json', + }; + + const authToken = this.resolveAuthToken(accountName); + if (authToken) { + headers.Authorization = `bearer ${authToken}`; + } + + const response = await fetch(url, { headers }); + if (!response.ok) { + throw new Error( + `Codecov API error: ${response.status} ${response.statusText} for ${url}`, + ); + } + return response.json() as Promise; + } +} diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/clients/types.ts b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/clients/types.ts new file mode 100644 index 0000000000..00a3048f6c --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/clients/types.ts @@ -0,0 +1,59 @@ +/* + * 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. + */ + +/** + * Represents the totals object from the Codecov repository API response. + */ +export interface CodecovTotals { + files: number; + lines: number; + hits: number; + misses: number; + partials: number; + coverage: number; + branches: number; + methods: number; + sessions: number; + complexity: number; + complexity_total: number; + complexity_ratio: number; + diff: number; +} + +/** + * Represents the author object from the Codecov repository API response. + */ +export interface CodecovAuthor { + service: string; + username: string; + name: string; +} + +/** + * Represents the full response from the Codecov repos_retrieve API. + * @see https://docs.codecov.com/reference/repos_retrieve + */ +export interface CodecovRepoResponse { + name: string; + private: boolean; + updatestamp: string; + author: CodecovAuthor; + language: string; + branch: string; + active: boolean; + activated: boolean; + totals: CodecovTotals; +} diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/index.ts b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/index.ts new file mode 100644 index 0000000000..939dab5612 --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/index.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * The codecov backend module for the scorecard plugin. + * + * @packageDocumentation + */ + +export { scorecardModuleCodecov as default } from './module'; diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/metricProviders/CodecovConfig.test.ts b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/metricProviders/CodecovConfig.test.ts new file mode 100644 index 0000000000..34dc530bfb --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/metricProviders/CodecovConfig.test.ts @@ -0,0 +1,149 @@ +/* + * 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. + */ + +import { Entity } from '@backstage/catalog-model'; +import { resolveCodecovEntityInfo } from './CodecovConfig'; + +function createEntity(annotations: Record): Entity { + return { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test-entity', + namespace: 'default', + annotations, + }, + spec: { + type: 'service', + owner: 'test', + lifecycle: 'production', + }, + }; +} + +describe('resolveCodecovEntityInfo', () => { + it('parses owner/repo from codecov.io/repo annotation', () => { + const entity = createEntity({ + 'codecov.io/repo': 'redhat-developer/rhdh-plugins', + 'github.com/project-slug': 'redhat-developer/rhdh-plugins', + }); + + const result = resolveCodecovEntityInfo(entity); + + expect(result).toEqual({ + service: 'github', + owner: 'redhat-developer', + repo: 'rhdh-plugins', + accountName: undefined, + }); + }); + + it('uses codecov.io/service annotation when present', () => { + const entity = createEntity({ + 'codecov.io/repo': 'myorg/myrepo', + 'codecov.io/service': 'gitlab', + }); + + const result = resolveCodecovEntityInfo(entity); + + expect(result).toEqual({ + service: 'gitlab', + owner: 'myorg', + repo: 'myrepo', + accountName: undefined, + }); + }); + + it('falls back to github service when github.com/project-slug is present', () => { + const entity = createEntity({ + 'codecov.io/repo': 'myorg/myrepo', + 'github.com/project-slug': 'myorg/myrepo', + }); + + const result = resolveCodecovEntityInfo(entity); + + expect(result.service).toBe('github'); + }); + + it('throws when service cannot be determined', () => { + const entity = createEntity({ + 'codecov.io/repo': 'myorg/myrepo', + }); + + expect(() => resolveCodecovEntityInfo(entity)).toThrow( + /Cannot determine Codecov service/, + ); + }); + + it('uses codecov.io/owner annotation when present', () => { + const entity = createEntity({ + 'codecov.io/repo': 'different-org/myrepo', + 'codecov.io/owner': 'custom-owner', + 'github.com/project-slug': 'whatever/whatever', + }); + + const result = resolveCodecovEntityInfo(entity); + + expect(result.owner).toBe('custom-owner'); + expect(result.repo).toBe('myrepo'); + }); + + it('uses repo annotation directly when no slash and owner annotation is set', () => { + const entity = createEntity({ + 'codecov.io/repo': 'myrepo', + 'codecov.io/owner': 'myowner', + 'github.com/project-slug': 'whatever/whatever', + }); + + const result = resolveCodecovEntityInfo(entity); + + expect(result.owner).toBe('myowner'); + expect(result.repo).toBe('myrepo'); + }); + + it('throws when owner cannot be determined (no slash, no owner annotation)', () => { + const entity = createEntity({ + 'codecov.io/repo': 'myrepo', + 'github.com/project-slug': 'whatever/whatever', + }); + + expect(() => resolveCodecovEntityInfo(entity)).toThrow( + /Cannot determine Codecov owner/, + ); + }); + + it('throws when codecov.io/repo annotation is missing', () => { + const entity = createEntity({ + 'github.com/project-slug': 'whatever/whatever', + }); + + expect(() => resolveCodecovEntityInfo(entity)).toThrow( + /Missing annotation 'codecov.io\/repo'/, + ); + }); + + it('includes account name from annotation', () => { + const entity = createEntity({ + 'codecov.io/repo': 'myorg/myrepo', + 'codecov.io/account': 'enterprise', + 'github.com/project-slug': 'myorg/myrepo', + }); + + const result = resolveCodecovEntityInfo(entity); + + expect(result.accountName).toBe('enterprise'); + }); +}); diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/metricProviders/CodecovConfig.ts b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/metricProviders/CodecovConfig.ts new file mode 100644 index 0000000000..6638d9faa1 --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/metricProviders/CodecovConfig.ts @@ -0,0 +1,211 @@ +/* + * 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. + */ + +import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; +import { ThresholdConfig } from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; +import type { CodecovTotals } from '../clients/types'; + +export const CODECOV_REPO_ANNOTATION = 'codecov.io/repo'; +export const CODECOV_ACCOUNT_ANNOTATION = 'codecov.io/account'; +export const CODECOV_SERVICE_ANNOTATION = 'codecov.io/service'; +export const CODECOV_OWNER_ANNOTATION = 'codecov.io/owner'; +export const GITHUB_PROJECT_SLUG_ANNOTATION = 'github.com/project-slug'; + +export type CodecovEntityInfo = { + service: string; + owner: string; + repo: string; + accountName?: string; +}; + +/** + * Resolves the Codecov entity information from annotations. + */ +export function resolveCodecovEntityInfo(entity: Entity): CodecovEntityInfo { + const repoAnnotation = entity.metadata.annotations?.[CODECOV_REPO_ANNOTATION]; + if (!repoAnnotation) { + throw new Error( + `Missing annotation '${CODECOV_REPO_ANNOTATION}' for entity ${stringifyEntityRef( + entity, + )}`, + ); + } + + const accountName = entity.metadata.annotations?.[CODECOV_ACCOUNT_ANNOTATION]; + + // Resolve service + const serviceAnnotation = + entity.metadata.annotations?.[CODECOV_SERVICE_ANNOTATION]; + let service: string; + if (serviceAnnotation) { + service = serviceAnnotation; + } else if (entity.metadata.annotations?.[GITHUB_PROJECT_SLUG_ANNOTATION]) { + service = 'github'; + } else { + throw new Error( + `Cannot determine Codecov service for entity ${stringifyEntityRef( + entity, + )}. ` + + `Set the '${CODECOV_SERVICE_ANNOTATION}' annotation or add '${GITHUB_PROJECT_SLUG_ANNOTATION}'.`, + ); + } + + // Resolve owner and repo + const ownerAnnotation = + entity.metadata.annotations?.[CODECOV_OWNER_ANNOTATION]; + let owner: string; + let repo: string; + + if (repoAnnotation.includes('/')) { + const slashIndex = repoAnnotation.indexOf('/'); + owner = ownerAnnotation ?? repoAnnotation.substring(0, slashIndex); + repo = repoAnnotation.substring(slashIndex + 1); + } else { + if (!ownerAnnotation) { + throw new Error( + `Cannot determine Codecov owner for entity ${stringifyEntityRef( + entity, + )}. ` + + `Set the '${CODECOV_OWNER_ANNOTATION}' annotation or use 'owner/repo' format in '${CODECOV_REPO_ANNOTATION}'.`, + ); + } + owner = ownerAnnotation; + repo = repoAnnotation; + } + + return { service, owner, repo, accountName }; +} + +export const CODECOV_METRICS = [ + 'coverage', + 'coverage_trend', + 'tracked_files', + 'tracked_lines', + 'covered_lines', + 'partial_lines', + 'missed_lines', +] as const; + +export type CodecovMetricId = (typeof CODECOV_METRICS)[number]; + +export const CODECOV_METRIC_CONFIG: Record< + CodecovMetricId, + { id: string; title: string; description: string } +> = { + coverage: { + id: 'codecov.coverage', + title: 'Codecov Code Coverage', + description: 'Current code coverage percentage for the default branch.', + }, + coverage_trend: { + id: 'codecov.coverage_trend', + title: 'Codecov Coverage Trend (7d)', + description: 'Code coverage trend for the last 7 days.', + }, + tracked_files: { + id: 'codecov.tracked_files', + title: 'Codecov Tracked Files', + description: 'Number of files tracked by Codecov.', + }, + tracked_lines: { + id: 'codecov.tracked_lines', + title: 'Codecov Tracked Lines', + description: 'Total lines of code tracked by Codecov.', + }, + covered_lines: { + id: 'codecov.covered_lines', + title: 'Codecov Covered Lines', + description: 'Number of lines covered by tests.', + }, + partial_lines: { + id: 'codecov.partial_lines', + title: 'Codecov Partial Lines', + description: 'Number of partially covered lines.', + }, + missed_lines: { + id: 'codecov.missed_lines', + title: 'Codecov Missed Lines', + description: 'Number of lines not covered by tests.', + }, +}; + +/** + * Maps scorecard metric IDs to the field in the Codecov API totals response. + */ +export const CODECOV_TOTALS_FIELD_MAP: Record< + CodecovMetricId, + keyof CodecovTotals | 'diff' +> = { + coverage: 'coverage', + coverage_trend: 'diff', + tracked_files: 'files', + tracked_lines: 'lines', + covered_lines: 'hits', + partial_lines: 'partials', + missed_lines: 'misses', +}; + +export const CODECOV_NUMBER_THRESHOLDS: Record< + CodecovMetricId, + ThresholdConfig +> = { + coverage: { + rules: [ + { key: 'success', expression: '>80' }, + { key: 'warning', expression: '50-80' }, + { key: 'error', expression: '<50' }, + ], + }, + coverage_trend: { + rules: [ + { key: 'success', expression: '>0' }, + { key: 'warning', expression: '==0' }, + { key: 'error', expression: '<0' }, + ], + }, + tracked_files: { + rules: [ + { key: 'success', expression: '>0' }, + { key: 'error', expression: '==0' }, + ], + }, + tracked_lines: { + rules: [ + { key: 'success', expression: '>0' }, + { key: 'error', expression: '==0' }, + ], + }, + covered_lines: { + rules: [ + { key: 'success', expression: '>0' }, + { key: 'error', expression: '==0' }, + ], + }, + partial_lines: { + rules: [ + { key: 'success', expression: '<10' }, + { key: 'warning', expression: '10-50' }, + { key: 'error', expression: '>50' }, + ], + }, + missed_lines: { + rules: [ + { key: 'success', expression: '<10' }, + { key: 'warning', expression: '10-50' }, + { key: 'error', expression: '>50' }, + ], + }, +}; diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/metricProviders/CodecovMetricProvider.test.ts b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/metricProviders/CodecovMetricProvider.test.ts new file mode 100644 index 0000000000..f0aa92fbac --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/metricProviders/CodecovMetricProvider.test.ts @@ -0,0 +1,262 @@ +/* + * 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. + */ + +import { ConfigReader } from '@backstage/config'; +import { Entity } from '@backstage/catalog-model'; +import { mockServices } from '@backstage/backend-test-utils'; +import { CodecovMetricProvider } from './CodecovMetricProvider'; +import { CODECOV_METRICS, CODECOV_METRIC_CONFIG } from './CodecovConfig'; + +const mockFetch = jest.fn(); +globalThis.fetch = mockFetch; + +const SAMPLE_RESPONSE = { + name: 'rhdh-plugins', + private: false, + updatestamp: '2026-06-19T10:29:51.283089Z', + author: { + service: 'github', + username: 'redhat-developer', + name: 'redhat-developer', + }, + language: 'typescript', + branch: 'main', + active: true, + activated: true, + totals: { + files: 2252, + lines: 85789, + hits: 45982, + misses: 38246, + partials: 1561, + coverage: 53.59, + branches: 24121, + methods: 13480, + sessions: 23, + complexity: 0.0, + complexity_total: 0.0, + complexity_ratio: 0, + diff: 0, + }, +}; + +function createEntity(annotations: Record): Entity { + return { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test-entity', + namespace: 'default', + annotations, + }, + spec: { + type: 'service', + owner: 'test', + lifecycle: 'production', + }, + }; +} + +describe('CodecovMetricProvider', () => { + const config = new ConfigReader({}); + const logger = mockServices.logger.mock(); + + const entity = createEntity({ + 'codecov.io/repo': 'redhat-developer/rhdh-plugins', + 'github.com/project-slug': 'redhat-developer/rhdh-plugins', + }); + + beforeEach(() => { + jest.clearAllMocks(); + mockFetch.mockResolvedValue({ + ok: true, + json: async () => SAMPLE_RESPONSE, + }); + }); + + describe('provider metadata', () => { + it('returns codecov as datasource ID', () => { + const provider = CodecovMetricProvider.fromConfig( + config, + logger, + 'coverage', + ); + expect(provider.getProviderDatasourceId()).toBe('codecov'); + }); + + it('returns correct provider ID for each metric', () => { + for (const metricId of CODECOV_METRICS) { + const provider = CodecovMetricProvider.fromConfig( + config, + logger, + metricId, + ); + expect(provider.getProviderId()).toBe( + CODECOV_METRIC_CONFIG[metricId].id, + ); + } + }); + + it('returns number as metric type', () => { + const provider = CodecovMetricProvider.fromConfig( + config, + logger, + 'coverage', + ); + expect(provider.getMetricType()).toBe('number'); + }); + + it('returns metric with history enabled', () => { + const provider = CodecovMetricProvider.fromConfig( + config, + logger, + 'coverage', + ); + expect(provider.getMetric().history).toBe(true); + }); + + it('returns catalog filter for codecov.io/repo annotation', () => { + const provider = CodecovMetricProvider.fromConfig( + config, + logger, + 'coverage', + ); + const filter = provider.getCatalogFilter(); + expect('metadata.annotations.codecov.io/repo' in filter).toBe(true); + }); + }); + + describe('batch methods', () => { + it('returns all metric IDs', () => { + const provider = CodecovMetricProvider.fromConfig( + config, + logger, + 'coverage', + ); + const ids = provider.getMetricIds(); + expect(ids).toHaveLength(7); + expect(ids).toContain('codecov.coverage'); + expect(ids).toContain('codecov.coverage_trend'); + expect(ids).toContain('codecov.tracked_files'); + }); + + it('returns all metrics', () => { + const provider = CodecovMetricProvider.fromConfig( + config, + logger, + 'coverage', + ); + const metrics = provider.getMetrics(); + expect(metrics).toHaveLength(7); + expect(metrics.every(m => m.type === 'number')).toBe(true); + }); + }); + + describe('calculateMetric', () => { + it('returns coverage value', async () => { + const provider = CodecovMetricProvider.fromConfig( + config, + logger, + 'coverage', + ); + const result = await provider.calculateMetric(entity); + expect(result).toBe(53.59); + }); + + it('returns tracked files count', async () => { + const provider = CodecovMetricProvider.fromConfig( + config, + logger, + 'tracked_files', + ); + const result = await provider.calculateMetric(entity); + expect(result).toBe(2252); + }); + + it('returns tracked lines count', async () => { + const provider = CodecovMetricProvider.fromConfig( + config, + logger, + 'tracked_lines', + ); + const result = await provider.calculateMetric(entity); + expect(result).toBe(85789); + }); + + it('returns covered lines count', async () => { + const provider = CodecovMetricProvider.fromConfig( + config, + logger, + 'covered_lines', + ); + const result = await provider.calculateMetric(entity); + expect(result).toBe(45982); + }); + + it('returns partial lines count', async () => { + const provider = CodecovMetricProvider.fromConfig( + config, + logger, + 'partial_lines', + ); + const result = await provider.calculateMetric(entity); + expect(result).toBe(1561); + }); + + it('returns missed lines count', async () => { + const provider = CodecovMetricProvider.fromConfig( + config, + logger, + 'missed_lines', + ); + const result = await provider.calculateMetric(entity); + expect(result).toBe(38246); + }); + + it('returns coverage trend value', async () => { + const provider = CodecovMetricProvider.fromConfig( + config, + logger, + 'coverage_trend', + ); + const result = await provider.calculateMetric(entity); + expect(result).toBe(0); + }); + }); + + describe('calculateMetrics (batch)', () => { + it('returns all 7 metrics from a single API call', async () => { + const provider = CodecovMetricProvider.fromConfig( + config, + logger, + 'coverage', + ); + const results = await provider.calculateMetrics(entity); + + expect(results.size).toBe(7); + expect(results.get('codecov.coverage')).toBe(53.59); + expect(results.get('codecov.coverage_trend')).toBe(0); + expect(results.get('codecov.tracked_files')).toBe(2252); + expect(results.get('codecov.tracked_lines')).toBe(85789); + expect(results.get('codecov.covered_lines')).toBe(45982); + expect(results.get('codecov.partial_lines')).toBe(1561); + expect(results.get('codecov.missed_lines')).toBe(38246); + + // Should only have made one fetch call + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/metricProviders/CodecovMetricProvider.ts b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/metricProviders/CodecovMetricProvider.ts new file mode 100644 index 0000000000..561a54e604 --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/metricProviders/CodecovMetricProvider.ts @@ -0,0 +1,137 @@ +/* + * 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. + */ + +import { + Metric, + ThresholdConfig, +} from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; +import { MetricProvider } from '@red-hat-developer-hub/backstage-plugin-scorecard-node'; +import type { LoggerService } from '@backstage/backend-plugin-api'; +import type { Config } from '@backstage/config'; +import { type Entity } from '@backstage/catalog-model'; +import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client'; + +import { CodecovClient } from '../clients/CodecovClient'; +import { + type CodecovMetricId, + CODECOV_METRICS, + CODECOV_METRIC_CONFIG, + CODECOV_NUMBER_THRESHOLDS, + CODECOV_TOTALS_FIELD_MAP, + CODECOV_REPO_ANNOTATION, + resolveCodecovEntityInfo, +} from './CodecovConfig'; + +export class CodecovMetricProvider implements MetricProvider<'number'> { + private constructor( + private readonly client: CodecovClient, + private readonly metricId: CodecovMetricId, + private readonly thresholds: ThresholdConfig, + ) {} + + getProviderDatasourceId(): string { + return 'codecov'; + } + + getProviderId(): string { + return CODECOV_METRIC_CONFIG[this.metricId].id; + } + + getMetricType(): 'number' { + return 'number'; + } + + getMetric(): Metric<'number'> { + const meta = CODECOV_METRIC_CONFIG[this.metricId]; + return { + id: meta.id, + title: meta.title, + description: meta.description, + type: this.getMetricType(), + history: true, + }; + } + + getMetricIds(): string[] { + return CODECOV_METRICS.map(id => CODECOV_METRIC_CONFIG[id].id); + } + + getMetrics(): Metric<'number'>[] { + return CODECOV_METRICS.map(id => ({ + id: CODECOV_METRIC_CONFIG[id].id, + title: CODECOV_METRIC_CONFIG[id].title, + description: CODECOV_METRIC_CONFIG[id].description, + type: 'number' as const, + history: true, + })); + } + + getMetricThresholds(): ThresholdConfig { + return this.thresholds; + } + + getCatalogFilter(): Record { + return { + [`metadata.annotations.${CODECOV_REPO_ANNOTATION}`]: + CATALOG_FILTER_EXISTS, + }; + } + + async calculateMetric(entity: Entity): Promise { + const { service, owner, repo, accountName } = + resolveCodecovEntityInfo(entity); + const repoInfo = await this.client.getRepoInfo( + service, + owner, + repo, + accountName, + ); + + const field = CODECOV_TOTALS_FIELD_MAP[this.metricId]; + return repoInfo.totals[field]; + } + + async calculateMetrics(entity: Entity): Promise> { + const { service, owner, repo, accountName } = + resolveCodecovEntityInfo(entity); + const repoInfo = await this.client.getRepoInfo( + service, + owner, + repo, + accountName, + ); + + const results = new Map(); + for (const id of CODECOV_METRICS) { + const field = CODECOV_TOTALS_FIELD_MAP[id]; + results.set(CODECOV_METRIC_CONFIG[id].id, repoInfo.totals[field]); + } + return results; + } + + static fromConfig( + config: Config, + logger: LoggerService, + metricId: CodecovMetricId, + ): CodecovMetricProvider { + const client = new CodecovClient(config, logger); + return new CodecovMetricProvider( + client, + metricId, + CODECOV_NUMBER_THRESHOLDS[metricId], + ); + } +} diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/metricProviders/CodecovMetricProviderFactory.test.ts b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/metricProviders/CodecovMetricProviderFactory.test.ts new file mode 100644 index 0000000000..856d728435 --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/metricProviders/CodecovMetricProviderFactory.test.ts @@ -0,0 +1,57 @@ +/* + * 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. + */ + +import { ConfigReader } from '@backstage/config'; +import { mockServices } from '@backstage/backend-test-utils'; +import { CodecovMetricProviderFactory } from './CodecovMetricProviderFactory'; + +describe('CodecovMetricProviderFactory', () => { + const config = new ConfigReader({}); + const logger = mockServices.logger.mock(); + + it('creates 7 metric providers', () => { + const providers = CodecovMetricProviderFactory.fromConfig(config, logger); + expect(providers).toHaveLength(7); + }); + + it('creates providers with correct IDs', () => { + const providers = CodecovMetricProviderFactory.fromConfig(config, logger); + const ids = providers.map(p => p.getProviderId()); + expect(ids).toEqual([ + 'codecov.coverage', + 'codecov.coverage_trend', + 'codecov.tracked_files', + 'codecov.tracked_lines', + 'codecov.covered_lines', + 'codecov.partial_lines', + 'codecov.missed_lines', + ]); + }); + + it('all providers have codecov datasource ID', () => { + const providers = CodecovMetricProviderFactory.fromConfig(config, logger); + for (const provider of providers) { + expect(provider.getProviderDatasourceId()).toBe('codecov'); + } + }); + + it('all providers have number metric type', () => { + const providers = CodecovMetricProviderFactory.fromConfig(config, logger); + for (const provider of providers) { + expect(provider.getMetricType()).toBe('number'); + } + }); +}); diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/metricProviders/CodecovMetricProviderFactory.ts b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/metricProviders/CodecovMetricProviderFactory.ts new file mode 100644 index 0000000000..a7c3948d65 --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/metricProviders/CodecovMetricProviderFactory.ts @@ -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. + */ + +import type { Config } from '@backstage/config'; +import type { LoggerService } from '@backstage/backend-plugin-api'; +import { MetricProvider } from '@red-hat-developer-hub/backstage-plugin-scorecard-node'; + +import { CodecovMetricProvider } from './CodecovMetricProvider'; +import { CODECOV_METRICS } from './CodecovConfig'; + +export class CodecovMetricProviderFactory { + private constructor() {} + + static fromConfig(config: Config, logger: LoggerService): MetricProvider[] { + return CODECOV_METRICS.map(metricId => + CodecovMetricProvider.fromConfig(config, logger, metricId), + ); + } +} diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/module.ts b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/module.ts new file mode 100644 index 0000000000..4648041f7d --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard-backend-module-codecov/src/module.ts @@ -0,0 +1,45 @@ +/* + * 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. + */ +import { + coreServices, + createBackendModule, +} from '@backstage/backend-plugin-api'; +import { scorecardMetricsExtensionPoint } from '@red-hat-developer-hub/backstage-plugin-scorecard-node'; +import { CodecovMetricProviderFactory } from './metricProviders/CodecovMetricProviderFactory'; + +export const scorecardModuleCodecov = createBackendModule({ + pluginId: 'scorecard', + moduleId: 'codecov', + register(reg) { + reg.registerInit({ + deps: { + metrics: scorecardMetricsExtensionPoint, + config: coreServices.rootConfig, + logger: coreServices.logger, + }, + + async init({ metrics, config, logger }) { + const providers = CodecovMetricProviderFactory.fromConfig( + config, + logger, + ); + for (const provider of providers) { + metrics.addMetricProvider(provider); + } + }, + }); + }, +}); diff --git a/workspaces/scorecard/yarn.lock b/workspaces/scorecard/yarn.lock index 1d401b224f..8540e6c953 100644 --- a/workspaces/scorecard/yarn.lock +++ b/workspaces/scorecard/yarn.lock @@ -12279,6 +12279,21 @@ __metadata: languageName: node linkType: hard +"@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-codecov@workspace:^, @red-hat-developer-hub/backstage-plugin-scorecard-backend-module-codecov@workspace:plugins/scorecard-backend-module-codecov": + version: 0.0.0-use.local + resolution: "@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-codecov@workspace:plugins/scorecard-backend-module-codecov" + dependencies: + "@backstage/backend-plugin-api": "npm:^1.8.0" + "@backstage/backend-test-utils": "npm:^1.11.1" + "@backstage/catalog-client": "npm:^1.14.0" + "@backstage/catalog-model": "npm:^1.7.7" + "@backstage/cli": "npm:^0.36.0" + "@backstage/config": "npm:^1.3.6" + "@red-hat-developer-hub/backstage-plugin-scorecard-common": "workspace:^" + "@red-hat-developer-hub/backstage-plugin-scorecard-node": "workspace:^" + languageName: unknown + linkType: soft + "@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-dependabot@workspace:^, @red-hat-developer-hub/backstage-plugin-scorecard-backend-module-dependabot@workspace:plugins/scorecard-backend-module-dependabot": version: 0.0.0-use.local resolution: "@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-dependabot@workspace:plugins/scorecard-backend-module-dependabot" @@ -18142,6 +18157,7 @@ __metadata: "@backstage/plugin-search-backend-node": "npm:^1.4.2" "@backstage/plugin-techdocs-backend": "npm:^2.1.6" "@red-hat-developer-hub/backstage-plugin-scorecard-backend": "workspace:^" + "@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-codecov": "workspace:^" "@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-dependabot": "workspace:^" "@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-filecheck": "workspace:^" "@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-github": "workspace:^"