Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
37 changes: 24 additions & 13 deletions servers/mumu/src/github/comment.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { redisStorage } from "../redis";
import type { SlackNotifier } from "../slack";
import type { SlackThread } from "../types";
import type { PullRequestReviewComment } from "./schema";
import type { Comment } from "./schema";

const MAX_CHARS = 200;

Expand All @@ -14,18 +14,31 @@ const truncateBody = (body: string): string => {
const escapeSlackLinkText = (text: string): string =>
text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");

export const handlePullRequestReviewComment = async (
payload: PullRequestReviewComment,
slackNotifier: SlackNotifier,
) => {
function buildCommentMessage(payload: Comment): string {
const preview = truncateBody(payload.comment.body);
const safePreview = escapeSlackLinkText(preview);

return [`> *${payload.comment.user.login}*`, `> <${payload.comment.html_url}|${safePreview}>`].join("\n");
Copy link
Member

@jogpfls jogpfls Mar 20, 2026

Choose a reason for hiding this comment

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

issueComment랑 reviewComment가 구분이 따로 안되고 있는 것 같은데 일부러 이렇게 구현하신걸까요 ? 아니시라면 앞에 [issueComment], [reviewComment] 이런식으로 붙여서 구분하는 건 어떻게 생각하시나요...? Just 개인적인 의견입니당....

Copy link
Member Author

Choose a reason for hiding this comment

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

네네 사실 issue 코멘트랑 review 코멘트를 구분할 필요성을 못느꼈어서 따로 처리해두지는 않았습니다.

혜린님이 생각하신 구분되어야 할 이유가 혹시 무엇일까요 ?!

}

export const handleComment = async (payload: Comment, slackNotifier: SlackNotifier): Promise<string> => {
if (payload.action !== "created") {
return JSON.stringify({ success: false, message: "Review comment action skipped." });
return JSON.stringify({ success: false, message: "Comment action skipped." });
}

const repoFullName = payload.repository.full_name;
const prNumber = payload.pull_request.number;
const cacheKey = `${repoFullName}#${prNumber}`;
let prNumber: number;

if ("issue" in payload) {
if (!payload.issue.pull_request) {
return JSON.stringify({ success: false, message: "Issue comment (not PR); skipped." });
}
prNumber = payload.issue.number;
} else {
prNumber = payload.pull_request.number;
}

const cacheKey = `${repoFullName}#${prNumber}`;
const thread = await redisStorage.get<SlackThread>(cacheKey);

if (!thread?.threadTs) {
Expand All @@ -35,15 +48,13 @@ export const handlePullRequestReviewComment = async (
});
}

const preview = truncateBody(payload.comment.body);
const safePreview = escapeSlackLinkText(preview);
const text = [`> *${payload.comment.user.login}*`, `> <${payload.comment.html_url}|${safePreview}>`].join("\n");
const text = buildCommentMessage(payload);

try {
await slackNotifier.createThreadReply(thread.threadTs, text);
} catch {
console.error(`${cacheKey}/${thread.channel}: review comment 슬랙 스레드 답변 전송 실패`);
console.error(`${cacheKey}/${thread.channel}: 슬랙 스레드 답변 전송 실패`);
}

return JSON.stringify({ success: true, message: "Review comment processed successfully" });
return JSON.stringify({ success: true, message: "Comment processed successfully" });
Comment on lines 53 to +60
Copy link
Member

Choose a reason for hiding this comment

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

createThreadReply이 실패했을 때 catch 블록이 에러를 잡으면 console.error만 찍고 끝난 다음에 success: true가 실행되고 있어서 slack 전송이 실패해도 성공으로 리턴하는 구조인 것 같은데 의도하신 구조이신가요 ?!

Copy link
Member Author

Choose a reason for hiding this comment

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

오 아니에요! 요건 사실 저번에도 비슷한 리뷰를 받았었는데요. 어느 계층에서 로그를 남길것이냐를 고민하다가 저런 식으로 작성되었는데,
여기는 comment 핸들러 안에서의 메인 액션인 것 같아서 return문 하나 더 찍어두도록 하겠습니다. 나머지는 추후 리팩토링하겠습니다..!

Copy link
Member Author

Choose a reason for hiding this comment

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

};
19 changes: 19 additions & 0 deletions servers/mumu/src/github/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { z } from "zod";

export type PullRequest = z.infer<typeof pullRequestSchema>;
export type PullRequestReviewComment = z.infer<typeof pullRequestReviewCommentSchema>;
export type IssueComment = z.infer<typeof issueCommentSchema>;

export type Comment = z.infer<typeof commentSchema>;

export const pullRequestSchema = z.object({
action: z.string(),
Expand Down Expand Up @@ -44,3 +47,19 @@ export const pullRequestReviewCommentSchema = z.object({
}),
repository: z.object({ full_name: z.string() }),
});

export const issueCommentSchema = z.object({
action: z.enum(["created", "edited", "deleted"]),
issue: z.object({
number: z.number(),
pull_request: z.object({}).passthrough().optional(),
Copy link
Member

Choose a reason for hiding this comment

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

제 ide에서 이렇게 뜨네용

Image

Copy link
Member

Choose a reason for hiding this comment

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

'passthrough' 이 부분입니다.

pull_request: z.object({}).passthrough().optional(),

Choose a reason for hiding this comment

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

찾아보니 zod v4에서 deprecated 되었다고 합니다..?!
https://zod.dev/v4/changelog
image

Copy link
Member Author

Choose a reason for hiding this comment

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

호호.. 커서가 그것까진 몰랐나봐요 ㅎ.. 수정하겠습니다

Copy link
Member Author

@wuzoo wuzoo Mar 21, 2026

Choose a reason for hiding this comment

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

}),
comment: z.object({
body: z.string(),
html_url: z.string(),
user: z.object({ login: z.string() }),
}),
repository: z.object({ full_name: z.string() }),
});

export const commentSchema = z.union([pullRequestReviewCommentSchema, issueCommentSchema]);
11 changes: 5 additions & 6 deletions servers/mumu/src/webhook.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Router, type Request, type Response } from "express";
import { createSlackNotifier } from "./slack";
import { assertNonNullish } from "./utils/assert";
import { pullRequestSchema, pullRequestReviewCommentSchema } from "./github/schema";
import { pullRequestSchema, commentSchema } from "./github/schema";
import { FRONTEND_BOT_CHANNEL } from "./constant";
import { handlePullRequest } from "./github/pull_request";
import { handlePullRequestReviewComment } from "./github/comment";
import { handleComment } from "./github/comment";
import { isValidRepository } from "./config";

export function createWebhookRouter(): Router {
Expand Down Expand Up @@ -37,10 +37,9 @@ export function createWebhookRouter(): Router {
handlePullRequest(pullRequestSchema.parse(req.body), slackNotifier).then((res) => console.log(res));
break;
}
case "pull_request_review_comment": {
handlePullRequestReviewComment(pullRequestReviewCommentSchema.parse(req.body), slackNotifier).then((res) =>
console.log(res),
);
case "pull_request_review_comment":
case "issue_comment": {
handleComment(commentSchema.parse(req.body), slackNotifier).then((res) => console.log(res));
break;
}
}
Expand Down