Files
zellyy-finance/scripts/linear-github-setup.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

647 lines
17 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Linear GitHub 앱 연동 및 기본 연결 설정 스크립트
* Linear와 GitHub 간의 기본 연동을 설정하고 검증
*/
const { execSync } = require("child_process");
const fs = require("fs");
const path = require("path");
// Simple command line argument parsing
const args = process.argv.slice(2);
const options = {
linearApiKey: null,
githubToken: null,
setup: args.includes("--setup"),
verify: args.includes("--verify"),
configure: args.includes("--configure"),
help: args.includes("--help") || args.includes("-h"),
};
// Extract API keys from arguments
const linearApiIndex = args.findIndex((arg) => arg.startsWith("--linear-api="));
if (linearApiIndex !== -1) {
options.linearApiKey = args[linearApiIndex].split("=")[1];
}
const githubTokenIndex = args.findIndex((arg) =>
arg.startsWith("--github-token=")
);
if (githubTokenIndex !== -1) {
options.githubToken = args[githubTokenIndex].split("=")[1];
}
/**
* Linear API 클라이언트
*/
class LinearClient {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = "https://api.linear.app/graphql";
}
async query(query, variables = {}) {
try {
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;
} catch (error) {
throw new Error(`Linear API call failed: ${error.message}`);
}
}
async getWorkspaceInfo() {
const query = `
query GetWorkspaceInfo {
organization {
id
name
urlKey
teams {
nodes {
id
name
key
issueCount
}
}
}
viewer {
id
name
email
}
}
`;
return await this.query(query);
}
async createWebhook(url, teamId) {
const query = `
mutation CreateWebhook($url: String!, $teamId: String!) {
webhookCreate(input: {
url: $url
teamId: $teamId
allPublicTeams: false
resourceTypes: ["Issue", "IssueLabel", "Comment"]
}) {
success
webhook {
id
url
enabled
}
}
}
`;
return await this.query(query, { url, teamId });
}
async listWebhooks() {
const query = `
query ListWebhooks {
webhooks {
nodes {
id
url
enabled
resourceTypes
team {
name
key
}
}
}
}
`;
return await this.query(query);
}
}
/**
* GitHub API 클라이언트
*/
class GitHubClient {
constructor(token, repo) {
this.token = token;
this.repo = repo;
this.baseUrl = "https://api.github.com";
}
async request(endpoint, method = "GET", data = null) {
const url = `${this.baseUrl}${endpoint}`;
const options = {
method,
headers: {
Authorization: `token ${this.token}`,
Accept: "application/vnd.github.v3+json",
"User-Agent": "Zellyy-Finance-Linear-Integration",
},
};
if (data) {
options.headers["Content-Type"] = "application/json";
options.body = JSON.stringify(data);
}
try {
const response = await fetch(url, options);
if (!response.ok) {
const errorData = await response.text();
throw new Error(`GitHub API Error: ${response.status} ${errorData}`);
}
return await response.json();
} catch (error) {
throw new Error(`GitHub API call failed: ${error.message}`);
}
}
async getRepository() {
return await this.request(`/repos/${this.repo}`);
}
async createWebhook(config) {
return await this.request(`/repos/${this.repo}/hooks`, "POST", config);
}
async listWebhooks() {
return await this.request(`/repos/${this.repo}/hooks`);
}
async createSecret(name, encryptedValue) {
return await this.request(
`/repos/${this.repo}/actions/secrets/${name}`,
"PUT",
{
encrypted_value: encryptedValue,
}
);
}
}
/**
* 환경 설정 검증
*/
function validateEnvironment() {
console.log("🔍 환경 설정 검증 중...");
const requiredFiles = [
".github/workflows/linear-integration.yml",
"scripts/linear-sync.cjs",
"scripts/linear-comment.cjs",
".env.linear.example",
];
const missingFiles = [];
requiredFiles.forEach((file) => {
if (!fs.existsSync(file)) {
missingFiles.push(file);
}
});
if (missingFiles.length > 0) {
console.error("❌ 필수 파일들이 누락되었습니다:");
missingFiles.forEach((file) => console.error(` - ${file}`));
return false;
}
console.log("✅ 모든 필수 파일이 존재합니다.");
return true;
}
/**
* Git 리포지토리 정보 확인
*/
function getGitInfo() {
try {
// GitHub remote이 있는지 먼저 확인
let remoteUrl;
try {
remoteUrl = execSync("git config --get remote.github.url", {
encoding: "utf8",
}).trim();
} catch {
// github remote가 없으면 origin 확인
remoteUrl = execSync("git config --get remote.origin.url", {
encoding: "utf8",
}).trim();
}
// GitHub URL에서 owner/repo 추출
const match = remoteUrl.match(/github\.com[/:](.*?)\/(.*)(?:\.git)?$/);
if (match) {
const owner = match[1];
const repo = match[2].replace(".git", "");
return { owner, repo, fullName: `${owner}/${repo}` };
}
// GitHub remote가 아닌 경우 수동으로 GitHub 정보 사용
console.log(
" GitHub remote가 감지되지 않았습니다. zellycloud/zellyy-finance를 사용합니다."
);
return {
owner: "zellycloud",
repo: "zellyy-finance",
fullName: "zellycloud/zellyy-finance",
};
} catch (error) {
throw new Error(`Git 정보를 가져올 수 없습니다: ${error.message}`);
}
}
/**
* Linear 워크스페이스 정보 출력
*/
async function displayLinearInfo(linear) {
console.log("\n📋 Linear 워크스페이스 정보:");
try {
const info = await linear.getWorkspaceInfo();
console.log(
` 조직: ${info.organization.name} (${info.organization.urlKey})`
);
console.log(` 사용자: ${info.viewer.name} (${info.viewer.email})`);
console.log(` 팀 수: ${info.organization.teams.nodes.length}`);
console.log("\n👥 팀 목록:");
info.organization.teams.nodes.forEach((team) => {
console.log(` - ${team.name} (${team.key}): ${team.issueCount}개 이슈`);
});
return info;
} catch (error) {
console.error("❌ Linear 정보를 가져올 수 없습니다:", error.message);
throw error;
}
}
/**
* GitHub 리포지토리 정보 출력
*/
async function displayGitHubInfo(github) {
console.log("\n📁 GitHub 리포지토리 정보:");
try {
const repo = await github.getRepository();
console.log(` 이름: ${repo.full_name}`);
console.log(` 설명: ${repo.description || "설명 없음"}`);
console.log(` 언어: ${repo.language || "N/A"}`);
console.log(` 프라이빗: ${repo.private ? "Yes" : "No"}`);
console.log(` 기본 브랜치: ${repo.default_branch}`);
return repo;
} catch (error) {
console.error("❌ GitHub 정보를 가져올 수 없습니다:", error.message);
throw error;
}
}
/**
* 환경 변수 파일 생성
*/
function createEnvFile(linearInfo, gitInfo) {
console.log("\n📝 환경 변수 파일 생성 중...");
const envContent = `# Linear GitHub 연동 설정
# 이 파일을 .env로 복사하고 실제 값으로 업데이트하세요
# Linear API 설정
LINEAR_API_KEY=${options.linearApiKey || "your-linear-api-key"}
LINEAR_WORKSPACE_ID=${linearInfo.organization.id}
LINEAR_TEAM_ID=${linearInfo.organization.teams.nodes[0]?.id || "your-team-id"}
# GitHub 설정
GITHUB_TOKEN=${options.githubToken || "your-github-token"}
GITHUB_REPOSITORY=${gitInfo.fullName}
# 웹훅 URL (GitHub Actions에서 자동 설정)
LINEAR_WEBHOOK_URL=https://api.github.com/repos/${gitInfo.fullName}/dispatches
# 디버그 모드
DEBUG=false
`;
const envFile = ".env.linear";
fs.writeFileSync(envFile, envContent);
console.log(`✅ 환경 변수 파일이 생성되었습니다: ${envFile}`);
console.log(" 이 파일을 .env로 복사하거나 기존 .env에 내용을 추가하세요.");
}
/**
* GitHub Secrets 설정 안내
*/
function displaySecretsGuide() {
console.log("\n🔐 GitHub Secrets 설정 안내:");
console.log("다음 secrets을 GitHub 리포지토리에 추가해야 합니다:");
console.log("");
console.log("1. Repository Settings → Secrets and variables → Actions");
console.log('2. "New repository secret" 클릭');
console.log("3. 다음 secrets 추가:");
console.log(" - Name: LINEAR_API_KEY");
console.log(` - Value: ${options.linearApiKey || "your-linear-api-key"}`);
console.log("");
console.log("선택적 secrets:");
console.log(" - SLACK_BOT_TOKEN (Slack 연동용)");
console.log(" - SLACK_WEBHOOK_URL (Slack 알림용)");
}
/**
* Linear 웹훅 설정
*/
async function setupLinearWebhooks(linear, gitInfo) {
console.log("\n🔗 Linear 웹훅 설정 중...");
try {
// 기존 웹훅 확인
const existingWebhooks = await linear.listWebhooks();
const githubWebhook = existingWebhooks.webhooks.nodes.find(
(webhook) =>
webhook.url.includes("github.com") ||
webhook.url.includes(gitInfo.fullName)
);
if (githubWebhook) {
console.log("✅ GitHub 웹훅이 이미 설정되어 있습니다:");
console.log(` URL: ${githubWebhook.url}`);
console.log(` 활성화: ${githubWebhook.enabled ? "Yes" : "No"}`);
console.log(` 팀: ${githubWebhook.team?.name || "All teams"}`);
return;
}
console.log(" Linear 웹훅 설정은 수동으로 진행해야 합니다:");
console.log("1. Linear → Settings → API → Webhooks");
console.log('2. "Create webhook" 클릭');
console.log(
`3. URL: https://api.github.com/repos/${gitInfo.fullName}/dispatches`
);
console.log("4. Resource types: Issue, Comment, IssueLabel 선택");
console.log("5. Team: 원하는 팀 선택 (또는 모든 팀)");
} catch (error) {
console.error("❌ 웹훅 설정 중 오류 발생:", error.message);
}
}
/**
* GitHub Actions 워크플로우 검증
*/
function verifyWorkflow() {
console.log("\n🔧 GitHub Actions 워크플로우 검증 중...");
const workflowPath = ".github/workflows/linear-integration.yml";
if (!fs.existsSync(workflowPath)) {
console.error("❌ Linear integration 워크플로우를 찾을 수 없습니다.");
return false;
}
const workflow = fs.readFileSync(workflowPath, "utf8");
const checks = [
{ name: "Pull request triggers", pattern: /on:\s*\n.*pull_request:/s },
{ name: "Push triggers", pattern: /push:/ },
{ name: "Linear ID extraction", pattern: /extract.*linear.*id/i },
{ name: "Environment variables", pattern: /LINEAR_API_KEY/ },
{ name: "Sync scripts", pattern: /linear-sync/ },
{ name: "Comment scripts", pattern: /linear-comment/ },
];
let allPassed = true;
checks.forEach((check) => {
if (check.pattern.test(workflow)) {
console.log(`${check.name}`);
} else {
console.log(`${check.name} 누락`);
allPassed = false;
}
});
return allPassed;
}
/**
* 통합 테스트 실행
*/
async function runIntegrationTest() {
console.log("\n🧪 통합 테스트 실행 중...");
try {
const testCommand = `node scripts/test-linear-integration.cjs --api-key=${options.linearApiKey}`;
const result = execSync(testCommand, { encoding: "utf8", stdio: "pipe" });
console.log("✅ 통합 테스트 성공");
console.log(result);
return true;
} catch (error) {
console.error("❌ 통합 테스트 실패:", error.message);
return false;
}
}
/**
* 설정 완료 안내
*/
function displaySetupComplete() {
console.log("\n🎉 Linear GitHub 연동 설정이 완료되었습니다!");
console.log("");
console.log("다음 단계:");
console.log("1. GitHub Secrets에 LINEAR_API_KEY 추가");
console.log("2. Linear에서 웹훅 설정 (위의 안내 참조)");
console.log("3. Pull Request 생성하여 연동 테스트");
console.log("");
console.log("테스트 방법:");
console.log(
"1. 새 브랜치 생성: git checkout -b feature/test-linear-integration"
);
console.log(
'2. 커밋 메시지에 Linear 이슈 ID 포함: git commit -m "feat: test integration [ZEL-1]"'
);
console.log(
'3. Pull Request 제목에 Linear 이슈 ID 포함: "Test Linear integration (ZEL-1)"'
);
console.log("4. GitHub Actions 로그에서 연동 동작 확인");
}
/**
* 사용법 출력
*/
function printUsage() {
console.log(`
🔧 Linear GitHub 연동 설정 도구
사용법:
node scripts/linear-github-setup.cjs [옵션]
옵션:
--setup 전체 설정 실행
--verify 기존 설정 검증
--configure 환경 설정만 생성
--linear-api=KEY Linear API 키 지정
--github-token=TOKEN GitHub 토큰 지정
--help, -h 이 도움말 출력
예시:
# 전체 설정 실행
node scripts/linear-github-setup.cjs --setup --linear-api=lin_api_xxx
# 기존 설정 검증
node scripts/linear-github-setup.cjs --verify
# 환경 설정만 생성
node scripts/linear-github-setup.cjs --configure --linear-api=lin_api_xxx
환경 변수:
LINEAR_API_KEY Linear API 키
GITHUB_TOKEN GitHub 개인 액세스 토큰
필수 조건:
- Git 리포지토리 (GitHub remote 설정됨)
- Linear API 키
- GitHub 개인 액세스 토큰 (repo 권한)
`);
}
/**
* 메인 함수
*/
async function main() {
if (options.help) {
printUsage();
process.exit(0);
}
console.log("🚀 Linear GitHub 연동 설정 도구");
console.log("=====================================\n");
// 환경 검증
if (!validateEnvironment()) {
console.error(
"❌ 환경 설정이 올바르지 않습니다. 먼저 필수 파일들을 생성해주세요."
);
process.exit(1);
}
// Git 정보 확인
let gitInfo;
try {
gitInfo = getGitInfo();
console.log(`✅ Git 리포지토리: ${gitInfo.fullName}`);
} catch (error) {
console.error("❌", error.message);
process.exit(1);
}
// API 키 설정
const linearApiKey = options.linearApiKey || process.env.LINEAR_API_KEY;
const githubToken = options.githubToken || process.env.GITHUB_TOKEN;
if (!linearApiKey) {
console.error(
"❌ Linear API 키가 필요합니다. --linear-api 옵션을 사용하거나 LINEAR_API_KEY 환경 변수를 설정하세요."
);
process.exit(1);
}
// 클라이언트 초기화
const linear = new LinearClient(linearApiKey);
const github = githubToken
? new GitHubClient(githubToken, gitInfo.fullName)
: null;
// 검증 모드
if (options.verify) {
console.log("🔍 기존 설정 검증 중...\n");
const linearInfo = await displayLinearInfo(linear);
if (github) await displayGitHubInfo(github);
const workflowValid = verifyWorkflow();
const testPassed = await runIntegrationTest();
if (workflowValid && testPassed) {
console.log("\n✅ 모든 검증이 성공했습니다!");
} else {
console.log("\n❌ 일부 검증이 실패했습니다.");
process.exit(1);
}
return;
}
// 설정 모드
if (options.setup || options.configure) {
console.log("⚙️ Linear GitHub 연동 설정 시작...\n");
// Linear 정보 확인
const linearInfo = await displayLinearInfo(linear);
// GitHub 정보 확인 (토큰이 있는 경우)
if (github) {
await displayGitHubInfo(github);
}
// 환경 설정 파일 생성
createEnvFile(linearInfo, gitInfo);
if (options.setup) {
// 웹훅 설정
await setupLinearWebhooks(linear, gitInfo);
// Secrets 안내
displaySecretsGuide();
// 워크플로우 검증
verifyWorkflow();
// 완료 안내
displaySetupComplete();
}
return;
}
// 기본 동작: 정보 표시
const linearInfo = await displayLinearInfo(linear);
if (github) await displayGitHubInfo(github);
console.log("\n💡 다음 명령어로 설정을 진행하세요:");
console.log(
` node scripts/linear-github-setup.cjs --setup --linear-api=${linearApiKey}`
);
}
// 실행
if (require.main === module) {
main().catch((error) => {
console.error("\n❌ 설정 중 오류 발생:", error.message);
process.exit(1);
});
}
module.exports = { LinearClient, GitHubClient, validateEnvironment };