Skip to content
Merged
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
58 changes: 58 additions & 0 deletions .github/scripts/assign-reviewer.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
module.exports = async ({ github, context, core }) => {
const creator = context.payload.pull_request.user.login;

// 환경 변수에서 사용자명 읽기
const { memberOh, memberJi, memberJin, memberHong } = JSON.parse(
process.env.COLLABORATORS,
);
// 모든 사용자명이 제대로 설정되었는지 확인
if (!memberOh || !memberJi || !memberJin || !memberHong) {
core.setFailed("필요한 환경 변수가 설정되지 않았습니다.");
return { reviewers: [] };
}

// 리뷰어 매핑 정의
const reviewerMap = {
[memberOh]: [memberJi, memberJin],
[memberJi]: [memberJin, memberHong],
[memberJin]: [memberHong, memberOh],
[memberHong]: [memberOh, memberJi],
};

// 해당 사용자의 리뷰어 배열 가져오기
const reviewers = reviewerMap[creator];

// 매핑된 리뷰어가 있으면 리뷰어 요청 및 담당자 지정
if (reviewers) {
try {
// 리뷰어 요청
await github.rest.pulls.requestReviewers({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
reviewers: reviewers,
});

// 본인을 담당자로 지정
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
assignees: [creator],
});

console.log(
`리뷰어 ${reviewers.join(", ")}와(과) 담당자 ${creator}가 성공적으로 할당되었습니다.`,
);

// 리뷰어 정보 반환
return { reviewers };
} catch (error) {
core.setFailed(`리뷰어 할당 중 오류가 발생했습니다: ${error.message}`);
return { reviewers: [] };
}
} else {
console.log(`${creator}에 대한 리뷰어 매핑을 찾을 수 없습니다.`);
return { reviewers: [] };
}
};
79 changes: 79 additions & 0 deletions .github/scripts/auto-pr-alarm.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//* =====================================
//* PR 알림 스크립트
//* =====================================
const { sendDiscordMessage } = require("./modules/discord-service.cjs");
const { generatePRMessages } = require("./modules/pr-processor.cjs");
const { safeJsonParse } = require("./modules/utils.cjs");

module.exports = async ({ github, context, core }) => {
const owner = context.repo.owner;
const repo = context.repo.repo;
const discordWebhook = process.env.DISCORD_WEBHOOK;

// Discord 멘션 데이터 파싱
const discordMentions = safeJsonParse(process.env.DISCORD_MENTION, {});

// auto_assigning 작업에서 전달된 할당된 리뷰어 정보
const assignedReviewersJson = process.env.ASSIGNED_REVIEWERS || "[]";
const assignedReviewers = safeJsonParse(assignedReviewersJson, []);

console.log("할당된 리뷰어 정보:", assignedReviewers);

try {
// PR 이벤트 타입 확인
const eventType = context.eventName;
const action = context.payload.action;

// 현재 PR 정보 가져오기
const pullRequest = context.payload.pull_request;

if (!pullRequest) {
console.log("처리할 PR 정보 없음");
return;
}

// 자동 할당된 리뷰어 정보를 PR 객체에 추가
// 이미 requested_reviewers가 있을 경우 병합, 없으면 새로 할당
if (assignedReviewers.length > 0 && !pullRequest.requested_reviewers) {
pullRequest.requested_reviewers = assignedReviewers.map((login) => ({
login,
}));
} else if (assignedReviewers.length > 0) {
// 이미 존재하는 리뷰어와 새로 할당된 리뷰어 합치기
const existingLogins = pullRequest.requested_reviewers.map(
(r) => r.login,
);
const newReviewers = assignedReviewers
.filter((login) => !existingLogins.includes(login))
.map((login) => ({ login }));

pullRequest.requested_reviewers = [
...pullRequest.requested_reviewers,
...newReviewers,
];
}

// PR 메시지 생성
const messages = await generatePRMessages(
github,
owner,
repo,
[pullRequest],
discordMentions,
);

// 메시지가 없으면 종료
if (messages.length === 0) {
console.log("생성된 메시지가 없습니다.");
return;
}

// Discord로 메시지 전송
await sendDiscordMessage(discordWebhook, messages, {
headerText: `🔔 PR 알림 (${action}) 🔔`,
});
} catch (error) {
console.error("PR 리마인더 처리 중 오류 발생:", error.message);
core.setFailed(`PR 리마인더 처리 실패: ${error.message}`);
}
};
20 changes: 20 additions & 0 deletions .github/scripts/auto-review-alarm.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//* =====================================
//* 디스코드 리뷰 리마인더
//* =====================================

