From f14d8bf459601ab2c3a7695ad5019ff9202a3073 Mon Sep 17 00:00:00 2001 From: Tomoya Kashifuku Date: Sun, 10 Mar 2024 15:13:53 +0900 Subject: [PATCH] Refinement comment functions (#15) * create get login names function * add getComments function * add function to check if the action has already send comment * return login array when - the action has not sent comment yet - the amount of comments has exceeded the threshold * fix function name * add message generate function * add comment post function * update main * update * uninstall unused package * run bundle * fix to add '@' to login names --- dist/index.js | 208 ++++++++++++++++++++++------- dist/licenses.txt | 10 -- src/comments/constants.ts | 2 + src/comments/getCommentContent.ts | 60 +++++++++ src/comments/getLoginNames.test.ts | 52 ++++++++ src/comments/getLoginNames.ts | 24 ++++ src/comments/isAlreadyCommented.ts | 8 ++ src/comments/postComment.ts | 47 +++++++ src/comments/types.ts | 29 ++++ src/main.ts | 74 ++-------- 10 files changed, 397 insertions(+), 117 deletions(-) create mode 100644 src/comments/constants.ts create mode 100644 src/comments/getCommentContent.ts create mode 100644 src/comments/getLoginNames.test.ts create mode 100644 src/comments/getLoginNames.ts create mode 100644 src/comments/isAlreadyCommented.ts create mode 100644 src/comments/postComment.ts create mode 100644 src/comments/types.ts diff --git a/dist/index.js b/dist/index.js index 55a2f69..a4b0137 100644 --- a/dist/index.js +++ b/dist/index.js @@ -28974,6 +28974,156 @@ function wrappy (fn, cb) { } +/***/ }), + +/***/ 1435: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.ACTION_IDENTIFY_TEXT = void 0; +exports.ACTION_IDENTIFY_TEXT = ''; + + +/***/ }), + +/***/ 4069: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getCommentContent = void 0; +const getLoginNames_1 = __nccwpck_require__(40); +const isAlreadyCommented_1 = __nccwpck_require__(8484); +async function getCommentContent(octokit, octokitContext, args) { + const { owner, repo, prNumber } = octokitContext; + const comments = (await octokit.rest.issues.listComments({ + owner, + repo, + issue_number: prNumber, + })).data; + if ((0, isAlreadyCommented_1.isAlreadyCommented)(comments)) { + return null; + } + const reviewComments = (await octokit.rest.pulls.listReviewComments({ + owner, + repo, + pull_number: prNumber, + })).data; + const numberOfComments = comments.length + reviewComments.length; + if (numberOfComments < args.threshold) { + return null; + } + const users1 = comments + .map((comment) => comment.user) + .filter((user) => user !== null); + const users2 = reviewComments + .map((comment) => comment.user) + .filter((user) => user !== null); + const logins = (0, getLoginNames_1.getLoginNames)(users1.concat(users2)); + return { + logins, + numberOfComments, + threshold: args.threshold, + }; +} +exports.getCommentContent = getCommentContent; + + +/***/ }), + +/***/ 40: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getLoginNames = void 0; +function uniqueStringArray(texts) { + if (texts.length === 0) + return []; + const sorted = texts.sort(); + const result = [sorted[0]]; + for (let i = 0; i < sorted.length - 1; i++) { + if (sorted[i + 1] !== sorted[i]) { + result.push(sorted[i + 1]); + } + } + return result; +} +function getLoginNames(users) { + const loginNameArray = users + .filter((user) => user.type === 'User') + .map((user) => user.login); + return uniqueStringArray(loginNameArray); +} +exports.getLoginNames = getLoginNames; + + +/***/ }), + +/***/ 8484: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.isAlreadyCommented = void 0; +const constants_1 = __nccwpck_require__(1435); +function isAlreadyCommented(comments) { + return comments.some((comment) => comment.body?.startsWith(constants_1.ACTION_IDENTIFY_TEXT)); +} +exports.isAlreadyCommented = isAlreadyCommented; + + +/***/ }), + +/***/ 7942: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.postComment = void 0; +const constants_1 = __nccwpck_require__(1435); +function MainText(content) { + return ` +Hey ${content.logins.map((login) => '@' + login).join(', ')}! + +It seems the discussion is dragging on. Perhaps instead of text communication, you could try having a conversation via face-to-face or video call, or even try mob programming? +`; +} +function debugText(content) { + return ` +
+number of comments +the number of the comments is ${content.numberOfComments} +threshold: ${content.threshold} +
+`; +} +function getText(content) { + return `${constants_1.ACTION_IDENTIFY_TEXT} + +${MainText(content)} + +${debugText(content)} +`; +} +async function postComment(octokit, octokitContext, content) { + const { owner, repo, prNumber } = octokitContext; + await octokit.rest.issues.createComment({ + owner, + repo, + issue_number: prNumber, + body: getText(content), + }); +} +exports.postComment = postComment; + + /***/ }), /***/ 9356: @@ -29009,18 +29159,8 @@ exports.run = void 0; const core = __importStar(__nccwpck_require__(9093)); const github_1 = __nccwpck_require__(5942); const option_1 = __nccwpck_require__(6335); -const uniqueStringArray = (texts) => { - if (texts.length === 0) - return []; - const sorted = texts.sort(); - const result = [sorted[0]]; - for (let i = 0; i < sorted.length - 1; i++) { - if (sorted[i + 1] !== sorted[i]) { - result.push(sorted[i + 1]); - } - } - return result; -}; +const getCommentContent_1 = __nccwpck_require__(4069); +const postComment_1 = __nccwpck_require__(7942); /** * The main function for the action. * @returns {Promise} Resolves when the action is complete. @@ -29031,42 +29171,18 @@ async function run() { const octokit = (0, github_1.getOctokit)(token); const owner = github_1.context.repo.owner; const repo = github_1.context.repo.repo; + const octokitContext = { + owner: github_1.context.repo.owner, + repo: github_1.context.repo.repo, + prNumber, + }; core.debug(`owner: ${owner}, repo: ${repo}, PR #${prNumber}`); - const comments = (await octokit.rest.issues.listComments({ - owner, - repo, - issue_number: prNumber, - })).data.filter((c) => c.user?.type !== 'Bot'); - const reviewComments = (await octokit.rest.pulls.listReviewComments({ - owner, - repo, - pull_number: prNumber, - })).data.filter((c) => c.user.type !== 'Bot'); - const hasMessageSent = comments.some((comment) => comment.body?.includes('It seems the discussion is dragging on.')); - const commentCount = comments.length + reviewComments.length; - if (commentCount < threshold) { - return; - } - if (hasMessageSent) { - core.debug('a message has been sent'); - return; - } - const userLogins = uniqueStringArray(comments - .map((comment) => comment.user?.login) - .concat(reviewComments.map((comment) => comment.user.login)) - .filter((comment) => !!comment)).map((login) => `@${login}`); - await octokit.rest.issues.createComment({ - owner, - repo, - issue_number: prNumber, - body: `Hey ${userLogins.join(', ')}! - -It seems the discussion is dragging on. Perhaps instead of text communication, you could try having a conversation via face-to-face or video call, or even try mob programming? - -the number of the comments is ${comments.length} and the review comments is ${reviewComments.length} -threshold: ${threshold}, commentCount: ${commentCount}`, + const commentContent = await (0, getCommentContent_1.getCommentContent)(octokit, octokitContext, { + threshold, }); - core.debug(`Commented on PR #${prNumber}`); + if (commentContent) { + await (0, postComment_1.postComment)(octokit, octokitContext, commentContent); + } } catch (error) { // Fail the workflow run if an error occurs diff --git a/dist/licenses.txt b/dist/licenses.txt index dbcbc7a..7cc89b6 100644 --- a/dist/licenses.txt +++ b/dist/licenses.txt @@ -241,16 +241,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -@vercel/ncc -MIT -Copyright 2018 ZEIT, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - before-after-hook Apache-2.0 Apache License diff --git a/src/comments/constants.ts b/src/comments/constants.ts new file mode 100644 index 0000000..f60a19b --- /dev/null +++ b/src/comments/constants.ts @@ -0,0 +1,2 @@ +export const ACTION_IDENTIFY_TEXT = + ''; diff --git a/src/comments/getCommentContent.ts b/src/comments/getCommentContent.ts new file mode 100644 index 0000000..0308380 --- /dev/null +++ b/src/comments/getCommentContent.ts @@ -0,0 +1,60 @@ +import { + type CommentContent, + type Octokit, + type OctokitContext, + type User, +} from './types'; +import { getLoginNames } from './getLoginNames'; +import { isAlreadyCommented } from './isAlreadyCommented'; + +type Args = { + threshold: number; +}; + +export async function getCommentContent( + octokit: Octokit, + octokitContext: OctokitContext, + args: Args, +): Promise { + const { owner, repo, prNumber } = octokitContext; + + const comments = ( + await octokit.rest.issues.listComments({ + owner, + repo, + issue_number: prNumber, + }) + ).data; + + if (isAlreadyCommented(comments)) { + return null; + } + + const reviewComments = ( + await octokit.rest.pulls.listReviewComments({ + owner, + repo, + pull_number: prNumber, + }) + ).data; + + const numberOfComments = comments.length + reviewComments.length; + if (numberOfComments < args.threshold) { + return null; + } + + const users1: User[] = comments + .map((comment) => comment.user) + .filter((user): user is Exclude => user !== null); + const users2: User[] = reviewComments + .map((comment) => comment.user) + .filter((user): user is Exclude => user !== null); + + const logins = getLoginNames(users1.concat(users2)); + + return { + logins, + numberOfComments, + threshold: args.threshold, + }; +} diff --git a/src/comments/getLoginNames.test.ts b/src/comments/getLoginNames.test.ts new file mode 100644 index 0000000..7c97c5d --- /dev/null +++ b/src/comments/getLoginNames.test.ts @@ -0,0 +1,52 @@ +import { getLoginNames } from './getLoginNames'; + +test('if the input is an empty array, the output is also empty', () => { + const result = getLoginNames([]); + expect(result).toStrictEqual([]); +}); + +describe('if the input is an array without duplicates, the output is a array with the same elements in alphabetical order', () => { + test('in case: ["alice", "bob", "chris"]', () => { + const result = getLoginNames([ + { login: 'alice', type: 'User' }, + { login: 'bob', type: 'User' }, + { login: 'chris', type: 'User' }, + ]); + expect(result).toStrictEqual(['alice', 'bob', 'chris']); + }); + + test('in case: ["freddy", "eric", "daniel"]', () => { + const result = getLoginNames([ + { login: 'freddy', type: 'User' }, + { login: 'eric', type: 'User' }, + { login: 'daniel', type: 'User' }, + ]); + expect(result).toStrictEqual(['daniel', 'eric', 'freddy']); + }); +}); + +describe('if the input is an array containing duplicates, the output is an array where each name appears only once', () => { + test('in case: ["alice", "bob", "chris", "alice", "chris", "alice"]', () => { + const result = getLoginNames([ + { login: 'alice', type: 'User' }, + { login: 'bob', type: 'User' }, + { login: 'chris', type: 'User' }, + { login: 'alice', type: 'User' }, + { login: 'chris', type: 'User' }, + { login: 'alice', type: 'User' }, + ]); + expect(result).toStrictEqual(['alice', 'bob', 'chris']); + }); +}); + +test('if the input has elements whose "type" property is "Bot", the output is an array without those elements', () => { + const result = getLoginNames([ + { login: 'bot1', type: 'Bot' }, + { login: 'bob', type: 'User' }, + { login: 'chris', type: 'User' }, + { login: 'bot2', type: 'Bot' }, + { login: 'bot3', type: 'Bot' }, + { login: 'bob', type: 'User' }, + ]); + expect(result).toStrictEqual(['bob', 'chris']); +}); diff --git a/src/comments/getLoginNames.ts b/src/comments/getLoginNames.ts new file mode 100644 index 0000000..bae5e18 --- /dev/null +++ b/src/comments/getLoginNames.ts @@ -0,0 +1,24 @@ +import { type User } from './types'; + +function uniqueStringArray(texts: string[]): string[] { + if (texts.length === 0) return []; + + const sorted = texts.sort(); + const result = [sorted[0]]; + + for (let i = 0; i < sorted.length - 1; i++) { + if (sorted[i + 1] !== sorted[i]) { + result.push(sorted[i + 1]); + } + } + + return result; +} + +export function getLoginNames(users: User[]): string[] { + const loginNameArray = users + .filter((user) => user.type === 'User') + .map((user) => user.login); + + return uniqueStringArray(loginNameArray); +} diff --git a/src/comments/isAlreadyCommented.ts b/src/comments/isAlreadyCommented.ts new file mode 100644 index 0000000..f4550a4 --- /dev/null +++ b/src/comments/isAlreadyCommented.ts @@ -0,0 +1,8 @@ +import { type Comment } from './types'; +import { ACTION_IDENTIFY_TEXT } from './constants'; + +export function isAlreadyCommented(comments: Comment[]): boolean { + return comments.some((comment) => + comment.body?.startsWith(ACTION_IDENTIFY_TEXT), + ); +} diff --git a/src/comments/postComment.ts b/src/comments/postComment.ts new file mode 100644 index 0000000..02329fd --- /dev/null +++ b/src/comments/postComment.ts @@ -0,0 +1,47 @@ +import { ACTION_IDENTIFY_TEXT } from './constants'; +import { + type CommentContent, + type Octokit, + type OctokitContext, +} from './types'; + +function MainText(content: CommentContent) { + return ` +Hey ${content.logins.map((login) => '@' + login).join(', ')}! + +It seems the discussion is dragging on. Perhaps instead of text communication, you could try having a conversation via face-to-face or video call, or even try mob programming? +`; +} +function debugText(content: CommentContent) { + return ` +
+number of comments +the number of the comments is ${content.numberOfComments} +threshold: ${content.threshold} +
+`; +} + +function getText(content: CommentContent) { + return `${ACTION_IDENTIFY_TEXT} + +${MainText(content)} + +${debugText(content)} +`; +} + +export async function postComment( + octokit: Octokit, + octokitContext: OctokitContext, + content: CommentContent, +) { + const { owner, repo, prNumber } = octokitContext; + + await octokit.rest.issues.createComment({ + owner, + repo, + issue_number: prNumber, + body: getText(content), + }); +} diff --git a/src/comments/types.ts b/src/comments/types.ts new file mode 100644 index 0000000..b56e009 --- /dev/null +++ b/src/comments/types.ts @@ -0,0 +1,29 @@ +import { getOctokit } from '@actions/github'; + +export type Octokit = ReturnType; + +export type OctokitContext = { + owner: string; + repo: string; + prNumber: number; +}; + +export type User = { + // login id of user: ex. tnyo43 + login: string; + + // type of user: "User" or "Bot" + type: string; +}; + +export type Comment = { + user: User | null; + body?: string; +}; + +export type CommentContent = { + logins: string[]; + + numberOfComments: number; + threshold: number; +}; diff --git a/src/main.ts b/src/main.ts index 7a86189..39233a7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,22 +1,9 @@ import * as core from '@actions/core'; import { getOctokit, context } from '@actions/github'; import { getOption } from './option'; - -const uniqueStringArray = (texts: string[]): string[] => { - if (texts.length === 0) return []; - - const sorted = texts.sort(); - const result = [sorted[0]]; - - for (let i = 0; i < sorted.length - 1; i++) { - if (sorted[i + 1] !== sorted[i]) { - result.push(sorted[i + 1]); - } - } - - return result; -}; - +import { getCommentContent } from './comments/getCommentContent'; +import { OctokitContext } from './comments/types'; +import { postComment } from './comments/postComment'; /** * The main function for the action. * @returns {Promise} Resolves when the action is complete. @@ -28,56 +15,21 @@ export async function run(): Promise { const octokit = getOctokit(token); const owner = context.repo.owner; const repo = context.repo.repo; + const octokitContext: OctokitContext = { + owner: context.repo.owner, + repo: context.repo.repo, + prNumber, + }; core.debug(`owner: ${owner}, repo: ${repo}, PR #${prNumber}`); - const comments = ( - await octokit.rest.issues.listComments({ - owner, - repo, - issue_number: prNumber, - }) - ).data.filter((c) => c.user?.type !== 'Bot'); - - const reviewComments = ( - await octokit.rest.pulls.listReviewComments({ - owner, - repo, - pull_number: prNumber, - }) - ).data.filter((c) => c.user.type !== 'Bot'); + const commentContent = await getCommentContent(octokit, octokitContext, { + threshold, + }); - const hasMessageSent = comments.some((comment) => - comment.body?.includes('It seems the discussion is dragging on.'), - ); - const commentCount = comments.length + reviewComments.length; - if (commentCount < threshold) { - return; - } - if (hasMessageSent) { - core.debug('a message has been sent'); - return; + if (commentContent) { + await postComment(octokit, octokitContext, commentContent); } - - const userLogins = uniqueStringArray( - comments - .map((comment) => comment.user?.login) - .concat(reviewComments.map((comment) => comment.user.login)) - .filter((comment): comment is string => !!comment), - ).map((login) => `@${login}`); - - await octokit.rest.issues.createComment({ - owner, - repo, - issue_number: prNumber, - body: `Hey ${userLogins.join(', ')}! - -It seems the discussion is dragging on. Perhaps instead of text communication, you could try having a conversation via face-to-face or video call, or even try mob programming? - -the number of the comments is ${comments.length} and the review comments is ${reviewComments.length} -threshold: ${threshold}, commentCount: ${commentCount}`, - }); - core.debug(`Commented on PR #${prNumber}`); } catch (error) { // Fail the workflow run if an error occurs if (error instanceof Error) core.setFailed(error.message);