Skip to content

Commit 0b90e54

Browse files
NasAminrheaditi
andauthored
Enable validation for jira issue status (ClearTax#33)
* Added status field to the JiraDetails object. Updated tests for the new field * Added support for validating jira issue status based on allowed list. Fixed eslint issues * Added issue status validation and also added status to the PR description * Tidied up flag names * Updated documentation * Removed duplication option * Fxed typo in readme * Fixed isIssueStatusInvalid condition * Regenerated index.js after merging from upstream * Added await to async call for adding a PR comment * Updated readme based on suggested changes on PR review Co-authored-by: Aditi Mohanty <[email protected]> * Update README.md - applied suggested markup changes from PR review Co-authored-by: Aditi Mohanty <[email protected]> * Update README.md - Fixed typo suggested during PR review Co-authored-by: Aditi Mohanty <[email protected]> * Fixed allowed_issue_statuses description from PR review Co-authored-by: Aditi Mohanty <[email protected]> * Shortened if conditions based on PR review suggestion Co-authored-by: Aditi Mohanty <[email protected]> * Removed duplicate status field from issue status markup Co-authored-by: Aditi Mohanty <[email protected]>
1 parent a97946c commit 0b90e54

File tree

7 files changed

+169
-6
lines changed

7 files changed

+169
-6
lines changed

README.md

+34-2
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ steps:
7878

7979
#### Description
8080

81-
When a PR passes the above check, `jira-lint` will also add the issue details to the top of the PR description. It will pick details such as the Issue summary, type, estimation points and labels and add them to the PR description.
81+
When a PR passes the above check, `jira-lint` will also add the issue details to the top of the PR description. It will pick details such as the Issue summary, type, estimation points, status and labels and add them to the PR description.
8282

8383
#### Labels
8484

@@ -96,6 +96,35 @@ When a PR passes the above check, `jira-lint` will also add the issue details to
9696
</figcaption>
9797
</figure>
9898

99+
#### Issue Status Validation
100+
Issue status is shown in the [Description](#description).
101+
**Why validate issue status?**
102+
In some cases, one may be pushing changes for a story that is set to `Done`/`Completed` or it may not have been pulled into working backlog or current sprint.
103+
104+
This option allows discouraging pushing to branches for stories that are set to statuses other than the ones allowed in the project; for example - you may want to only allow PRs for stories that are in `To Do`/`Planning`/`In Progress` states.
105+
106+
The following flags can be used to validate issue status:
107+
- `validate_issue_status`
108+
- If set to `true`, `jira-lint` will validate the issue status based on `allowed_issue_statuses`
109+
- `allowed_issue_statuses`
110+
- This will only be used when `validate_issue_status` is `true`. This should be a comma separated list of statuses. If the detected issue's status is not in one of the `allowed_issue_statuses` then `jira-lint` will fail the status check.
111+
112+
**Example of invalid status**
113+
<p>:broken_heart: The detected issue is not in one of the allowed statuses :broken_heart: </p>
114+
<table>
115+
<tr>
116+
<th>Detected Status</th>
117+
<td>${issueStatus}</td>
118+
<td>:x:</td>
119+
</tr>
120+
<tr>
121+
<th>Allowed Statuses</th>
122+
<td>${allowedStatuses}</td>
123+
<td>:heavy_check_mark:</td>
124+
</tr>
125+
</table>
126+
<p>Please ensure your jira story is in one of the allowed statuses</p>
127+
99128
#### Soft-validations via comments
100129

101130
`jira-lint` will add comments to a PR to encourage better PR practices:
@@ -140,11 +169,14 @@ When a PR passes the above check, `jira-lint` will also add the issue details to
140169
| `skip-branches` | A regex to ignore running `jira-lint` on certain branches, like production etc. | false | ' ' |
141170
| `skip-comments` | A `Boolean` if set to `true` then `jira-lint` will skip adding lint comments for PR title. | false | false |
142171
| `pr-threshold` | An `Integer` based on which `jira-lint` will add a comment discouraging huge PRs. | false | 800 |
172+
| `validate_issue_status` | A `Boolean` based on which `jira-lint` will validate the status of the detected jira issue | false | false |
173+
| `allowed_issue_statuses` | A comma separated list of allowed statuses. The detected jira issue's status will be compared against this list and if a match is not found then the status check will fail. *Note*: Requires `validate_issue_status` to be set to `true`. | false | `"In Progress"` |
143174

144-
Since tokens are private, we suggest adding them as [GitHub secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets).
145175

146176
### `jira-token`
147177

178+
Since tokens are private, we suggest adding them as [GitHub secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets).
179+
148180
The Jira token is used to fetch issue information via the Jira REST API. To get the token:-
149181
1. Generate an [API token via JIRA](https://confluence.atlassian.com/cloud/api-tokens-938839638.html)
150182
2. Create the encoded token in the format of `base64Encode(<username>:<api_token>)`.

__tests__/utils.test.ts

+40
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
shouldSkipBranchLint,
1111
shouldUpdatePRDescription,
1212
getJIRAClient,
13+
getInvalidIssueStatusComment,
14+
isIssueStatusValid,
1315
} from '../src/utils';
1416
import { HIDDEN_MARKER } from '../src/constants';
1517
import { JIRADetails } from '../src/types';
@@ -183,12 +185,14 @@ describe('getPRDescription()', () => {
183185
labels: [{ name: 'frontend', url: 'frontend-url' }],
184186
summary: 'Story title or summary',
185187
project: { name: 'project', url: 'project-url', key: 'abc' },
188+
status: 'In Progress',
186189
};
187190
const description = getPRDescription('some_body', issue);
188191

189192
expect(shouldUpdatePRDescription(description)).toBeFalsy();
190193
expect(description).toContain(issue.key);
191194
expect(description).toContain(issue.estimate);
195+
expect(description).toContain(issue.status);
192196
expect(description).toContain(issue.labels[0].name);
193197
});
194198
});
@@ -240,3 +244,39 @@ describe('JIRA Client', () => {
240244
expect(details).not.toBeNull();
241245
});
242246
});
247+
248+
describe('isIssueStatusValid()', () => {
249+
const issue: JIRADetails = {
250+
key: 'ABC-123',
251+
url: 'url',
252+
type: { name: 'feature', icon: 'feature-icon-url' },
253+
estimate: 1,
254+
labels: [{ name: 'frontend', url: 'frontend-url' }],
255+
summary: 'Story title or summary',
256+
project: { name: 'project', url: 'project-url', key: 'abc' },
257+
status: 'Assessment',
258+
};
259+
260+
it('should return false if issue validation was enabled but invalid issue status', () => {
261+
const expectedStatuses = ['In Test', 'In Progress'];
262+
expect(isIssueStatusValid(true, expectedStatuses, issue)).toBeFalsy();
263+
});
264+
265+
it('should return true if issue validation was enabled but issue has a valid status', () => {
266+
const expectedStatuses = ['In Test', 'In Progress'];
267+
issue.status = 'In Progress';
268+
expect(isIssueStatusValid(true, expectedStatuses, issue)).toBeTruthy();
269+
});
270+
271+
it('should return true if issue status validation is not enabled', () => {
272+
const expectedStatuses = ['In Test', 'In Progress'];
273+
expect(isIssueStatusValid(false, expectedStatuses, issue)).toBeTruthy();
274+
});
275+
});
276+
277+
describe('getInvalidIssueStatusComment()', () => {
278+
it('should return content with the passed in issue status and allowed statses', () => {
279+
expect(getInvalidIssueStatusComment('Assessment', 'In Progress')).toContain('Assessment');
280+
expect(getInvalidIssueStatusComment('Assessment', 'In Progress')).toContain('In Progress');
281+
});
282+
});