const { ReviewAlarmService } = require("./modules/review-processor.cjs");
const { safeJsonParse } = require("./modules/utils.cjs");

module.exports = async ({ github, context, core }) => {
// 설정 생성
const config = {
discordWebhook: process.env.DISCORD_WEBHOOK,
discordMentions: safeJsonParse(process.env.DISCORD_MENTION, {}),
};

// 서비스 인스턴스 생성
const reviewAlarmService = new ReviewAlarmService(config);

// 리뷰 알림 전송
await reviewAlarmService.sendReviewAlarm(github, context, core);
};
52 changes: 52 additions & 0 deletions .github/scripts/auto-review-reminder.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//* =====================================
//* 디스코드 커스텀 메시지 보내기 - 메인 모듈
//* =====================================

const { sendDiscordMessage } = require("./modules/discord-service.cjs");
const { fetchOpenPullRequests } = require("./modules/github-service.cjs");
const { generatePRMessages } = require("./modules/pr-processor.cjs");
const { isAfterEndDate, safeJsonParse } = require("./modules/utils.cjs");

module.exports = async ({ github, context }) => {
// 현재 날짜 확인
const currentDate = new Date();
const endDate = new Date("2025-05-12T23:59:59Z"); // 2025년 5월 12일 23:59:59 UTC

// 현재 날짜가 종료 날짜보다 이후인지 확인
if (isAfterEndDate(currentDate, endDate)) {
console.log(
"지정된 종료 날짜(2025년 5월 12일)이 지났습니다. 작업을 수행하지 않습니다.",
);
return; // 함수 종료
}

const owner = context.repo.owner;
const repo = context.repo.repo;
const discordWebhook = process.env.DISCORD_WEBHOOK;

// Discord 멘션 데이터 파싱
const discordMentions = safeJsonParse(process.env.DISCORD_MENTION, {});

try {
// 열린 PR 목록 가져오기
const openPRs = await fetchOpenPullRequests(github, owner, repo);
if (openPRs.length === 0) return;

// PR 정보 처리하여 메시지 생성
const messages = await generatePRMessages(
github,
owner,
repo,
openPRs,
discordMentions,
);
if (messages.length === 0) return;

// Discord로 메시지 전송
await sendDiscordMessage(discordWebhook, messages);
} catch (error) {
console.error("Error processing PR reminders:", error.message);
console.error(error.stack);
throw error; // 워크플로우 실패 상태 반환
}
};
22 changes: 22 additions & 0 deletions .github/scripts/modules/constants.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//* =====================================
//* 상수 정의 모듈
//* =====================================

// GitHub에서 반환하는 리뷰 상태 값
const GITHUB_REVIEW_STATES = {
APPROVED: "APPROVED",
CHANGES_REQUESTED: "CHANGES_REQUESTED",
COMMENTED: "COMMENTED",
};

// 리뷰 상태 표시용 약어
const STATE_ABBREVIATIONS = {
[GITHUB_REVIEW_STATES.APPROVED]: "Approved",
[GITHUB_REVIEW_STATES.CHANGES_REQUESTED]: "Changes Requested",
[GITHUB_REVIEW_STATES.COMMENTED]: "Commented",
};

