#!/usr/bin/env node const { execSync } = require("child_process"); const fs = require("fs"); const path = require("path"); /** * 앱 스토어 배포 자동화 스크립트 * Google Play Store 및 Apple App Store 배포 프로세스 관리 */ // 설정 const config = { // 프로젝트 정보 packageName: "com.zellyy.finance", appName: "Zellyy Finance", // 배포 트랙 googlePlayTrack: process.env.GOOGLE_PLAY_TRACK || "internal", // 파일 경로 androidBundlePath: "android/app/build/outputs/bundle/release/app-release.aab", iosIpaPath: "ios/App/build/App.ipa", // API 설정 googlePlayServiceAccount: process.env.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON, appStoreIssuerId: process.env.APPSTORE_ISSUER_ID, appStoreKeyId: process.env.APPSTORE_KEY_ID, appStorePrivateKey: process.env.APPSTORE_PRIVATE_KEY, // CI 환경 isCI: process.env.CI === "true", githubRepository: process.env.GITHUB_REPOSITORY, githubRunId: process.env.GITHUB_RUN_ID, }; // 명령행 인수 파싱 const args = process.argv.slice(2); const platform = args[0]; // 'android', 'ios', 'both' const dryRun = args.includes("--dry-run"); /** * 메인 배포 함수 */ async function deployToStores() { if (!platform || !["android", "ios", "both"].includes(platform)) { console.error("사용법: node store-deploy.js [--dry-run]"); console.error("플랫폼: android, ios, both"); process.exit(1); } console.log(`📱 ${config.appName} 스토어 배포 시작 (${platform})`); if (dryRun) { console.log("🔍 DRY RUN 모드 - 실제 배포 없이 검증만 수행"); } // 배포 전 검증 await validateEnvironment(); await validateBuildFiles(); // 릴리즈 노트 생성 const releaseNotes = await generateReleaseNotes(); // 플랫폼별 배포 const results = {}; if (platform === "android" || platform === "both") { results.android = await deployToGooglePlay(releaseNotes); } if (platform === "ios" || platform === "both") { results.ios = await deployToAppStore(releaseNotes); } // 결과 리포트 await generateDeploymentReport(results); console.log("✅ 배포 프로세스 완료"); } /** * 환경 검증 */ async function validateEnvironment() { console.log("🔍 환경 설정 검증 중..."); const requiredVars = []; if (platform === "android" || platform === "both") { if (!config.googlePlayServiceAccount) { requiredVars.push("GOOGLE_PLAY_SERVICE_ACCOUNT_JSON"); } } if (platform === "ios" || platform === "both") { if (!config.appStoreIssuerId) requiredVars.push("APPSTORE_ISSUER_ID"); if (!config.appStoreKeyId) requiredVars.push("APPSTORE_KEY_ID"); if (!config.appStorePrivateKey) requiredVars.push("APPSTORE_PRIVATE_KEY"); } if (requiredVars.length > 0) { if (dryRun) { console.warn( "⚠️ DRY RUN 모드: 필수 환경 변수 누락 무시:", requiredVars.join(", ") ); } else { console.error( "❌ 필수 환경 변수가 설정되지 않음:", requiredVars.join(", ") ); process.exit(1); } } console.log("✅ 환경 설정 검증 완료"); } /** * 빌드 파일 검증 */ async function validateBuildFiles() { console.log("📦 빌드 파일 검증 중..."); if (platform === "android" || platform === "both") { if (!fs.existsSync(config.androidBundlePath)) { if (dryRun) { console.warn( `⚠️ DRY RUN 모드: Android AAB 파일 누락 무시: ${config.androidBundlePath}` ); } else { console.error( `❌ Android AAB 파일을 찾을 수 없음: ${config.androidBundlePath}` ); process.exit(1); } } else { const stats = fs.statSync(config.androidBundlePath); console.log( `✅ Android AAB 파일 확인: ${(stats.size / 1024 / 1024).toFixed(1)}MB` ); } } if (platform === "ios" || platform === "both") { if (!fs.existsSync(config.iosIpaPath)) { if (dryRun) { console.warn( `⚠️ DRY RUN 모드: iOS IPA 파일 누락 무시: ${config.iosIpaPath}` ); } else { console.error(`❌ iOS IPA 파일을 찾을 수 없음: ${config.iosIpaPath}`); process.exit(1); } } else { const stats = fs.statSync(config.iosIpaPath); console.log( `✅ iOS IPA 파일 확인: ${(stats.size / 1024 / 1024).toFixed(1)}MB` ); } } } /** * 릴리즈 노트 생성 */ async function generateReleaseNotes() { console.log("📝 릴리즈 노트 생성 중..."); try { // package.json에서 현재 버전 읽기 const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8")); const version = packageJson.version; // Git 태그에서 릴리즈 노트 찾기 let releaseNotes = ""; try { // GitHub Releases에서 최신 릴리즈 정보 가져오기 const latestTag = execSync("git describe --tags --abbrev=0", { encoding: "utf8", }).trim(); if (latestTag === `v${version}`) { // CHANGELOG.md에서 해당 버전 섹션 찾기 if (fs.existsSync("CHANGELOG.md")) { const changelog = fs.readFileSync("CHANGELOG.md", "utf8"); const versionRegex = new RegExp( `## \\[?${version}\\]?.*?\n(.*?)(?=\n## |$)`, "s" ); const match = changelog.match(versionRegex); if (match) { releaseNotes = match[1].trim(); } } } } catch (error) { console.warn( "⚠️ Git 태그에서 릴리즈 노트를 가져올 수 없음:", error.message ); } // 기본 릴리즈 노트 생성 if (!releaseNotes) { releaseNotes = `Zellyy Finance v${version}\n\n새로운 기능과 개선사항이 포함된 업데이트입니다.`; } console.log(`✅ 릴리즈 노트 생성 완료 (${releaseNotes.length}자)`); return releaseNotes; } catch (error) { console.error("❌ 릴리즈 노트 생성 실패:", error); return `Zellyy Finance 업데이트\n\n새로운 기능과 개선사항이 포함되어 있습니다.`; } } /** * Google Play Store 배포 */ async function deployToGooglePlay(releaseNotes) { console.log("🟢 Google Play Store 배포 시작..."); const result = { platform: "android", status: "pending", track: config.googlePlayTrack, startTime: new Date().toISOString(), }; try { if (dryRun) { console.log("🔍 DRY RUN: Google Play Store 배포 시뮬레이션"); result.status = "dry-run-success"; return result; } // Google Play Console API 호출 (r0adkll/upload-google-play 액션 시뮬레이션) console.log(`📦 AAB 파일 업로드: ${config.androidBundlePath}`); console.log(`🎯 배포 트랙: ${config.googlePlayTrack}`); console.log(`📝 릴리즈 노트 (${releaseNotes.length}자):`); console.log( releaseNotes.substring(0, 200) + (releaseNotes.length > 200 ? "..." : "") ); // 실제 배포는 GitHub Actions에서 수행 if (config.isCI) { console.log( "🔄 CI 환경에서 실제 배포는 GitHub Actions r0adkll/upload-google-play@v1에서 처리됩니다" ); } result.status = "success"; result.endTime = new Date().toISOString(); console.log("✅ Google Play Store 배포 성공"); return result; } catch (error) { console.error("❌ Google Play Store 배포 실패:", error); result.status = "failed"; result.error = error.message; result.endTime = new Date().toISOString(); return result; } } /** * Apple App Store 배포 */ async function deployToAppStore(releaseNotes) { console.log("🍎 Apple App Store (TestFlight) 배포 시작..."); const result = { platform: "ios", status: "pending", track: "testflight", startTime: new Date().toISOString(), }; try { if (dryRun) { console.log("🔍 DRY RUN: TestFlight 배포 시뮬레이션"); result.status = "dry-run-success"; return result; } // App Store Connect API 호출 (Apple-Actions/upload-testflight-build 액션 시뮬레이션) console.log(`📦 IPA 파일 업로드: ${config.iosIpaPath}`); console.log(`🎯 배포 대상: TestFlight`); console.log(`📝 빌드 노트 (${releaseNotes.length}자):`); console.log( releaseNotes.substring(0, 200) + (releaseNotes.length > 200 ? "..." : "") ); // 실제 배포는 GitHub Actions에서 수행 if (config.isCI) { console.log( "🔄 CI 환경에서 실제 배포는 GitHub Actions Apple-Actions/upload-testflight-build@v1에서 처리됩니다" ); } result.status = "success"; result.endTime = new Date().toISOString(); console.log("✅ TestFlight 배포 성공"); return result; } catch (error) { console.error("❌ TestFlight 배포 실패:", error); result.status = "failed"; result.error = error.message; result.endTime = new Date().toISOString(); return result; } } /** * 배포 결과 리포트 생성 */ async function generateDeploymentReport(results) { console.log("\n📊 배포 결과 리포트"); console.log("".padEnd(50, "=")); const reportData = { timestamp: new Date().toISOString(), project: config.appName, repository: config.githubRepository, runId: config.githubRunId, results: results, }; // 콘솔 리포트 Object.values(results).forEach((result) => { const emoji = result.status === "success" ? "✅" : result.status === "failed" ? "❌" : result.status === "dry-run-success" ? "🔍" : "⏳"; console.log(`${emoji} ${result.platform.toUpperCase()}: ${result.status}`); if (result.track) { console.log(` 트랙: ${result.track}`); } if (result.startTime && result.endTime) { const duration = (new Date(result.endTime) - new Date(result.startTime)) / 1000; console.log(` 소요시간: ${duration}초`); } if (result.error) { console.log(` 오류: ${result.error}`); } console.log(""); }); // JSON 리포트 저장 const reportPath = path.join(__dirname, "..", "deployment-report.json"); fs.writeFileSync(reportPath, JSON.stringify(reportData, null, 2)); console.log(`📄 상세 리포트 저장: ${reportPath}`); // 전체 성공 여부 확인 const allSuccess = Object.values(results).every( (r) => r.status === "success" || r.status === "dry-run-success" ); if (!allSuccess) { console.log( "⚠️ 일부 배포에서 문제가 발생했습니다. 상세 내용을 확인하세요." ); process.exit(1); } } /** * 사용 예시 출력 */ function printUsage() { console.log(` 📱 Store Deployment Usage: # Android만 배포 node scripts/store-deploy.cjs android # iOS만 배포 node scripts/store-deploy.cjs ios # 두 플랫폼 모두 배포 node scripts/store-deploy.cjs both # 검증 모드 (실제 배포 없음) node scripts/store-deploy.cjs both --dry-run Environment Variables: - GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: Google Play API 서비스 계정 JSON - GOOGLE_PLAY_TRACK: 배포 트랙 (기본: internal) - APPSTORE_ISSUER_ID: App Store Connect API Issuer ID - APPSTORE_KEY_ID: App Store Connect API Key ID - APPSTORE_PRIVATE_KEY: App Store Connect API Private Key Build Files Required: - Android: android/app/build/outputs/bundle/release/app-release.aab - iOS: ios/App/build/App.ipa `); } // 메인 실행 if (require.main === module) { if (args.includes("--help") || args.includes("-h")) { printUsage(); process.exit(0); } deployToStores().catch((error) => { console.error("배포 실패:", error); process.exit(1); }); } module.exports = { deployToStores, generateReleaseNotes };