action.yml

+11
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,17 @@ inputs:
2323
description: 'An `Integer` based on which jira-lint add a comment discouraging huge PRs. Is disabled by `skip-comments`'
2424
required: false
2525
default: 800
26+
validate_issue_status:
27+
description: 'Set this to true if you want jira-lint to validate the status of the detected jira issues'
28+
required: false
29+
default: false
30+
allowed_issue_statuses:
31+
description: |
32+
A comma separated list of acceptable Jira issue statuses. You must provide a value for this if validate_issue_status is set to true
33+
Requires validate_issue_status to be set to true.
34+
required: false
35+
default: "In Progress"
36+
2637
runs:
2738
using: 'node12'
2839
main: 'lib/index.js'

lib/index.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main.ts

+28-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
shouldSkipBranchLint,
1818
shouldUpdatePRDescription,
1919
updatePrDetails,
20+
isIssueStatusValid,
21+
getInvalidIssueStatusComment,
2022
} from './utils';
2123
import { PullRequestParams, JIRADetails, JIRALintActionInputs } from './types';
2224
import { DEFAULT_PR_ADDITIONS_THRESHOLD } from './constants';
@@ -28,6 +30,8 @@ const getInputs = (): JIRALintActionInputs => {
2830
const BRANCH_IGNORE_PATTERN: string = core.getInput('skip-branches', { required: false }) || '';
2931
const SKIP_COMMENTS: boolean = core.getInput('skip-comments', { required: false }) === 'true';
3032
const PR_THRESHOLD = parseInt(core.getInput('pr-threshold', { required: false }), 10);
33+
const VALIDATE_ISSUE_STATUS: boolean = core.getInput('validate_issue_status', { required: false }) === 'true';
34+
const ALLOWED_ISSUE_STATUSES: string = core.getInput('allowed_issue_statuses');
3135

3236
return {
3337
JIRA_TOKEN,
@@ -36,12 +40,23 @@ const getInputs = (): JIRALintActionInputs => {
3640
SKIP_COMMENTS,
3741
PR_THRESHOLD: isNaN(PR_THRESHOLD) ? DEFAULT_PR_ADDITIONS_THRESHOLD : PR_THRESHOLD,
3842
JIRA_BASE_URL: JIRA_BASE_URL.endsWith('/') ? JIRA_BASE_URL.replace(/\/$/, '') : JIRA_BASE_URL,
43+
VALIDATE_ISSUE_STATUS,
44+
ALLOWED_ISSUE_STATUSES,
3945
};
4046
};
4147

4248
async function run(): Promise<void> {
4349
try {
44-
const { JIRA_TOKEN, JIRA_BASE_URL, GITHUB_TOKEN, BRANCH_IGNORE_PATTERN, SKIP_COMMENTS, PR_THRESHOLD } = getInputs();
50+
const {
51+
JIRA_TOKEN,
52+
JIRA_BASE_URL,
53+
GITHUB_TOKEN,
54+
BRANCH_IGNORE_PATTERN,
55+
SKIP_COMMENTS,
56+
PR_THRESHOLD,
57+
VALIDATE_ISSUE_STATUS,
58+
ALLOWED_ISSUE_STATUSES,
59+
} = getInputs();
4560

4661
const defaultAdditionsCount = 800;
4762
const prThreshold: number = PR_THRESHOLD ? Number(PR_THRESHOLD) : defaultAdditionsCount;
@@ -129,6 +144,18 @@ async function run(): Promise<void> {
129144
labels,
130145
});
131146

147+
if (!isIssueStatusValid(VALIDATE_ISSUE_STATUS, ALLOWED_ISSUE_STATUSES.split(','), details)) {
148+
const invalidIssueStatusComment: IssuesCreateCommentParams = {
149+
...commonPayload,
150+
body: getInvalidIssueStatusComment(details.status, ALLOWED_ISSUE_STATUSES),
151+
};
152+
console.log('Adding comment for invalid issue status');
153+
await addComment(client, invalidIssueStatusComment);
154+
155+
core.setFailed('The found jira issue does is not in acceptable statuses');
156+
process.exit(1);
157+
}
158+
132159
if (shouldUpdatePRDescription(prBody)) {
133160
const prData: PullsUpdateParams = {
134161
owner,

src/types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export namespace JIRA {
8181
id: string;
8282
key: string;
8383
self: string;
84+
status: string;
8485
fields: {
8586
summary: string;
8687
status: IssueStatus;
@@ -97,6 +98,7 @@ export interface JIRADetails {
9798
key: string;
9899
summary: string;
99100
url: string;
101+
status: string;
100102
type: {
101103
name: string;
102104
icon: string;
@@ -117,6 +119,8 @@ export interface JIRALintActionInputs {
117119
BRANCH_IGNORE_PATTERN: string;
118120
SKIP_COMMENTS: boolean;
119121
PR_THRESHOLD: number;
122+
VALIDATE_ISSUE_STATUS: boolean;
123+
ALLOWED_ISSUE_STATUSES: string;
120124
}
121125

122126
export interface JIRAClient {

src/utils.ts

+51-2
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export const getJIRAClient = (baseURL: string, token: string): JIRAClient => {
4848
const getIssue = async (id: string): Promise<JIRA.Issue> => {
4949
try {
5050
const response = await client.get<JIRA.Issue>(
51-
`/issue/${id}?fields=project,summary,issuetype,labels,customfield_10016`
51+
`/issue/${id}?fields=project,summary,issuetype,labels,status,customfield_10016`
5252
);
5353
return response.data;
5454
} catch (e) {
@@ -60,7 +60,14 @@ export const getJIRAClient = (baseURL: string, token: string): JIRAClient => {
6060
try {
6161
const issue: JIRA.Issue = await getIssue(key);
6262
const {
63-
fields: { issuetype: type, project, summary, customfield_10016: estimate, labels: rawLabels },
63+
fields: {
64+
issuetype: type,
65+
project,
66+
summary,
67+
customfield_10016: estimate,
68+
labels: rawLabels,
69+
status: issueStatus,
70+
},
6471
} = issue;
6572

6673
const labels = rawLabels.map((label) => ({
@@ -74,6 +81,7 @@ export const getJIRAClient = (baseURL: string, token: string): JIRAClient => {
7481
key,
7582
summary,
7683
url: `${baseURL}/browse/${key}`,
84+
status: issueStatus.name,
7785
type: {
7886
name: type.name,
7987
icon: type.iconUrl,
@@ -255,6 +263,10 @@ export const getPRDescription = (body = '', details: JIRADetails): string => {
255263
${details.type.name}
256264
</td>
257265
</tr>
266+
<tr>
267+
<th>Status</th>
268+
<td>${details.status}</td>
269+
</tr>
258270
<tr>
259271
<th>Points</th>
260272
<td>${details.estimate || 'N/A'}</td>
@@ -321,3 +333,40 @@ Valid sample branch names:
321333
‣ 'bugfix/fix-some-strange-bug_GAL-2345'
322334
`;
323335
};
336+
337+
/** Check if jira issue status validation is enabled then compare the issue status will the allowed statuses. */
338+
export const isIssueStatusValid = (
339+
shouldValidate: boolean,
340+
allowedIssueStatuses: string[],
341+
details: JIRADetails
342+
): boolean => {
343+
if (!shouldValidate) {
344+
core.info('Skipping Jira issue status validation as shouldValidate is false');
345+
return true;
346+
}
347+
348+
return allowedIssueStatuses.includes(details.status);
349+
};
350+
351+
/** Get the comment body for very huge PR. */
352+
export const getInvalidIssueStatusComment = (
353+
/** Number of additions. */
354+
issueStatus: string,
355+
/** Threshold of additions allowed. */
356+
allowedStatuses: string
357+
): string =>
358+
`<p>:broken_heart: The detected issue is not in one of the allowed statuses :broken_heart: </p>
359+
<table>
360+
<tr>
361+
<th>Detected Status</th>
362+
<td>${issueStatus}</td>
363+
<td>:x:</td>
364+
</tr>
365+
<tr>
366+
<th>Allowed Statuses</th>
367+
<td>${allowedStatuses}</td>
368+
<td>:heavy_check_mark:</td>
369+
</tr>
370+
</table>
371+
<p>Please ensure your jira story is in one of the allowed statuses</p>
372+
`;

0 commit comments

Comments
 (0)