Skip to content

Commit 7252143

Browse files
kwaszczukquezak
andauthored
feat: migrate logic from ClickUp to Jira #PLTM-233 (#12)
* chore: remove ClickUp logic * feat: logic for Jira issues * refactor: simplify task reference parsing * fix: typo * feat: ignore duplicates and return tasks in alphabetical order * fix: promise not awaited * fix: invalid Jira request auth and response parsing * feat: search just the commit title for issue references * docs: change package.json description * docs: update README.md * fix: remove references to tickets in description * cleanup: remove redundant join operation Co-authored-by: Artur Kozak <[email protected]> * feat: do not sort tasks alphabetically * feat: match issue references only in the end of the title * test: adjust tests * chore: add information about placing issue references at the end of title * feat: use Github variable for token's user --------- Co-authored-by: Artur Kozak <[email protected]>
1 parent eacaf13 commit 7252143

File tree

6 files changed

+79
-120
lines changed

6 files changed

+79
-120
lines changed

README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22
![CliCop logo](clicop.png)
33

44

5-
CliCop is an Github Actions action that enforces pinning ClickUp ticket id reference in a PR.\
6-
I will fail if there is no task id in PR title or body.\
7-
It does not yet check if the task really exists in ClickUp.\
5+
CliCop is an Github Actions action that enforces pinning ticket id reference in a PR.\
6+
It will fail if there is no task id in PR title.\
87
It utilizes [DangerJS](https://danger.systems/js/) to perform the check.
98

109
## Inputs
1110
github_token - Github authentification token.
12-
clickup_token - ClickUp authentification token.
11+
jira_token - Jira authentification token.
1312

1413
## Testing
1514
The action conitains some unit tests. To run them:
@@ -42,5 +41,5 @@ jobs:
4241
- uses: RampNetwork/github-actions/[email protected]
4342
with:
4443
github_token: ${{ secrets.GITHUB_TOKEN }} # this is passed automatically https://docs.github.com/en/actions/security-guides/automatic-token-authentication
45-
clickup_token: ${{ secrets.CLICKUP_TOKEN }}
44+
jira_token: ${{ secrets.JIRA_TOKEN }}
4645
```

action.yml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
name: CliCop
2-
description: "Run DangerJS to check if the PR is has a valid Clickup ticket reference"
2+
description: "Run DangerJS to check if the PR is has a valid Jira ticket reference"
33

44
inputs:
55
github_token:
66
description: "Github authentification token"
77
required: true
8-
clickup_token:
9-
description: "Clickup authentification token"
8+
jira_token:
9+
description: "Jira authentification token"
10+
required: true
11+
jira_token_user:
12+
description: "User email of Jira user the jira_token belongs to"
1013
required: true
1114

1215
runs:
@@ -23,6 +26,7 @@ runs:
2326
shell: bash
2427
env:
2528
GITHUB_TOKEN: ${{ inputs.github_token }}
26-
CLICKUP_TOKEN: ${{ inputs.clickup_token }}
29+
JIRA_TOKEN: ${{ inputs.jira_token }}
30+
JIRA_TOKEN_USER: ${{ inputs.jira_token_user }}
2731
NODE_PATH: ${{ github.action_path }}
2832
run: npx danger -i CliCop --dangerfile ${{ github.action_path }}/dangerfile.js ci

dangerfile.js

Lines changed: 36 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -2,103 +2,69 @@ const axios = require('axios');
22

33
const getTasks = require('./lib/get-tasks.js');
44

5-
const CLICKUP_API_URL = 'https://api.clickup.com/api/v2';
6-
const CLICKUP_TEAM_ID = '24301226';
5+
const JIRA_BASE_URL = 'https://rampnetwork.atlassian.net';
6+
const JIRA_API_URL = `${JIRA_BASE_URL}/rest/api/2`;
77

8-
const clickupRequest = async ({ resource, method = 'POST', data = {} }) =>
8+
const jiraRequest = async ({ resource, method = 'GET', data = {} }) =>
99
axios({
1010
method,
1111
url: resource,
12-
baseURL: CLICKUP_API_URL,
13-
headers: {
14-
Authorization: process.env.CLICKUP_TOKEN,
15-
'Content-Type': 'application/json',
12+
baseURL: JIRA_API_URL,
13+
auth: {
14+
username: process.env.JIRA_TOKEN_USER,
15+
password: process.env.JIRA_TOKEN,
1616
},
1717
data,
18-
}).catch(
19-
function (error) {
20-
console.log(`Error getting ${resource}`)
21-
}
22-
);
23-
24-
const getClickupTicketName = async (taskId, isCustom) => {
25-
const resource = getTaskResource({ taskId, isCustom }, "")
26-
27-
const response = await clickupRequest({
28-
resource,
29-
method: 'GET',
18+
}).catch(error => {
19+
console.error(`Error getting ${resource} - ${error}`)
3020
});
31-
return response?.data?.name ?? undefined
32-
}
33-
34-
const getTaskResource = ({ taskId, isCustom }, field = '') => {
35-
const customQuery = isCustom ? `?custom_task_ids=true&team_id=${CLICKUP_TEAM_ID}` : '';
36-
return `/task/${taskId}${field}${customQuery}`
37-
}
3821

39-
const addClickupRefComment = async (taskId, isCustom) => {
40-
const resource = getTaskResource({ taskId, isCustom }, '/comment')
41-
const text = `This issue is referenced in ${danger.github.pr.html_url}`;
22+
const getJiraIssueName = async (issueId) => {
23+
const resource = `/issue/${issueId}?fields=summary`
4224

43-
const {
44-
data: { comments },
45-
} = await clickupRequest({
25+
const response = await jiraRequest({
4626
resource,
4727
method: 'GET',
4828
});
49-
50-
const hasRefComment = comments.find(({ comment_text }) =>
51-
comment_text.includes(text)
52-
);
53-
54-
if (hasRefComment) {
55-
return;
56-
}
57-
58-
await clickupRequest({
59-
resource,
60-
data: {
61-
comment: [
62-
{
63-
text,
64-
},
65-
],
66-
},
67-
});
68-
};
29+
return response?.data?.fields?.summary ?? undefined
30+
}
6931

7032
const parallelRequests = (tasks = [], req) => {
7133
if (tasks.length === 0) {
72-
return[];
34+
return [];
7335
}
7436

7537
return Promise.all(tasks.map(req));
7638
};
7739

78-
const checkAndUpdateClickupIssues = async () => {
79-
const source = [danger.github.pr.title, danger.github.pr.body].join(' ');
40+
const checkTasks = async () => {
41+
const source = danger.github.pr.title;
8042
const tasks = getTasks(source);
81-
if (tasks.length === 0) {
43+
const allTasks = await parallelRequests(tasks, async ({ taskId }) => {
44+
return {
45+
taskId: taskId,
46+
name: await getJiraIssueName(taskId),
47+
}
48+
});
49+
50+
const tasksWithName = allTasks.filter(({ name }) => name);
51+
if (tasksWithName.length === 0) {
8252
fail(
83-
'<b>Please add the issue key to the PR e.g.: #28zfr1a or #DATAENG-98</b>\n' +
84-
'(remember to add hash)\n\n' +
85-
'<i>You can find ticket key eg. in the last part of URL when ticket is viewed in the browser eg.:\n' +
86-
'URL: https://app.clickup.com/t/28zfr1a -> ticket issue key: 28zfr1a -> what should be added to PR: #28zfr1a\n' +
87-
'URL: https://app.clickup.com/t/24301226/DATAENG-98 -> ticket issue key: DATAENG-98 -> what should be added to PR: #DATAENG-98\n\n' +
88-
'You can add more than one ticket issue key in the PR title or/and description.</i>'
53+
'<b>Please add the Jira issue key at the end of PR title e.g.: #DATA-98</b> (remember to add hash)\n\n' +
54+
'<i>You can find issue key eg. in the last part of URL when issue is viewed in the browser eg.:\n' +
55+
`URL: ${JIRA_BASE_URL}/browse/DATA-98 -> issue key: DATA-98 -> what should be added to the PR title: #DATA-98\n\n` +
56+
'You can add more than one issue key in the PR title.</i>'
8957
);
9058
return;
9159
}
9260

