Skip to content

Commit

Permalink
feat: migrate logic from ClickUp to Jira #PLTM-233 (RampNetwork#12)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
kwaszczuk and quezak authored Oct 10, 2024
1 parent eacaf13 commit 7252143
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 120 deletions.
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
![CliCop logo](clicop.png)


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

## Inputs
github_token - Github authentification token.
clickup_token - ClickUp authentification token.
jira_token - Jira authentification token.

## Testing
The action conitains some unit tests. To run them:
Expand Down Expand Up @@ -42,5 +41,5 @@ jobs:
- uses: RampNetwork/github-actions/[email protected]
with:
github_token: ${{ secrets.GITHUB_TOKEN }} # this is passed automatically https://docs.github.com/en/actions/security-guides/automatic-token-authentication
clickup_token: ${{ secrets.CLICKUP_TOKEN }}
jira_token: ${{ secrets.JIRA_TOKEN }}
```
12 changes: 8 additions & 4 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
name: CliCop
description: "Run DangerJS to check if the PR is has a valid Clickup ticket reference"
description: "Run DangerJS to check if the PR is has a valid Jira ticket reference"

inputs:
github_token:
description: "Github authentification token"
required: true
clickup_token:
description: "Clickup authentification token"
jira_token:
description: "Jira authentification token"
required: true
jira_token_user:
description: "User email of Jira user the jira_token belongs to"
required: true

runs:
Expand All @@ -23,6 +26,7 @@ runs:
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.github_token }}
CLICKUP_TOKEN: ${{ inputs.clickup_token }}
JIRA_TOKEN: ${{ inputs.jira_token }}
JIRA_TOKEN_USER: ${{ inputs.jira_token_user }}
NODE_PATH: ${{ github.action_path }}
run: npx danger -i CliCop --dangerfile ${{ github.action_path }}/dangerfile.js ci
106 changes: 36 additions & 70 deletions dangerfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,103 +2,69 @@ const axios = require('axios');

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

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

const clickupRequest = async ({ resource, method = 'POST', data = {} }) =>
const jiraRequest = async ({ resource, method = 'GET', data = {} }) =>
axios({
method,
url: resource,
baseURL: CLICKUP_API_URL,
headers: {
Authorization: process.env.CLICKUP_TOKEN,
'Content-Type': 'application/json',
baseURL: JIRA_API_URL,
auth: {
username: process.env.JIRA_TOKEN_USER,
password: process.env.JIRA_TOKEN,
},
data,
}).catch(
function (error) {
console.log(`Error getting ${resource}`)
}
);

const getClickupTicketName = async (taskId, isCustom) => {
const resource = getTaskResource({ taskId, isCustom }, "")

const response = await clickupRequest({
resource,
method: 'GET',
}).catch(error => {
console.error(`Error getting ${resource} - ${error}`)
});
return response?.data?.name ?? undefined
}

const getTaskResource = ({ taskId, isCustom }, field = '') => {
const customQuery = isCustom ? `?custom_task_ids=true&team_id=${CLICKUP_TEAM_ID}` : '';
return `/task/${taskId}${field}${customQuery}`
}

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

const {
data: { comments },
} = await clickupRequest({
const response = await jiraRequest({
resource,
method: 'GET',
});

const hasRefComment = comments.find(({ comment_text }) =>
comment_text.includes(text)
);

if (hasRefComment) {
return;
}

await clickupRequest({
resource,
data: {
comment: [
{
text,
},
],
},
});
};
return response?.data?.fields?.summary ?? undefined
}

const parallelRequests = (tasks = [], req) => {
if (tasks.length === 0) {
return[];
return [];
}

return Promise.all(tasks.map(req));
};

const checkAndUpdateClickupIssues = async () => {
const source = [danger.github.pr.title, danger.github.pr.body].join(' ');
const checkTasks = async () => {
const source = danger.github.pr.title;
const tasks = getTasks(source);
if (tasks.length === 0) {
const allTasks = await parallelRequests(tasks, async ({ taskId }) => {
return {
taskId: taskId,
name: await getJiraIssueName(taskId),
}
});

const tasksWithName = allTasks.filter(({ name }) => name);
if (tasksWithName.length === 0) {
fail(
'<b>Please add the issue key to the PR e.g.: #28zfr1a or #DATAENG-98</b>\n' +
'(remember to add hash)\n\n' +
'<i>You can find ticket key eg. in the last part of URL when ticket is viewed in the browser eg.:\n' +
'URL: https://app.clickup.com/t/28zfr1a -> ticket issue key: 28zfr1a -> what should be added to PR: #28zfr1a\n' +
'URL: https://app.clickup.com/t/24301226/DATAENG-98 -> ticket issue key: DATAENG-98 -> what should be added to PR: #DATAENG-98\n\n' +
'You can add more than one ticket issue key in the PR title or/and description.</i>'
'<b>Please add the Jira issue key at the end of PR title e.g.: #DATA-98</b> (remember to add hash)\n\n' +
'<i>You can find issue key eg. in the last part of URL when issue is viewed in the browser eg.:\n' +
`URL: ${JIRA_BASE_URL}/browse/DATA-98 -> issue key: DATA-98 -> what should be added to the PR title: #DATA-98\n\n` +
'You can add more than one issue key in the PR title.</i>'
);
return;
}

