From cd3d4f947bccd7523194cf38e73e29adf23f1735 Mon Sep 17 00:00:00 2001 From: Yu Jin Date: Tue, 30 Dec 2025 15:09:44 +0900 Subject: [PATCH 1/6] chore: .gitignore --- .gitignore | 88 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 53 insertions(+), 35 deletions(-) diff --git a/.gitignore b/.gitignore index 55db79b45..db09f6aeb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # Built application files *.apk -*.aar *.ap_ *.aab @@ -14,21 +13,14 @@ bin/ gen/ out/ -# Uncomment the following line in case you need and you don't have the release build type files in your app -# release/ +release/ # Gradle files .gradle/ -build +build/ -# release -/app/release/ - -# Local configuration file (sdk path, etc) -> APP_KEY 포함 -/build +# Local configuration file (sdk path, etc) local.properties -app/src/main/res/values/kakao_string.xml -app/google-services.json # Proguard folder generated by Eclipse proguard/ @@ -44,17 +36,12 @@ captures/ # IntelliJ *.iml -.idea/ -.idea/* -.idea/compiler.xml -.idea/misc.xml .idea/workspace.xml .idea/tasks.xml .idea/gradle.xml .idea/assetWizardSettings.xml .idea/dictionaries .idea/libraries -.idea/deploymentTargetDropDown.xml # Android Studio 3 in .gitignore file. .idea/caches .idea/modules.xml @@ -63,8 +50,8 @@ captures/ # Keystore files # Uncomment the following lines if you do not want to check your keystore files in. -#*.jks -#*.keystore +*.jks +*.keystore # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild @@ -78,24 +65,55 @@ freeline.py freeline/ freeline_project_description.json +# fastlane reports +*/fastlane/report.xml +*/fastlane/Preview.html +*/fastlane/screenshots +*/fastlane/test_output + +# fastlane match +**/fastlane/match + +# Bundle artifact +*.jsbundle + +# CocoaPods +/ios/Pods/ + +# Temporary files created by Metro to check the health of the file watcher +.metro-health-check* + # fastlane fastlane/report.xml fastlane/Preview.html -fastlane/screenshots +fastlane/screenshots/**/*.png fastlane/test_output -fastlane/readme.md - -# Version control -vcs.xml - -# lint -lint/intermediates/ -lint/generated/ -lint/outputs/ -lint/tmp/ -# lint/reports/ - -.DS_Store -._.DS_Store -**/.DS_Store -**/._.DS_Store \ No newline at end of file + +# Ruby +*.gem +*.rbc +/.config +/coverage/ +/InstalledFiles +/pkg/ +/spec/reports/ +/spec/examples.txt +/test/tmp/ +/test/version_tmp/ +/tmp/ + +# Ruby version manager +.ruby-version +.ruby-gemset + +# Bundler +.bundle/ +vendor/bundle +lib/bundler/man/ + +# fastlane specific +fastlane/.env +fastlane/.env.* + +# Appfile +# fastlane/Appfile (주석 해제하면 Appfile도 gitignore에 포함) From 486c38f32eac92f0b52ca450c1dfb85b92d3fc0f Mon Sep 17 00:00:00 2001 From: Yu Jin Date: Tue, 30 Dec 2025 15:09:44 +0900 Subject: [PATCH 2/6] chore: .gitignore --- .gitignore | 87 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 53 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index 55db79b45..8f5400b3a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # Built application files *.apk -*.aar *.ap_ *.aab @@ -14,21 +13,14 @@ bin/ gen/ out/ -# Uncomment the following line in case you need and you don't have the release build type files in your app -# release/ +release/ # Gradle files .gradle/ -build +build/ -# release -/app/release/ - -# Local configuration file (sdk path, etc) -> APP_KEY 포함 -/build +# Local configuration file (sdk path, etc) local.properties -app/src/main/res/values/kakao_string.xml -app/google-services.json # Proguard folder generated by Eclipse proguard/ @@ -45,16 +37,12 @@ captures/ # IntelliJ *.iml .idea/ -.idea/* -.idea/compiler.xml -.idea/misc.xml .idea/workspace.xml .idea/tasks.xml .idea/gradle.xml .idea/assetWizardSettings.xml .idea/dictionaries .idea/libraries -.idea/deploymentTargetDropDown.xml # Android Studio 3 in .gitignore file. .idea/caches .idea/modules.xml @@ -63,8 +51,8 @@ captures/ # Keystore files # Uncomment the following lines if you do not want to check your keystore files in. -#*.jks -#*.keystore +*.jks +*.keystore # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild @@ -78,24 +66,55 @@ freeline.py freeline/ freeline_project_description.json +# fastlane reports +*/fastlane/report.xml +*/fastlane/Preview.html +*/fastlane/screenshots +*/fastlane/test_output + +# fastlane match +**/fastlane/match + +# Bundle artifact +*.jsbundle + +# CocoaPods +/ios/Pods/ + +# Temporary files created by Metro to check the health of the file watcher +.metro-health-check* + # fastlane fastlane/report.xml fastlane/Preview.html -fastlane/screenshots +fastlane/screenshots/**/*.png fastlane/test_output -fastlane/readme.md - -# Version control -vcs.xml - -# lint -lint/intermediates/ -lint/generated/ -lint/outputs/ -lint/tmp/ -# lint/reports/ - -.DS_Store -._.DS_Store -**/.DS_Store -**/._.DS_Store \ No newline at end of file + +# Ruby +*.gem +*.rbc +/.config +/coverage/ +/InstalledFiles +/pkg/ +/spec/reports/ +/spec/examples.txt +/test/tmp/ +/test/version_tmp/ +/tmp/ + +# Ruby version manager +.ruby-version +.ruby-gemset + +# Bundler +.bundle/ +vendor/bundle +lib/bundler/man/ + +# fastlane specific +fastlane/.env +fastlane/.env.* + +# Appfile +# fastlane/Appfile (주석 해제하면 Appfile도 gitignore에 포함) From 7cf7dc1d9bd9a650a3591eb57399ddf40997f555 Mon Sep 17 00:00:00 2001 From: Yu Jin Date: Tue, 30 Dec 2025 17:13:00 +0900 Subject: [PATCH 3/6] feat: fastlane --- .github/workflows/fastlane.yml | 135 ++++++++++++++++ Gemfile | 9 ++ README.md | 39 ++++- RELEASE_NOTES.txt | 2 + fastlane/Appfile | 6 + fastlane/Fastfile | 272 +++++++++++++++++++++++++++++++++ 6 files changed, 462 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/fastlane.yml create mode 100644 Gemfile create mode 100644 RELEASE_NOTES.txt create mode 100644 fastlane/Appfile create mode 100644 fastlane/Fastfile diff --git a/.github/workflows/fastlane.yml b/.github/workflows/fastlane.yml new file mode 100644 index 000000000..d765fe691 --- /dev/null +++ b/.github/workflows/fastlane.yml @@ -0,0 +1,135 @@ +name: Fastlane CI/CD + +on: + workflow_dispatch: + inputs: + lane: + description: 'Fastlane lane to run' + required: true + default: 'build_qa_apk' + type: choice + options: + - build_qa_apk + - build_release_apk + - build_release_aab + - deploy_internal_test + - ci_build + environment: + description: 'Build environment' + required: true + default: 'qa' + type: choice + options: + - qa + - production + push: + branches: + - develop + - main + - master + pull_request: + branches: + - develop + - main + - master + +jobs: + fastlane: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2' + bundler-cache: true + + - name: Install dependencies + run: | + bundle install + + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Setup environment variables + env: + DEV_BASE_URL: ${{ secrets.DEV_BASE_URL }} + PROD_BASE_URL: ${{ secrets.PROD_BASE_URL }} + KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} + NAVER_MAPS_CLIENT_ID: ${{ secrets.NAVER_MAPS_CLIENT_ID }} + POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} + POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }} + GOOGLE_SERVICE: ${{ secrets.GOOGLE_SERVICE }} + GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} + run: | + echo "Environment variables set" + + - name: Run fastlane + env: + DEV_BASE_URL: ${{ secrets.DEV_BASE_URL }} + PROD_BASE_URL: ${{ secrets.PROD_BASE_URL }} + KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} + NAVER_MAPS_CLIENT_ID: ${{ secrets.NAVER_MAPS_CLIENT_ID }} + POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} + POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }} + GOOGLE_SERVICE: ${{ secrets.GOOGLE_SERVICE }} + GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + bundle exec fastlane ${{ github.event.inputs.lane }} environment:${{ github.event.inputs.environment }} + else + bundle exec fastlane ci_build + fi + + - name: Upload APK artifact + if: | + github.event_name == 'workflow_dispatch' && + (contains(github.event.inputs.lane, 'apk') || github.event.inputs.lane == 'build_qa_apk') + uses: actions/upload-artifact@v4 + with: + name: app-apk + path: | + app/build/outputs/apk/**/*.apk + retention-days: 7 + + - name: Upload AAB artifact + if: | + github.event_name == 'workflow_dispatch' && + (contains(github.event.inputs.lane, 'aab') || github.event.inputs.lane == 'build_release_aab' || github.event.inputs.lane == 'deploy_internal_test') + uses: actions/upload-artifact@v4 + with: + name: app-aab + path: | + app/build/outputs/bundle/**/*.aab + retention-days: 30 + + - name: Slack notification + if: always() + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + fields: workflow,commit,repo,author,job,ref,took + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + diff --git a/Gemfile b/Gemfile new file mode 100644 index 000000000..c428b9679 --- /dev/null +++ b/Gemfile @@ -0,0 +1,9 @@ +# Gemfile for fastlane +source "https://rubygems.org" + +gem "fastlane" + +# 필요한 경우 추가 gem 설치 +# gem "cocoapods" # iOS용 +# gem "google-cloud-storage" # Google Cloud Storage 사용 시 + diff --git a/README.md b/README.md index 4c74d5321..97c8f5d13 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,43 @@ - minSDK : 28 - targetSDK : 35 -## 🐚 Convertion +## 🐚 Convention - [Android Convention Docs](https://github.com/EAT-SSU/Android/wiki/Android-convention) - [Git Convention Docs](https://github.com/EAT-SSU/Android/wiki/Git-convention) + +## 🚀 CI/CD with Fastlane + +이 프로젝트는 fastlane을 사용하여 자동화된 빌드 및 배포를 지원합니다. + +**📖 상세한 가이드는 다음 문서를 참조하세요:** +👉 **[Fastlane을 이용한 배포 총 정리](.github/FASTLANE_DEPLOYMENT_GUIDE.md)** + +### 빠른 시작 + +#### 로컬에서 사용 + +```bash +# 의존성 설치 +bundle install + +# QA APK 빌드 +bundle exec fastlane build_qa_apk + +# Release AAB 빌드 +bundle exec fastlane build_release_aab environment:production +``` + +#### GitHub Actions에서 사용 + +1. **Actions** 탭 → **Fastlane CI/CD** 워크플로우 선택 +2. **Run workflow** 클릭하여 수동 빌드 +3. 또는 `develop` 브랜치에 push하면 자동 CI 빌드 + +#### Release 배포 + +`release/3.2.0` 브랜치를 `develop`에 머지하면 자동으로: + +- AAB 빌드 +- Play Store 배포 +- GitHub Release 생성 (자동 릴리즈 노트) +- Git 태그 생성 diff --git a/RELEASE_NOTES.txt b/RELEASE_NOTES.txt new file mode 100644 index 000000000..2d08b8980 --- /dev/null +++ b/RELEASE_NOTES.txt @@ -0,0 +1,2 @@ +- 작은 기능을 개선했어요. +- 앱 안정성을 강화했어요. \ No newline at end of file diff --git a/fastlane/Appfile b/fastlane/Appfile new file mode 100644 index 000000000..cee621f70 --- /dev/null +++ b/fastlane/Appfile @@ -0,0 +1,6 @@ +# Appfile for fastlane +# 앱 정보 설정 + +json_key_file("") # Google Play Service Account JSON 키 경로 (선택사항) +package_name("com.eatssu.android") + diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 000000000..0b54d833c --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,272 @@ +# Fastfile for Android CI/CD +# 자동화된 빌드 및 배포 설정 + +default_platform(:android) + +platform :android do + desc "빌드 환경 설정 (local.properties 생성)" + lane :setup_environment do |options| + environment = options[:environment] || "qa" + + # 기존 local.properties 파일 삭제 (있다면) + File.delete("local.properties") if File.exist?("local.properties") + + # local.properties 파일 생성 + sh("touch local.properties") + + # 환경 변수에서 값 읽어오기 (GitHub Actions에서 설정됨) + dev_base_url = ENV["DEV_BASE_URL"] || "" + prod_base_url = ENV["PROD_BASE_URL"] || "" + kakao_key = ENV["KAKAO_NATIVE_APP_KEY"] || "" + naver_maps_id = ENV["NAVER_MAPS_CLIENT_ID"] || "" + posthog_api_key = ENV["POSTHOG_API_KEY"] || "" + posthog_host = ENV["POSTHOG_HOST"] || "" + + base_url = environment == "production" ? prod_base_url : dev_base_url + + # local.properties에 값 추가 + File.open("local.properties", "a") do |f| + f.puts("DEV_BASE_URL=\"#{dev_base_url}\"") + f.puts("PROD_BASE_URL=\"#{prod_base_url}\"") + f.puts("KAKAO_NATIVE_APP_KEY=#{kakao_key}") + f.puts("NAVER_MAPS_CLIENT_ID=#{naver_maps_id}") + f.puts("POSTHOG_API_KEY=#{posthog_api_key}") + f.puts("POSTHOG_HOST=#{posthog_host}") + end + + UI.success("✅ Environment setup completed for #{environment}") + end + + desc "google-services.json 생성" + lane :setup_google_services do + google_service = ENV["GOOGLE_SERVICE"] + if google_service.nil? || google_service.empty? + UI.important("⚠️ GOOGLE_SERVICE 환경 변수가 설정되지 않았습니다.") + return + end + + sh("echo '#{google_service}' > app/google-services.json.b64") + sh("base64 -d -i app/google-services.json.b64 > app/google-services.json") + sh("rm app/google-services.json.b64") + + UI.success("✅ google-services.json 생성 완료") + end + + desc "Debug APK 빌드" + lane :build_debug do |options| + environment = options[:environment] || "qa" + + setup_environment(environment: environment) + setup_google_services + + sh("./gradlew assembleDebug --stacktrace") + + UI.success("✅ Debug APK 빌드 완료") + end + + desc "Release APK 빌드" + lane :build_release_apk do |options| + environment = options[:environment] || "production" + + setup_environment(environment: environment) + setup_google_services + + sh("./gradlew assembleRelease --stacktrace") + + UI.success("✅ Release APK 빌드 완료") + end + + desc "Release AAB 빌드 (Play Store용)" + lane :build_release_aab do |options| + environment = options[:environment] || "production" + + setup_environment(environment: environment) + setup_google_services + + sh("./gradlew bundleRelease --stacktrace") + + UI.success("✅ Release AAB 빌드 완료") + end + + desc "QA APK 빌드 및 아티팩트 업로드" + lane :build_qa_apk do |options| + build_debug(environment: "qa") + + # APK 경로 + apk_path = "app/build/outputs/apk/debug/app-debug.apk" + + if File.exist?(apk_path) + UI.success("✅ QA APK 빌드 완료: #{apk_path}") + else + UI.user_error!("❌ APK 파일을 찾을 수 없습니다: #{apk_path}") + end + end + + desc "Release AAB 빌드 및 Play Store 내부 테스트 트랙 배포" + lane :deploy_internal_test do |options| + environment = options[:environment] || "production" + track = options[:track] || "internal" # internal, alpha, beta, production + + build_release_aab(environment: environment) + + # AAB 경로 + aab_path = "app/build/outputs/bundle/release/app-release.aab" + + unless File.exist?(aab_path) + UI.user_error!("❌ AAB 파일을 찾을 수 없습니다: #{aab_path}") + end + + # Google Play Store 배포 (선택사항 - 서비스 계정 키 필요) + if ENV["GOOGLE_PLAY_SERVICE_ACCOUNT_JSON"] + # 서비스 계정 JSON을 파일로 저장 + service_account_path = "/tmp/google-play-service-account.json" + File.write(service_account_path, ENV["GOOGLE_PLAY_SERVICE_ACCOUNT_JSON"]) + + upload_to_play_store( + track: track, + aab: aab_path, + json_key: service_account_path, + skip_upload_apk: true, + skip_upload_metadata: true, + skip_upload_images: true, + skip_upload_screenshots: true + ) + + # 임시 파일 삭제 + File.delete(service_account_path) if File.exist?(service_account_path) + + UI.success("✅ Play Store #{track} 트랙에 배포 완료") + else + UI.important("⚠️ GOOGLE_PLAY_SERVICE_ACCOUNT_JSON이 설정되지 않아 Play Store 배포를 건너뜁니다.") + UI.success("✅ AAB 빌드 완료: #{aab_path}") + end + end + + desc "전체 빌드 및 테스트" + lane :test do + setup_environment(environment: "qa") + setup_google_services + + sh("./gradlew test --stacktrace") + + UI.success("✅ 테스트 완료") + end + + desc "CI 빌드 (develop 브랜치용)" + lane :ci_build do + setup_environment(environment: "qa") + setup_google_services + + sh("./gradlew build --stacktrace") + + UI.success("✅ CI 빌드 완료") + end + + desc "버전 정보 출력" + lane :version_info do + # build.gradle.kts에서 버전 정보 읽기 + version_name = sh("grep 'versionName' app/build.gradle.kts | head -1 | sed 's/.*versionName = \"\\(.*\\)\".*/\\1/'", log: false).strip + version_code = sh("grep 'versionCode' app/build.gradle.kts | head -1 | sed 's/.*versionCode = \\([0-9]*\\).*/\\1/'", log: false).strip + + UI.message("📱 App Version: #{version_name} (#{version_code})") + + { + version_name: version_name, + version_code: version_code + } + end + + desc "Release 배포 (태그 기반) - AAB 빌드 및 Play Store 배포" + lane :release do |options| + version = options[:version] || ENV["VERSION"] + track = options[:track] || "internal" # internal, alpha, beta, production + deploy_to_play_store = options[:deploy] != false + + if version.nil? || version.empty? + UI.user_error!("❌ 버전 정보가 필요합니다. version 파라미터를 제공하거나 VERSION 환경 변수를 설정하세요.") + end + + UI.message("🚀 Release 배포 시작: #{version}") + + # Production 환경으로 빌드 + build_release_aab(environment: "production") + + # AAB 경로 + aab_path = "app/build/outputs/bundle/release/app-release.aab" + + unless File.exist?(aab_path) + UI.user_error!("❌ AAB 파일을 찾을 수 없습니다: #{aab_path}") + end + + UI.success("✅ AAB 빌드 완료: #{aab_path}") + + # Play Store 배포 + if deploy_to_play_store && ENV["GOOGLE_PLAY_SERVICE_ACCOUNT_JSON"] + UI.message("📤 Play Store #{track} 트랙에 배포 중...") + + # 서비스 계정 JSON을 파일로 저장 + service_account_path = "/tmp/google-play-service-account.json" + File.write(service_account_path, ENV["GOOGLE_PLAY_SERVICE_ACCOUNT_JSON"]) + + # 릴리즈 노트 파일 찾기 (프로젝트 루트의 RELEASE_NOTES.txt) + release_notes_path = "RELEASE_NOTES.txt" + + unless File.exist?(release_notes_path) + UI.important("⚠️ RELEASE_NOTES.txt 파일을 찾을 수 없습니다.") + UI.important("⚠️ Play Store 릴리즈 노트 없이 배포됩니다.") + release_notes_path = nil + else + release_notes_content = File.read(release_notes_path).strip + if release_notes_content.empty? + UI.important("⚠️ RELEASE_NOTES.txt 파일이 비어있습니다.") + release_notes_path = nil + else + UI.message("📝 Play Store 릴리즈 노트 파일 발견: #{release_notes_path}") + UI.message("📄 릴리즈 노트 내용:") + UI.message(release_notes_content) + end + end + + begin + upload_params = { + track: track, + aab: aab_path, + json_key: service_account_path, + skip_upload_apk: true, + skip_upload_metadata: true, + skip_upload_images: true, + skip_upload_screenshots: true + } + + # 릴리즈 노트 파일이 있으면 추가 + if release_notes_path && File.exist?(release_notes_path) + upload_params[:release_notes_file] = release_notes_path + end + + upload_to_play_store(upload_params) + + UI.success("✅ Play Store #{track} 트랙에 배포 완료") + if release_notes_path + UI.success("✅ Play Store 릴리즈 노트 업데이트 완료") + end + rescue => ex + UI.important("⚠️ Play Store 배포 중 오류 발생: #{ex.message}") + raise + ensure + # 임시 파일 삭제 + File.delete(service_account_path) if File.exist?(service_account_path) + end + elsif deploy_to_play_store + UI.important("⚠️ GOOGLE_PLAY_SERVICE_ACCOUNT_JSON이 설정되지 않아 Play Store 배포를 건너뜁니다.") + end + + UI.success("🎉 Release 배포 완료: #{version}") + + { + version: version, + aab_path: aab_path, + track: track + } + end +end + From faf529ffdd9249c1e68eb3907c48c07e37b16475 Mon Sep 17 00:00:00 2001 From: Yu Jin Date: Tue, 30 Dec 2025 17:13:09 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20github=20release&tag=20=EB=A6=B4?= =?UTF-8?q?=EB=A6=AC=EC=A6=88=20=EB=85=B8=ED=8A=B8=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=EC=B6=94=EC=B6=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release_tag.yml | 265 ++++++++++++++++++++++++++++-- 1 file changed, 254 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release_tag.yml b/.github/workflows/release_tag.yml index 776a87ff8..6a699cb3b 100644 --- a/.github/workflows/release_tag.yml +++ b/.github/workflows/release_tag.yml @@ -1,31 +1,274 @@ -name: Release Tag +name: Release Tag & Deploy on: pull_request: types: [closed] branches: - develop # develop 브랜치로의 PR만 대상, 나중에 master나 prodution으로 바꾸는게 좋을듯 + workflow_dispatch: + inputs: + version: + description: 'Release version (예: 3.2.0)' + required: true + type: string + release_notes: + description: 'Release notes (마크다운 지원)' + required: false + type: string + default: | + ## 주요 변경사항 + + - 버그 수정 및 성능 개선 + + ## 다운로드 + - AAB 파일은 아래에서 다운로드할 수 있습니다. + track: + description: 'Play Store 배포 트랙' + required: true + type: choice + default: 'internal' + options: + - internal + - alpha + - beta + - production jobs: - on-merge: + release: if: | - github.event.pull_request.merged == true && - startsWith(github.event.pull_request.head.ref, 'release/') + (github.event_name == 'workflow_dispatch') || + (github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/')) runs-on: ubuntu-latest + steps: - name: Checkout code uses: actions/checkout@v4 + with: + fetch-depth: 0 # 태그 생성을 위해 전체 히스토리 필요 - - name: Extract version #브랜치명에서 버전 정보 추출 - id: extract_version + - name: Extract version and generate release notes + id: extract_info + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} run: | - VERSION=$(echo "${{ github.event.pull_request.head.ref }}" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + # 수동 실행 시 입력받은 값 사용 + VERSION="${{ github.event.inputs.version }}" + RELEASE_NOTES="${{ github.event.inputs.release_notes }}" + TRACK="${{ github.event.inputs.track }}" + else + # PR 머지 시 브랜치명에서 버전 추출 + VERSION=$(echo "${{ github.event.pull_request.head.ref }}" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') + TRACK="internal" + + # GitHub Release 노트는 GitHub의 자동 생성 기능 사용 + # Play Store용 릴리즈 노트는 별도로 생성 + fi + + # 버전 검증 + if [ -z "$VERSION" ]; then + echo "❌ 버전 정보를 찾을 수 없습니다." + exit 1 + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "track=$TRACK" >> $GITHUB_OUTPUT + + echo "📦 Version: $VERSION" + echo "📝 Track: $TRACK" + echo "📄 GitHub Release 노트는 자동 생성됩니다." + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2' + bundler-cache: true + + - name: Install dependencies + run: | + bundle install + + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew - - name: Create Release - uses: actions/create-release@v1 + - name: Setup environment variables + env: + DEV_BASE_URL: ${{ secrets.DEV_BASE_URL }} + PROD_BASE_URL: ${{ secrets.PROD_BASE_URL }} + KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} + NAVER_MAPS_CLIENT_ID: ${{ secrets.NAVER_MAPS_CLIENT_ID }} + POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} + POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }} + GOOGLE_SERVICE: ${{ secrets.GOOGLE_SERVICE }} + GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} + run: | + echo "Environment variables set" + + - name: Find previous tag for release notes + id: previous_tag + run: | + # 가장 최근 태그 찾기 (vX.Y.Z 형식) + PREVIOUS_TAG=$(git tag --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1) + + if [ -z "$PREVIOUS_TAG" ]; then + echo "⚠️ 이전 태그를 찾을 수 없습니다. GitHub가 자동으로 첫 번째 릴리스 노트를 생성합니다." + PREVIOUS_TAG="" + else + echo "📦 이전 태그: $PREVIOUS_TAG" + fi + + echo "previous_tag=$PREVIOUS_TAG" >> $GITHUB_OUTPUT + + - name: Check Play Store release notes file + id: check_release_notes + run: | + if [ -f "RELEASE_NOTES.txt" ]; then + echo "✅ RELEASE_NOTES.txt 파일 발견" + echo "exists=true" >> $GITHUB_OUTPUT + # 릴리즈 노트 내용을 output에 저장 + RELEASE_NOTES_CONTENT=$(cat RELEASE_NOTES.txt) + echo "release_notes<> $GITHUB_OUTPUT + echo "$RELEASE_NOTES_CONTENT" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + echo "📄 릴리즈 노트 내용:" + head -10 RELEASE_NOTES.txt + else + echo "⚠️ RELEASE_NOTES.txt 파일을 찾을 수 없습니다." + echo "⚠️ Play Store 릴리즈 노트 없이 배포됩니다." + echo "exists=false" >> $GITHUB_OUTPUT + echo "release_notes=" >> $GITHUB_OUTPUT + fi + + - name: Build and Deploy with Fastlane + env: + DEV_BASE_URL: ${{ secrets.DEV_BASE_URL }} + PROD_BASE_URL: ${{ secrets.PROD_BASE_URL }} + KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} + NAVER_MAPS_CLIENT_ID: ${{ secrets.NAVER_MAPS_CLIENT_ID }} + POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} + POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }} + GOOGLE_SERVICE: ${{ secrets.GOOGLE_SERVICE }} + GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} + VERSION: ${{ steps.extract_info.outputs.version }} + run: | + # fastlane release lane 실행 + # RELEASE_NOTES.txt 파일이 있으면 자동으로 사용됨 + bundle exec fastlane release version:${{ steps.extract_info.outputs.version }} track:${{ steps.extract_info.outputs.track }} + + - name: Create GitHub Release with auto-generated notes + if: success() + uses: softprops/action-gh-release@v1 + with: + tag_name: "v${{ steps.extract_info.outputs.version }}" + name: "Release ${{ steps.extract_info.outputs.version }}" + target_commitish: ${{ github.event_name == 'pull_request' && github.event.pull_request.merge_commit_sha || github.sha }} + generate_release_notes: true + previous_tag: ${{ steps.previous_tag.outputs.previous_tag != '' && steps.previous_tag.outputs.previous_tag || 'Auto' }} + body: | + + --- + + ### 빌드 정보 + - **버전**: ${{ steps.extract_info.outputs.version }} + - **배포 트랙**: ${{ steps.extract_info.outputs.track }} + ${{ github.event_name == 'pull_request' && format('- **빌드 시간**: {0}', github.event.pull_request.merged_at) || '' }} + ${{ github.event_name == 'pull_request' && format('- **커밋**: {0}', github.event.pull_request.merge_commit_sha) || format('- **커밋**: {0}', github.sha) }} + ${{ github.event_name == 'pull_request' && format('- **PR**: #{0}', github.event.pull_request.number) || '' }} + + ### 다운로드 + - AAB 파일은 아래에서 다운로드할 수 있습니다. + - GitHub Actions Artifacts에서도 다운로드 가능합니다. + files: | + app/build/outputs/bundle/release/app-release.aab + draft: false + prerelease: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload AAB artifact + if: always() + uses: actions/upload-artifact@v4 with: - tag_name: ${{ steps.extract_version.outputs.version }} - release_name: ${{ steps.extract_version.outputs.version }} + name: release-aab-${{ steps.extract_info.outputs.version }} + path: | + app/build/outputs/bundle/release/app-release.aab + retention-days: 90 + + - name: Get GitHub Release notes + id: github_release_notes + if: success() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + VERSION: ${{ steps.extract_info.outputs.version }} + run: | + # GitHub Release가 생성된 후 노트를 가져오기 위해 잠시 대기 + sleep 3 + + # GitHub API로 릴리즈 노트 가져오기 + RELEASE_TAG="v$VERSION" + RELEASE_RESPONSE=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ + "https://api.github.com/repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG") + + # 릴리즈 노트 본문 추출 (자동 생성된 부분만) + GITHUB_NOTES=$(echo "$RELEASE_RESPONSE" | jq -r '.body // ""' | \ + grep -A 100 "## What'"'"'s Changed" | \ + grep -v "^---" | \ + grep -v "^### 빌드 정보" | \ + head -30 | \ + sed 's/^\*\s*/- /' | \ + sed 's/^##\s*//' | \ + sed '/^$/d') + + if [ -z "$GITHUB_NOTES" ] || [ "$GITHUB_NOTES" = "null" ] || [ -z "$(echo "$GITHUB_NOTES" | tr -d '\n')" ]; then + GITHUB_NOTES="- [Fix] 릴리즈 노트를 가져올 수 없습니다." + fi + + echo "github_notes<> $GITHUB_OUTPUT + echo "$GITHUB_NOTES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Slack notification + if: always() + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + text: | + [Android 업데이트 올렸습니다!] + + 버전명: ${{ steps.extract_info.outputs.version }} + + PlayStore 릴리즈 노트 + + ${{ steps.check_release_notes.outputs.release_notes != '' && steps.check_release_notes.outputs.release_notes || '릴리즈 노트가 없습니다.' }} + + GitHub 릴리즈노트 + + ${{ steps.github_release_notes.outputs.github_notes != '' && steps.github_release_notes.outputs.github_notes || '- [Fix] 릴리즈 노트를 가져올 수 없습니다.' }} + custom_payload: | + { + attachments: [{ + color: '${{ job.status }}' === 'success' ? 'good' : 'danger', + text: '[Android 업데이트 올렸습니다!]\n\n버전명: ${{ steps.extract_info.outputs.version }}\n\nPlayStore 릴리즈 노트\n\n${{ steps.check_release_notes.outputs.release_notes != '' && steps.check_release_notes.outputs.release_notes || '릴리즈 노트가 없습니다.' }}\n\nGitHub 릴리즈노트\n\n${{ steps.github_release_notes.outputs.github_notes != '' && steps.github_release_notes.outputs.github_notes || '- [Fix] 릴리즈 노트를 가져올 수 없습니다.' }}' + }] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} From e87c7e5c6de438ecd3757b2ec706c0fb3edbd634 Mon Sep 17 00:00:00 2001 From: Yu Jin Date: Tue, 30 Dec 2025 17:43:30 +0900 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20internal=20->=20production=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/fastlane.yml | 21 ++++++--------------- .github/workflows/release_tag.yml | 4 ++-- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/.github/workflows/fastlane.yml b/.github/workflows/fastlane.yml index d765fe691..dc8f830fe 100644 --- a/.github/workflows/fastlane.yml +++ b/.github/workflows/fastlane.yml @@ -22,16 +22,10 @@ on: options: - qa - production - push: - branches: - - develop - - main - - master - pull_request: - branches: - - develop - - main - - master + # 일반 develop push는 android.yml에서 처리하므로 제거 + # push: + # branches: + # - develop jobs: fastlane: @@ -95,11 +89,8 @@ jobs: GOOGLE_SERVICE: ${{ secrets.GOOGLE_SERVICE }} GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} run: | - if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - bundle exec fastlane ${{ github.event.inputs.lane }} environment:${{ github.event.inputs.environment }} - else - bundle exec fastlane ci_build - fi + # workflow_dispatch만 지원 (수동 실행) + bundle exec fastlane ${{ github.event.inputs.lane }} environment:${{ github.event.inputs.environment }} - name: Upload APK artifact if: | diff --git a/.github/workflows/release_tag.yml b/.github/workflows/release_tag.yml index 6a699cb3b..92731444d 100644 --- a/.github/workflows/release_tag.yml +++ b/.github/workflows/release_tag.yml @@ -26,7 +26,7 @@ on: description: 'Play Store 배포 트랙' required: true type: choice - default: 'internal' + default: 'production' options: - internal - alpha @@ -60,7 +60,7 @@ jobs: else # PR 머지 시 브랜치명에서 버전 추출 VERSION=$(echo "${{ github.event.pull_request.head.ref }}" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') - TRACK="internal" + TRACK="production" # release 브랜치 머지 시 프로덕션 트랙에 배포 # GitHub Release 노트는 GitHub의 자동 생성 기능 사용 # Play Store용 릴리즈 노트는 별도로 생성 From 0ca5f6b13b16ff78e02cb5ae5656dc88fc386591 Mon Sep 17 00:00:00 2001 From: Yu Jin Date: Tue, 30 Dec 2025 17:43:42 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20app=20=ED=8F=B4=EB=8D=94=EA=B0=80=20?= =?UTF-8?q?=EC=97=86=EC=9D=84=EB=95=8C=20=EC=B6=94=EA=B0=80=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastlane/Fastfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 0b54d833c..f700f6bb3 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -45,6 +45,12 @@ platform :android do return end + # app 디렉토리가 없으면 생성 + unless Dir.exist?("app") + UI.message("📁 app 디렉토리 생성 중...") + Dir.mkdir("app") + end + sh("echo '#{google_service}' > app/google-services.json.b64") sh("base64 -d -i app/google-services.json.b64 > app/google-services.json") sh("rm app/google-services.json.b64")