Skip to content

Commit 8a8e702

Browse files
authored
[#760] App Store 후보 빌드의 Firebase 연결 설정을 (default)와 공통 api로 전환한다 (#761)
* chore: App Store Firebase 설정을 비공개 구성으로 정렬 * fix: 스토어 빌드의 공통 API 경로 검증 추가 * chore: Firebase 식별자를 비공개 구성으로 이동 * docs: Firebase 환경별 데이터베이스 문서 정정
1 parent ece9e86 commit 8a8e702

3 files changed

Lines changed: 116 additions & 39 deletions

File tree

Application/App/Project.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ let project = Project(
6565
release: [
6666
"APP_ENVIRONMENT": "prod",
6767
"APS_ENVIRONMENT": "production",
68-
"FIRESTORE_DATABASE_ID": "prod",
68+
"FIRESTORE_DATABASE_ID": "(default)",
6969
]
7070
)
7171
),

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,16 +190,16 @@ Application/App/Sources/Resource/
190190
└── GoogleService-Info.plist
191191
```
192192

193-
Firestore database는 build configuration 기준으로 분리함
193+
Firebase 프로젝트와 Firestore database는 build configuration 기준으로 분리함
194194

195195
```text
196-
Debug, Staging -> staging
197-
Release -> prod
196+
Debug, Staging -> staging Firebase project / Firestore (default)
197+
Release -> prod Firebase project / Firestore (default)
198198
```
199199

200200
TestFlight archive는 `Staging`, App Store 실제 서비스 archive는 `Release` configuration을 사용함
201201
GitHub Actions 배포 workflow는 PR label 기반 자동 실행 없이 수동 실행함
202-
TestFlight build는 App Store 심사 제출 대상으로 승격하지 않고, 실제 배포는 같은 `MARKETING_VERSION`의 별도 `Release/prod` build로 생성함
202+
TestFlight build는 App Store 심사 제출 대상으로 승격하지 않고, 실제 배포는 같은 `MARKETING_VERSION`의 별도 `Release` configuration build로 생성함
203203
build number는 TestFlight와 App Store upload가 공유하는 App Store Connect build number 공간에서 자동 증가함
204204

205205
- TestFlight build: `bundle exec fastlane testflight_build_only`

fastlane/Fastfile

Lines changed: 111 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ APPSTORE_CONFIGURATION = "Release"
1212
TESTFLIGHT_APP_ENVIRONMENT = "staging"
1313
APPSTORE_APP_ENVIRONMENT = "prod"
1414
TESTFLIGHT_DATABASE_ID = "(default)"
15-
APPSTORE_DATABASE_ID = "prod"
16-
TESTFLIGHT_FUNCTION_API_BASE_URL = "https://asia-northeast3-devlog-staging.cloudfunctions.net/api/api"
17-
APPSTORE_FUNCTION_API_BASE_URL = "https://asia-northeast3-devlog-c87b6.cloudfunctions.net/prodApi/api"
18-
TESTFLIGHT_FIREBASE_PROJECT_ID = "devlog-staging"
19-
TESTFLIGHT_GOOGLE_APP_ID = "1:686659799179:ios:eba4e86096153f0539ec8e"
15+
APPSTORE_DATABASE_ID = "(default)"
16+
SHARED_FUNCTION_API_PATH = "/api/api"
17+
APP_CONFIG_XCCONFIG_PATH = File.expand_path(
18+
"../Application/App/Sources/Resource/Config.xcconfig",
19+
__dir__
20+
)
2021
VERSION_XCCONFIG_PATH = File.expand_path(
2122
"../Application/Shared/Version.xcconfig",
2223
__dir__
@@ -54,6 +55,79 @@ rescue SystemCallError => error
5455
UI.user_error!("Could not read version configuration file at #{expanded_path}: #{error.message}")
5556
end
5657

58+
def function_api_base_url_from_xcconfig(configuration)
59+
expanded_path = File.expand_path(APP_CONFIG_XCCONFIG_PATH)
60+
UI.user_error!("Missing app configuration file: #{expanded_path}") if !File.file?(expanded_path)
61+
62+
escaped_configuration = Regexp.escape(configuration)
63+
assignments = File.foreach(expanded_path).filter_map do |line|
64+
assignment = line.match(
65+
/^\s*FUNCTION_API_BASE_URL\s*\[\s*config\s*=\s*#{escaped_configuration}\s*\]\s*=\s*(.*)$/
66+
)
67+
next if assignment.nil?
68+
69+
assignment[1].strip
70+
end
71+
72+
if assignments.empty?
73+
UI.user_error!("Missing FUNCTION_API_BASE_URL for #{configuration} in #{expanded_path}")
74+
end
75+
76+
function_api_base_url = assignments.last.gsub("/$()/", "//")
77+
if function_api_base_url.empty?
78+
UI.user_error!("Empty FUNCTION_API_BASE_URL for #{configuration} in #{expanded_path}")
79+
end
80+
81+
function_api_base_url
82+
rescue SystemCallError => error
83+
UI.user_error!("Could not read app configuration file at #{expanded_path}: #{error.message}")
84+
end
85+
86+
def expected_firebase_configuration_from_xcconfig(configuration)
87+
expanded_path = File.expand_path(APP_CONFIG_XCCONFIG_PATH)
88+
UI.user_error!("Missing app configuration file: #{expanded_path}") if !File.file?(expanded_path)
89+
90+
escaped_configuration = Regexp.escape(configuration)
91+
expected_settings = {
92+
firebase_project_id: "EXPECTED_FIREBASE_PROJECT_ID",
93+
google_app_id: "EXPECTED_GOOGLE_APP_ID"
94+
}
95+
96+
expected_settings.to_h do |key, setting|
97+
assignments = File.foreach(expanded_path).filter_map do |line|
98+
assignment = line.match(
99+
/^\s*#{setting}\s*\[\s*config\s*=\s*#{escaped_configuration}\s*\]\s*=\s*(.*)$/
100+
)
101+
next if assignment.nil?
102+
103+
assignment[1].strip
104+
end
105+
106+
if assignments.empty? || assignments.last.empty?
107+
UI.user_error!("Missing #{setting} for #{configuration} in #{expanded_path}")
108+
end
109+
110+
[key, assignments.last]
111+
end
112+
rescue SystemCallError => error
113+
UI.user_error!("Could not read app configuration file at #{expanded_path}: #{error.message}")
114+
end
115+
116+
def verify_function_api_base_url_policy(function_api_base_url)
117+
require "uri"
118+
119+
uri = URI.parse(function_api_base_url)
120+
is_valid = uri.is_a?(URI::HTTPS) &&
121+
!uri.host.to_s.empty? &&
122+
uri.path == SHARED_FUNCTION_API_PATH &&
123+
uri.query.nil? &&
124+
uri.fragment.nil?
125+
126+
UI.user_error!("Invalid FUNCTION_API_BASE_URL for store build") if !is_valid
127+
rescue URI::InvalidURIError
128+
UI.user_error!("Invalid FUNCTION_API_BASE_URL for store build")
129+
end
130+
57131
default_platform(:ios)
58132

59133
platform :ios do
@@ -105,12 +179,12 @@ platform :ios do
105179
when TESTFLIGHT_CONFIGURATION
106180
{
107181
database_id: TESTFLIGHT_DATABASE_ID,
108-
function_api_base_url: TESTFLIGHT_FUNCTION_API_BASE_URL
182+
function_api_base_url: function_api_base_url_from_xcconfig(TESTFLIGHT_CONFIGURATION)
109183
}
110184
when APPSTORE_CONFIGURATION
111185
{
112186
database_id: APPSTORE_DATABASE_ID,
113-
function_api_base_url: APPSTORE_FUNCTION_API_BASE_URL
187+
function_api_base_url: function_api_base_url_from_xcconfig(APPSTORE_CONFIGURATION)
114188
}
115189
else
116190
UI.user_error!("Unsupported store configuration: #{configuration}")
@@ -119,6 +193,8 @@ platform :ios do
119193
UI.user_error!("Missing Firestore database ID for #{configuration}") if database_id.empty?
120194
UI.user_error!("Missing Function API base URL for #{configuration}") if function_api_base_url.empty?
121195

196+
verify_function_api_base_url_policy(function_api_base_url)
197+
122198
if database_id != expected_configuration[:database_id]
123199
UI.user_error!(
124200
"Invalid store configuration: #{configuration} must use FIRESTORE_DATABASE_ID=#{expected_configuration[:database_id]}, got #{database_id}"
@@ -127,11 +203,11 @@ platform :ios do
127203

128204
if function_api_base_url != expected_configuration[:function_api_base_url]
129205
UI.user_error!(
130-
"Invalid store configuration: #{configuration} must use FUNCTION_API_BASE_URL=#{expected_configuration[:function_api_base_url]}, got #{function_api_base_url}"
206+
"Invalid store configuration: #{configuration} uses an unexpected FUNCTION_API_BASE_URL"
131207
)
132208
end
133209

134-
UI.message("Verified store configuration: #{configuration}/#{database_id}/#{function_api_base_url}")
210+
UI.message("Verified store configuration: #{configuration}/#{database_id}")
135211
end
136212

137213
private_lane :verify_store_info_plist do |options|
@@ -205,7 +281,8 @@ platform :ios do
205281
path: plist_path,
206282
source: "app Info.plist",
207283
key: "FUNCTION_API_BASE_URL",
208-
expected: expected_configuration[:function_api_base_url]
284+
expected: expected_configuration[:function_api_base_url],
285+
redact_values: true
209286
)
210287

211288
verify_plist_value(
@@ -235,15 +312,11 @@ platform :ios do
235312
)
236313

237314
if !expected_firebase_project_id.empty? && actual_firebase_project_id != expected_firebase_project_id
238-
UI.user_error!(
239-
"Unexpected PROJECT_ID in GoogleService-Info.plist: expected #{expected_firebase_project_id}, got #{actual_firebase_project_id}"
240-
)
315+
UI.user_error!("Unexpected PROJECT_ID in GoogleService-Info.plist")
241316
end
242317

243318
if !expected_google_app_id.empty? && actual_google_app_id != expected_google_app_id
244-
UI.user_error!(
245-
"Unexpected GOOGLE_APP_ID in GoogleService-Info.plist: expected #{expected_google_app_id}, got #{actual_google_app_id}"
246-
)
319+
UI.user_error!("Unexpected GOOGLE_APP_ID in GoogleService-Info.plist")
247320
end
248321

249322
UI.message("Verified PROJECT_ID in GoogleService-Info.plist")
@@ -283,9 +356,11 @@ platform :ios do
283356
UI.user_error!("Missing expected #{options[:key]}") if expected_value.empty?
284357

285358
if actual_value != expected_value
286-
UI.user_error!(
287-
"Unexpected #{options[:key]} in #{options[:source]}: expected #{expected_value}, got #{actual_value}"
288-
)
359+
message = "Unexpected #{options[:key]} in #{options[:source]}"
360+
if options[:redact_values] != true
361+
message += ": expected #{expected_value}, got #{actual_value}"
362+
end
363+
UI.user_error!(message)
289364
end
290365

291366
UI.message("Verified #{options[:key]} in #{options[:source]}")
@@ -297,7 +372,7 @@ platform :ios do
297372
expected_database_id = options[:database_id] ||
298373
(configuration == APPSTORE_CONFIGURATION ? APPSTORE_DATABASE_ID : TESTFLIGHT_DATABASE_ID)
299374
expected_function_api_base_url = options[:function_api_base_url] ||
300-
(configuration == APPSTORE_CONFIGURATION ? APPSTORE_FUNCTION_API_BASE_URL : TESTFLIGHT_FUNCTION_API_BASE_URL)
375+
function_api_base_url_from_xcconfig(configuration)
301376
expected_app_environment = configuration == APPSTORE_CONFIGURATION ?
302377
APPSTORE_APP_ENVIRONMENT : TESTFLIGHT_APP_ENVIRONMENT
303378

@@ -307,6 +382,8 @@ platform :ios do
307382
function_api_base_url: expected_function_api_base_url
308383
)
309384

385+
firebase_configuration = expected_firebase_configuration_from_xcconfig(configuration)
386+
310387
if ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"].to_s.strip.empty?
311388
ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "30"
312389
end
@@ -398,8 +475,8 @@ platform :ios do
398475
bundle_id: APP_IDENTIFIER,
399476
database_id: expected_database_id,
400477
function_api_base_url: expected_function_api_base_url,
401-
firebase_project_id: options[:firebase_project_id],
402-
google_app_id: options[:google_app_id]
478+
firebase_project_id: firebase_configuration[:firebase_project_id],
479+
google_app_id: firebase_configuration[:google_app_id]
403480
}
404481
)
405482

@@ -415,9 +492,7 @@ platform :ios do
415492
build_for_store(
416493
configuration: TESTFLIGHT_CONFIGURATION,
417494
database_id: TESTFLIGHT_DATABASE_ID,
418-
function_api_base_url: TESTFLIGHT_FUNCTION_API_BASE_URL,
419-
firebase_project_id: TESTFLIGHT_FIREBASE_PROJECT_ID,
420-
google_app_id: TESTFLIGHT_GOOGLE_APP_ID
495+
function_api_base_url: function_api_base_url_from_xcconfig(TESTFLIGHT_CONFIGURATION)
421496
)
422497

423498
upload_testflight_build
@@ -427,9 +502,7 @@ platform :ios do
427502
build_for_store(
428503
configuration: TESTFLIGHT_CONFIGURATION,
429504
database_id: TESTFLIGHT_DATABASE_ID,
430-
function_api_base_url: TESTFLIGHT_FUNCTION_API_BASE_URL,
431-
firebase_project_id: TESTFLIGHT_FIREBASE_PROJECT_ID,
432-
google_app_id: TESTFLIGHT_GOOGLE_APP_ID
505+
function_api_base_url: function_api_base_url_from_xcconfig(TESTFLIGHT_CONFIGURATION)
433506
)
434507
end
435508

@@ -438,7 +511,7 @@ platform :ios do
438511
configuration: APPSTORE_CONFIGURATION,
439512
output_directory: APPSTORE_BUILD_OUTPUT_DIRECTORY,
440513
database_id: APPSTORE_DATABASE_ID,
441-
function_api_base_url: APPSTORE_FUNCTION_API_BASE_URL
514+
function_api_base_url: function_api_base_url_from_xcconfig(APPSTORE_CONFIGURATION)
442515
)
443516
end
444517

@@ -447,7 +520,7 @@ platform :ios do
447520
configuration: APPSTORE_CONFIGURATION,
448521
output_directory: APPSTORE_BUILD_OUTPUT_DIRECTORY,
449522
database_id: APPSTORE_DATABASE_ID,
450-
function_api_base_url: APPSTORE_FUNCTION_API_BASE_URL
523+
function_api_base_url: function_api_base_url_from_xcconfig(APPSTORE_CONFIGURATION)
451524
)
452525

453526
upload_appstore_build
@@ -468,6 +541,7 @@ platform :ios do
468541

469542
lane :upload_testflight_build do
470543
api_key = asc_api_key
544+
firebase_configuration = expected_firebase_configuration_from_xcconfig(TESTFLIGHT_CONFIGURATION)
471545
# lane_context는 같은 fastlane 실행 내에서만 유지되므로, 별도 CI step에서는 고정 ipa 경로를 사용한다.
472546
ipa_output_path = lane_context[SharedValues::IPA_OUTPUT_PATH].to_s
473547
ipa_output_path = TESTFLIGHT_IPA_OUTPUT_PATH if ipa_output_path.empty?
@@ -480,9 +554,9 @@ platform :ios do
480554
app_environment: TESTFLIGHT_APP_ENVIRONMENT,
481555
bundle_id: APP_IDENTIFIER,
482556
database_id: TESTFLIGHT_DATABASE_ID,
483-
function_api_base_url: TESTFLIGHT_FUNCTION_API_BASE_URL,
484-
firebase_project_id: TESTFLIGHT_FIREBASE_PROJECT_ID,
485-
google_app_id: TESTFLIGHT_GOOGLE_APP_ID
557+
function_api_base_url: function_api_base_url_from_xcconfig(TESTFLIGHT_CONFIGURATION),
558+
firebase_project_id: firebase_configuration[:firebase_project_id],
559+
google_app_id: firebase_configuration[:google_app_id]
486560
}
487561
)
488562

@@ -495,6 +569,7 @@ platform :ios do
495569

496570
lane :upload_appstore_build do
497571
api_key = asc_api_key
572+
firebase_configuration = expected_firebase_configuration_from_xcconfig(APPSTORE_CONFIGURATION)
498573
# lane_context는 같은 fastlane 실행 내에서만 유지되므로, 별도 CI step에서는 고정 ipa 경로를 사용한다.
499574
ipa_output_path = lane_context[SharedValues::IPA_OUTPUT_PATH].to_s
500575
ipa_output_path = APPSTORE_IPA_OUTPUT_PATH if ipa_output_path.empty?
@@ -507,7 +582,9 @@ platform :ios do
507582
app_environment: APPSTORE_APP_ENVIRONMENT,
508583
bundle_id: APP_IDENTIFIER,
509584
database_id: APPSTORE_DATABASE_ID,
510-
function_api_base_url: APPSTORE_FUNCTION_API_BASE_URL
585+
function_api_base_url: function_api_base_url_from_xcconfig(APPSTORE_CONFIGURATION),
586+
firebase_project_id: firebase_configuration[:firebase_project_id],
587+
google_app_id: firebase_configuration[:google_app_id]
511588
}
512589
)
513590

0 commit comments

Comments
 (0)