Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/github/src/schema/PR/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ export const pullRequestSchema = z.object({
});

export { type PullRequestReviewComment, pullRequestReviewCommentSchema } from "./review-comment";
export { type PullRequestReviewRequested, pullRequestReviewRequestedSchema } from "./review-request";
15 changes: 15 additions & 0 deletions packages/github/src/schema/PR/review-request.ts
Original file line number Diff line number Diff line change
@@ -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<typeof pullRequestReviewRequestedSchema>;
43 changes: 43 additions & 0 deletions packages/slack-blocks/src/PR/PR_재리뷰.ts
Original file line number Diff line number Diff line change
@@ -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;
Comment on lines +9 to +11
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예전에 주용님 리뷰에도 달았던건데, blocks가 뭘 의미하는건가요?

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}>`,
},
},
Comment on lines +25 to +31
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

처음 스레드가 만들어질 때 PR과 PR번호, 제목이 보이는데 스레드에 코멘트가 쌓였을 때 상단으로 이동하지 않고 바로 확인하기 위해서 넣은건가욥?? Just Wonder!

];
};

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),
});
2 changes: 2 additions & 0 deletions packages/slack-blocks/src/index.ts
Original file line number Diff line number Diff line change
@@ -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 "./피그마/리뷰_요청";
35 changes: 32 additions & 3 deletions servers/mumu/src/handler/github-webhook/pull_request.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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);
Expand Down Expand Up @@ -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);

Expand All @@ -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);

Expand Down
42 changes: 39 additions & 3 deletions servers/mumu/src/slack/index.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down Expand Up @@ -50,3 +50,39 @@ export const createPullRequestReviewCommentReply = async (comment: PullRequestRe
}
return null;
};

/** PR 스레드에 리뷰 재요청 reply를 생성합니다. */
export const createPullRequestReRequestedReply = async (
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 함수가 바로 위의 createPullRequestReviewCommentReply와 동작이 거의 같아 보이는것 갘아요! Slack에 보낼 메시지를 만드는 부분만 다르고 thread 조회, chat.postMessage 호출, 실패 처리 흐름은 동일한 것 같아요. 그래서 두 함수를 그대로 유지하면서 둘다 reply에 해당하는 내용이니 공통 reply 전송 로직만 내부 함수로 분리하는건 어떤가욥??

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;
};