#!/usr/bin/env node /** * Linear 이슈 상태 동기화 스크립트 * GitHub 이벤트에 따라 Linear 이슈 상태를 자동으로 업데이트 */ // Simple command line argument parsing without commander dependency // 임시 Linear 클라이언트 (실제 구현 시 SDK 사용) 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 { id name } team { id name } } } `; const result = await this.query(query, { id: issueId }); return result.issue; } async updateIssue(issueId, input) { const query = ` mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) { issueUpdate(id: $id, input: $input) { success issue { id state { name } } } } `; const result = await this.query(query, { id: issueId, input }); return result.issueUpdate; } async getWorkflowStates(teamId) { const query = ` query GetWorkflowStates($teamId: ID!) { workflowStates(filter: { team: { id: { eq: $teamId } } }) { nodes { id name type } } } `; const result = await this.query(query, { teamId }); return result.workflowStates.nodes; } async createComment(issueId, body) { const query = ` mutation CreateComment($issueId: String!, $body: String!) { commentCreate(input: { issueId: $issueId, body: $body }) { success comment { id body } } } `; 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, prMerged: "false", apiKey: 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("--pr-merged=")) { options.prMerged = arg.split("=")[1]; } else if (arg.startsWith("--api-key=")) { options.apiKey = arg.split("=")[1]; } }); // Linear 클라이언트 초기화 const apiKey = options.apiKey || process.env.LINEAR_API_KEY; if (!apiKey) { console.error("Error: Linear API key not provided"); process.exit(1); } const linear = new LinearClient({ apiKey }); /** * 이슈 상태 업데이트 */ async function updateIssueStatus() { try { if (!options.issueId) { console.log("No Linear issue ID provided, skipping"); return; } console.log(`Processing 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}`); console.log(`Current state: ${issue.state.name}`); // 상태 변경 로직 let newStateName = null; let comment = null; if (options.event === "pull_request") { switch (options.action) { case "opened": newStateName = "In Progress"; comment = `🔗 Pull Request opened: ${options.prUrl}\nAuthor: @${options.prAuthor}`; break; case "ready_for_review": newStateName = "In Review"; comment = `👀 Pull Request is ready for review: ${options.prUrl}`; break; case "closed": if (options.prMerged === "true") { newStateName = "Done"; comment = `✅ Pull Request merged: ${options.prUrl}`; } else { comment = `❌ Pull Request closed without merging: ${options.prUrl}`; } break; } } else if (options.event === "push" && options.action === "commit") { // 커밋 푸시 시 In Progress로 변경 if (issue.state.name === "Todo" || issue.state.name === "Backlog") { newStateName = "In Progress"; comment = `💻 Work started with new commits`; } } // 상태 업데이트 필요시 if (newStateName && newStateName !== issue.state.name) { console.log(`Updating state: ${issue.state.name} → ${newStateName}`); // 팀의 워크플로우 상태 조회 const states = await linear.getWorkflowStates(issue.team.id); const targetState = states.find((s) => s.name === newStateName); if (targetState) { const result = await linear.updateIssue(issue.id, { stateId: targetState.id, }); if (result.success) { console.log( `✅ Successfully updated issue state to: ${newStateName}` ); } else { console.error("Failed to update issue state"); } } else { console.warn(`State "${newStateName}" not found in team workflow`); } } // 코멘트 추가 if (comment) { const commentResult = await linear.createComment(issue.id, comment); if (commentResult.success) { console.log("✅ Added comment to issue"); } } } catch (error) { console.error("Error syncing with Linear:", error.message); process.exit(1); } } /** * 디버그 정보 출력 */ function debugInfo() { console.log("\n=== Linear Sync Debug Info ==="); console.log("Options:", { issueId: options.issueId, event: options.event, action: options.action, prUrl: options.prUrl, prAuthor: options.prAuthor, prMerged: options.prMerged, }); console.log("API Key:", apiKey ? "Provided" : "Missing"); console.log("==============================\n"); } // 메인 실행 async function main() { console.log("🔄 Starting Linear sync..."); if (process.env.DEBUG) { debugInfo(); } await updateIssueStatus(); console.log("✅ Linear sync completed"); } // 실행 main().catch((error) => { console.error("Fatal error:", error); process.exit(1); });