From 0a17e8da97db8cddad8fe94529e7841a79db1d0a Mon Sep 17 00:00:00 2001 From: shikajiro Date: Wed, 11 Oct 2023 16:51:49 +0900 Subject: [PATCH] =?UTF-8?q?=E4=BD=9C=E3=82=8A=E6=9B=BF=E3=81=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 3 +- LICENSE | 1 + __tests__/main.test.ts | 359 ------------------ __tests__/modules/github.test.ts | 20 +- __tests__/modules/mappingConfig.test.ts | 6 +- __tests__/modules/slack.test.ts | 65 ---- package-lock.json | 22 +- package.json | 3 +- src/domain/chatwork.ts | 64 ++++ src/{modules => domain}/github.ts | 43 +-- src/main.ts | 363 ++----------------- src/model.ts | 56 +++ src/modules/chatwork.ts | 144 -------- src/modules/slack.ts | 124 ------- src/repository/chatwork.ts | 105 ++++++ src/repository/github.ts | 27 ++ src/{modules => repository}/mappingConfig.ts | 18 +- src/usecase.ts | 280 ++++++++++++++ 18 files changed, 615 insertions(+), 1088 deletions(-) delete mode 100644 __tests__/main.test.ts delete mode 100644 __tests__/modules/slack.test.ts create mode 100644 src/domain/chatwork.ts rename src/{modules => domain}/github.ts (75%) create mode 100644 src/model.ts delete mode 100644 src/modules/chatwork.ts delete mode 100644 src/modules/slack.ts create mode 100644 src/repository/chatwork.ts create mode 100644 src/repository/github.ts rename src/{modules => repository}/mappingConfig.ts (88%) create mode 100644 src/usecase.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 33685c0b..9de5061d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,8 +29,7 @@ jobs: restore-keys: | ${{ runner.os }}-npm- - run: npm install - # しばらく停止 -# - run: npm run test -- --coverage + - run: npm run test -- --coverage lint: runs-on: ubuntu-latest timeout-minutes: 10 diff --git a/LICENSE b/LICENSE index 455a0c6e..d2cc4f20 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2020 abeyuya +Copyright (c) 2023 shikajiro Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts deleted file mode 100644 index 9816bb23..00000000 --- a/__tests__/main.test.ts +++ /dev/null @@ -1,359 +0,0 @@ -import { cloneDeep } from "lodash"; - -import { - convertToSlackUsername, - execPrReviewRequestedMention, - execNormalMention, - execApproveMention, - AllInputs, - arrayDiff, -} from "../src/main"; - -import { prApprovePayload } from "./fixture/real-payload-20211024-pr-approve"; - -describe("src/main", () => { - describe("arrayDiff", () => { - it("should return empty array when the same array is given", () => { - const a = [1, 2, 3]; - const b = [1, 2, 3]; - expect(arrayDiff(a, b)).toEqual([]); - }); - - it("should return empty array when b is big", () => { - const a = [1, 2, 3]; - const b = [1, 2, 3, 4]; - expect(arrayDiff(a, b)).toEqual([]); - }); - - it("should return diff array when a is big", () => { - const a = [1, 2, 3, 4]; - const b = [1, 2, 3]; - expect(arrayDiff(a, b)).toEqual([4]); - }); - }); - - describe("convertToSlackUsername", () => { - const mapping = { - github_user_1: "slack_user_1", - github_user_2: "slack_user_2", - }; - - it("should return hits slack member ids", async () => { - const result = convertToSlackUsername( - ["github_user_1", "github_user_2"], - mapping - ); - - expect(result).toEqual(["slack_user_1", "slack_user_2"]); - }); - - it("should return empty when no listed github_user", async () => { - const result = convertToSlackUsername( - ["github_user_not_listed"], - mapping - ); - - expect(result).toEqual([]); - }); - }); - - describe("execPrReviewRequestedMention", () => { - const dummyInputs: AllInputs = { - repoToken: "", - configurationPath: "", - slackWebhookUrl: "dummy_url", - iconUrl: "", - botName: "", - }; - - const dummyMapping = { - github_user_1: "slack_user_1", - github_team_1: "slack_user_2", - }; - - it("should call postToSlack if requested_user is listed in mapping", async () => { - const slackMock = { - postToSlack: jest.fn(), - }; - - const dummyPayload = { - requested_reviewer: { - login: "github_user_1", - }, - pull_request: { - title: "pr_title", - html_url: "pr_url", - }, - sender: { - login: "sender_github_username", - }, - }; - - await execPrReviewRequestedMention( - dummyPayload as any, - dummyInputs, - dummyMapping, - slackMock - ); - - expect(slackMock.postToSlack).toHaveBeenCalledTimes(1); - - const call = slackMock.postToSlack.mock.calls[0]; - expect(call[0]).toEqual("dummy_url"); - expect(call[1].includes("<@slack_user_1>")).toEqual(true); - expect(call[1].includes("")).toEqual(true); - expect(call[1].includes("by sender_github_username")).toEqual(true); - }); - - it("should not call postToSlack if requested_user is not listed in mapping", async () => { - const slackMock = { - postToSlack: jest.fn(), - }; - - const dummyPayload = { - requested_reviewer: { - login: "github_user_not_linsted", - }, - pull_request: { - title: "pr_title", - html_url: "pr_url", - }, - sender: { - login: "sender_github_username", - }, - }; - - await execPrReviewRequestedMention( - dummyPayload as any, - dummyInputs, - dummyMapping, - slackMock - ); - - expect(slackMock.postToSlack).not.toHaveBeenCalled(); - }); - - it("should call postToSlack if requested_user is team account", async () => { - const slackMock = { - postToSlack: jest.fn(), - }; - - const dummyPayload = { - pull_request: { - title: "pr_title", - html_url: "pr_url", - }, - requested_team: { - name: "github_team_1", - }, - sender: { - login: "sender_github_username", - }, - }; - - await execPrReviewRequestedMention( - dummyPayload as any, - dummyInputs, - dummyMapping, - slackMock - ); - - expect(slackMock.postToSlack).toHaveBeenCalledTimes(1); - }); - }); - - describe("execNormalMention", () => { - const dummyInputs: AllInputs = { - repoToken: "", - configurationPath: "./path/to/yaml", - slackWebhookUrl: "dummy_url", - iconUrl: "", - botName: "", - }; - - const dummyMapping = { - github_user_1: "slack_user_1", - }; - - it("should call postToSlack if requested_user is listed in mapping", async () => { - const slackMock = { - postToSlack: jest.fn(), - }; - - const dummyPayload = { - action: "submitted", - review: { - body: "@github_user_1 LGTM!", - html_url: "review_comment_url", - }, - pull_request: { - title: "pr_title", - }, - sender: { - login: "sender_github_username", - }, - }; - - await execNormalMention( - dummyPayload as any, - dummyInputs, - dummyMapping, - slackMock, - [] - ); - - expect(slackMock.postToSlack).toHaveBeenCalledTimes(1); - - const call = slackMock.postToSlack.mock.calls[0]; - expect(call[0]).toEqual("dummy_url"); - expect(call[1].includes("<@slack_user_1>")).toEqual(true); - expect(call[1].includes("")).toEqual(true); - expect(call[1].includes("> @github_user_1 LGTM!")).toEqual(true); - expect(call[1].includes("by sender_github_username")).toEqual(true); - }); - - it("should not call postToSlack if requested_user is not listed in mapping", async () => { - const slackMock = { - postToSlack: jest.fn(), - }; - - const dummyPayload = { - action: "submitted", - review: { - body: "@github_user_1 LGTM!", - html_url: "review_comment_url", - }, - pull_request: { - title: "pr_title", - }, - sender: { - login: "sender_github_username", - }, - }; - - await execNormalMention( - dummyPayload as any, - dummyInputs, - { - some_github_user: "some_slack_user_id", - }, - slackMock, - [] - ); - - expect(slackMock.postToSlack).not.toHaveBeenCalled(); - }); - - describe("with execApproveMention", () => { - describe("no mention in body", () => { - it("should not call slack post", async () => { - const slackMock = { - postToSlack: jest.fn(), - }; - - await execNormalMention( - prApprovePayload as any, - dummyInputs, - { - "abeyuya-bot": "pr_owner_slack_user_id", - }, - slackMock, - [] - ); - - expect(slackMock.postToSlack).not.toHaveBeenCalled(); - }); - }); - - describe("another user mention in body", () => { - it("should call slack post", async () => { - const slackMock = { - postToSlack: jest.fn(), - }; - - const overwritePayload = cloneDeep(prApprovePayload); - overwritePayload.review.body = - "this is approve comment. @github_user hello"; - - await execNormalMention( - overwritePayload as any, - dummyInputs, - { - "abeyuya-bot": "pr_owner_slack_user_id", - github_user: "slack_user_id_1", - }, - slackMock, - [] - ); - - expect(slackMock.postToSlack).toHaveBeenCalledTimes(1); - }); - }); - - describe("pr-owner-user mention in body", () => { - it("should not call slack post. (because pr-owner-user already mention by execApproveMention)", async () => { - const slackMock = { - postToSlack: jest.fn(), - }; - - const overwritePayload = cloneDeep(prApprovePayload); - overwritePayload.review.body = - "this is approve comment. @abeyuya-bot hello"; - - await execNormalMention( - overwritePayload as any, - dummyInputs, - { - "abeyuya-bot": "pr_owner_slack_user_id", - }, - slackMock, - ["pr_owner_slack_user_id"] - ); - - expect(slackMock.postToSlack).not.toHaveBeenCalled(); - }); - }); - }); - }); - - describe("execApproveMention", () => { - const dummyInputs: AllInputs = { - repoToken: "", - configurationPath: "", - slackWebhookUrl: "dummy_url", - iconUrl: "", - botName: "", - }; - - const dummyMapping = { - "abeyuya-bot": "pr_owner_slack_user", - }; - - describe("real payload test", () => { - it("should send slack mention", async () => { - const slackMock = { - postToSlack: jest.fn(), - }; - - const result = await execApproveMention( - prApprovePayload as any, - dummyInputs, - dummyMapping, - slackMock - ); - - expect(slackMock.postToSlack).toHaveBeenCalledTimes(1); - expect(result).toEqual("pr_owner_slack_user"); - - const call = slackMock.postToSlack.mock.calls[0]; - expect(call[0]).toEqual("dummy_url"); - expect(call[1]).toMatch("<@pr_owner_slack_user>"); - expect(call[1]).toMatch( - "" - ); - expect(call[1]).toMatch("by abeyuya"); - expect(call[1]).toMatch("> approve comment"); - }); - }); - }); -}); diff --git a/__tests__/modules/github.test.ts b/__tests__/modules/github.test.ts index 84dc094b..21a447d3 100644 --- a/__tests__/modules/github.test.ts +++ b/__tests__/modules/github.test.ts @@ -1,10 +1,9 @@ -import { - pickupUsername, - pickupInfoFromGithubPayload, -} from "../../src/modules/github"; - import { realPayload } from "../fixture/real-payload-20211017"; import { prApprovePayload } from "../fixture/real-payload-20211024-pr-approve"; +import { + pickupInfoFromGithubPayload, + pickupUsername, +} from "../../src/domain/github"; describe("modules/github", () => { describe("pickupUsername", () => { @@ -183,6 +182,9 @@ describe("modules/github", () => { title: "pr title", html_url: "pr url", }, + comment: { + body: "comment body", + }, sender: { login: "sender_github_username", }, @@ -190,13 +192,12 @@ describe("modules/github", () => { }; it("should return when pr opend", () => { - const dummyPayload = buildPrPayload("opened"); + const dummyPayload = buildPrPayload("created"); const result = pickupInfoFromGithubPayload(dummyPayload as any); expect(result).toEqual({ - body: "pr body", + body: "comment body", title: "pr title", - url: "pr url", senderName: "sender_github_username", }); }); @@ -206,9 +207,8 @@ describe("modules/github", () => { const result = pickupInfoFromGithubPayload(dummyPayload as any); expect(result).toEqual({ - body: "pr body", + body: "comment body", title: "pr title", - url: "pr url", senderName: "sender_github_username", }); }); diff --git a/__tests__/modules/mappingConfig.test.ts b/__tests__/modules/mappingConfig.test.ts index 8882b962..b95fe4e8 100644 --- a/__tests__/modules/mappingConfig.test.ts +++ b/__tests__/modules/mappingConfig.test.ts @@ -3,13 +3,13 @@ import axios from "axios"; import { isUrl, MappingConfigRepositoryImpl, -} from "../../src/modules/mappingConfig"; +} from "../../src/repository/mappingConfig"; describe("mappingConfig", () => { describe("isUrl", () => { it("true https://github.com/abeyuya/actions-mention-to-slack", () => { const result = isUrl( - "https://github.com/abeyuya/actions-mention-to-slack" + "https://github.com/abeyuya/actions-mention-to-slack", ); expect(result).toEqual(true); }); @@ -28,7 +28,7 @@ describe("mappingConfig", () => { .mockResolvedValueOnce({ data: 'github_user_id: "XXXXXXX"' }); const result = await MappingConfigRepositoryImpl.loadFromUrl( - "https://example.com" + "https://example.com", ); expect(spy).toHaveBeenCalledTimes(1); diff --git a/__tests__/modules/slack.test.ts b/__tests__/modules/slack.test.ts deleted file mode 100644 index 92e087f9..00000000 --- a/__tests__/modules/slack.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - buildSlackPostMessage, - buildSlackErrorMessage, -} from "../../src/modules/slack"; - -describe("modules/slack", () => { - describe("buildSlackPostMessage", () => { - it("should include all info", () => { - const result = buildSlackPostMessage( - ["slackUser1"], - "title", - "link", - "message", - "sender_github_username" - ); - - expect(result).toEqual( - `<@slackUser1> has been mentioned at by sender_github_username -> message` - ); - }); - - it("should be correct format with blockquotes", () => { - const result = buildSlackPostMessage( - ["slackUser1"], - "title", - "link", - "> message\nhello", - "sender_github_username" - ); - - expect(result).toEqual( - `<@slackUser1> has been mentioned at by sender_github_username -> -> > message -> hello` - ); - }); - - it("should be correct format with blockquotes2", () => { - const result = buildSlackPostMessage( - ["slackUser1"], - "title", - "link", - "message\n> hello", - "sender_github_username" - ); - - expect(result).toEqual( - `<@slackUser1> has been mentioned at by sender_github_username -> message -> > hello` - ); - }); - }); - - describe("buildSlackErrorMessage", () => { - it("should include all info", () => { - const e = new Error("dummy error"); - const result = buildSlackErrorMessage(e); - - expect(result.includes("dummy error")).toEqual(true); - }); - }); -}); diff --git a/package-lock.json b/package-lock.json index dec6e023..ea4b9934 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,8 @@ "@actions/github": "^5.0.0", "axios": "^0.27.0", "js-yaml": "^4.0.0", - "lodash": "^4.17.21" + "lodash": "^4.17.21", + "prettier": "^3.0.3" }, "devDependencies": { "@types/jest": "29.5.0", @@ -4395,6 +4396,20 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "29.5.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", @@ -8545,6 +8560,11 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "prettier": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==" + }, "pretty-format": { "version": "29.5.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", diff --git a/package.json b/package.json index 6977c977..5a4b3c61 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "@actions/github": "^5.0.0", "axios": "^0.27.0", "js-yaml": "^4.0.0", - "lodash": "^4.17.21" + "lodash": "^4.17.21", + "prettier": "^3.0.3" }, "devDependencies": { "@types/jest": "29.5.0", diff --git a/src/domain/chatwork.ts b/src/domain/chatwork.ts new file mode 100644 index 00000000..951027c7 --- /dev/null +++ b/src/domain/chatwork.ts @@ -0,0 +1,64 @@ +export const buildChatworkPostMentionMessage = ( + chatworkIdsForMention: string[], + issueTitle: string, + commentLink: string, + githubBody: string, + senderName: string, +): string => { + const mentionBlock = chatworkIdsForMention + .map((id) => `[To:${id}]`) + .join(" "); + return `[info][title]${senderName}がメンションしました[/title]${mentionBlock} ${issueTitle}\n${commentLink}\n[hr]\n${githubBody}\n[/info]`; +}; + +export const buildChatworkPostApproveMessage = ( + chatworkIdsForMention: string[], + issueTitle: string, + commentLink: string, + githubBody: string, + senderName: string, +): string => { + const mentionBlock = chatworkIdsForMention + .map((id) => `[To:${id}]`) + .join(" "); + return `[info][title](cracker)${senderName}が承認しました[/title]${mentionBlock} ${issueTitle}\n${commentLink}\n[hr]\n${githubBody}\n[/info]`; +}; + +export const buildChatworkPostMessage = ( + issueTitle: string, + commentLink: string, + githubBody: string, + senderName: string, +): string => { + return `[info][title]${senderName}がコメントしました[/title] ${issueTitle}\n${commentLink}\n[hr]\n${githubBody}\n[/info]`; +}; + +export const buildChatworkErrorMessage = ( + error: Error, + currentJobUrl?: string, +): string => { + const jobTitle = "mention-to-chatwork action"; + const jobLinkMessage = currentJobUrl + ? `${currentJobUrl} ${jobTitle}` + : jobTitle; + + const issueBody = error.stack + ? encodeURI(["```", error.stack, "```"].join("\n")) + : ""; + + const link = encodeURI( + `${openIssueLink}?title=${error.message}&body=${issueBody}`, + ); + + return [ + `❗ An internal error occurred in ${jobLinkMessage}`, + "(but action didn't fail as this action is not critical).", + `To solve the problem, please ${link} open an issue`, + "", + "```", + error.stack || error.message, + "```", + ].join("\n"); +}; +const openIssueLink = + "https://github.com/shikajiro/actions-mention-to-chatwork/issues/new"; diff --git a/src/modules/github.ts b/src/domain/github.ts similarity index 75% rename from src/modules/github.ts rename to src/domain/github.ts index c291700b..21cb3257 100644 --- a/src/modules/github.ts +++ b/src/domain/github.ts @@ -1,11 +1,16 @@ +import { context } from "@actions/github"; +import { stringify } from "ts-jest"; import { WebhookPayload } from "@actions/github/lib/interfaces"; -import axios from "axios"; +import { convertToChatworkUsername, MappingFile } from "../model"; import * as core from "@actions/core"; -import {convertToChatworkUsername} from "../main"; -import {MappingFile} from "./mappingConfig"; const uniq = (arr: T[]): T[] => [...new Set(arr)]; +export const buildCurrentJobUrl = (runId: string) => { + const { owner, repo } = context.repo; + return `https://github.com/${owner}/${repo}/actions/runs/${runId}`; +}; + export const pickupUsername = (text: string): string[] => { const pattern = /\B@[a-z0-9_-]+/gi; const hits = text.match(pattern); @@ -26,9 +31,7 @@ const acceptActionTypes = { }; const buildError = (payload: unknown): Error => { - return new Error( - `unknown event hook: ${JSON.stringify(payload, undefined, 2)}` - ); + return new Error(`unknown event hook: ${stringify(payload)}`); }; export const needToSendApproveMention = (payload: WebhookPayload): boolean => { @@ -39,7 +42,10 @@ export const needToSendApproveMention = (payload: WebhookPayload): boolean => { return false; }; -export const needToMention = (payload: WebhookPayload, mapping: MappingFile,): boolean => { +export const needToMention = ( + payload: WebhookPayload, + mapping: MappingFile, +): boolean => { const info = pickupInfoFromGithubPayload(payload); if (info.body === null) { @@ -62,29 +68,8 @@ export const needToMention = (payload: WebhookPayload, mapping: MappingFile,): b return true; }; -type GithubGetReviewerResult = { - users: GithubGetReviewerNameResult[] -}; - -type GithubGetReviewerNameResult = { - login: string -}; - -export const latestReviewer = async (repoName: string, prNumber: number, repoToken:string): Promise => { - core.info(`repoName:${repoName} prNumber: ${prNumber}`); - const result = await axios.get( - `https://api.github.com/repos/${repoName}/pulls/${prNumber}/requested_reviewers`, - { - headers: { "authorization": `Bearer ${repoToken}` }, - } - ); - if(result.data.users.length == 0) return null; - - return result.data.users.map((user) => user.login); -}; - export const pickupInfoFromGithubPayload = ( - payload: WebhookPayload + payload: WebhookPayload, ): { body: string; title: string; diff --git a/src/main.ts b/src/main.ts index 018f54cc..ea8a6eeb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,359 +1,48 @@ +import { stringify } from "ts-jest"; + import * as core from "@actions/core"; import { context } from "@actions/github"; -import { WebhookPayload } from "@actions/github/lib/interfaces"; import { - pickupUsername, - pickupInfoFromGithubPayload, - needToSendApproveMention, latestReviewer, needToMention, -} from "./modules/github"; -import { - buildChatworkErrorMessage, buildChatworkPostApproveMessage, buildChatworkPostMentionMessage, - buildChatworkPostMessage, - ChatworkRepositoryImpl, -} from "./modules/chatwork"; -import { - MappingConfigRepositoryImpl, - isUrl, - MappingFile, Account, -} from "./modules/mappingConfig"; - -export type AllInputs = { - repoToken: string; - configurationPath: string; - apiToken: string; - runId?: string; - reviewRequest?: boolean; -}; - -export const arrayDiff = (arr1: T[], arr2: T[]) => - arr1.filter((i) => arr2.indexOf(i) === -1); - -export const convertToChatworkUsername = ( - githubUsernames: string[], - mapping: MappingFile -): Account[] => { - core.info(JSON.stringify({ githubUsernames }, null, 2)); - const slackIds = githubUsernames - .map((githubUsername) => mapping[githubUsername]); - - core.info(JSON.stringify({ slackIds }, null, 2)); - - return slackIds; -}; - -export const execPrReviewRequestedMention = async ( - payload: WebhookPayload, - allInputs: AllInputs, - mapping: MappingFile, - chatworkClient: Pick -): Promise => { - core.info("start execPrReviewRequestedMention()"); - - const name = payload.repository?.full_name; - if (name === undefined) { - throw new Error("Can not find repository name."); - } - - const number = payload.pull_request?.number; - if (number === undefined) { - throw new Error("Can not find pull request number."); - } - - const reviewers = await latestReviewer(name, number, allInputs.repoToken) - if (reviewers === null || reviewers.length == 0) { - throw new Error("Can not find review requested user."); - } - core.info(`reviewers ${ reviewers }`); - - core.info(`labels ${ payload.pull_request?.labels[0]?.name}`); - const labels = payload.pull_request?.labels - ?.map((label:any) => label.name) - ?.filter((name:any) => name === 'hurry' || name === '2days' || name === '2weeks') as string[]; - - const slackIds = convertToChatworkUsername(reviewers, mapping); - if (slackIds.length === 0) { - core.info("finish execPrReviewRequestedMention because slackIds.length === 0"); - return; - } - - for (const account of slackIds) { - const roomId = account.room_id; - if (roomId === undefined) { - throw new Error("Can not find room ID."); - } - - const requestUsername = payload.sender?.login; - const prUrl = payload.pull_request?.html_url; - const prTitle = payload.pull_request?.title; - - - const message = `[To:${account.account_id}] (bow) has been requested to review PR:${prTitle} ${prUrl} by ${requestUsername}.`; - const { apiToken } = allInputs; - - const exist = await chatworkClient.existChatworkTask(apiToken, roomId, account.account_id, message); - if (exist) { - core.info(`already exist ${message}`); - return; - } - - await chatworkClient.createChatworkTask(apiToken, account.room_id, account.account_id, message, labels); - } -}; - -export const execNormalComment = async ( - payload: WebhookPayload, - allInputs: AllInputs, - mapping: MappingFile, - chatworkClient: Pick -): Promise => { - core.info("start execNormalComment()"); - - const info = pickupInfoFromGithubPayload(payload); - - if (info.body === null) { - core.info("finish execNormalMention because info.body === null"); - return; - } - - const message = buildChatworkPostMessage( - info.title, - info.url, - info.body, - info.senderName - ); - - const account = mapping[info.senderName]; - - const result = await chatworkClient.postToChatwork(allInputs.apiToken, account.room_id, message); - - core.info( - ["postToSlack result", JSON.stringify({result}, null, 2)].join("\n") - ); - -}; - -export const execNormalMention = async ( - payload: WebhookPayload, - allInputs: AllInputs, - mapping: MappingFile, - chatworkClient: Pick -): Promise => { - core.info("start execNormalMention()"); - - const info = pickupInfoFromGithubPayload(payload); - - if (info.body === null) { - core.info("finish execNormalMention because info.body === null"); - return; - } - - const githubUsernames = pickupUsername(info.body); - if (githubUsernames.length === 0) { - core.info("finish execNormalMention because githubUsernames.length === 0"); - return; - } - - const slackIds = convertToChatworkUsername(githubUsernames, mapping); - - if (slackIds.length === 0) { - core.info("finish execNormalMention because slackIds.length === 0"); - return; - } - - for (const account of slackIds) { - const roomId = account.room_id; - if (roomId === undefined) { - continue; - } - - const message = buildChatworkPostMentionMessage( - [account.account_id], - info.title, - info.url, - info.body, - info.senderName - ); - - const {apiToken} = allInputs; - - const result = await chatworkClient.postToChatwork(apiToken, roomId, message); - - core.info( - ["postToSlack result", JSON.stringify({result}, null, 2)].join("\n") - ); - } -}; - -export const execApproveMention = async ( - payload: WebhookPayload, - allInputs: AllInputs, - mapping: MappingFile, - chatworkClient: Pick -): Promise => { - core.info("start execApproveMention()"); - - if (!needToSendApproveMention(payload)) { - throw new Error("failed to parse payload"); - } - - const prOwnerGithubUsername = payload.pull_request?.user?.login; - - if (!prOwnerGithubUsername) { - throw new Error("Can not find pr owner user."); - } - - const slackIds = convertToChatworkUsername([prOwnerGithubUsername], mapping); - - if (slackIds.length === 0) { - core.info("finish execApproveMention because slackIds.length === 0"); - return null; - } - - const account = slackIds[0]; - const roomId = account.room_id; - if (roomId === undefined) { - throw new Error("Can not find room ID."); - } - - const info = pickupInfoFromGithubPayload(payload); - const message = buildChatworkPostApproveMessage( - [account.account_id], - info.title, - info.url, - info.body, - payload.sender?.login - ) - const { apiToken} = allInputs; - - const postResult = await chatworkClient.postToChatwork( - apiToken, - account.room_id, - message - ); - - core.info( - ["postToSlack result", JSON.stringify({ postSlackResult: postResult }, null, 2)].join( - "\n" - ) - ); - - return account.account_id; -}; - -const buildCurrentJobUrl = (runId: string) => { - const { owner, repo } = context.repo; - return `https://github.com/${owner}/${repo}/actions/runs/${runId}`; -}; - -export const execPostError = async ( - error: Error, - allInputs: AllInputs -): Promise => { - const { runId } = allInputs; - const currentJobUrl = runId ? buildCurrentJobUrl(runId) : undefined; - const message = buildChatworkErrorMessage(error, currentJobUrl); - - core.warning(message); -}; - -const getAllInputs = (): AllInputs => { - const configurationPath = core.getInput("configuration-path", { - required: true, - }); - const repoToken = core.getInput("repo-token", { required: true }); - if (!repoToken) { - core.setFailed("Error! Need to set `repo-token`."); - } - const apiToken = core.getInput("api-token", { required: true }); - const runId = core.getInput("run-id", { required: false }); - const reviewRequest = core.getBooleanInput("review-request", { required: true }); - - return { - repoToken, - configurationPath, - apiToken, - runId, - reviewRequest, - }; -}; + execApproveMention, + execLoadMapping, + execNormalComment, + execNormalMention, + execPrReviewRequestedMention, + postError, +} from "./usecase"; +import { getAllInputs } from "./model"; +import { needToMention, needToSendApproveMention } from "./domain/github"; export const main = async (): Promise => { - core.info("start main()"); + core.info("start main"); const { payload } = context; - core.info(JSON.stringify({ payload }, null, 2)); + core.info(stringify(payload)); const allInputs = getAllInputs(); - core.info(JSON.stringify({ allInputs }, null, 2)); + core.info(stringify(allInputs)); const { repoToken, configurationPath, reviewRequest } = allInputs; try { - const mapping = await (async () => { - if (isUrl(configurationPath)) { - return MappingConfigRepositoryImpl.loadFromUrl(configurationPath); - } - - return MappingConfigRepositoryImpl.loadFromGithubPath( - repoToken, - context.repo.owner, - context.repo.repo, - configurationPath, - context.sha - ); - })(); - core.info(JSON.stringify({ mapping }, null, 2)); + const mapping = await execLoadMapping(configurationPath, repoToken); + core.info(stringify(mapping)); if (reviewRequest) { - await execPrReviewRequestedMention( - payload, - allInputs, - mapping, - ChatworkRepositoryImpl - ); + await execPrReviewRequestedMention(payload, allInputs, mapping); core.info("finish execPrReviewRequestedMention()"); - return; - } - - if (needToSendApproveMention(payload)) { - const sentSlackUserId = await execApproveMention( - payload, - allInputs, - mapping, - ChatworkRepositoryImpl - ); - - core.info( - [ - "execApproveMention()", - JSON.stringify({ sentSlackUserId }, null, 2), - ].join("\n") - ); - return; - } - - if (needToMention(payload, mapping)) { - await execNormalMention( - payload, - allInputs, - mapping, - ChatworkRepositoryImpl, - ); + } else if (needToSendApproveMention(payload)) { + await execApproveMention(payload, allInputs, mapping); + core.info("finish execApproveMention()"); + } else if (needToMention(payload, mapping)) { + await execNormalMention(payload, allInputs, mapping); core.info("finish execNormalMention()"); - return; + } else { + await execNormalComment(payload, allInputs, mapping); + core.info("finish execNormalComment()"); } - - await execNormalComment( - payload, - allInputs, - mapping, - ChatworkRepositoryImpl, - ); - core.info("finish execNormalComment()"); - } catch (error: any) { - await execPostError(error, allInputs); - core.warning(JSON.stringify({ payload }, null, 2)); + await postError(error, allInputs); } }; diff --git a/src/model.ts b/src/model.ts new file mode 100644 index 00000000..5a20ce5b --- /dev/null +++ b/src/model.ts @@ -0,0 +1,56 @@ +import * as core from "@actions/core"; +import { stringify } from "ts-jest"; + +export type AllInputs = { + repoToken: string; + configurationPath: string; + apiToken: string; + runId?: string; + reviewRequest?: boolean; +}; + +export const getAllInputs = (): AllInputs => { + const configurationPath = core.getInput("configuration-path", { + required: true, + }); + const repoToken = core.getInput("repo-token", { required: true }); + if (!repoToken) { + core.setFailed("Error! Need to set `repo-token`."); + } + const apiToken = core.getInput("api-token", { required: true }); + const runId = core.getInput("run-id", { required: false }); + const reviewRequest = core.getBooleanInput("review-request", { + required: true, + }); + + return { + repoToken, + configurationPath, + apiToken, + runId, + reviewRequest, + }; +}; + +export type Account = { + room_id: string; + account_id: string; +}; + +export type MappingFile = { + [githubUsername: string]: Account; +}; + +export const convertToChatworkUsername = ( + githubUsernames: string[], + mapping: MappingFile, +): Account[] => { + core.info(stringify(githubUsernames)); + + const slackIds = githubUsernames.map( + (githubUsername) => mapping[githubUsername], + ); + core.info(stringify(slackIds)); + + return slackIds; +}; diff --git a/src/modules/chatwork.ts b/src/modules/chatwork.ts deleted file mode 100644 index 82cd0059..00000000 --- a/src/modules/chatwork.ts +++ /dev/null @@ -1,144 +0,0 @@ -import * as core from "@actions/core"; -import axios from "axios"; - -export const buildChatworkPostMentionMessage = ( - chatworkIdsForMention: string[], - issueTitle: string, - commentLink: string, - githubBody: string, - senderName: string -): string => { - const mentionBlock = chatworkIdsForMention.map((id) => `[To:${id}]`).join(" "); - return `[info][title]${senderName}がメンションしました[/title]${mentionBlock} ${issueTitle}\n${commentLink}\n[hr]\n${githubBody}\n[/info]`; -}; - -export const buildChatworkPostApproveMessage = ( - chatworkIdsForMention: string[], - issueTitle: string, - commentLink: string, - githubBody: string, - senderName: string -): string => { - const mentionBlock = chatworkIdsForMention.map((id) => `[To:${id}]`).join(" "); - return `[info][title](cracker)${senderName}が承認しました[/title]${mentionBlock} ${issueTitle}\n${commentLink}\n[hr]\n${githubBody}\n[/info]`; -}; - -export const buildChatworkPostMessage = ( - issueTitle: string, - commentLink: string, - githubBody: string, - senderName: string -): string => { - return `[info][title]${senderName}がコメントしました[/title] ${issueTitle}\n${commentLink}\n[hr]\n${githubBody}\n[/info]`; -}; - -export const buildChatworkErrorMessage = ( - error: Error, - currentJobUrl?: string -): string => { - const jobTitle = "mention-to-chatwork action"; - const jobLinkMessage = currentJobUrl - ? `${currentJobUrl} ${jobTitle}` - : jobTitle; - - const issueBody = error.stack - ? encodeURI(["```", error.stack, "```"].join("\n")) - : ""; - - const link = encodeURI( - `${openIssueLink}?title=${error.message}&body=${issueBody}` - ); - - return [ - `❗ An internal error occurred in ${jobLinkMessage}`, - "(but action didn't fail as this action is not critical).", - `To solve the problem, please ${link} open an issue`, - "", - "```", - error.stack || error.message, - "```", - ].join("\n"); -}; -const openIssueLink = - "https://github.com/shikajiro/actions-mention-to-chatwork/issues/new"; -type ChatworkPostResult = Record; -type ChatworkGetTaskResult = [ - { - body: string - } -]; - -export const ChatworkRepositoryImpl = { - postToChatwork: async ( - apiToken: string, - roomId: string, - message: string - ): Promise => { - const chatworkUrl = `https://api.chatwork.com/v2/rooms/${roomId}/messages` - - const result = await axios.post( - chatworkUrl, - `body=${message}`, - { - headers: { "X-ChatWorkToken": apiToken }, - } - ); - - return result.data; - }, - - createChatworkTask: async ( - apiToken: string, - roomId: string, - accountId: string, - message: string, - labels: string[] - ): Promise => { - const isHurry = labels.find((label) => label === 'hurry'); - const is2days = labels.find((label) => label === '2days'); - let limit = 0; - const now = new Date(); - if(isHurry !== undefined) { - limit = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59).getTime(); - }else if(is2days !== undefined) { - limit = new Date(now.getFullYear(), now.getMonth(), now.getDate()+2, 23, 59, 59).getTime(); - }else{ - // is2weeks or default - limit = new Date(now.getFullYear(), now.getMonth(), now.getDate()+14, 23, 59, 59).getTime(); - } - const encodedParams = new URLSearchParams(); - encodedParams.set('body', message); - encodedParams.set('to_ids', accountId); - encodedParams.set('limit', `${limit / 1000}`); - encodedParams.set('limit_type', "date"); - core.info(`param ${encodedParams}`); - const result = await axios.post( - `https://api.chatwork.com/v2/rooms/${roomId}/tasks`, - encodedParams, - { - headers: { "X-ChatWorkToken": apiToken }, - } - ); - - return result.data; - }, - - existChatworkTask: async ( - apiToken: string, - roomId: string, - accountId: string, - message: string, - ): Promise => { - const result = await axios.get( - `https://api.chatwork.com/v2/rooms/${roomId}/tasks?account_id=${accountId}&status=open`, - { - headers: { "X-ChatWorkToken": apiToken }, - } - ); - core.info(`result data ${JSON.stringify(result.data, null, 2)}`); - if(!result.data) return false; - - const task = result.data.find((task) => task.body === message ); - return !!task; - }, -}; diff --git a/src/modules/slack.ts b/src/modules/slack.ts deleted file mode 100644 index 35fba723..00000000 --- a/src/modules/slack.ts +++ /dev/null @@ -1,124 +0,0 @@ -import axios from "axios"; - -export const convertGithubTextToBlockquotesText = (githubText: string) => { - const t = githubText - .split("\n") - .map((line, i) => { - // fix slack layout collapse problem when first line starts with blockquotes. - if (i === 0 && line.startsWith(">")) { - return `>\n> ${line}`; - } - - return `> ${line}`; - }) - .join("\n"); - - return t; -}; - -export const buildSlackPostMessage = ( - slackIdsForMention: string[], - issueTitle: string, - commentLink: string, - githubBody: string, - senderName: string -): string => { - const mentionBlock = slackIdsForMention.map((id) => `<@${id}>`).join(" "); - const body = convertGithubTextToBlockquotesText(githubBody); - - const message = [ - mentionBlock, - `${slackIdsForMention.length === 1 ? "has" : "have"}`, - `been mentioned at ${commentLink} ${issueTitle} by ${senderName}`, - ].join(" "); - - return `${message}\n\n${body}`; -}; - -const openIssueLink = - "https://github.com/abeyuya/actions-mention-to-slack/issues/new"; - -export const buildSlackErrorMessage = ( - error: Error, - currentJobUrl?: string -): string => { - const jobTitle = "mention-to-slack action"; - const jobLinkMessage = currentJobUrl - ? `<${currentJobUrl}|${jobTitle}>` - : jobTitle; - - const issueBody = error.stack - ? encodeURI(["```", error.stack, "```"].join("\n")) - : ""; - - const link = encodeURI( - `${openIssueLink}?title=${error.message}&body=${issueBody}` - ); - - return [ - `❗ An internal error occurred in ${jobLinkMessage}`, - "(but action didn't fail as this action is not critical).", - `To solve the problem, please <${link}|open an issue>`, - "", - "```", - error.stack || error.message, - "```", - ].join("\n"); -}; - -export type SlackOption = { - iconUrl?: string; - botName?: string; -}; - -type SlackPostParam = { - text: string; - link_names: 0 | 1; - username: string; - icon_url?: string; - icon_emoji?: string; -}; - -const defaultBotName = "Github Mention To Slack"; -const defaultIconEmoji = ":bell:"; - -type SlackPostResult = Record; - -export const SlackRepositoryImpl = { - postToSlack: async ( - webhookUrl: string, - message: string, - options?: SlackOption - ): Promise => { - const botName = (() => { - const n = options?.botName; - if (n && n !== "") { - return n; - } - return defaultBotName; - })(); - - const slackPostParam: SlackPostParam = { - text: message, - link_names: 0, - username: botName, - }; - - const u = options?.iconUrl; - if (u && u !== "") { - slackPostParam.icon_url = u; - } else { - slackPostParam.icon_emoji = defaultIconEmoji; - } - - const result = await axios.post( - webhookUrl, - JSON.stringify(slackPostParam), - { - headers: { "Content-Type": "application/json" }, - } - ); - - return result.data; - }, -}; diff --git a/src/repository/chatwork.ts b/src/repository/chatwork.ts new file mode 100644 index 00000000..ae3bf6ad --- /dev/null +++ b/src/repository/chatwork.ts @@ -0,0 +1,105 @@ +import * as core from "@actions/core"; +import axios from "axios"; + +type ChatworkPostResult = Record; +type ChatworkGetTaskResult = [ + { + body: string; + }, +]; + +export const ChatworkRepositoryImpl = { + postToChatwork: async ( + apiToken: string, + roomId: string, + message: string, + ): Promise => { + const chatworkUrl = `https://api.chatwork.com/v2/rooms/${roomId}/messages`; + + const result = await axios.post( + chatworkUrl, + `body=${message}`, + { + headers: { "X-ChatWorkToken": apiToken }, + }, + ); + + return result.data; + }, + + createChatworkTask: async ( + apiToken: string, + roomId: string, + accountId: string, + message: string, + labels: string[], + ): Promise => { + const isHurry = labels.find((label) => label === "hurry"); + const is2days = labels.find((label) => label === "2days"); + let limit = 0; + const now = new Date(); + if (isHurry !== undefined) { + limit = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate(), + 23, + 59, + 59, + ).getTime(); + } else if (is2days !== undefined) { + limit = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate() + 2, + 23, + 59, + 59, + ).getTime(); + } else { + // is2weeks or default + limit = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate() + 14, + 23, + 59, + 59, + ).getTime(); + } + const encodedParams = new URLSearchParams(); + encodedParams.set("body", message); + encodedParams.set("to_ids", accountId); + encodedParams.set("limit", `${limit / 1000}`); + encodedParams.set("limit_type", "date"); + core.info(`param ${encodedParams}`); + const result = await axios.post( + `https://api.chatwork.com/v2/rooms/${roomId}/tasks`, + encodedParams, + { + headers: { "X-ChatWorkToken": apiToken }, + }, + ); + + return result.data; + }, + + existChatworkTask: async ( + apiToken: string, + roomId: string, + accountId: string, + message: string, + ): Promise => { + const result = await axios.get( + `https://api.chatwork.com/v2/rooms/${roomId}/tasks?account_id=${accountId}&status=open`, + { + headers: { "X-ChatWorkToken": apiToken }, + }, + ); + core.info(`result data ${JSON.stringify(result.data, null, 2)}`); + if (!result.data) return false; + + const task = result.data.find((task) => task.body === message); + return !!task; + }, +}; diff --git a/src/repository/github.ts b/src/repository/github.ts new file mode 100644 index 00000000..eafee854 --- /dev/null +++ b/src/repository/github.ts @@ -0,0 +1,27 @@ +import axios from "axios"; +import * as core from "@actions/core"; + +type GithubGetReviewerResult = { + users: GithubGetReviewerNameResult[]; +}; + +type GithubGetReviewerNameResult = { + login: string; +}; + +export const latestReviewer = async ( + repoName: string, + prNumber: number, + repoToken: string, +): Promise => { + core.info(`repoName:${repoName} prNumber: ${prNumber}`); + const result = await axios.get( + `https://api.github.com/repos/${repoName}/pulls/${prNumber}/requested_reviewers`, + { + headers: { authorization: `Bearer ${repoToken}` }, + }, + ); + if (result.data.users.length == 0) return null; + + return result.data.users.map((user) => user.login); +}; diff --git a/src/modules/mappingConfig.ts b/src/repository/mappingConfig.ts similarity index 88% rename from src/modules/mappingConfig.ts rename to src/repository/mappingConfig.ts index bc8f6639..d7906b72 100644 --- a/src/modules/mappingConfig.ts +++ b/src/repository/mappingConfig.ts @@ -1,19 +1,11 @@ import axios from "axios"; import { load } from "js-yaml"; import { getOctokit } from "@actions/github"; +import { MappingFile } from "../model"; const pattern = /https?:\/\/[-_.!~*'()a-zA-Z0-9;/?:@&=+$,%#]+/g; export const isUrl = (text: string) => pattern.test(text); -export type MappingFile = { - [githubUsername: string]: Account -}; - -export type Account = { - room_id: string, - account_id: string -}; - export const MappingConfigRepositoryImpl = { downloadFromUrl: async (url: string) => { const response = await axios.get(url); @@ -25,7 +17,7 @@ export const MappingConfigRepositoryImpl = { if (configObject === undefined) { throw new Error( - ["failed to load yaml", JSON.stringify({ data }, null, 2)].join("\n") + ["failed to load yaml", JSON.stringify({ data }, null, 2)].join("\n"), ); } @@ -42,7 +34,7 @@ export const MappingConfigRepositoryImpl = { owner: string, repo: string, configurationPath: string, - sha: string + sha: string, ) => { const githubClient = getOctokit(repoToken); const response = await githubClient.rest.repos.getContent({ @@ -55,8 +47,8 @@ export const MappingConfigRepositoryImpl = { if (!("content" in response.data)) { throw new Error( ["Unexpected response", JSON.stringify({ response }, null, 2)].join( - "\n" - ) + "\n", + ), ); } diff --git a/src/usecase.ts b/src/usecase.ts new file mode 100644 index 00000000..c480b99f --- /dev/null +++ b/src/usecase.ts @@ -0,0 +1,280 @@ +import { WebhookPayload } from "@actions/github/lib/interfaces"; +import * as core from "@actions/core"; +import { context } from "@actions/github"; +import { ChatworkRepositoryImpl } from "./repository/chatwork"; +import { latestReviewer } from "./repository/github"; +import { isUrl, MappingConfigRepositoryImpl } from "./repository/mappingConfig"; +import { AllInputs, convertToChatworkUsername, MappingFile } from "./model"; +import { + buildChatworkErrorMessage, + buildChatworkPostApproveMessage, + buildChatworkPostMentionMessage, + buildChatworkPostMessage, +} from "./domain/chatwork"; +import { + buildCurrentJobUrl, + needToSendApproveMention, + pickupInfoFromGithubPayload, + pickupUsername, +} from "./domain/github"; + +/** + * レビュー依頼があった際にタスクを作成する + */ +export const execPrReviewRequestedMention = async ( + payload: WebhookPayload, + allInputs: AllInputs, + mapping: MappingFile, +): Promise => { + core.info("start execPrReviewRequestedMention()"); + + const name = payload.repository?.full_name; + if (name === undefined) { + throw new Error("Can not find repository name."); + } + + const number = payload.pull_request?.number; + if (number === undefined) { + throw new Error("Can not find pull request number."); + } + + const reviewers = await latestReviewer(name, number, allInputs.repoToken); + if (reviewers === null || reviewers.length == 0) { + throw new Error("Can not find review requested user."); + } + core.info(`reviewers ${reviewers}`); + + core.info(`labels ${payload.pull_request?.labels[0]?.name}`); + const labels = payload.pull_request?.labels?.map( + (label: any) => label.name, + ) as string[]; + + const slackIds = convertToChatworkUsername(reviewers, mapping); + if (slackIds.length === 0) { + core.info( + "finish execPrReviewRequestedMention because slackIds.length === 0", + ); + return; + } + + for (const account of slackIds) { + const roomId = account.room_id; + if (roomId === undefined) { + throw new Error("Can not find room ID."); + } + + const requestUsername = payload.sender?.login; + const prUrl = payload.pull_request?.html_url; + const prTitle = payload.pull_request?.title; + + const message = `[To:${account.account_id}] (bow) has been requested to review PR:${prTitle} ${prUrl} by ${requestUsername}.`; + const { apiToken } = allInputs; + + const exist = await ChatworkRepositoryImpl.existChatworkTask( + apiToken, + roomId, + account.account_id, + message, + ); + + if (exist) { + core.info(`already exist ${message}`); + return; + } + + await ChatworkRepositoryImpl.createChatworkTask( + apiToken, + account.room_id, + account.account_id, + message, + labels, + ); + } +}; + +/** + * PRにコメントが合った際にチャットルームにメッセージを送る + */ +export const execNormalComment = async ( + payload: WebhookPayload, + allInputs: AllInputs, + mapping: MappingFile, +): Promise => { + core.info("start execNormalComment()"); + + const info = pickupInfoFromGithubPayload(payload); + + if (info.body === null) { + core.info("finish execNormalMention because info.body === null"); + return; + } + + const message = buildChatworkPostMessage( + info.title, + info.url, + info.body, + info.senderName, + ); + + const account = mapping[info.senderName]; + + const result = await ChatworkRepositoryImpl.postToChatwork( + allInputs.apiToken, + account.room_id, + message, + ); + + core.info( + ["postToSlack result", JSON.stringify({ result }, null, 2)].join("\n"), + ); +}; + +/** + * PRにメンション付きコメントが合った際にチャットルームにメンション付きメッセージを送る + */ +export const execNormalMention = async ( + payload: WebhookPayload, + allInputs: AllInputs, + mapping: MappingFile, +): Promise => { + core.info("start execNormalMention()"); + + const info = pickupInfoFromGithubPayload(payload); + + if (info.body === null) { + core.info("finish execNormalMention because info.body === null"); + return; + } + + const githubUsernames = pickupUsername(info.body); + if (githubUsernames.length === 0) { + core.info("finish execNormalMention because githubUsernames.length === 0"); + return; + } + + const slackIds = convertToChatworkUsername(githubUsernames, mapping); + + if (slackIds.length === 0) { + core.info("finish execNormalMention because slackIds.length === 0"); + return; + } + + for (const account of slackIds) { + const roomId = account.room_id; + if (roomId === undefined) { + continue; + } + + const message = buildChatworkPostMentionMessage( + [account.account_id], + info.title, + info.url, + info.body, + info.senderName, + ); + + const { apiToken } = allInputs; + + const result = await ChatworkRepositoryImpl.postToChatwork( + apiToken, + roomId, + message, + ); + + core.info( + ["postToSlack result", JSON.stringify({ result }, null, 2)].join("\n"), + ); + } +}; + +/** + * PRがapproveされた際にPR作成者にメンションを付けてチャットルームにメッセージを送る + */ +export const execApproveMention = async ( + payload: WebhookPayload, + allInputs: AllInputs, + mapping: MappingFile, +): Promise => { + core.info("start execApproveMention()"); + + if (!needToSendApproveMention(payload)) { + throw new Error("failed to parse payload"); + } + + const prOwnerGithubUsername = payload.pull_request?.user?.login; + + if (!prOwnerGithubUsername) { + throw new Error("Can not find pr owner user."); + } + + const slackIds = convertToChatworkUsername([prOwnerGithubUsername], mapping); + + if (slackIds.length === 0) { + core.info("finish execApproveMention because slackIds.length === 0"); + return null; + } + + const account = slackIds[0]; + const roomId = account.room_id; + if (roomId === undefined) { + throw new Error("Can not find room ID."); + } + + const info = pickupInfoFromGithubPayload(payload); + const message = buildChatworkPostApproveMessage( + [account.account_id], + info.title, + info.url, + info.body, + payload.sender?.login, + ); + const { apiToken } = allInputs; + + const postResult = await ChatworkRepositoryImpl.postToChatwork( + apiToken, + account.room_id, + message, + ); + + core.info( + [ + "postToSlack result", + JSON.stringify({ postSlackResult: postResult }, null, 2), + ].join("\n"), + ); + + return account.account_id; +}; + +/** + * マッピングファイルを解釈 + */ +export const execLoadMapping = async ( + configurationPath: string, + repoToken: string, +) => { + if (isUrl(configurationPath)) { + return MappingConfigRepositoryImpl.loadFromUrl(configurationPath); + } + + return MappingConfigRepositoryImpl.loadFromGithubPath( + repoToken, + context.repo.owner, + context.repo.repo, + configurationPath, + context.sha, + ); +}; + +/** + * エラーハンドリングを行う + */ +export const postError = async ( + error: Error, + allInputs: AllInputs, +): Promise => { + const { runId } = allInputs; + const currentJobUrl = runId ? buildCurrentJobUrl(runId) : undefined; + const message = buildChatworkErrorMessage(error, currentJobUrl); + core.warning(message); +};