Skip to content

[REFACTOR] MySQL에서 카테고리를 ENUM 대신 VARCHAR(20)으로 관리 #24

[REFACTOR] MySQL에서 카테고리를 ENUM 대신 VARCHAR(20)으로 관리

[REFACTOR] MySQL에서 카테고리를 ENUM 대신 VARCHAR(20)으로 관리 #24

name: FE Review Reminder for Discord
on:
pull_request:
branches:
- develop
schedule:
- cron: '0 2 * * 1-5' # 매주 월요일부터 금요일까지, 한국 시간 오전 11시에 실행
workflow_dispatch:
jobs:
review-reminder:
runs-on: ubuntu-latest
steps:
- name: Send Reminder to Discord
uses: actions/github-script@v7
env:
DISCORD_WEBHOOK: ${{ secrets.FE_REVIEW_NOTIFICATION_WEBHOOK_URL }}
DISCORD_MENTION: ${{ secrets.FE_GITHUB_DISCORD_ID }}
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
// GitHub 사용자명과 Discord 멘션 정보 매핑
const discordMentions = JSON.parse(process.env.DISCORD_MENTION);
const discordNotMentions = {
'useon': '썬데이',
'novice0840': '포메',
'rbgksqkr': '마루',
};
async function getReviews(owner, repo, prNumber) {
// 특정 PR의 리뷰 상태 가져오기
const reviews = await github.rest.pulls.listReviews({
owner,
repo,
pull_number: prNumber,
});
return reviews.data;
}
try {
// 열려 있는 PR 목록 가져오기
const pullRequests = await github.rest.pulls.list({
owner,
repo,
state: 'open',
});
// FE 라벨이 달린 PR을 D-DAY가 임박한 순서로 정렬
const fePrs = pullRequests.data
.filter(pr => pr.labels.some(label => label.name.includes('FE')))
.map(pr => {
const dLabel = pr.labels.find(label => label.name.startsWith('D-'));
const urgency = dLabel ? parseInt(dLabel.name.split('-')[1], 10) : Number.MAX_SAFE_INTEGER;
return {
...pr,
urgency,
dLabelName: dLabel?.name || 'D-unknown',
updatedAt: pr.updated_at,
createdAt: pr.created_at,
};
})
.sort((a, b) => a.urgency - b.urgency);
// 열린 PR 중 FE PR이 없는 경우 실행 종료
if (fePrs.length === 0) {
console.log('No FE PRs to remind.');
return;
}
const messages = await Promise.all(
fePrs.map(async pr => {
const reviews = await getReviews(owner, repo, pr.number);
const requestedReviewers = pr.requested_reviewers.map(r => r.login);
// 리뷰 상태를 관리하는 Map 객체 생성
const reviewStates = new Map();
reviews.forEach(review => {
const reviewer = review.user.login;
const state = review.state;
if (reviewer !== pr.user.login) { // PR 작성자는 제외
reviewStates.set(reviewer, state);
}
});
// 리뷰 상태 메시지 생성
const reviewStatuses = Array.from(reviewStates.entries()).map(([reviewer, state]) => {
const discordUsername = discordMentions[reviewer] || `${reviewer}`;
const stateAbbreviations = {
APPROVED: 'A',
CHANGES_REQUESTED: 'RC',
COMMENTED: 'C',
};
const reviewState = stateAbbreviations[state] || state.toLowerCase();
return state === 'APPROVED'
? `${discordNotMentions[reviewer]}(${reviewState})` // APPROVED인 경우 멘션 없이 이름만 표시
: `<@${discordMentions[reviewer]}>(${reviewState})`; // 나머지 상태인 경우 멘션
});
// 리뷰를 시작하지 않은 리뷰어 추가
const notStartedReviewers = requestedReviewers.filter(
reviewer => !reviewStates.has(reviewer) && reviewer !== pr.user.login // PR 작성자 제외
);
const notStartedMentions = notStartedReviewers.map(reviewer => {
return `<@${discordMentions[reviewer]}>(X)`;
});
const reviewStatusMessage = [...reviewStatuses, ...notStartedMentions];
const allReviewersApproved = requestedReviewers.every(
reviewer => reviewStates.get(reviewer) === 'APPROVED'
);
const noPendingReviews = notStartedReviewers.length === 0;
// 모든 리뷰어가 APPROVED 상태이고 리뷰를 시작하지 않은 리뷰어가 없는 경우
if (allReviewersApproved && noPendingReviews) {
const authorMention = discordMentions[pr.user.login] || `${pr.user.login}`;
return `[[${pr.dLabelName}] ${pr.title}](<${pr.html_url}>)\n리뷰어: ${reviewStatusMessage.join(', ')}\n<@${authorMention}>, 모든 리뷰어의 승인 완료! 코멘트를 확인 후 머지해 주세요 🚀`;
}
// 일반적인 리마인드 메시지
return `[[${pr.dLabelName}] ${pr.title}](<${pr.html_url}>)\n리뷰어: ${reviewStatusMessage.join(', ')}`;
})
);
// 최종 메시지 Discord에 전송
const response = await fetch(process.env.DISCORD_WEBHOOK, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: `🍀 [FE] 리뷰가 필요한 PR 목록 🍀\n\n${messages.join('\n\n')}`,
allowed_mentions: {
parse: ["users"], // 멘션 가능한 사용자만 허용
},
}),
});
console.log('Response status:', response.status);
} catch (error) {
console.error('Error processing FE PR reminders:', error.message);
throw error; // 워크플로우 실패 상태 반환
}