Skip to content

Commit

Permalink
Merge pull request #11 from Doist/luke/allow_author_to_twist_mapping
Browse files Browse the repository at this point in the history
feat: Allow Author to Twist Mapping
  • Loading branch information
lukemerrett authored Aug 26, 2024
2 parents 7e44322 + fcebb5c commit da96f80
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 31 deletions.
22 changes: 12 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,23 @@ jobs:
review_time_ms: 86400000 # 1 day in milliseconds
twist_url: 'https://twist.com/api/v3/integration_incoming/post_data?install_id=[install id]&install_token=[install token]'
token: ${{ secrets.DOIST_BOT_TOKEN }}
author_to_twist_mapping: 'github_username_a:123,github_username_b:456'

```

### Parameters

|name|required?|example|description|
|----|---------|-------|-----------|
|review_time_ms|yes|`86400000`|The time in milliseconds a PR has to wait before a reminder will be sen, example is 24 hours|
|message|yes|`%reviewer%, `<br/>`please review `<br/>`[#%pr_number% - %pr_title%](%pr_url%)`|The reminder message to send, takes 4 parameters for string interpolation: `%reviewer%`, `%pr_number%`, `%pr_title%` and `%pr_url%`|
|twist_url|yes|`https://twist.com/api/v3/integration_incoming/`<br/>`post_data?`<br/>`install_id=[install id]`<br/>`&install_token=[install token]`|The installed integration url for posting a message to a Twist thread|
|token|yes|`adbc12431414`|The token for accessing the GitHub API to query the state of the PRs in a repo|
|ignore_authors|no|`tom, renovate`|Usernames of PR creators who's PRs will be ignored|
|ignore_draft_prs|no|`false`|Whether we should ignore draft PRs when checking reviews, defaults to false|
|ignore_labels|no|`do not merge, blocked`|If provided any PRs with these labels will skip the review reminder check|
|ignore_prs_with_failing_checks|no|`false`|If the PR has any failing or errored status checks, ignore it|
|name|required?|description|
|----|---------|-----------|
|review_time_ms|yes|The time in milliseconds a PR has to wait before a reminder will be sen, example is 24 hours|
|message|yes|The reminder message to send, takes 4 parameters for string interpolation: `%reviewer%`, `%pr_number%`, `%pr_title%` and `%pr_url%`|
|twist_url|yes|The installed integration url for posting a message to a Twist thread|
|token|yes|The token for accessing the GitHub API to query the state of the PRs in a repo|
|ignore_authors|no|Usernames of PR creators who's PRs will be ignored|
|ignore_draft_prs|no|Whether we should ignore draft PRs when checking reviews, defaults to false|
|ignore_labels|no|If provided any PRs with these labels will skip the review reminder check|
|ignore_prs_with_failing_checks|no|If the PR has any failing or errored status checks, ignore it|
|author_to_twist_mapping|no|A mapping of each possible reviewer's GitHub username to their associated Twist user id. If provided it will ensure the correct user is notified in Twist when a review is overdue|

## Development

Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ inputs:
description: 'If the PR has any failing or errored status checks, ignore it'
required: true
default: false
author_to_twist_mapping:
description: 'A mapping of each possible reviewer's GitHub username to their associated Twist user id. If provided it will ensure the correct user is notified in Twist when a review is overdue'
required: false
runs:
using: 'node20'
main: 'dist/index.js'
50 changes: 40 additions & 10 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9551,13 +9551,15 @@ const REVIEW_TIME_MS = parseInt(core.getInput('review_time_ms', { required: true
const IGNORE_AUTHORS = core.getInput('ignore_authors', { required: false });
const TWIST_URL = core.getInput('twist_url', { required: true });
const REMINDER_MESSAGE = core.getInput('message', { required: true });
const AUTHOR_TO_TWIST_MAPPING = core.getInput('author_to_twist_mapping', { required: false });
const IGNORE_DRAFT_PRS = core.getBooleanInput('ignore_draft_prs', { required: true });
const IGNORE_LABELS = core.getInput('ignore_labels', { required: false });
const IGNORE_PRS_WITH_FAILING_CHECKS = core.getBooleanInput('ignore_prs_with_failing_checks', {
required: true,
});
function run() {
return __awaiter(this, void 0, void 0, function* () {
const authorToTwistMap = createAuthorToTwistMap(AUTHOR_TO_TWIST_MAPPING);
const pullRequests = yield (0, pullrequest_1.fetchPullRequests)(GITHUB_TOKEN, GITHUB_REPO_OWNER, GITHUB_REPO);
for (const pullRequest of pullRequests) {
if ((0, pullrequest_1.shouldIgnore)(pullRequest, IGNORE_AUTHORS, IGNORE_DRAFT_PRS, IGNORE_LABELS)) {
Expand All @@ -9575,7 +9577,7 @@ function run() {
const remind = yield (0, pullrequest_1.isMissingReview)(pullRequest, REVIEW_TIME_MS, GITHUB_TOKEN, GITHUB_REPO_OWNER, GITHUB_REPO);
if (remind) {
core.info(`Sending reminder`);
const response = yield (0, reminder_1.sendReminder)(pullRequest, REMINDER_MESSAGE, TWIST_URL);
const response = yield (0, reminder_1.sendReminder)(pullRequest, REMINDER_MESSAGE, TWIST_URL, authorToTwistMap);
const statusCode = response.message.statusCode;
if (statusCode >= 300) {
const message = response.message.statusMessage;
Expand All @@ -9586,6 +9588,27 @@ function run() {
}
});
}
/**
* Takes in a string in the format `username:twist_user_id,username:twist_user_id` (eg `bob:123,jane:456`)
* and parses it into a map of GitHub usernames to their associated Twist User IDs.
*
* @param input The string to process.
* @returns A map of GitHub usernames to their associated Twist User IDs.
*/
function createAuthorToTwistMap(input) {
const mapping = {};
if (!input) {
return mapping;
}
for (const individual of input.split(',')) {
const [username, twistUserID] = individual.split(':');
if (!username || !twistUserID) {
continue;
}
mapping[username] = parseInt(twistUserID);
}
return mapping;
}
run().catch((error) => core.setFailed(error.message));


Expand Down Expand Up @@ -9824,22 +9847,29 @@ const http_client_1 = __nccwpck_require__(6255);
* @param twistUrl The integration link for Twist used to post the message to a thread / channel
* @returns Awaitable http post response
*/
function sendReminder(pullRequest, messageTemplate, twistUrl) {
function sendReminder(pullRequest, messageTemplate, twistUrl, authorToTwistMapping) {
return __awaiter(this, void 0, void 0, function* () {
const httpClient = new http_client_1.HttpClient();
const reviewers = pullRequest.requested_reviewers.map((rr) => `${rr.login}`).join(', ');
const recipients = [];
const reviewers = pullRequest.requested_reviewers
.map((rr) => {
const twistUserID = authorToTwistMapping[rr.login];
if (twistUserID) {
recipients.push(twistUserID);
return `[${rr.login}](twist-mention://${twistUserID})`;
}
return `${rr.login}`;
})
.join(', ');
const message = messageTemplate
.replace('%reviewer%', reviewers)
.replace('%pr_number%', pullRequest.number.toString())
.replace('%pr_title%', pullRequest.title)
.replace('%pr_url%', pullRequest.html_url);
const data = {
const httpClient = new http_client_1.HttpClient();
return httpClient.post(twistUrl, JSON.stringify({
content: message,
};
const headers = {
'content-type': 'application/json',
};
return httpClient.post(twistUrl, JSON.stringify(data), headers);
recipients: recipients,
}), { 'content-type': 'application/json' });
});
}
exports.sendReminder = sendReminder;
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

36 changes: 35 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ const REVIEW_TIME_MS = parseInt(core.getInput('review_time_ms', { required: true
const IGNORE_AUTHORS = core.getInput('ignore_authors', { required: false })
const TWIST_URL = core.getInput('twist_url', { required: true })
const REMINDER_MESSAGE = core.getInput('message', { required: true })
const AUTHOR_TO_TWIST_MAPPING = core.getInput('author_to_twist_mapping', { required: false })
const IGNORE_DRAFT_PRS = core.getBooleanInput('ignore_draft_prs', { required: true })
const IGNORE_LABELS = core.getInput('ignore_labels', { required: false })
const IGNORE_PRS_WITH_FAILING_CHECKS = core.getBooleanInput('ignore_prs_with_failing_checks', {
required: true,
})

async function run(): Promise<void> {
const authorToTwistMap = createAuthorToTwistMap(AUTHOR_TO_TWIST_MAPPING)
const pullRequests = await fetchPullRequests(GITHUB_TOKEN, GITHUB_REPO_OWNER, GITHUB_REPO)
for (const pullRequest of pullRequests) {
if (shouldIgnore(pullRequest, IGNORE_AUTHORS, IGNORE_DRAFT_PRS, IGNORE_LABELS)) {
Expand Down Expand Up @@ -57,7 +59,12 @@ async function run(): Promise<void> {
)
if (remind) {
core.info(`Sending reminder`)
const response = await sendReminder(pullRequest, REMINDER_MESSAGE, TWIST_URL)
const response = await sendReminder(
pullRequest,
REMINDER_MESSAGE,
TWIST_URL,
authorToTwistMap,
)
const statusCode = response.message.statusCode as number
if (statusCode >= 300) {
const message = response.message.statusMessage as string
Expand All @@ -68,4 +75,31 @@ async function run(): Promise<void> {
}
}

/**
* Takes in a string in the format `username:twist_user_id,username:twist_user_id` (eg `bob:123,jane:456`)
* and parses it into a map of GitHub usernames to their associated Twist User IDs.
*
* @param input The string to process.
* @returns A map of GitHub usernames to their associated Twist User IDs.
*/
function createAuthorToTwistMap(input: string): { [id: string]: number } {
const mapping: { [id: string]: number } = {}

if (!input) {
return mapping
}

for (const individual of input.split(',')) {
const [username, twistUserID] = individual.split(':')

if (!username || !twistUserID) {
continue
}

mapping[username] = parseInt(twistUserID)
}

return mapping
}

run().catch((error) => core.setFailed((error as Error).message))
33 changes: 24 additions & 9 deletions src/reminder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,34 @@ export async function sendReminder(
pullRequest: PullRequest,
messageTemplate: string,
twistUrl: string,
authorToTwistMapping: { [id: string]: number },
): Promise<HttpClientResponse> {
const httpClient = new HttpClient()
const reviewers = pullRequest.requested_reviewers.map((rr) => `${rr.login}`).join(', ')
const recipients: Array<number> = []
const reviewers = pullRequest.requested_reviewers
.map((rr) => {
const twistUserID = authorToTwistMapping[rr.login]

if (twistUserID) {
recipients.push(twistUserID)
return `[${rr.login}](twist-mention://${twistUserID})`
}
return `${rr.login}`
})
.join(', ')

const message = messageTemplate
.replace('%reviewer%', reviewers)
.replace('%pr_number%', pullRequest.number.toString())
.replace('%pr_title%', pullRequest.title)
.replace('%pr_url%', pullRequest.html_url)
const data = {
content: message,
}
const headers = {
'content-type': 'application/json',
}
return httpClient.post(twistUrl, JSON.stringify(data), headers)

const httpClient = new HttpClient()
return httpClient.post(
twistUrl,
JSON.stringify({
content: message,
recipients: recipients,
}),
{ 'content-type': 'application/json' },
)
}

0 comments on commit da96f80

Please sign in to comment.