Skip to content

Commit

Permalink
Enable validation for jira issue status (#33)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
NasAmin and rheaditi authored Dec 20, 2020
1 parent a97946c commit 0b90e54
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 6 deletions.
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ steps:

#### Description

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.
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.

#### Labels

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

#### Issue Status Validation
Issue status is shown in the [Description](#description).
**Why validate issue status?**
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.

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.

The following flags can be used to validate issue status:
- `validate_issue_status`
- If set to `true`, `jira-lint` will validate the issue status based on `allowed_issue_statuses`
- `allowed_issue_statuses`
- 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.

**Example of invalid status**
<p>:broken_heart: The detected issue is not in one of the allowed statuses :broken_heart: </p>
<table>
<tr>
<th>Detected Status</th>
<td>${issueStatus}</td>
<td>:x:</td>
</tr>
<tr>
<th>Allowed Statuses</th>
<td>${allowedStatuses}</td>
<td>:heavy_check_mark:</td>
</tr>
</table>
<p>Please ensure your jira story is in one of the allowed statuses</p>

#### Soft-validations via comments

`jira-lint` will add comments to a PR to encourage better PR practices:
Expand Down Expand Up @@ -140,11 +169,14 @@ When a PR passes the above check, `jira-lint` will also add the issue details to
| `skip-branches` | A regex to ignore running `jira-lint` on certain branches, like production etc. | false | ' ' |
| `skip-comments` | A `Boolean` if set to `true` then `jira-lint` will skip adding lint comments for PR title. | false | false |
| `pr-threshold` | An `Integer` based on which `jira-lint` will add a comment discouraging huge PRs. | false | 800 |
| `validate_issue_status` | A `Boolean` based on which `jira-lint` will validate the status of the detected jira issue | false | false |
| `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"` |

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).

### `jira-token`

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).

The Jira token is used to fetch issue information via the Jira REST API. To get the token:-
1. Generate an [API token via JIRA](https://confluence.atlassian.com/cloud/api-tokens-938839638.html)
2. Create the encoded token in the format of `base64Encode(<username>:<api_token>)`.
Expand Down
40 changes: 40 additions & 0 deletions __tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
shouldSkipBranchLint,
shouldUpdatePRDescription,
getJIRAClient,
getInvalidIssueStatusComment,
isIssueStatusValid,
} from '../src/utils';
import { HIDDEN_MARKER } from '../src/constants';
import { JIRADetails } from '../src/types';
Expand Down Expand Up @@ -183,12 +185,14 @@ describe('getPRDescription()', () => {
labels: [{ name: 'frontend', url: 'frontend-url' }],
summary: 'Story title or summary',
project: { name: 'project', url: 'project-url', key: 'abc' },
status: 'In Progress',
};
const description = getPRDescription('some_body', issue);

expect(shouldUpdatePRDescription(description)).toBeFalsy();
expect(description).toContain(issue.key);
expect(description).toContain(issue.estimate);
expect(description).toContain(issue.status);
expect(description).toContain(issue.labels[0].name);
});
});
Expand Down Expand Up @@ -240,3 +244,39 @@ describe('JIRA Client', () => {
expect(details).not.toBeNull();
});
});

describe('isIssueStatusValid()', () => {
const issue: JIRADetails = {
key: 'ABC-123',
url: 'url',
type: { name: 'feature', icon: 'feature-icon-url' },
estimate: 1,
labels: [{ name: 'frontend', url: 'frontend-url' }],
summary: 'Story title or summary',
project: { name: 'project', url: 'project-url', key: 'abc' },
status: 'Assessment',
};

it('should return false if issue validation was enabled but invalid issue status', () => {
const expectedStatuses = ['In Test', 'In Progress'];
expect(isIssueStatusValid(true, expectedStatuses, issue)).toBeFalsy();
});

it('should return true if issue validation was enabled but issue has a valid status', () => {
const expectedStatuses = ['In Test', 'In Progress'];
issue.status = 'In Progress';
expect(isIssueStatusValid(true, expectedStatuses, issue)).toBeTruthy();
});

it('should return true if issue status validation is not enabled', () => {
const expectedStatuses = ['In Test', 'In Progress'];
expect(isIssueStatusValid(false, expectedStatuses, issue)).toBeTruthy();
});
});