message(
'Ticket(s) related to this PR:\n\n' +
tasks
.map(
({ taskId }) =>
`- :link: #${taskId}`
)
.join('\n')
'Jira issue(s) related to this PR:\n' +
tasksWithName.map(
({ taskId, name }) =>
`+ :link: <a href="${JIRA_BASE_URL}/browse/${taskId}">${name} [#${taskId}]</a>`
).join('\n')
);
};

checkAndUpdateClickupIssues();
checkTasks();
34 changes: 15 additions & 19 deletions lib/get-tasks.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
/**
* Acceptable formats:
* - #20jt35r
* - #CORE-123
*/
const getTasks = (source) => {
const ticketRefRegex = /(?<=\s|^)#(([A-Z]{2,10}-\d+)|(\w{7,10}))/g;
const ticketIdRegex = /#(?<taskId>([A-Z]{2,10}-\d+)|(\w{7,10}))/;
const customTicketIdRegex = /#[A-Z]{2,10}-\d+/;
const ids =
source.match(ticketRefRegex)?.map(v => ({
...v.match(ticketIdRegex).groups,
isCustom: customTicketIdRegex.test(v),
})) || [];
// Matches the prefix of the string containg only issue references delimited with whitespaces
const prefixWithRefsRegex = /((\s+|^)#[A-Z]{2,10}-\d+)+$/;
const [prefix] = source.match(prefixWithRefsRegex) || [''];
// Picks individual issue references from the string
const taskRefRegex = /(?<=(\s|^)#)[A-Z]{2,10}-\d+/g;
const refs = prefix.match(taskRefRegex) || [];
const uniqueRefs = new Set(refs);

return Object.values(
ids.reduce(
(tasks, task) => ({
...tasks,
[task.taskId]: task,
}),
{}
)
);
};
let tasks = [];
for (const ref of uniqueRefs) {
tasks.push({
taskId: ref,
});
}
return tasks;
};

module.exports = getTasks;
36 changes: 15 additions & 21 deletions lib/get-tasks.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,45 @@ const getTasks = require('./get-tasks.js');

const correctSources = [
[
"This PR closes #BUNNNY-1",
"This PR closes #BUNNY-1",
[
{ taskId: "BUNNNY-1", isCustom: true }
{ taskId: "BUNNY-1" }
]
],
[
"This PR closes #86bwmzcyf",
"This PR has duplicated reference #BUNNY-1 #BUNNY-1",
[
{ taskId: "86bwmzcyf", isCustom: false }
{ taskId: "BUNNY-1" },
]
],
[
"This PR closes #86bwmzcyf but it's in the middle of the text",
"This PR closes #CORE-123 but the reference is not at the end of the title #BUNNY-1",
[
{ taskId: "86bwmzcyf", isCustom: false }
{ taskId: "BUNNY-1" },
]
],
[
"This PR closes #CORE-123 and is related to #20jt35r",
"This PR closes two issues #CORE-123 #BUNNY-1",
[
{ taskId: "CORE-123", isCustom: true },
{ taskId: "20jt35r", isCustom: false }
{ taskId: "CORE-123" },
{ taskId: "BUNNY-1" },
]
],
[
"This PR closes #SRE-12345 and is related to #86bwmzcyf",
"This PR closes two issues #CORE-123 #BUNNY-1",
[
{ taskId: "SRE-12345", isCustom: true },
{ taskId: "86bwmzcyf", isCustom: false }
{ taskId: "CORE-123" },
{ taskId: "BUNNY-1" },
]
],
[
"This PR has a newline before ticket id\n#SRE-12345",
[
{ taskId: "SRE-12345", isCustom: true },
]
]
];

const incorrectSources = [
"This PR has too short regular ticket reference #20jt",
"This PR has too short custom ticket reference #C-123",
"There is no # in this ticket reference CORE-123",
"Regular ticket reference is blended with the text#86bwmzcyf",
"Custom ticket reference is blended with the text#BUNNNY-1",
"Ticket reference is blended with the text#BUNNY-1",
"Ticket reference has doubled # sign ##BUNNY-1",
"Ticket reference #BUNNY-1 is in the middle of the title",
];

const emptySource = "A PR without ticket references";
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "clicop",
"version": "1.0.0",
"description": "Action that enforces pinning ClickUp ticket id reference in a PR using DangerJS",
"description": "Action that enforces pinning Jira ticket id reference in a PR using DangerJS",
"main": "dangerfile.js",
"scripts": {
"test": "jest"
Expand Down

0 comments on commit 7252143

Please sign in to comment.