9361
message(
94-
'Ticket(s) related to this PR:\n\n' +
95-
tasks
96-
.map(
97-
({ taskId }) =>
98-
`- :link: #${taskId}`
99-
)
100-
.join('\n')
62+
'Jira issue(s) related to this PR:\n' +
63+
tasksWithName.map(
64+
({ taskId, name }) =>
65+
`+ :link: <a href="${JIRA_BASE_URL}/browse/${taskId}">${name} [#${taskId}]</a>`
66+
).join('\n')
10167
);
10268
};
10369

104-
checkAndUpdateClickupIssues();
70+
checkTasks();

lib/get-tasks.js

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,23 @@
11
/**
22
* Acceptable formats:
3-
* - #20jt35r
43
* - #CORE-123
54
*/
65
const getTasks = (source) => {
7-
const ticketRefRegex = /(?<=\s|^)#(([A-Z]{2,10}-\d+)|(\w{7,10}))/g;
8-
const ticketIdRegex = /#(?<taskId>([A-Z]{2,10}-\d+)|(\w{7,10}))/;
9-
const customTicketIdRegex = /#[A-Z]{2,10}-\d+/;
10-
const ids =
11-
source.match(ticketRefRegex)?.map(v => ({
12-
...v.match(ticketIdRegex).groups,
13-
isCustom: customTicketIdRegex.test(v),
14-
})) || [];
6+
// Matches the prefix of the string containg only issue references delimited with whitespaces
7+
const prefixWithRefsRegex = /((\s+|^)#[A-Z]{2,10}-\d+)+$/;
8+
const [prefix] = source.match(prefixWithRefsRegex) || [''];
9+
// Picks individual issue references from the string
10+
const taskRefRegex = /(?<=(\s|^)#)[A-Z]{2,10}-\d+/g;
11+
const refs = prefix.match(taskRefRegex) || [];
12+
const uniqueRefs = new Set(refs);
1513

16-
return Object.values(
17-
ids.reduce(
18-
(tasks, task) => ({
19-
...tasks,
20-
[task.taskId]: task,
21-
}),
22-
{}
23-
)
24-
);
25-
};
14+
let tasks = [];
15+
for (const ref of uniqueRefs) {
16+
tasks.push({
17+
taskId: ref,
18+
});
19+
}
20+
return tasks;
21+
};
2622

2723
module.exports = getTasks;

lib/get-tasks.spec.js

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,51 +2,45 @@ const getTasks = require('./get-tasks.js');
22

33
const correctSources = [
44
[
5-
"This PR closes #BUNNNY-1",
5+
"This PR closes #BUNNY-1",
66
[
7-
{ taskId: "BUNNNY-1", isCustom: true }
7+
{ taskId: "BUNNY-1" }
88
]
99
],
1010
[
11-
"This PR closes #86bwmzcyf",
11+
"This PR has duplicated reference #BUNNY-1 #BUNNY-1",
1212
[
13-
{ taskId: "86bwmzcyf", isCustom: false }
13+
{ taskId: "BUNNY-1" },
1414
]
1515
],
1616
[
17-
"This PR closes #86bwmzcyf but it's in the middle of the text",
17+
"This PR closes #CORE-123 but the reference is not at the end of the title #BUNNY-1",
1818
[
19-
{ taskId: "86bwmzcyf", isCustom: false }
19+
{ taskId: "BUNNY-1" },
2020
]
2121
],
2222
[
23-
"This PR closes #CORE-123 and is related to #20jt35r",
23+
"This PR closes two issues #CORE-123 #BUNNY-1",
2424
[
25-
{ taskId: "CORE-123", isCustom: true },
26-
{ taskId: "20jt35r", isCustom: false }
25+
{ taskId: "CORE-123" },
26+
{ taskId: "BUNNY-1" },
2727
]
2828
],
2929
[
30-
"This PR closes #SRE-12345 and is related to #86bwmzcyf",
30+
"This PR closes two issues #CORE-123 #BUNNY-1",
3131
[
32-
{ taskId: "SRE-12345", isCustom: true },
33-
{ taskId: "86bwmzcyf", isCustom: false }
32+
{ taskId: "CORE-123" },
33+
{ taskId: "BUNNY-1" },
3434
]
3535
],
36-
[
37-
"This PR has a newline before ticket id\n#SRE-12345",
38-
[
39-
{ taskId: "SRE-12345", isCustom: true },
40-
]
41-
]
4236
];
4337

4438
const incorrectSources = [
45-
"This PR has too short regular ticket reference #20jt",
4639
"This PR has too short custom ticket reference #C-123",
4740
"There is no # in this ticket reference CORE-123",
48-
"Regular ticket reference is blended with the text#86bwmzcyf",
49-
"Custom ticket reference is blended with the text#BUNNNY-1",
41+
"Ticket reference is blended with the text#BUNNY-1",
42+
"Ticket reference has doubled # sign ##BUNNY-1",
43+
"Ticket reference #BUNNY-1 is in the middle of the title",
5044
];
5145

5246
const emptySource = "A PR without ticket references";

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "clicop",
33
"version": "1.0.0",
4-
"description": "Action that enforces pinning ClickUp ticket id reference in a PR using DangerJS",
4+
"description": "Action that enforces pinning Jira ticket id reference in a PR using DangerJS",
55
"main": "dangerfile.js",
66
"scripts": {
77
"test": "jest"

0 commit comments

Comments
 (0)