From ed3dea02bc820cc3b304e11915d5caf136fc373b Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 23 Jul 2026 12:36:40 +0900 Subject: [PATCH 1/4] =?UTF-8?q?chore:=20App=20Store=20Firebase=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=9D=84=20=EB=B9=84=EA=B3=B5=EA=B0=9C=20=EA=B5=AC?= =?UTF-8?q?=EC=84=B1=EC=9C=BC=EB=A1=9C=20=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Application/App/Project.swift | 2 +- fastlane/Fastfile | 69 ++++++++++++++++++++++++++--------- 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/Application/App/Project.swift b/Application/App/Project.swift index d7fd2a98..08a77304 100644 --- a/Application/App/Project.swift +++ b/Application/App/Project.swift @@ -65,7 +65,7 @@ let project = Project( release: [ "APP_ENVIRONMENT": "prod", "APS_ENVIRONMENT": "production", - "FIRESTORE_DATABASE_ID": "prod", + "FIRESTORE_DATABASE_ID": "(default)", ] ) ), diff --git a/fastlane/Fastfile b/fastlane/Fastfile index af402582..a6d26205 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -12,11 +12,13 @@ APPSTORE_CONFIGURATION = "Release" TESTFLIGHT_APP_ENVIRONMENT = "staging" APPSTORE_APP_ENVIRONMENT = "prod" TESTFLIGHT_DATABASE_ID = "(default)" -APPSTORE_DATABASE_ID = "prod" -TESTFLIGHT_FUNCTION_API_BASE_URL = "https://asia-northeast3-devlog-staging.cloudfunctions.net/api/api" -APPSTORE_FUNCTION_API_BASE_URL = "https://asia-northeast3-devlog-c87b6.cloudfunctions.net/prodApi/api" +APPSTORE_DATABASE_ID = "(default)" TESTFLIGHT_FIREBASE_PROJECT_ID = "devlog-staging" TESTFLIGHT_GOOGLE_APP_ID = "1:686659799179:ios:eba4e86096153f0539ec8e" +APP_CONFIG_XCCONFIG_PATH = File.expand_path( + "../Application/App/Sources/Resource/Config.xcconfig", + __dir__ +) VERSION_XCCONFIG_PATH = File.expand_path( "../Application/Shared/Version.xcconfig", __dir__ @@ -54,6 +56,34 @@ rescue SystemCallError => error UI.user_error!("Could not read version configuration file at #{expanded_path}: #{error.message}") end +def function_api_base_url_from_xcconfig(configuration) + expanded_path = File.expand_path(APP_CONFIG_XCCONFIG_PATH) + UI.user_error!("Missing app configuration file: #{expanded_path}") if !File.file?(expanded_path) + + escaped_configuration = Regexp.escape(configuration) + assignments = File.foreach(expanded_path).filter_map do |line| + assignment = line.match( + /^\s*FUNCTION_API_BASE_URL\s*\[\s*config\s*=\s*#{escaped_configuration}\s*\]\s*=\s*(.*)$/ + ) + next if assignment.nil? + + assignment[1].strip + end + + if assignments.empty? + UI.user_error!("Missing FUNCTION_API_BASE_URL for #{configuration} in #{expanded_path}") + end + + function_api_base_url = assignments.last.gsub("/$()/", "//") + if function_api_base_url.empty? + UI.user_error!("Empty FUNCTION_API_BASE_URL for #{configuration} in #{expanded_path}") + end + + function_api_base_url +rescue SystemCallError => error + UI.user_error!("Could not read app configuration file at #{expanded_path}: #{error.message}") +end + default_platform(:ios) platform :ios do @@ -105,12 +135,12 @@ platform :ios do when TESTFLIGHT_CONFIGURATION { database_id: TESTFLIGHT_DATABASE_ID, - function_api_base_url: TESTFLIGHT_FUNCTION_API_BASE_URL + function_api_base_url: function_api_base_url_from_xcconfig(TESTFLIGHT_CONFIGURATION) } when APPSTORE_CONFIGURATION { database_id: APPSTORE_DATABASE_ID, - function_api_base_url: APPSTORE_FUNCTION_API_BASE_URL + function_api_base_url: function_api_base_url_from_xcconfig(APPSTORE_CONFIGURATION) } else UI.user_error!("Unsupported store configuration: #{configuration}") @@ -127,11 +157,11 @@ platform :ios do if function_api_base_url != expected_configuration[:function_api_base_url] UI.user_error!( - "Invalid store configuration: #{configuration} must use FUNCTION_API_BASE_URL=#{expected_configuration[:function_api_base_url]}, got #{function_api_base_url}" + "Invalid store configuration: #{configuration} uses an unexpected FUNCTION_API_BASE_URL" ) end - UI.message("Verified store configuration: #{configuration}/#{database_id}/#{function_api_base_url}") + UI.message("Verified store configuration: #{configuration}/#{database_id}") end private_lane :verify_store_info_plist do |options| @@ -205,7 +235,8 @@ platform :ios do path: plist_path, source: "app Info.plist", key: "FUNCTION_API_BASE_URL", - expected: expected_configuration[:function_api_base_url] + expected: expected_configuration[:function_api_base_url], + redact_values: true ) verify_plist_value( @@ -283,9 +314,11 @@ platform :ios do UI.user_error!("Missing expected #{options[:key]}") if expected_value.empty? if actual_value != expected_value - UI.user_error!( - "Unexpected #{options[:key]} in #{options[:source]}: expected #{expected_value}, got #{actual_value}" - ) + message = "Unexpected #{options[:key]} in #{options[:source]}" + if options[:redact_values] != true + message += ": expected #{expected_value}, got #{actual_value}" + end + UI.user_error!(message) end UI.message("Verified #{options[:key]} in #{options[:source]}") @@ -297,7 +330,7 @@ platform :ios do expected_database_id = options[:database_id] || (configuration == APPSTORE_CONFIGURATION ? APPSTORE_DATABASE_ID : TESTFLIGHT_DATABASE_ID) expected_function_api_base_url = options[:function_api_base_url] || - (configuration == APPSTORE_CONFIGURATION ? APPSTORE_FUNCTION_API_BASE_URL : TESTFLIGHT_FUNCTION_API_BASE_URL) + function_api_base_url_from_xcconfig(configuration) expected_app_environment = configuration == APPSTORE_CONFIGURATION ? APPSTORE_APP_ENVIRONMENT : TESTFLIGHT_APP_ENVIRONMENT @@ -415,7 +448,7 @@ platform :ios do build_for_store( configuration: TESTFLIGHT_CONFIGURATION, database_id: TESTFLIGHT_DATABASE_ID, - function_api_base_url: TESTFLIGHT_FUNCTION_API_BASE_URL, + function_api_base_url: function_api_base_url_from_xcconfig(TESTFLIGHT_CONFIGURATION), firebase_project_id: TESTFLIGHT_FIREBASE_PROJECT_ID, google_app_id: TESTFLIGHT_GOOGLE_APP_ID ) @@ -427,7 +460,7 @@ platform :ios do build_for_store( configuration: TESTFLIGHT_CONFIGURATION, database_id: TESTFLIGHT_DATABASE_ID, - function_api_base_url: TESTFLIGHT_FUNCTION_API_BASE_URL, + function_api_base_url: function_api_base_url_from_xcconfig(TESTFLIGHT_CONFIGURATION), firebase_project_id: TESTFLIGHT_FIREBASE_PROJECT_ID, google_app_id: TESTFLIGHT_GOOGLE_APP_ID ) @@ -438,7 +471,7 @@ platform :ios do configuration: APPSTORE_CONFIGURATION, output_directory: APPSTORE_BUILD_OUTPUT_DIRECTORY, database_id: APPSTORE_DATABASE_ID, - function_api_base_url: APPSTORE_FUNCTION_API_BASE_URL + function_api_base_url: function_api_base_url_from_xcconfig(APPSTORE_CONFIGURATION) ) end @@ -447,7 +480,7 @@ platform :ios do configuration: APPSTORE_CONFIGURATION, output_directory: APPSTORE_BUILD_OUTPUT_DIRECTORY, database_id: APPSTORE_DATABASE_ID, - function_api_base_url: APPSTORE_FUNCTION_API_BASE_URL + function_api_base_url: function_api_base_url_from_xcconfig(APPSTORE_CONFIGURATION) ) upload_appstore_build @@ -480,7 +513,7 @@ platform :ios do app_environment: TESTFLIGHT_APP_ENVIRONMENT, bundle_id: APP_IDENTIFIER, database_id: TESTFLIGHT_DATABASE_ID, - function_api_base_url: TESTFLIGHT_FUNCTION_API_BASE_URL, + function_api_base_url: function_api_base_url_from_xcconfig(TESTFLIGHT_CONFIGURATION), firebase_project_id: TESTFLIGHT_FIREBASE_PROJECT_ID, google_app_id: TESTFLIGHT_GOOGLE_APP_ID } @@ -507,7 +540,7 @@ platform :ios do app_environment: APPSTORE_APP_ENVIRONMENT, bundle_id: APP_IDENTIFIER, database_id: APPSTORE_DATABASE_ID, - function_api_base_url: APPSTORE_FUNCTION_API_BASE_URL + function_api_base_url: function_api_base_url_from_xcconfig(APPSTORE_CONFIGURATION) } ) From 22333e098f4707b61ab2c9c83811a507d6b71df9 Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 23 Jul 2026 12:41:02 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=EC=8A=A4=ED=86=A0=EC=96=B4=20?= =?UTF-8?q?=EB=B9=8C=EB=93=9C=EC=9D=98=20=EA=B3=B5=ED=86=B5=20API=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastlane/Fastfile | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index a6d26205..43dbf54f 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -13,6 +13,7 @@ TESTFLIGHT_APP_ENVIRONMENT = "staging" APPSTORE_APP_ENVIRONMENT = "prod" TESTFLIGHT_DATABASE_ID = "(default)" APPSTORE_DATABASE_ID = "(default)" +SHARED_FUNCTION_API_PATH = "/api/api" TESTFLIGHT_FIREBASE_PROJECT_ID = "devlog-staging" TESTFLIGHT_GOOGLE_APP_ID = "1:686659799179:ios:eba4e86096153f0539ec8e" APP_CONFIG_XCCONFIG_PATH = File.expand_path( @@ -84,6 +85,21 @@ rescue SystemCallError => error UI.user_error!("Could not read app configuration file at #{expanded_path}: #{error.message}") end +def verify_function_api_base_url_policy(function_api_base_url) + require "uri" + + uri = URI.parse(function_api_base_url) + is_valid = uri.is_a?(URI::HTTPS) && + !uri.host.to_s.empty? && + uri.path == SHARED_FUNCTION_API_PATH && + uri.query.nil? && + uri.fragment.nil? + + UI.user_error!("Invalid FUNCTION_API_BASE_URL for store build") if !is_valid +rescue URI::InvalidURIError + UI.user_error!("Invalid FUNCTION_API_BASE_URL for store build") +end + default_platform(:ios) platform :ios do @@ -149,6 +165,8 @@ platform :ios do UI.user_error!("Missing Firestore database ID for #{configuration}") if database_id.empty? UI.user_error!("Missing Function API base URL for #{configuration}") if function_api_base_url.empty? + verify_function_api_base_url_policy(function_api_base_url) + if database_id != expected_configuration[:database_id] UI.user_error!( "Invalid store configuration: #{configuration} must use FIRESTORE_DATABASE_ID=#{expected_configuration[:database_id]}, got #{database_id}" From 31686c44bd26ed8b23ad63eb82fb42be9650cda0 Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 23 Jul 2026 13:00:56 +0900 Subject: [PATCH 3/4] =?UTF-8?q?chore:=20Firebase=20=EC=8B=9D=EB=B3=84?= =?UTF-8?q?=EC=9E=90=EB=A5=BC=20=EB=B9=84=EA=B3=B5=EA=B0=9C=20=EA=B5=AC?= =?UTF-8?q?=EC=84=B1=EC=9C=BC=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastlane/Fastfile | 64 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 43dbf54f..9d942244 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -14,8 +14,6 @@ APPSTORE_APP_ENVIRONMENT = "prod" TESTFLIGHT_DATABASE_ID = "(default)" APPSTORE_DATABASE_ID = "(default)" SHARED_FUNCTION_API_PATH = "/api/api" -TESTFLIGHT_FIREBASE_PROJECT_ID = "devlog-staging" -TESTFLIGHT_GOOGLE_APP_ID = "1:686659799179:ios:eba4e86096153f0539ec8e" APP_CONFIG_XCCONFIG_PATH = File.expand_path( "../Application/App/Sources/Resource/Config.xcconfig", __dir__ @@ -85,6 +83,36 @@ rescue SystemCallError => error UI.user_error!("Could not read app configuration file at #{expanded_path}: #{error.message}") end +def expected_firebase_configuration_from_xcconfig(configuration) + expanded_path = File.expand_path(APP_CONFIG_XCCONFIG_PATH) + UI.user_error!("Missing app configuration file: #{expanded_path}") if !File.file?(expanded_path) + + escaped_configuration = Regexp.escape(configuration) + expected_settings = { + firebase_project_id: "EXPECTED_FIREBASE_PROJECT_ID", + google_app_id: "EXPECTED_GOOGLE_APP_ID" + } + + expected_settings.to_h do |key, setting| + assignments = File.foreach(expanded_path).filter_map do |line| + assignment = line.match( + /^\s*#{setting}\s*\[\s*config\s*=\s*#{escaped_configuration}\s*\]\s*=\s*(.*)$/ + ) + next if assignment.nil? + + assignment[1].strip + end + + if assignments.empty? || assignments.last.empty? + UI.user_error!("Missing #{setting} for #{configuration} in #{expanded_path}") + end + + [key, assignments.last] + end +rescue SystemCallError => error + UI.user_error!("Could not read app configuration file at #{expanded_path}: #{error.message}") +end + def verify_function_api_base_url_policy(function_api_base_url) require "uri" @@ -284,15 +312,11 @@ platform :ios do ) if !expected_firebase_project_id.empty? && actual_firebase_project_id != expected_firebase_project_id - UI.user_error!( - "Unexpected PROJECT_ID in GoogleService-Info.plist: expected #{expected_firebase_project_id}, got #{actual_firebase_project_id}" - ) + UI.user_error!("Unexpected PROJECT_ID in GoogleService-Info.plist") end if !expected_google_app_id.empty? && actual_google_app_id != expected_google_app_id - UI.user_error!( - "Unexpected GOOGLE_APP_ID in GoogleService-Info.plist: expected #{expected_google_app_id}, got #{actual_google_app_id}" - ) + UI.user_error!("Unexpected GOOGLE_APP_ID in GoogleService-Info.plist") end UI.message("Verified PROJECT_ID in GoogleService-Info.plist") @@ -358,6 +382,8 @@ platform :ios do function_api_base_url: expected_function_api_base_url ) + firebase_configuration = expected_firebase_configuration_from_xcconfig(configuration) + if ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"].to_s.strip.empty? ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "30" end @@ -449,8 +475,8 @@ platform :ios do bundle_id: APP_IDENTIFIER, database_id: expected_database_id, function_api_base_url: expected_function_api_base_url, - firebase_project_id: options[:firebase_project_id], - google_app_id: options[:google_app_id] + firebase_project_id: firebase_configuration[:firebase_project_id], + google_app_id: firebase_configuration[:google_app_id] } ) @@ -466,9 +492,7 @@ platform :ios do build_for_store( configuration: TESTFLIGHT_CONFIGURATION, database_id: TESTFLIGHT_DATABASE_ID, - function_api_base_url: function_api_base_url_from_xcconfig(TESTFLIGHT_CONFIGURATION), - firebase_project_id: TESTFLIGHT_FIREBASE_PROJECT_ID, - google_app_id: TESTFLIGHT_GOOGLE_APP_ID + function_api_base_url: function_api_base_url_from_xcconfig(TESTFLIGHT_CONFIGURATION) ) upload_testflight_build @@ -478,9 +502,7 @@ platform :ios do build_for_store( configuration: TESTFLIGHT_CONFIGURATION, database_id: TESTFLIGHT_DATABASE_ID, - function_api_base_url: function_api_base_url_from_xcconfig(TESTFLIGHT_CONFIGURATION), - firebase_project_id: TESTFLIGHT_FIREBASE_PROJECT_ID, - google_app_id: TESTFLIGHT_GOOGLE_APP_ID + function_api_base_url: function_api_base_url_from_xcconfig(TESTFLIGHT_CONFIGURATION) ) end @@ -519,6 +541,7 @@ platform :ios do lane :upload_testflight_build do api_key = asc_api_key + firebase_configuration = expected_firebase_configuration_from_xcconfig(TESTFLIGHT_CONFIGURATION) # lane_context는 같은 fastlane 실행 내에서만 유지되므로, 별도 CI step에서는 고정 ipa 경로를 사용한다. ipa_output_path = lane_context[SharedValues::IPA_OUTPUT_PATH].to_s ipa_output_path = TESTFLIGHT_IPA_OUTPUT_PATH if ipa_output_path.empty? @@ -532,8 +555,8 @@ platform :ios do bundle_id: APP_IDENTIFIER, database_id: TESTFLIGHT_DATABASE_ID, function_api_base_url: function_api_base_url_from_xcconfig(TESTFLIGHT_CONFIGURATION), - firebase_project_id: TESTFLIGHT_FIREBASE_PROJECT_ID, - google_app_id: TESTFLIGHT_GOOGLE_APP_ID + firebase_project_id: firebase_configuration[:firebase_project_id], + google_app_id: firebase_configuration[:google_app_id] } ) @@ -546,6 +569,7 @@ platform :ios do lane :upload_appstore_build do api_key = asc_api_key + firebase_configuration = expected_firebase_configuration_from_xcconfig(APPSTORE_CONFIGURATION) # lane_context는 같은 fastlane 실행 내에서만 유지되므로, 별도 CI step에서는 고정 ipa 경로를 사용한다. ipa_output_path = lane_context[SharedValues::IPA_OUTPUT_PATH].to_s ipa_output_path = APPSTORE_IPA_OUTPUT_PATH if ipa_output_path.empty? @@ -558,7 +582,9 @@ platform :ios do app_environment: APPSTORE_APP_ENVIRONMENT, bundle_id: APP_IDENTIFIER, database_id: APPSTORE_DATABASE_ID, - function_api_base_url: function_api_base_url_from_xcconfig(APPSTORE_CONFIGURATION) + function_api_base_url: function_api_base_url_from_xcconfig(APPSTORE_CONFIGURATION), + firebase_project_id: firebase_configuration[:firebase_project_id], + google_app_id: firebase_configuration[:google_app_id] } ) From 8c7ffa747e4b6c67ab6ab3cc635f090ef129f12b Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 23 Jul 2026 18:48:31 +0900 Subject: [PATCH 4/4] =?UTF-8?q?docs:=20Firebase=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EB=B3=84=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=B2=A0=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EB=AC=B8=EC=84=9C=20=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index df38de89..b9cfbe54 100644 --- a/README.md +++ b/README.md @@ -190,16 +190,16 @@ Application/App/Sources/Resource/ └── GoogleService-Info.plist ``` -Firestore database는 build configuration 기준으로 분리함 +Firebase 프로젝트와 Firestore database는 build configuration 기준으로 분리함 ```text -Debug, Staging -> staging -Release -> prod +Debug, Staging -> staging Firebase project / Firestore (default) +Release -> prod Firebase project / Firestore (default) ``` TestFlight archive는 `Staging`, App Store 실제 서비스 archive는 `Release` configuration을 사용함 GitHub Actions 배포 workflow는 PR label 기반 자동 실행 없이 수동 실행함 -TestFlight build는 App Store 심사 제출 대상으로 승격하지 않고, 실제 배포는 같은 `MARKETING_VERSION`의 별도 `Release/prod` build로 생성함 +TestFlight build는 App Store 심사 제출 대상으로 승격하지 않고, 실제 배포는 같은 `MARKETING_VERSION`의 별도 `Release` configuration build로 생성함 build number는 TestFlight와 App Store upload가 공유하는 App Store Connect build number 공간에서 자동 증가함 - TestFlight build: `bundle exec fastlane testflight_build_only`