name: Mobile Build and Release on: push: branches: [main] tags: ["v*"] pull_request: branches: [main] env: NODE_VERSION: "18" JAVA_VERSION: "17" XCODE_VERSION: "15.0" jobs: test: name: Test and Lint runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: "npm" - name: Install dependencies run: npm ci - name: Run linting run: npm run lint - name: Run type checking run: npm run type-check - name: Run comprehensive tests run: npm run test:ci build-web: name: Build Web App runs-on: ubuntu-latest needs: test steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: "npm" - name: Install dependencies run: npm ci - name: Build web app run: npm run build:prod env: VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }} VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }} VITE_CLERK_PUBLISHABLE_KEY: ${{ secrets.VITE_CLERK_PUBLISHABLE_KEY }} VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }} VITE_SENTRY_ENVIRONMENT: production - name: Upload web build artifacts uses: actions/upload-artifact@v4 with: name: web-build path: dist/ build-android: name: Build Android App runs-on: ubuntu-latest needs: build-web if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: "npm" - name: Setup Java uses: actions/setup-java@v4 with: distribution: "temurin" java-version: ${{ env.JAVA_VERSION }} - name: Setup Android SDK uses: android-actions/setup-android@v3 - name: Install dependencies run: npm ci - name: Download web build uses: actions/download-artifact@v4 with: name: web-build path: dist/ - name: Sync Capacitor run: npm run mobile:sync - name: Create keystore directory if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') run: mkdir -p android/app/keystore - name: Decode keystore if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') run: | echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > android/app/keystore/release.keystore ls -la android/app/keystore/ - name: Set CI environment if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') run: echo "CI=true" >> $GITHUB_ENV env: ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} - name: Build Android Debug APK if: github.event_name == 'pull_request' run: | cd android ./gradlew assembleDebug - name: Build Android Release Bundle if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') run: | cd android ./gradlew bundleRelease --info env: CI: true ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} - name: Build Android Release APK if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') run: | cd android ./gradlew assembleRelease --info env: CI: true ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} - name: Upload Android artifacts uses: actions/upload-artifact@v4 with: name: android-artifacts path: | android/app/build/outputs/bundle/release/*.aab android/app/build/outputs/apk/release/*.apk android/app/build/outputs/apk/debug/*.apk build-ios: name: Build iOS App runs-on: macos-14 needs: build-web if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: "npm" - name: Setup Xcode uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: ${{ env.XCODE_VERSION }} - name: Install dependencies run: npm ci - name: Download web build uses: actions/download-artifact@v4 with: name: web-build path: dist/ - name: Sync Capacitor run: npm run mobile:sync - name: Install CocoaPods dependencies run: | cd ios/App pod install - name: Import Code-Signing Certificates if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') uses: Apple-Actions/import-codesign-certs@v3 with: p12-file-base64: ${{ secrets.IOS_CERTIFICATES_P12_BASE64 }} p12-password: ${{ secrets.IOS_CERTIFICATES_P12_PASSWORD }} - name: Download Provisioning Profiles if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') uses: Apple-Actions/download-provisioning-profiles@v3 with: bundle-id: com.zellyy.finance profile-type: "IOS_APP_STORE" issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }} api-key-id: ${{ secrets.APPSTORE_KEY_ID }} api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }} - name: Build iOS Debug if: github.event_name == 'pull_request' run: | cd ios/App xcodebuild -workspace App.xcworkspace -scheme App -configuration Debug -destination 'generic/platform=iOS Simulator' build - name: Build iOS Release Archive if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') run: | cd ios/App xcodebuild -workspace App.xcworkspace -scheme App -configuration Release -destination 'generic/platform=iOS' -archivePath App.xcarchive archive - name: Export iOS IPA if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') run: | cd ios/App xcodebuild -exportArchive -archivePath App.xcarchive -exportPath ./build -exportOptionsPlist ExportOptions.plist - name: Upload iOS artifacts if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') uses: actions/upload-artifact@v4 with: name: ios-artifacts path: ios/App/build/*.ipa release: name: Semantic Release runs-on: ubuntu-latest needs: [build-android, build-ios] if: github.ref == 'refs/heads/main' steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: "npm" - name: Install dependencies run: npm ci - name: Download Android artifacts uses: actions/download-artifact@v4 with: name: android-artifacts path: android/app/build/outputs/ - name: Download iOS artifacts uses: actions/download-artifact@v4 with: name: ios-artifacts path: ios/App/build/ - name: Sync versions before release run: npm run version:sync - name: Update store metadata run: npm run store:metadata - name: Semantic Release run: npx semantic-release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} HUSKY: 0 deploy-android: name: Deploy to Google Play runs-on: ubuntu-latest needs: release if: github.ref == 'refs/heads/main' steps: - name: Checkout code uses: actions/checkout@v4 - name: Download Android artifacts uses: actions/download-artifact@v4 with: name: android-artifacts path: android/app/build/outputs/ - name: Generate release notes for Google Play id: release-notes-android run: | # 최신 릴리즈 노트 추출 (Google Play 500자 제한) if [ -f "CHANGELOG.md" ]; then NOTES=$(head -c 450 CHANGELOG.md | sed 's/## \[.*\]//' | sed 's/### /• /' | tr '\n' ' ') else NOTES="Zellyy Finance 새 버전이 출시되었습니다. 향상된 성능과 새로운 기능을 경험해보세요." fi echo "RELEASE_NOTES=${NOTES}" >> $GITHUB_OUTPUT - name: Upload to Google Play uses: r0adkll/upload-google-play@v1 with: serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} packageName: com.zellyy.finance releaseFiles: android/app/build/outputs/bundle/release/*.aab track: internal status: completed whatsNewDirectory: android/metadata/ releaseNotes: ${{ steps.release-notes-android.outputs.RELEASE_NOTES }} deploy-ios: name: Deploy to TestFlight runs-on: macos-14 needs: release if: github.ref == 'refs/heads/main' steps: - name: Checkout code uses: actions/checkout@v4 - name: Download iOS artifacts uses: actions/download-artifact@v4 with: name: ios-artifacts path: ios/App/build/ - name: Generate release notes for TestFlight id: release-notes-ios run: | # 최신 릴리즈 노트 추출 (TestFlight 4000자 제한) if [ -f "CHANGELOG.md" ]; then NOTES=$(head -c 3000 CHANGELOG.md | sed 's/## \[.*\]/Zellyy Finance 업데이트/' | sed 's/### /• /') else NOTES="Zellyy Finance 새 버전이 출시되었습니다.\n\n향상된 성능과 새로운 기능을 경험해보세요.\n\n문의사항이 있으시면 개발팀에 연락주세요." fi echo "RELEASE_NOTES=${NOTES}" >> $GITHUB_OUTPUT - name: Upload to TestFlight uses: Apple-Actions/upload-testflight-build@v1 with: app-path: ios/App/build/App.ipa issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }} api-key-id: ${{ secrets.APPSTORE_KEY_ID }} api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }} changelog: ${{ steps.release-notes-ios.outputs.RELEASE_NOTES }} notify: name: Notify Build Status runs-on: ubuntu-latest needs: [ test, build-web, build-android, build-ios, release, deploy-android, deploy-ios, ] if: always() steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - name: Notify Success if: needs.deploy-android.result == 'success' && needs.deploy-ios.result == 'success' run: | node scripts/notification-handler.cjs success "🎉 배포 성공!" "Android 및 iOS 앱이 성공적으로 배포되었습니다." env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} NOTIFICATION_EMAIL: ${{ secrets.NOTIFICATION_EMAIL }} - name: Notify Failure if: | needs.test.result == 'failure' || needs.build-web.result == 'failure' || needs.build-android.result == 'failure' || needs.build-ios.result == 'failure' || needs.release.result == 'failure' || needs.deploy-android.result == 'failure' || needs.deploy-ios.result == 'failure' run: | node scripts/notification-handler.cjs failure "💥 빌드/배포 실패" "파이프라인 실행 중 오류가 발생했습니다. 상세 내용은 GitHub Actions 로그를 확인하세요." env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} NOTIFICATION_EMAIL: ${{ secrets.NOTIFICATION_EMAIL }} - name: Notify Warning if: | !contains(fromJSON('["failure"]'), needs.test.result) && !contains(fromJSON('["failure"]'), needs.build-web.result) && !contains(fromJSON('["failure"]'), needs.build-android.result) && !contains(fromJSON('["failure"]'), needs.build-ios.result) && !contains(fromJSON('["failure"]'), needs.release.result) && (needs.deploy-android.result == 'failure' || needs.deploy-ios.result == 'failure') run: | node scripts/notification-handler.cjs warning "⚠️ 부분 배포 성공" "빌드는 성공했지만 일부 배포에서 문제가 발생했습니다." env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} NOTIFICATION_EMAIL: ${{ secrets.NOTIFICATION_EMAIL }}