#!/usr/bin/env node const { execSync } = require("child_process"); const fs = require("fs"); const path = require("path"); /** * 빌드 결과 알림 핸들러 * GitHub Actions, Slack, 이메일 등 다양한 알림 채널 지원 */ // 설정 const config = { // GitHub Actions 환경 변수 isCI: process.env.CI === "true", githubRepository: process.env.GITHUB_REPOSITORY, githubRunId: process.env.GITHUB_RUN_ID, githubWorkflow: process.env.GITHUB_WORKFLOW, githubActor: process.env.GITHUB_ACTOR, githubRef: process.env.GITHUB_REF, // Slack 설정 (환경 변수에서) slackWebhookUrl: process.env.SLACK_WEBHOOK_URL, slackChannel: process.env.SLACK_CHANNEL || "#deployments", // 이메일 설정 notificationEmail: process.env.NOTIFICATION_EMAIL, // 알림 레벨 notifyOnSuccess: process.env.NOTIFY_ON_SUCCESS !== "false", notifyOnFailure: process.env.NOTIFY_ON_FAILURE !== "false", notifyOnWarning: process.env.NOTIFY_ON_WARNING !== "false", }; // 명령행 인수 파싱 const args = process.argv.slice(2); const event = args[0]; // 'success', 'failure', 'warning' const message = args[1] || ""; const details = args[2] || ""; /** * 메인 알림 처리 함수 */ async function handleNotification() { if (!event) { console.error( "사용법: node notification-handler.js [message] [details]" ); console.error("이벤트: success, failure, warning"); process.exit(1); } // 알림 여부 확인 const shouldNotify = (event === "success" && config.notifyOnSuccess) || (event === "failure" && config.notifyOnFailure) || (event === "warning" && config.notifyOnWarning); if (!shouldNotify) { console.log(`${event} 이벤트에 대한 알림이 비활성화되어 있습니다.`); return; } console.log(`🔔 ${event} 알림 처리 시작...`); // 알림 메시지 생성 const notification = createNotificationMessage(event, message, details); // 다양한 채널로 알림 전송 const results = await Promise.allSettled([ sendSlackNotification(notification), sendGitHubNotification(notification), sendEmailNotification(notification), ]); // 결과 출력 results.forEach((result, index) => { const channels = ["Slack", "GitHub", "Email"]; if (result.status === "fulfilled") { console.log(`✅ ${channels[index]} 알림 전송 성공`); } else { console.log(`❌ ${channels[index]} 알림 전송 실패:`, result.reason); } }); } /** * 알림 메시지 생성 */ function createNotificationMessage(event, message, details) { const timestamp = new Date().toLocaleString("ko-KR"); const projectName = "Zellyy Finance"; // 이벤트별 이모지와 색상 const eventConfig = { success: { emoji: "✅", color: "good", level: "SUCCESS" }, failure: { emoji: "❌", color: "danger", level: "FAILURE" }, warning: { emoji: "⚠️", color: "warning", level: "WARNING" }, }; const { emoji, color, level } = eventConfig[event] || eventConfig.warning; // 기본 메시지 구성 const baseMessage = { event, level, emoji, color, timestamp, projectName, message: message || `빌드 ${level}`, details: details || "", }; // CI 환경 정보 추가 if (config.isCI) { baseMessage.ci = { repository: config.githubRepository, workflow: config.githubWorkflow, runId: config.githubRunId, actor: config.githubActor, ref: config.githubRef, runUrl: `https://github.com/${config.githubRepository}/actions/runs/${config.githubRunId}`, }; } return baseMessage; } /** * Slack 알림 전송 */ async function sendSlackNotification(notification) { if (!config.slackWebhookUrl) { throw new Error("Slack webhook URL이 설정되지 않음"); } const slackMessage = { channel: config.slackChannel, username: "Zellyy Finance CI/CD", icon_emoji: ":rocket:", attachments: [ { color: notification.color, title: `${notification.emoji} ${notification.projectName} - ${notification.level}`, text: notification.message, fields: [ { title: "Timestamp", value: notification.timestamp, short: true, }, ], footer: "Zellyy Finance CI/CD", }, ], }; // CI 정보 추가 if (notification.ci) { slackMessage.attachments[0].fields.push( { title: "Repository", value: notification.ci.repository, short: true, }, { title: "Workflow", value: notification.ci.workflow, short: true, }, { title: "Triggered by", value: notification.ci.actor, short: true, }, { title: "Branch/Tag", value: notification.ci.ref .replace("refs/heads/", "") .replace("refs/tags/", ""), short: true, } ); slackMessage.attachments[0].actions = [ { type: "button", text: "View Run", url: notification.ci.runUrl, }, ]; } // 세부 정보 추가 if (notification.details) { slackMessage.attachments[0].fields.push({ title: "Details", value: notification.details, short: false, }); } // Slack API 호출 try { const response = await fetch(config.slackWebhookUrl, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(slackMessage), }); if (!response.ok) { throw new Error( `Slack API 오류: ${response.status} ${response.statusText}` ); } return { success: true, channel: "Slack" }; } catch (error) { throw new Error(`Slack 전송 실패: ${error.message}`); } } /** * GitHub 알림 (GitHub Actions 환경에서만) */ async function sendGitHubNotification(notification) { if (!config.isCI) { throw new Error("GitHub 알림은 CI 환경에서만 지원"); } // GitHub Actions 출력 형식 const githubMessage = `::${notification.event}::${notification.emoji} ${notification.message}`; console.log(githubMessage); // Job Summary 생성 const summary = ` ## ${notification.emoji} ${notification.projectName} - ${notification.level} **Message:** ${notification.message} **Timestamp:** ${notification.timestamp} ${notification.details ? `**Details:**\n${notification.details}` : ""} ${ notification.ci ? `**Run Details:** - Repository: ${notification.ci.repository} - Workflow: ${notification.ci.workflow} - Triggered by: ${notification.ci.actor} - Branch/Tag: ${notification.ci.ref} - [View Run](${notification.ci.runUrl})` : "" } `; // GITHUB_STEP_SUMMARY에 작성 if (process.env.GITHUB_STEP_SUMMARY) { fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary); } return { success: true, channel: "GitHub" }; } /** * 이메일 알림 (선택사항) */ async function sendEmailNotification(notification) { if (!config.notificationEmail) { throw new Error("알림 이메일이 설정되지 않음"); } // 간단한 로그만 출력 (실제 이메일 전송은 외부 서비스 필요) console.log(`📧 이메일 알림 대상: ${config.notificationEmail}`); console.log( `제목: [${notification.projectName}] ${notification.level} - ${notification.message}` ); return { success: true, channel: "Email", note: "Log only (external service required)", }; } /** * 빌드 결과 파일에서 정보 읽기 */ function readBuildResults() { const resultsPath = path.join(__dirname, "..", "test-results.json"); if (fs.existsSync(resultsPath)) { try { const results = JSON.parse(fs.readFileSync(resultsPath, "utf8")); return results; } catch (error) { console.warn("빌드 결과 파일 읽기 실패:", error.message); } } return null; } /** * 사용 예시 출력 */ function printUsage() { console.log(` 🔔 Notification Handler Usage: # 성공 알림 node scripts/notification-handler.cjs success "빌드 성공" "모든 테스트 통과" # 실패 알림 node scripts/notification-handler.cjs failure "빌드 실패" "3개 테스트 실패" # 경고 알림 node scripts/notification-handler.cjs warning "빌드 완료 (경고)" "선택적 테스트 실패" Environment Variables: - SLACK_WEBHOOK_URL: Slack 웹훅 URL - SLACK_CHANNEL: Slack 채널 (기본: #deployments) - NOTIFICATION_EMAIL: 알림 이메일 - NOTIFY_ON_SUCCESS: 성공 시 알림 (기본: true) - NOTIFY_ON_FAILURE: 실패 시 알림 (기본: true) - NOTIFY_ON_WARNING: 경고 시 알림 (기본: true) `); } // 메인 실행 if (require.main === module) { if (args.includes("--help") || args.includes("-h")) { printUsage(); process.exit(0); } handleNotification().catch((error) => { console.error("알림 처리 실패:", error); process.exit(1); }); }