describe('getInvalidIssueStatusComment()', () => {
it('should return content with the passed in issue status and allowed statses', () => {
expect(getInvalidIssueStatusComment('Assessment', 'In Progress')).toContain('Assessment');
expect(getInvalidIssueStatusComment('Assessment', 'In Progress')).toContain('In Progress');
});
});
11 changes: 11 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ inputs:
description: 'An `Integer` based on which jira-lint add a comment discouraging huge PRs. Is disabled by `skip-comments`'
required: false
default: 800
validate_issue_status:
description: 'Set this to true if you want jira-lint to validate the status of the detected jira issues'
required: false
default: false
allowed_issue_statuses:
description: |
A comma separated list of acceptable Jira issue statuses. You must provide a value for this if validate_issue_status is set to true
Requires validate_issue_status to be set to true.
required: false
default: "In Progress"

runs:
using: 'node12'
main: 'lib/index.js'
Expand Down
2 changes: 1 addition & 1 deletion lib/index.js

Large diffs are not rendered by default.

29 changes: 28 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
shouldSkipBranchLint,
shouldUpdatePRDescription,
updatePrDetails,
isIssueStatusValid,
getInvalidIssueStatusComment,
} from './utils';
import { PullRequestParams, JIRADetails, JIRALintActionInputs } from './types';
import { DEFAULT_PR_ADDITIONS_THRESHOLD } from './constants';
Expand All @@ -28,6 +30,8 @@ const getInputs = (): JIRALintActionInputs => {
const BRANCH_IGNORE_PATTERN: string = core.getInput('skip-branches', { required: false }) || '';
const SKIP_COMMENTS: boolean = core.getInput('skip-comments', { required: false }) === 'true';
const PR_THRESHOLD = parseInt(core.getInput('pr-threshold', { required: false }), 10);
const VALIDATE_ISSUE_STATUS: boolean = core.getInput('validate_issue_status', { required: false }) === 'true';
const ALLOWED_ISSUE_STATUSES: string = core.getInput('allowed_issue_statuses');

return {
JIRA_TOKEN,
Expand All @@ -36,12 +40,23 @@ const getInputs = (): JIRALintActionInputs => {
SKIP_COMMENTS,
PR_THRESHOLD: isNaN(PR_THRESHOLD) ? DEFAULT_PR_ADDITIONS_THRESHOLD : PR_THRESHOLD,
JIRA_BASE_URL: JIRA_BASE_URL.endsWith('/') ? JIRA_BASE_URL.replace(/\/$/, '') : JIRA_BASE_URL,
VALIDATE_ISSUE_STATUS,
ALLOWED_ISSUE_STATUSES,
};
};

async function run(): Promise<void> {
try {
const { JIRA_TOKEN, JIRA_BASE_URL, GITHUB_TOKEN, BRANCH_IGNORE_PATTERN, SKIP_COMMENTS, PR_THRESHOLD } = getInputs();
const {
JIRA_TOKEN,
JIRA_BASE_URL,
GITHUB_TOKEN,
BRANCH_IGNORE_PATTERN,
SKIP_COMMENTS,
PR_THRESHOLD,
VALIDATE_ISSUE_STATUS,
ALLOWED_ISSUE_STATUSES,
} = getInputs();

const defaultAdditionsCount = 800;
const prThreshold: number = PR_THRESHOLD ? Number(PR_THRESHOLD) : defaultAdditionsCount;
Expand Down Expand Up @@ -129,6 +144,18 @@ async function run(): Promise<void> {
labels,
});

if (!isIssueStatusValid(VALIDATE_ISSUE_STATUS, ALLOWED_ISSUE_STATUSES.split(','), details)) {
const invalidIssueStatusComment: IssuesCreateCommentParams = {
...commonPayload,
body: getInvalidIssueStatusComment(details.status, ALLOWED_ISSUE_STATUSES),
};
console.log('Adding comment for invalid issue status');
await addComment(client, invalidIssueStatusComment);

core.setFailed('The found jira issue does is not in acceptable statuses');
process.exit(1);
}

if (shouldUpdatePRDescription(prBody)) {
const prData: PullsUpdateParams = {
owner,
Expand Down
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export namespace JIRA {
id: string;
key: string;
self: string;
status: string;
fields: {
summary: string;
status: IssueStatus;
Expand All @@ -97,6 +98,7 @@ export interface JIRADetails {
key: string;
summary: string;
url: string;
status: string;
type: {
name: string;
icon: string;
Expand All @@ -117,6 +119,8 @@ export interface JIRALintActionInputs {
BRANCH_IGNORE_PATTERN: string;
SKIP_COMMENTS: boolean;
PR_THRESHOLD: number;
VALIDATE_ISSUE_STATUS: boolean;
ALLOWED_ISSUE_STATUSES: string;
}

export interface JIRAClient {
Expand Down
53 changes: 51 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const getJIRAClient = (baseURL: string, token: string): JIRAClient => {
const getIssue = async (id: string): Promise<JIRA.Issue> => {
try {
const response = await client.get<JIRA.Issue>(
`/issue/${id}?fields=project,summary,issuetype,labels,customfield_10016`
`/issue/${id}?fields=project,summary,issuetype,labels,status,customfield_10016`
);
return response.data;
} catch (e) {
Expand All @@ -60,7 +60,14 @@ export const getJIRAClient = (baseURL: string, token: string): JIRAClient => {
try {
const issue: JIRA.Issue = await getIssue(key);
const {
fields: { issuetype: type, project, summary, customfield_10016: estimate, labels: rawLabels },
fields: {
issuetype: type,
project,
summary,
customfield_10016: estimate,
labels: rawLabels,
status: issueStatus,
},
} = issue;

const labels = rawLabels.map((label) => ({
Expand All @@ -74,6 +81,7 @@ export const getJIRAClient = (baseURL: string, token: string): JIRAClient => {
key,
summary,
url: `${baseURL}/browse/${key}`,
status: issueStatus.name,
type: {
name: type.name,
icon: type.iconUrl,
Expand Down Expand Up @@ -255,6 +263,10 @@ export const getPRDescription = (body = '', details: JIRADetails): string => {
${details.type.name}
</td>
</tr>
<tr>
<th>Status</th>
<td>${details.status}</td>
</tr>
<tr>
<th>Points</th>
<td>${details.estimate || 'N/A'}</td>
Expand Down Expand Up @@ -321,3 +333,40 @@ Valid sample branch names:
‣ 'bugfix/fix-some-strange-bug_GAL-2345'
`;
};

/** Check if jira issue status validation is enabled then compare the issue status will the allowed statuses. */
export const isIssueStatusValid = (
shouldValidate: boolean,
allowedIssueStatuses: string[],
details: JIRADetails
): boolean => {
if (!shouldValidate) {
core.info('Skipping Jira issue status validation as shouldValidate is false');
return true;
}

return allowedIssueStatuses.includes(details.status);
};

/** Get the comment body for very huge PR. */
export const getInvalidIssueStatusComment = (
/** Number of additions. */
issueStatus: string,
/** Threshold of additions allowed. */
allowedStatuses: string
): string =>
`<p>:broken_heart: The detected issue is not in one of the allowed statuses :broken_heart: </p>
<table>
<tr>
<th>Detected Status</th>
<td>${issueStatus}</td>
<td>:x:</td>
</tr>
<tr>
<th>Allowed Statuses</th>
<td>${allowedStatuses}</td>
<td>:heavy_check_mark:</td>
</tr>
</table>
<p>Please ensure your jira story is in one of the allowed statuses</p>
`;

0 comments on commit 0b90e54

Please sign in to comment.