Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable validation for jira issue status #33

Merged
merged 17 commits into from
Dec 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
NasAmin marked this conversation as resolved.
Show resolved Hide resolved
<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>
`;