Skip to content

Commit 469dc17

Browse files
authored
RHIDP-8354: Implement logic to load Jira issue by configuration (#1389)
* feat(scorecard): implement logic to load Jira issue by configuration Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * refactor(scorecard): move jira integration constants from scorecard-common to the plugin folder Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * refactor(scorecard): logic for Jira integration Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * refactor(scorecard): preparing jira request logic Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * feat(scorecard): add configuration steps to README.md for jira integration Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * fix(scorecard): jira integration --------- Signed-off-by: Ihor Mykhno <imykhno@redhat.com>
1 parent 4f820aa commit 469dc17

18 files changed

Lines changed: 1718 additions & 10 deletions
Lines changed: 153 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,155 @@
1-
# @@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-jira
1+
# Scorecard Backend Module for Jira
22

3-
The jira backend module for the scorecard plugin.
3+
This is an extension module to the `backstage-plugin-scorecard-backend` plugin. It provides Jira-specific metrics for software components registered in the Backstage catalog.
44

5-
_This plugin was created through the Backstage CLI_
5+
## Prerequisites
6+
7+
Before installing this module, ensure that the Scorecard backend plugin is integrated into your Backstage instance. Follow the [Scorecard backend plugin README](../scorecard-backend/README.md) for setup instructions.
8+
9+
This module also requires a Jira integration to be configured in your `app-config.yaml`. The following example of configuration can help:
10+
11+
**Configuration `token`:**
12+
13+
- For the `cloud` product:
14+
15+
- Obtain your personal token from Jira
16+
- Create a Base64-encoded string from the following plain text format: `your-atlassian-email:your-jira-api-token`:
17+
18+
```bash
19+
// Node
20+
new Buffer('your-atlassian-email:your-jira-api-token').toString(
21+
'base64',
22+
);
23+
24+
// Browser console
25+
btoa('your-atlassian-email:your-jira-api-token');
26+
27+
// Bash
28+
echo -n 'your-atlassian-email:your-jira-api-token' | base64
29+
```
30+
31+
- For the `datacenter` product:
32+
- Obtain your personal token from Jira
33+
- Use the Jira token without changing
34+
35+
```yaml
36+
jira:
37+
# Required
38+
baseUrl: ${JIRA_URL}
39+
# Required
40+
token: ${JIRA_TOKEN}
41+
# Required: Supported products: `cloud` or `datacenter`
42+
product: cloud
43+
# By default, the latest version is used. You can omit this prop when using the latest version.
44+
apiVersion: '3'
45+
```
46+
47+
## Installation
48+
49+
To install this backend module:
50+
51+
```bash
52+
# From your root directory
53+
yarn workspace backend add @red-hat-developer-hub/backstage-plugin-scorecard-backend-module-jira
54+
```
55+
56+
```ts
57+
// packages/backend/src/index.ts
58+
import { createBackend } from '@backstage/backend-defaults';
59+
60+
const backend = createBackend();
61+
62+
// Scorecard backend plugin
63+
backend.add(
64+
import('@red-hat-developer-hub/backstage-plugin-scorecard-backend'),
65+
);
66+
67+
// Install the Jira module
68+
/* highlight-add-next-line */
69+
backend.add(
70+
import(
71+
'@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-jira'
72+
),
73+
);
74+
75+
backend.start();
76+
```
77+
78+
### Entity Annotations
79+
80+
For the Jira metrics to work, your catalog entities must have the required Jira annotations:
81+
82+
```yaml
83+
# catalog-info.yaml
84+
apiVersion: backstage.io/v1alpha1
85+
kind: Component
86+
metadata:
87+
name: my-service
88+
annotations:
89+
# Required: Jira project key
90+
jira/project-key: PROJECT
91+
# Optional: Jira component name
92+
jira/component: Component
93+
# Optional: Jira label
94+
jira/label: UI
95+
# Optional: recommended to use Jira team ID instead of team title
96+
jira/team: 9d3ea319-fb5b-4621-9dab-05fe502283e
97+
# Optional: Custom filters for loading data request. This filter replaces customFilters form app-config.yaml
98+
jira/custom-filter: 'reporter = "psycon98@yahoo.com" AND resolution is not EMPTY'
99+
spec:
100+
type: website
101+
lifecycle: experimental
102+
owner: guests
103+
system: examples
104+
providesApis: [example-grpc-api]
105+
```
106+
107+
## Available Metrics
108+
109+
### Jira Issues (`jira.open_issues`)
110+
111+
This metric counts all jira issues that match the filter condition specified in annotation and app-config.yaml
112+
113+
- **Metric ID**: `jira.open_issues`
114+
- **Type**: `Number`
115+
- **Datasource**: `jira`
116+
- **Default thresholds**:
117+
118+
```yaml
119+
# app-config.yaml
120+
scorecard:
121+
plugins:
122+
jira:
123+
open_issues:
124+
thresholds:
125+
rules:
126+
- key: success
127+
expression: '<=50'
128+
- key: warning
129+
expression: '>50'
130+
- key: error
131+
expression: '>100'
132+
```
133+
134+
## Configuration
135+
136+
### Threshold Configuration
137+
138+
Thresholds define conditions that determine which category a metric value belongs to ( `error`, `warning`, or `success`). You can configure custom thresholds for the Jira metrics. Check out detailed explanation of [threshold configuration](../scorecard-backend/docs/thresholds.md).
139+
140+
### Options Configuration
141+
142+
Options define configuration that affect fetch jira issues global configuration, but all options are optional. This settings are closely related with annotation settings and whole jira issues loading process.
143+
144+
```yaml
145+
# app-config.yaml
146+
scorecard:
147+
plugins:
148+
jira:
149+
open_issues:
150+
options:
151+
# Optional: use mandatoryFilter filter if need to replaces default which is "type = Bug AND resolution = Unresolved"
152+
mandatoryFilter: Type = Task AND Resolution = Resolved
153+
# Optional: use to specify global customFilter, however the annotation `jira/custom-filter` will replaces them
154+
customFilter: priority in ("Critical", "Blocker")
155+
```

workspaces/scorecard/plugins/scorecard-backend-module-jira/config.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,24 @@
1515
*/
1616

1717
export interface Config {
18+
/** Configuration for jira plugin */
19+
jira: {
20+
baseUrl: string;
21+
token: string;
22+
product: string;
23+
apiVersion?: string;
24+
};
1825
/** Configuration for scorecard plugin */
1926
scorecard?: {
2027
/** Configuration for scorecard plugins/datasources */
2128
plugins?: {
2229
/** JIRA datasource configuration */
2330
jira?: {
2431
open_issues?: {
32+
options?: {
33+
mandatoryFilter?: string;
34+
customFilter?: string;
35+
};
2536
thresholds?: {
2637
rules?: Array<{
2738
key: 'error' | 'warning' | 'success';
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export enum ScorecardJiraAnnotations {
18+
PROJECT_KEY = 'jira/project-key',
19+
COMPONENT = 'jira/component',
20+
LABEL = 'jira/label',
21+
TEAM = 'jira/team',
22+
CUSTOM_FILTER = 'jira/custom-filter',
23+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import type { Config } from '@backstage/config';
18+
import { mockServices } from '@backstage/backend-test-utils';
19+
import { JiraDataCenterClient } from '../clients/JiraDataCenterClient';
20+
import { JiraClientFactory } from './JiraClientFactory';
21+
import { JiraCloudClient } from '../clients/JiraCloudClient';
22+
23+
jest.mock('../clients/JiraDataCenterClient');
24+
jest.mock('../clients/JiraCloudClient');
25+
26+
const getConfig = (product: string) => {
27+
return mockServices.rootConfig({
28+
data: {
29+
jira: {
30+
product,
31+
},
32+
},
33+
});
34+
};
35+
36+
describe('JiraClientFactory', () => {
37+
let config: Config;
38+
39+
beforeEach(() => {
40+
config = getConfig('datacenter');
41+
});
42+
43+
afterEach(() => {
44+
jest.clearAllMocks();
45+
});
46+
47+
describe('when product is datacenter', () => {
48+
it('should create a JiraDataCenterClient', () => {
49+
const client = JiraClientFactory.create(config);
50+
expect(JiraDataCenterClient).toHaveBeenCalledWith(config);
51+
expect(client).toBeInstanceOf(JiraDataCenterClient);
52+
});
53+
});
54+
55+
describe('when product is cloud', () => {
56+
beforeEach(() => {
57+
config = getConfig('cloud');
58+
});
59+
60+
it('should create a JiraCloudClient', () => {
61+
const client = JiraClientFactory.create(config);
62+
expect(JiraCloudClient).toHaveBeenCalledWith(config);
63+
expect(client).toBeInstanceOf(JiraCloudClient);
64+
});
65+
});
66+
67+
describe('when product is invalid', () => {
68+
beforeEach(() => {
69+
config = getConfig('foo');
70+
});
71+
72+
it('should throw an error', () => {
73+
expect(() => JiraClientFactory.create(config)).toThrow(
74+
"Invalid Jira product: foo. Valid products for 'jira.product' are: datacenter, cloud",
75+
);
76+
});
77+
});
78+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import type { Config } from '@backstage/config';
18+
import { JIRA_CONFIG_PATH } from '../constants';
19+
import { JiraClient } from '../clients/base';
20+
import { JiraDataCenterClient } from '../clients/JiraDataCenterClient';
21+
import { JiraCloudClient } from '../clients/JiraCloudClient';
22+
23+
export class JiraClientFactory {
24+
static create(config: Config): JiraClient {
25+
const jiraConfig = config.getConfig(JIRA_CONFIG_PATH);
26+
const product = jiraConfig.getString('product');
27+
28+
switch (product) {
29+
case 'datacenter':
30+
return new JiraDataCenterClient(config);
31+
case 'cloud':
32+
return new JiraCloudClient(config);
33+
default:
34+
throw new Error(
35+
`Invalid Jira product: ${product}. Valid products for 'jira.product' are: datacenter, cloud`,
36+
);
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)