✨ 주요 개선사항: - 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>
430 lines
11 KiB
JavaScript
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();
|