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
38 changes: 25 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 코멘트를 구분할 필요성을 못느꼈어서 따로 처리해두지는 않았습니다.

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

Copy link
Member

Choose a reason for hiding this comment

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

꼭 필요하다고 생각했다기보다는 issue comment랑 review comment가 성격이 조금 다르다 보니 앞에 구분이 있어도 좋겠다고 생각해서 가볍게 이야기해봤어요 !! 정말 개인적인 의견이였습니다 !

}

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,14 @@ 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: false, message: "Slack thread reply failed" });
}

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.looseObject({}).optional(),
}),
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