diff --git a/packages/github/src/schema/PR/index.ts b/packages/github/src/schema/PR/index.ts index 83df918..0873d32 100644 --- a/packages/github/src/schema/PR/index.ts +++ b/packages/github/src/schema/PR/index.ts @@ -31,3 +31,4 @@ export const pullRequestSchema = z.object({ }); export { type PullRequestReviewComment, pullRequestReviewCommentSchema } from "./review-comment"; +export { type PullRequestReviewRequested, pullRequestReviewRequestedSchema } from "./review-request"; diff --git a/packages/github/src/schema/PR/review-request.ts b/packages/github/src/schema/PR/review-request.ts new file mode 100644 index 0000000..d7e4a32 --- /dev/null +++ b/packages/github/src/schema/PR/review-request.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; + +export const pullRequestReviewRequestedSchema = z.object({ + action: z.enum(["review_requested"]), + sender: z.object({ login: z.string() }), + requested_reviewer: z.object({ login: z.string() }), + pull_request: z.object({ + number: z.number(), + title: z.string(), + html_url: z.string(), + }), + repository: z.object({ full_name: z.string() }), +}); + +export type PullRequestReviewRequested = z.infer; diff --git "a/packages/slack-blocks/src/PR/PR_\354\236\254\353\246\254\353\267\260.ts" "b/packages/slack-blocks/src/PR/PR_\354\236\254\353\246\254\353\267\260.ts" new file mode 100644 index 0000000..43f0424 --- /dev/null +++ "b/packages/slack-blocks/src/PR/PR_\354\236\254\353\246\254\353\267\260.ts" @@ -0,0 +1,43 @@ +import type { PullRequestReviewRequested } from "@makers-devops/github"; +import type { KnownBlock } from "@slack/types"; +import type { SlackBlockPayload } from "../types"; + +export type PR리뷰재요청Options = { + senderId: string; + reviewerId: string; +}; + +export const blocks = (payload: PullRequestReviewRequested, options: PR리뷰재요청Options): KnownBlock[] => { + const { pull_request } = payload; + const { html_url: prUrl, number: prNumber, title } = pull_request; + + const senderMention = `<@${options.senderId}>`; + const reviewerMention = `<@${options.reviewerId}>`; + + return [ + { + type: "section", + text: { + type: "mrkdwn", + text: `*[${senderMention}]이 [${reviewerMention}]님에게 리뷰를 다시 요청했어요!* 🙏🏻`, + }, + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `> *PR:* <${prUrl}|#${prNumber} ${title}>`, + }, + }, + ]; +}; + +export const fallbackText = (payload: PullRequestReviewRequested): string => { + const { number, title } = payload.pull_request; + return `PR #${number}: ${title} - 리뷰 재요청`; +}; + +export const slackPayload = (payload: PullRequestReviewRequested, options: PR리뷰재요청Options): SlackBlockPayload => ({ + text: fallbackText(payload), + blocks: blocks(payload, options), +}); diff --git a/packages/slack-blocks/src/index.ts b/packages/slack-blocks/src/index.ts index 1bfa53d..95d2da3 100644 --- a/packages/slack-blocks/src/index.ts +++ b/packages/slack-blocks/src/index.ts @@ -1,9 +1,11 @@ export type { SlackBlockPayload } from "./types"; export type { PR열림Options } from "./PR/PR_열림"; +export type { PR리뷰재요청Options } from "./PR/PR_재리뷰"; export type { 리뷰요청Options } from "./피그마/리뷰_요청"; export * as PR_열림 from "./PR/PR_열림"; export * as PR_닫힘 from "./PR/PR_닫힘"; export * as PR_리뷰 from "./PR/PR_리뷰"; +export * as PR_재리뷰 from "./PR/PR_재리뷰"; export * as 피그마_리뷰_요청 from "./피그마/리뷰_요청"; diff --git a/servers/mumu/src/handler/github-webhook/pull_request.ts b/servers/mumu/src/handler/github-webhook/pull_request.ts index 704abdb..b4dc535 100644 --- a/servers/mumu/src/handler/github-webhook/pull_request.ts +++ b/servers/mumu/src/handler/github-webhook/pull_request.ts @@ -1,6 +1,6 @@ import type { Request, Response } from "express"; -import { pullRequestSchema, type PullRequest } from "@makers-devops/github"; -import { createPullRequestThread } from "../../slack"; +import { pullRequestReviewRequestedSchema, pullRequestSchema, type PullRequest } from "@makers-devops/github"; +import { createPullRequestReRequestedReply, createPullRequestThread } from "../../slack"; import { getPullRequestThreadKey } from "../../slack/key"; import { deleteSlackThreadData, findSlackThread, slackClient } from "@makers-devops/slack"; import { PR_닫힘 } from "@makers-devops/slack-blocks"; @@ -9,7 +9,7 @@ import { selectReviewers } from "../../github/review"; import { config } from "../../config"; type HandledAction = (typeof HANDLED_ACTIONS)[number]; -const HANDLED_ACTIONS = ["opened", "reopened", "closed"] as const; +const HANDLED_ACTIONS = ["opened", "reopened", "closed", "review_requested"] as const; const handlePullRequestClosed = async (_req: Request, res: Response, pullRequest: PullRequest) => { const key = getPullRequestThreadKey(pullRequest); @@ -39,6 +39,31 @@ const handlePullRequestClosed = async (_req: Request, res: Response, pullRequest return res.json({ success: true, message: "Pull request closed." }); }; +const handlePullRequestReRequested = async (req: Request, res: Response) => { + const payload = pullRequestReviewRequestedSchema.parse(req.body); + + const senderLogin = payload.sender.login; + const reviewerLogin = payload.requested_reviewer.login; + + const sender = config.frontend.admins.find((admin) => admin.github === senderLogin); + const reviewer = config.frontend.admins.find((admin) => admin.github === reviewerLogin); + + if (!sender || !reviewer) { + return res.json({ success: false, message: "Sender or reviewer is not admin user" }); + } + + const result = await createPullRequestReRequestedReply(payload, { + senderId: sender.slack, + reviewerId: reviewer.slack, + }); + + if (!result) { + return res.json({ success: false, message: "Slack thread reply failed" }); + } + + return res.json({ success: true, message: "Review requested processed successfully", result }); +}; + export const handlePullRequest = async (req: Request, res: Response) => { const pullRequest = pullRequestSchema.parse(req.body); @@ -50,6 +75,10 @@ export const handlePullRequest = async (req: Request, res: Response) => { return await handlePullRequestClosed(req, res, pullRequest); } + if (pullRequest.action === "review_requested") { + return await handlePullRequestReRequested(req, res); + } + const authorLogin = pullRequest.pull_request.user.login; const author = config.frontend.admins.find((admin) => admin.github === authorLogin); diff --git a/servers/mumu/src/slack/index.ts b/servers/mumu/src/slack/index.ts index f45b0a4..0950dce 100644 --- a/servers/mumu/src/slack/index.ts +++ b/servers/mumu/src/slack/index.ts @@ -1,9 +1,9 @@ import { createSlackThread, findSlackThread, slackClient } from "@makers-devops/slack"; import { getPullRequestThreadKey } from "./key"; -import type { PullRequest, PullRequestReviewComment } from "@makers-devops/github"; +import type { PullRequest, PullRequestReviewComment, PullRequestReviewRequested } from "@makers-devops/github"; import { CHANNELS } from "../constant"; -import { PR_리뷰, PR_열림 } from "@makers-devops/slack-blocks"; -import type { PR열림Options } from "@makers-devops/slack-blocks"; +import { PR_리뷰, PR_열림, PR_재리뷰 } from "@makers-devops/slack-blocks"; +import type { PR열림Options, PR리뷰재요청Options } from "@makers-devops/slack-blocks"; /** PR에 대한 스레드를 생성합니다. */ export const createPullRequestThread = async (pull: PullRequest, options: PR열림Options) => { @@ -50,3 +50,39 @@ export const createPullRequestReviewCommentReply = async (comment: PullRequestRe } return null; }; + +/** PR 스레드에 리뷰 재요청 reply를 생성합니다. */ +export const createPullRequestReRequestedReply = async ( + payload: PullRequestReviewRequested, + options: PR리뷰재요청Options, +) => { + const key = getPullRequestThreadKey(payload); + const thread = await findSlackThread(key); + + if (!thread) { + console.error(`${key}: Slack thread not found`); + return null; + } + + try { + const response = await slackClient.chat.postMessage({ + channel: thread.channel, + thread_ts: thread.thread_ts, + ...PR_재리뷰.slackPayload(payload, options), + }); + + if (!response.ok) { + console.error(`${key}: Slack thread reply failed`); + return null; + } + + return { + id: key, + channel: thread.channel, + thread_ts: response.ts, + }; + } catch { + console.error(`${key}: Slack thread reply failed`); + } + return null; +};