#!/usr/bin/env node /** * Linear 릴리즈 완료 스크립트 * semantic-release 완료 후 Linear 이슈들을 아카이브하고 정리 */ const fs = require("fs"); const path = require("path"); // Linear 클라이언트 class LinearClient { constructor({ apiKey }) { this.apiKey = apiKey; this.baseUrl = "https://api.linear.app/graphql"; } async query(query, variables = {}) { const response = await fetch(this.baseUrl, { method: "POST", headers: { "Content-Type": "application/json", Authorization: this.apiKey, }, body: JSON.stringify({ query, variables }), }); const data = await response.json(); if (data.errors) { throw new Error(`Linear API Error: ${JSON.stringify(data.errors)}`); } return data.data; } async createProject(name, description) { const query = ` mutation CreateProject($name: String!, $description: String!) { projectCreate(input: { name: $name, description: $description }) { success project { id name } } } `; const result = await this.query(query, { name, description }); return result.projectCreate; } async createMilestone(projectId, name, targetDate) { const query = ` mutation CreateMilestone($projectId: String!, $name: String!, $targetDate: DateTime) { projectMilestoneCreate(input: { projectId: $projectId, name: $name, targetDate: $targetDate }) { success projectMilestone { id name } } } `; const result = await this.query(query, { projectId, name, targetDate }); return result.projectMilestoneCreate; } async getIssuesFromMetadata(metadataPath) { if (!fs.existsSync(metadataPath)) { console.warn(`Metadata file not found: ${metadataPath}`); return []; } const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf8")); return metadata.issues || []; } async addCommentToIssue(issueId, body) { const query = ` mutation CreateComment($issueId: String!, $body: String!) { commentCreate(input: { issueId: $issueId, body: $body }) { success comment { id } } } `; const result = await this.query(query, { issueId, body }); return result.commentCreate; } async moveIssuesToProject(issueIds, projectId) { const query = ` mutation MoveIssuesToProject($issueIds: [String!]!, $projectId: String!) { issueBatchUpdate(input: { ids: $issueIds, projectId: $projectId }) { success } } `; const result = await this.query(query, { issueIds, projectId }); return result.issueBatchUpdate; } } const version = process.argv[2]; const apiKey = process.env.LINEAR_API_KEY; if (!version) { console.error("Usage: node linear-release-complete.js "); process.exit(1); } if (!apiKey) { console.error("Error: LINEAR_API_KEY environment variable not set"); process.exit(1); } const linear = new LinearClient({ apiKey }); /** * 릴리즈 알림 메시지 생성 */ function generateReleaseMessage(version, githubUrl) { return `🎉 **릴리즈 v${version} 완료!** 이 이슈는 v${version} 릴리즈에 포함되었습니다. **릴리즈 정보:** - 버전: v${version} - 날짜: ${new Date().toLocaleDateString("ko-KR")} - GitHub: ${githubUrl} 감사합니다! 🚀`; } /** * 릴리즈 통계 생성 */ function generateReleaseStats(issues) { const stats = { total: issues.length, byTeam: {}, byPriority: {}, byType: {}, }; issues.forEach((issue) => { // 팀별 통계 const team = issue.team || "Unassigned"; stats.byTeam[team] = (stats.byTeam[team] || 0) + 1; // 우선순위별 통계 const priority = `P${issue.priority || 0}`; stats.byPriority[priority] = (stats.byPriority[priority] || 0) + 1; // 타입별 통계 (라벨 기반) const labels = issue.labels || []; if (labels.includes("feature")) { stats.byType.features = (stats.byType.features || 0) + 1; } else if (labels.includes("bug")) { stats.byType.bugs = (stats.byType.bugs || 0) + 1; } else if (labels.includes("improvement")) { stats.byType.improvements = (stats.byType.improvements || 0) + 1; } else { stats.byType.other = (stats.byType.other || 0) + 1; } }); return stats; } /** * Slack 알림 전송 (시뮬레이션) */ async function sendSlackNotification(version, stats, githubUrl) { const slackWebhook = process.env.SLACK_WEBHOOK_URL; if (!slackWebhook) { console.log("📢 Slack webhook not configured, skipping notification"); return; } const message = { text: `🎉 Zellyy Finance v${version} 릴리즈 완료!`, blocks: [ { type: "header", text: { type: "plain_text", text: `🎉 Zellyy Finance v${version} 릴리즈!`, }, }, { type: "section", text: { type: "mrkdwn", text: `*총 ${stats.total}개 이슈*가 완료되었습니다.`, }, }, { type: "section", fields: [ { type: "mrkdwn", text: `*새 기능:* ${stats.byType.features || 0}개`, }, { type: "mrkdwn", text: `*버그 수정:* ${stats.byType.bugs || 0}개`, }, { type: "mrkdwn", text: `*개선사항:* ${stats.byType.improvements || 0}개`, }, { type: "mrkdwn", text: `*기타:* ${stats.byType.other || 0}개`, }, ], }, { type: "actions", elements: [ { type: "button", text: { type: "plain_text", text: "GitHub 릴리즈 보기", }, url: githubUrl, style: "primary", }, ], }, ], }; try { const response = await fetch(slackWebhook, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(message), }); if (response.ok) { console.log("✅ Slack notification sent"); } else { console.warn("⚠️ Failed to send Slack notification"); } } catch (error) { console.warn("⚠️ Error sending Slack notification:", error.message); } } /** * 릴리즈 완료 메인 함수 */ async function completeRelease() { try { console.log(`🏁 Completing release v${version}...`); // 메타데이터 파일에서 이슈 정보 로드 const metadataPath = path.join( process.cwd(), "releases", `v${version}-metadata.json` ); const issues = await linear.getIssuesFromMetadata(metadataPath); if (issues.length === 0) { console.log( "⚠️ No issues found in metadata, skipping post-release tasks" ); return; } console.log(`📋 Processing ${issues.length} issues from v${version}`); // GitHub 릴리즈 URL 생성 const githubUrl = `https://github.com/zellyy-finance/zellyy-finance/releases/tag/v${version}`; // 각 이슈에 릴리즈 완료 코멘트 추가 console.log("💬 Adding release completion comments to issues..."); const releaseMessage = generateReleaseMessage(version, githubUrl); let commentCount = 0; for (const issue of issues) { try { const result = await linear.addCommentToIssue(issue.id, releaseMessage); if (result.success) { commentCount++; console.log(` ✅ Comment added to ${issue.id}`); } } catch (error) { console.warn( ` ⚠️ Failed to add comment to ${issue.id}:`, error.message ); } } console.log(`✅ Added comments to ${commentCount}/${issues.length} issues`); // 릴리즈 프로젝트 생성 (옵션) if (process.env.CREATE_RELEASE_PROJECT === "true") { console.log("📁 Creating release project..."); try { const projectResult = await linear.createProject( `Release v${version}`, `Issues completed in release v${version}` ); if (projectResult.success) { console.log( `✅ Created release project: ${projectResult.project.id}` ); // 이슈들을 릴리즈 프로젝트로 이동 const issueIds = issues.map((i) => i.id); const moveResult = await linear.moveIssuesToProject( issueIds, projectResult.project.id ); if (moveResult.success) { console.log( `✅ Moved ${issueIds.length} issues to release project` ); } } } catch (error) { console.warn("⚠️ Failed to create release project:", error.message); } } // 릴리즈 통계 생성 console.log("📊 Generating release statistics..."); const stats = generateReleaseStats(issues); // 통계 출력 console.log("\n📈 Release Statistics:"); console.log(` Total Issues: ${stats.total}`); console.log(` By Type:`); Object.entries(stats.byType).forEach(([type, count]) => { console.log(` ${type}: ${count}`); }); console.log(` By Team:`); Object.entries(stats.byTeam).forEach(([team, count]) => { console.log(` ${team}: ${count}`); }); // 통계 파일 저장 const statsPath = path.join( process.cwd(), "releases", `v${version}-stats.json` ); fs.writeFileSync(statsPath, JSON.stringify(stats, null, 2)); console.log(`✅ Statistics saved: ${statsPath}`); // Slack 알림 전송 console.log("📢 Sending notifications..."); await sendSlackNotification(version, stats, githubUrl); // 릴리즈 요약 리포트 생성 const summaryPath = path.join( process.cwd(), "releases", `v${version}-summary.md` ); const summaryContent = generateSummaryReport(version, stats, githubUrl); fs.writeFileSync(summaryPath, summaryContent); console.log(`✅ Summary report saved: ${summaryPath}`); console.log("\n🎉 Release completion tasks finished successfully!"); } catch (error) { console.error("❌ Failed to complete release:", error.message); process.exit(1); } } /** * 요약 리포트 생성 */ function generateSummaryReport(version, stats, githubUrl) { return `# Release v${version} Summary ## 📊 Statistics - **Total Issues Completed**: ${stats.total} - **Release Date**: ${new Date().toLocaleDateString("ko-KR")} - **GitHub Release**: [v${version}](${githubUrl}) ### By Type ${Object.entries(stats.byType) .map(([type, count]) => `- **${type}**: ${count}`) .join("\n")} ### By Team ${Object.entries(stats.byTeam) .map(([team, count]) => `- **${team}**: ${count}`) .join("\n")} ### By Priority ${Object.entries(stats.byPriority) .map(([priority, count]) => `- **${priority}**: ${count}`) .join("\n")} ## 🎯 Key Achievements This release represents significant progress in the Zellyy Finance project with ${stats.total} completed issues across multiple teams. --- *Generated automatically by Linear release automation* `; } // 실행 completeRelease();