module.exports = {
GITHUB_REVIEW_STATES,
STATE_ABBREVIATIONS,
};
131 changes: 131 additions & 0 deletions .github/scripts/modules/discord-service.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
//* =====================================
//* Discord 서비스 모듈
//* =====================================

/**
* Discord 메시지 전송 서비스 클래스
*/
class DiscordService {
/**
* 생성자
* @param {Object} config - 서비스 설정
* @param {number} [config.timeout=10000] - 요청 타임아웃 (기본값 10초)
* @param {Function} [config.logger=console] - 로깅 객체 (기본값 console)
* @param {string} [config.headerText='🍀 리뷰가 필요한 PR 목록 🍀'] - 메시지 헤더 텍스트
*/
constructor(config = {}) {
this.timeout = config.timeout || 10000;
this.logger = config.logger || console;
this.headerText = config.headerText || "🍀 리뷰가 필요한 PR 목록 🍀";
}

/**
* Discord 웹훅 URL 유효성 검사
* @param {string} webhookUrl - 검사할 웹훅 URL
* @throws {Error} 웹훅 URL이 없는 경우 에러 발생
*/
validateWebhookUrl(webhookUrl) {
if (!webhookUrl) {
this.logger.error("Discord 웹훅 URL이 제공되지 않았습니다.");
throw new Error("Discord 웹훅 URL이 필요합니다.");
}
}

/**
* 메시지 페이로드 생성
* @param {Array} messages - 전송할 메시지 배열
* @returns {Object} Discord API 요청 페이로드
*/
createMessagePayload(messages) {
return {
content: `${this.headerText}\n\n${messages.join("\n\n")}`,
allowed_mentions: {
parse: ["users"], // 멘션 가능한 사용자만 허용
},
};
}

/**
* 요청 옵션 생성
* @param {Object} payload - 메시지 페이로드
* @returns {Object} Fetch API 요청 옵션
*/
createFetchOptions(payload) {
return {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
timeout: this.timeout,
};
}

/**
* 응답 처리
* @param {Response} response - Fetch API 응답
* @throws {Error} 요청 실패 시 에러 발생
*/
async handleResponse(response) {
if (response.ok) {
this.logger.log(
`Discord 메시지 전송 성공! 상태 코드: ${response.status}`,
);
return;
}

// 실패 시 상세 로깅
const responseText = await response.text();
this.logger.error(
`Discord 메시지 전송 실패. 상태 코드: ${response.status}`,
);
this.logger.error("응답 내용:", responseText);
throw new Error(`Discord 메시지 전송 실패: ${response.status}`);
}

/**
* Discord로 메시지 전송
* @param {string} webhookUrl - Discord 웹훅 URL
* @param {Array} messages - 전송할 메시지 배열
* @returns {Promise<void>}
*/
async sendMessage(webhookUrl, messages) {
// 웹훅 URL 검증
this.validateWebhookUrl(webhookUrl);

// 로깅
this.logger.log(`Discord에 ${messages.length}개의 PR 정보 전송 중...`);

try {
// 페이로드 및 요청 옵션 생성
const payload = this.createMessagePayload(messages);
const fetchOptions = this.createFetchOptions(payload);

// 메시지 전송
const response = await fetch(webhookUrl, fetchOptions);

// 응답 처리
await this.handleResponse(response);
} catch (error) {
this.logger.error("Discord 메시지 전송 중 오류 발생:", error.message);
throw error;
}
}
}

/**
* Discord 서비스 인스턴스 생성 및 메시지 전송 함수
* @param {string} webhookUrl - Discord 웹훅 URL
* @param {Array} messages - 전송할 메시지 배열
* @param {Object} [options] - 추가 옵션
* @returns {Promise<void>}
*/
async function sendDiscordMessage(webhookUrl, messages, options = {}) {
const discordService = new DiscordService({
headerText: options.headerText || "🍀 리뷰가 필요한 PR 목록 🍀",
});
await discordService.sendMessage(webhookUrl, messages);
}

module.exports = {
DiscordService,
sendDiscordMessage,
};
Loading