Files
zellyy-finance/scripts/linear-release-complete.cjs
hansoo 8343b25439 feat: Stage 2 TypeScript 타입 안전성 개선 - any 타입 83개 → 62개 대폭 감소
 주요 개선사항:
- any 타입 83개에서 62개로 21개 수정 (25% 감소)
- 모든 ESLint 에러 11개 → 0개 완전 해결
- 타입 안전성 대폭 향상으로 런타임 오류 가능성 감소

🔧 수정된 파일들:
• PWADebug.tsx - 사용하지 않는 import들에 _ prefix 추가
• categoryUtils.ts - 불필요한 any 캐스트 제거
• TransactionsHeader.tsx - BudgetData 인터페이스 정의
• storageUtils.ts - generic 타입과 unknown 타입 적용
• 각종 error handler들 - Error | {message?: string} 타입 적용
• test 파일들 - 적절한 mock 인터페이스 정의
• 유틸리티 파일들 - any → unknown 또는 적절한 타입으로 교체

🏆 성과:
- 코드 품질 크게 향상 (280 → 80 문제로 71% 감소)
- TypeScript 컴파일러의 타입 체크 효과성 증대
- 개발자 경험 개선 (IDE 자동완성, 타입 추론 등)

🧹 추가 정리:
- ESLint no-console/no-alert 경고 해결
- Prettier 포맷팅 적용으로 코드 스타일 통일

🎯 다음 단계: 남은 62개 any 타입 계속 개선 예정

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 10:08:51 +09:00

430 lines
11 KiB
JavaScript

#!/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 <version>");
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();