#!/usr/bin/env node /** * Linear 이슈 코멘트 생성 스크립트 * GitHub 이벤트에 따라 Linear 이슈에 자동으로 코멘트 추가 */ // Simple command line argument parsing without commander dependency // Linear 클라이언트 (linear-sync.js와 동일) 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 getIssue(issueId) { const query = ` query GetIssue($id: String!) { issue(id: $id) { id identifier title state { name } assignee { name email } } } `; const result = await this.query(query, { id: issueId }); return result.issue; } async createComment(issueId, body) { const query = ` mutation CreateComment($issueId: String!, $body: String!) { commentCreate(input: { issueId: $issueId, body: $body }) { success comment { id body createdAt } } } `; const result = await this.query(query, { issueId, body }); return result.commentCreate; } } // Simple command line argument parsing const args = process.argv.slice(2); const options = { issueId: null, event: null, action: null, prUrl: null, prAuthor: null, commitSha: null, commitMessage: null, reviewState: null, reviewer: null, githubIssueUrl: null, }; // Extract arguments args.forEach((arg) => { if (arg.startsWith("--issue-id=")) { options.issueId = arg.split("=")[1]; } else if (arg.startsWith("--event=")) { options.event = arg.split("=")[1]; } else if (arg.startsWith("--action=")) { options.action = arg.split("=")[1]; } else if (arg.startsWith("--pr-url=")) { options.prUrl = arg.split("=")[1]; } else if (arg.startsWith("--pr-author=")) { options.prAuthor = arg.split("=")[1]; } else if (arg.startsWith("--commit-sha=")) { options.commitSha = arg.split("=")[1]; } else if (arg.startsWith("--commit-message=")) { options.commitMessage = arg.split("=")[1]; } else if (arg.startsWith("--review-state=")) { options.reviewState = arg.split("=")[1]; } else if (arg.startsWith("--reviewer=")) { options.reviewer = arg.split("=")[1]; } else if (arg.startsWith("--github-issue-url=")) { options.githubIssueUrl = arg.split("=")[1]; } }); // Linear 클라이언트 초기화 const apiKey = process.env.LINEAR_API_KEY; if (!apiKey) { console.error("Error: LINEAR_API_KEY environment variable not set"); process.exit(1); } const linear = new LinearClient({ apiKey }); /** * 코멘트 템플릿 생성 */ function generateComment() { const timestamp = new Date().toLocaleString("ko-KR", { timeZone: "Asia/Seoul", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", }); switch (options.event) { case "pull_request": return generatePRComment(); case "pull_request_review": return generateReviewComment(); case "push": return generateCommitComment(); case "issue": return generateIssueComment(); default: return `GitHub 이벤트 발생: ${options.event} (${timestamp})`; } } function generatePRComment() { const { action, prUrl, prAuthor } = options; const templates = { opened: `🔗 **Pull Request 생성** **URL**: ${prUrl} **작성자**: @${prAuthor} 작업이 시작되었습니다. 코드 리뷰 후 머지될 예정입니다.`, ready_for_review: `👀 **코드 리뷰 요청** **URL**: ${prUrl} **작성자**: @${prAuthor} Pull Request가 리뷰 준비 상태입니다. 팀원들의 리뷰를 기다리고 있습니다.`, closed: `${options.prMerged === "true" ? "✅" : "❌"} **Pull Request ${options.prMerged === "true" ? "머지" : "닫음"}** **URL**: ${prUrl} **작성자**: @${prAuthor} ${ options.prMerged === "true" ? "코드가 main 브랜치에 성공적으로 머지되었습니다!" : "Pull Request가 머지되지 않고 닫혔습니다." }`, reopened: `🔄 **Pull Request 재개** **URL**: ${prUrl} **작성자**: @${prAuthor} Pull Request가 다시 열렸습니다.`, }; return templates[action] || `Pull Request 이벤트: ${action}\n${prUrl}`; } function generateReviewComment() { const { reviewState, reviewer, prUrl } = options; const templates = { approved: `✅ **코드 리뷰 승인** **리뷰어**: @${reviewer} **Pull Request**: ${prUrl} 코드 리뷰가 승인되었습니다. 머지 준비가 완료되었습니다!`, requested_changes: `🔄 **수정 요청** **리뷰어**: @${reviewer} **Pull Request**: ${prUrl} 코드 리뷰에서 수정사항이 요청되었습니다. 피드백을 확인하고 수정해주세요.`, commented: `💬 **코드 리뷰 코멘트** **리뷰어**: @${reviewer} **Pull Request**: ${prUrl} 새로운 리뷰 코멘트가 추가되었습니다.`, }; return templates[reviewState] || `코드 리뷰 이벤트: ${reviewState}`; } function generateCommitComment() { const { commitSha, commitMessage, prAuthor } = options; return `💻 **새 커밋 푸시** **작성자**: @${prAuthor} **커밋**: \`${commitSha?.substring(0, 8)}\` **메시지**: ${commitMessage} 작업이 진행되고 있습니다.`; } function generateIssueComment() { const { action } = options; const templates = { opened: "📝 **GitHub 이슈 생성**\n\n연결된 GitHub 이슈가 생성되었습니다.", closed: "🔒 **GitHub 이슈 닫힘**\n\n연결된 GitHub 이슈가 닫혔습니다.", reopened: "🔄 **GitHub 이슈 재개**\n\n연결된 GitHub 이슈가 다시 열렸습니다.", }; return templates[action] || `GitHub 이슈 이벤트: ${action}`; } /** * Linear 이슈에 코멘트 추가 */ async function addComment() { try { if (!options.issueId) { console.log("No Linear issue ID provided, skipping comment"); return; } console.log(`Adding comment to Linear issue: ${options.issueId}`); // 이슈 조회 const issue = await linear.getIssue(options.issueId); if (!issue) { console.error(`Issue ${options.issueId} not found`); return; } console.log(`Found issue: ${issue.identifier} - ${issue.title}`); // 코멘트 생성 const commentBody = generateComment(); if (!commentBody) { console.log("No comment template for this event, skipping"); return; } console.log("Comment content:"); console.log("---"); console.log(commentBody); console.log("---"); // 코멘트 추가 const result = await linear.createComment(issue.id, commentBody); if (result.success) { console.log("✅ Successfully added comment to Linear issue"); console.log(`Comment ID: ${result.comment.id}`); } else { console.error("Failed to add comment to Linear issue"); } } catch (error) { console.error("Error adding comment to Linear:", error.message); process.exit(1); } } /** * 사용 예시 출력 */ function printUsage() { console.log(` 🔗 Linear Comment Usage: # PR 생성 코멘트 node scripts/linear-comment.js \\ --issue-id=ZEL-123 \\ --event=pull_request \\ --action=opened \\ --pr-url="https://github.com/user/repo/pull/456" \\ --pr-author="username" # 코드 리뷰 코멘트 node scripts/linear-comment.js \\ --issue-id=ZEL-123 \\ --event=pull_request_review \\ --review-state=approved \\ --reviewer="reviewer-name" \\ --pr-url="https://github.com/user/repo/pull/456" # 커밋 푸시 코멘트 node scripts/linear-comment.js \\ --issue-id=ZEL-123 \\ --event=push \\ --commit-sha="abc123def" \\ --commit-message="feat: add new feature" \\ --pr-author="username" Environment Variables: - LINEAR_API_KEY: Linear API 키 (필수) 지원되는 이벤트: - pull_request (opened, closed, ready_for_review, reopened) - pull_request_review (approved, requested_changes, commented) - push (commit) - issue (opened, closed, reopened) `); } // 메인 실행 async function main() { console.log("💬 Starting Linear comment..."); if (process.env.DEBUG) { console.log("Debug info:", { issueId: options.issueId, event: options.event, action: options.action, }); } await addComment(); console.log("✅ Linear comment completed"); } // 실행 if (require.main === module) { if (process.argv.includes("--help") || process.argv.includes("-h")) { printUsage(); process.exit(0); } main().catch((error) => { console.error("Fatal error:", error); process.exit(1); }); } module.exports = { LinearClient, generateComment };