Repository navigation
๐[๊ธฐ๋ฅ๊ฐ์ ][CICD] AppStore/PlayStore ๋ฐฐํฌ ์ง์ ๋ํญ ๊ฐ์ > ์ฌ์ฌ ์๋ ์ ์ถ + ๋ฐฐํฌ ๋ชจ๋ 3๋จ๊ณ ํต์ผย #934
Description
๐ ๊ฐ์
iOS(App Store) ยท Android(Play Store) ์คํ ์ด ๋ฐฐํฌ CICD๋ฅผ ์ ๋ฉด ๊ฐ์ ์๋ฃํ๋ค. ์ด ๋ฌธ์๋ ์ด ์ด์๋ง ๋ณด๊ณ ๋ ์ฒ์ ๋ณด๋ ์ฌ๋์ด ๋์ผํ ๋ฐฐํฌ ํ์ดํ๋ผ์ธ์ ์์ฑยท์ ์ง๋ณด์ํ ์ ์๋๋ก ํ์ผ๋ณ ์ญํ , ์์กด์ฑ, ์ํฌ๋ฆฟ, ์ค์ ์ฝ๋, ๋์ ํ๋ฆ, ํธ๋ฌ๋ธ์ํ ์ ๋ชจ๋ ์ ๋ฆฌํ๋ค.
๊ด๋ จ ์ด์: #930(Android ๊ฑฐ์ง ์ฑ๊ณต ์ ๊ฑฐ), #931(iOS ์ฌ์ฌ ์๋ ์ ์ถ ๋๋ฝ). ์์ธ ๊ตฌํ ๋ณด๊ณ ์๋ #931 ๋๊ธ ์ฐธ์กฐ.
์ต์ข
๊ฒฐ๊ณผ: ์ฝ๋ push ํ ๋ฒ์ผ๋ก ๋น๋ โ ์คํ ์ด ์
๋ก๋ โ (๋ชจ๋์ ๋ฐ๋ผ) ์ฌ์ฌ ์ง์ ๋๋ ์ฌ์ฌ ์๋ ์ ์ถ๊น์ง ์๋ํ. iOS 1.10.104๊ฐ ์ค์ App Store ์ฌ์ฌ ํ(Waiting for Review)์ ์๋ ์ง์
์ค์ธก ๊ฒ์ฆ ์๋ฃ. Android 1.10.101 production "๊ฒํ ์ค" ์ง์
๊ฒ์ฆ ์๋ฃ.
๐๏ธ 1. ํ์ผ ๊ตฌ์ฑ๊ณผ ์์กด์ฑ
1-1. ์ด์ ์ํฌํ๋ก์ฐ (์ค์ ๋ฐฐํฌ๊ฐ ๋๋ ๊ณณ)
| ํ์ผ | ์ญํ | ํธ๋ฆฌ๊ฑฐ |
|---|---|---|
.github/workflows/ROMROM-IOS-TESTFLIGHT.yaml |
iOS ๋น๋ โ TestFlight โ App Store deliver | push: deploy / workflow_run(CHANGELOG) / workflow_dispatch |
.github/workflows/ROMROM-ANDROID-PLAYSTORE-CICD.yaml |
Android ๋น๋ โ internal โ production promote | ๋์ผ |
์ค์: ๋ ์ํฌํ๋ก์ฐ ๋ชจ๋
ref: deploy๋ฅผ checkout ํ๋ค. ์ฆ deploy ๋ธ๋์น์ ์ฝ๋/Fastfile๋ก ๋น๋ํ๋ค. main์๋ง ๋จธ์งํ๋ฉด ๋ฐฐํฌ์ ๋ฐ์ ์ ๋จ โ main โ deploy ๋จธ์ง ํ์.
1-2. Fastlane ์ค์ ํ์ผ
| ํ์ผ | ์ญํ | ๋๊ฐ ์ฌ์ฉ |
|---|---|---|
android/fastlane/Fastfile.playstore |
Android lane(deploy_internal, promote_internal_to_production) |
Android ์ํฌํ๋ก์ฐ๊ฐ Fastfile๋ก ๋ณต์ฌํด ์ฌ์ฉ |
| (iOS) ์ธ๋ผ์ธ Fastfile | iOS ์ํฌํ๋ก์ฐ step ์์์ cat > ios/fastlane/Fastfile ๋ก ๋์ ์์ฑ |
iOS ์ํฌํ๋ก์ฐ ์์ฒด |
iOS๋ ๋ณ๋ Fastfile ํ์ผ์ด ์๋ค. ์ํฌํ๋ก์ฐ step "Install Fastlane and create Fastfile"์ด heredoc์ผ๋ก
ios/fastlane/Fastfile์ ๊ทธ ์๋ฆฌ์์ ๋ง๋ ๋ค. iOS deliver ๋ก์ง์ ๊ณ ์น๋ ค๋ฉด ์ด ์ํฌํ๋ก์ฐ YAML ์์ heredoc์ ์์ ํด์ผ ํ๋ค.
1-3. ๋ง๋ฒ์ฌ(wizard) ํ ํ๋ฆฟ โ ์ ๊ท ํ๋ก์ ํธ ์ ์ ์ฉ
| ํ์ผ | ์ญํ |
|---|---|
.github/util/flutter/testflight-wizard/templates/Fastfile |
iOS Fastfile ํ
ํ๋ฆฟ (์ ๊ท ํ๋ก์ ํธ๊ฐ ios/fastlane/Fastfile๋ก ๋ฐ์). ์ด์ ์ธ๋ผ์ธ Fastfile๊ณผ ๋์ผ ๋ก์ง ์ ์ง |
.github/util/flutter/testflight-wizard/templates/Gemfile |
fastlane ~> 2.228 + multi_json |
.github/util/flutter/testflight-wizard/testflight-wizard-setup.sh |
์ ํ ํ๋ฆฟ๋ค์ ํ๋ก์ ํธ์ ์ค์น + Xcode Manual Signing ํจ์น |
.github/util/flutter/playstore-wizard/playstore-wizard-setup.sh/.ps1 |
Android ์
์
(+multi_json) |
์ ์ง๋ณด์ ์์น: ์ด์ ์ํฌํ๋ก์ฐ(์ธ๋ผ์ธ Fastfile)๋ฅผ ๊ณ ์น๋ฉด wizard ํ ํ๋ฆฟ๋ ๊ฐ์ด ๊ณ ์ณ์ผ ๋์ด ๋ถ๊ธฐ๋์ง ์๋๋ค.
โ๏ธ 2. ํ์ํ GitHub Secrets / Variables
2-1. iOS Secrets
| Secret | ์ฉ๋ |
|---|---|
APPLE_CERTIFICATE_BASE64 / APPLE_CERTIFICATE_PASSWORD |
Distribution ์ธ์ฆ์(.p12) |
APPLE_PROVISIONING_PROFILE_BASE64 |
Provisioning Profile |
APP_STORE_CONNECT_API_KEY_BASE64 / APP_STORE_CONNECT_API_KEY_ID / APP_STORE_CONNECT_ISSUER_ID |
App Store Connect API Key(.p8) โ pilotยทdeliver ์ธ์ฆ |
ENV_FILE, SECRETS_XCCONFIG, GOOGLE_SERVICE_INFO_PLIST |
์ฑ ํ๊ฒฝ/Firebase |
2-2. Android Secrets
| Secret | ์ฉ๋ |
|---|---|
| Release Keystore ๊ด๋ จ | AAB ์๋ช |
GOOGLE_PLAY_JSON_KEY(์๋น์ค ๊ณ์ ) |
Play Console API |
2-3. Repository Variables (๋ฐฐํฌ ๋ชจ๋ ์ ์ด)
| Variable | ๊ธฐ๋ณธ(ํด๋ฐฑ) | ํจ๊ณผ |
|---|---|---|
IOS_DEPLOY_MODE |
testflight_only |
iOS ํ์ ์๋ ๋ฐฐํฌ ๋ชจ๋ |
ANDROID_DEPLOY_MODE |
store_submit |
Android ํ์ ์๋ ๋ฐฐํฌ ๋ชจ๋ |
์ฐ์ ์์:
workflow_dispatchinput > repo variable > ํด๋ฐฑ.
๐๏ธ 3. ๋ฐฐํฌ ๋ชจ๋ 3๋จ๊ณ (์ ํ๋ซํผ ๊ณตํต ๊ฐ๋ )
| ๋ชจ๋ | iOS (App Store) | Android (Play Store) | ์ฌ๋ ๊ฐ์ |
|---|---|---|---|
store_only |
TestFlight ์ ๋ก๋๊น์ง๋ง (pilot) | internal ํธ๋ ์ ๋ก๋๊น์ง๋ง | ์์(ํ ์คํธ์ฉ) |
store_prepare |
๋ฒ์ ์์ฑ + ๋น๋ ์ ํ + What's New ์ฑ์ Prepare for Submission | production draft ์น๊ธ | ASC์์ Add for Review / ์ฝ์์์ "์ถ์ ์์" |
store_submit |
submit_for_review๋ก ์ฌ์ฌ ์๋ ์ ์ถ |
production completed ์น๊ธ(์ฌ์ฌ ์๋ ๋ฑ๋ก) | ์์(์์ ์๋) |
iOS๋ testflight_only/appstore_prepare/appstore_submit ๋ค์ด๋ฐ๊ณผ store_* ๋ ๋ค ์ธ์(Fastfile์์ ๋ณ์นญ ์ฒ๋ฆฌ).
flowchart TD
A(["์ฝ๋ push โ deploy"]) --> B["Flutter ๋น๋ + ์ฝ๋์ฌ์ธ + IPA/AAB"]
B --> C["์คํ ์ด ์
๋ก๋ (pilot / internal)"]
C --> D{"DEPLOY_MODE?"}
D -->|store_only| E(["์
๋ก๋๊น์ง๋ง โ ์ข
๋ฃ"])
D -->|store_prepare| F["๋น๋ ์ฒ๋ฆฌ ๋๊ธฐ โ deliver/promote(draft)"]
D -->|store_submit| G["๋น๋ ์ฒ๋ฆฌ ๋๊ธฐ โ deliver/promote(completed)"]
F --> H(["์ ์ถ ์ง์ โ ์ฌ๋์ด ๋ฒํผ ํด๋ฆญ"])
G --> I(["์ฌ์ฌ ์๋ ์ ์ถ โ Waiting for Review"])
๐ 4. iOS deliver ํต์ฌ ์ฝ๋ (์ธ๋ผ์ธ Fastfile)
iOS ์ํฌํ๋ก์ฐ์ deploy_appstore lane ์ค deliver ๋ถ๋ถ. 6๊ฐ ์ต์
์ด ๊ฐ๊ฐ ํน์ ์คํจ๋ฅผ ๋ง๋๋ค โ ํ๋๋ผ๋ ๋น ์ง๋ฉด ๊ทธ ๋จ๊ณ์์ ์คํจํ๋ค.
# ๋ชจ๋ ํ์ (store_* / appstore_* ๋ณ์นญ ํธํ)
deploy_mode = (ENV["DEPLOY_MODE"] || "testflight_only").strip
prepare_appstore = ["appstore_prepare","appstore_submit","store_prepare","store_submit"].include?(deploy_mode)
submit_to_appstore = ["appstore_submit","store_submit"].include?(deploy_mode)
# 1) TestFlight ์
๋ก๋ (ํญ์). ASC ๋ชจ๋๋ฉด ๋น๋ ์ฒ๋ฆฌ ์๋ฃ๊น์ง ๋๊ธฐ(deliver๊ฐ ๊ทธ ๋น๋๋ฅผ ๊ณจ๋ผ์ผ ํ๋ฏ๋ก)
pilot(
api_key: api_key,
app_identifier: app_id, # ์์ผ๋ฉด ๋น๋ํํ crash
ipa: ENV["IPA_PATH"],
changelog: ENV["RELEASE_NOTES"],
skip_waiting_for_build_processing: !prepare_appstore # store_only๋ฉด ๋๊ธฐ ์ ํจ
)
if prepare_appstore
# whatsNew(What's New) ๋ฉํ๋ฅผ ์ ๋๊ฒฝ๋ก๋ก ์์ฑ โ โ ๏ธ ๋ก์ผ์ผ์ 'ko'๋ง ์ ํจ(ko-KR ๋ฌดํจ)
metadata_path = File.join(ENV["GITHUB_WORKSPACE"], "ios", "fastlane", "metadata")
FileUtils.mkdir_p(File.join(metadata_path, "ko"))
File.write(File.join(metadata_path, "ko", "release_notes.txt"), release_notes)
# ์ฌ์ฌ Notes ์ด๊ธฐํ(์ด์ ๊ฑฐ์ ์๋ช
์ ์๋์น๊ณ ๋ฐฉ์ง) โ notes.txt๋ง ๋น์, ์ฐ๋ฝ์ฒยท๋ฐ๋ชจ๋ ASC ๊ธฐ์กด๊ฐ ๋ณด์กด
FileUtils.mkdir_p(File.join(metadata_path, "review_information"))
File.write(File.join(metadata_path, "review_information", "notes.txt"), "")
deliver(
api_key: api_key,
app_identifier: app_id, # [์์ธ1] Bundle ID ๋ฏธ์ ๋ฌ โ crash
app_version: ENV["APP_VERSION"], # [์์ธ2] editable ๋ฒ์ ์๋ ์์ฑ/์ ํ
build_number: ENV["BUILD_NUMBER"], # [์์ธ5] ๋น๋ ๋ช
์ ์ ํ(skip_metadata:false๋ฉด ์๋์ ํ ๋๋ฝ)
submit_for_review: submit_to_appstore, # store_submit์ด๋ฉด ์ฌ์ฌ ์ ์ถ
automatic_release: submit_to_appstore,
force: true, # CI ๋น๋ํํ
run_precheck_before_submit: false,
skip_binary_upload: true, # pilot์ผ๋ก ์ฌ๋ฆฐ ๋น๋ ์ฌ์ฉ
skip_metadata: false, # whatsNew/notes ์
๋ก๋ ์ํด false
metadata_path: metadata_path, # [์์ธ3] ์ ๋๊ฒฝ๋ก (์๋๊ฒฝ๋ก๋ CWD ์ด๊ธ๋จ)
skip_screenshots: true
# app_review_information์ ์ ๋๊น โ ASC ๊ธฐ์กด ์ฐ๋ฝ์ฒยท๋ฐ๋ชจ๊ณ์ ๋ณด์กด(๊ฐ์ธ์ ๋ณด ํ๋์ฝ๋ฉ ๋ฐฉ์ง)
)
end์ํฌํ๋ก์ฐ step์ด deliver์ ๋๊ธฐ๋ ENV (deploy-testflight job):
export APP_IDENTIFIER="com.alom.romrom"
export APP_VERSION="${{ needs.prepare-build.outputs.version }}"
export BUILD_NUMBER="${{ needs.prepare-build.outputs.build_number }}"
export DEPLOY_MODE="${{ env.DEPLOY_MODE }}"
export RELEASE_NOTES="$(cat final_release_notes.txt)"๐ค 5. Android promote ํต์ฌ ์ฝ๋ (Fastfile.playstore)
deploy_mode = (ENV["DEPLOY_MODE"] || "store_submit").strip
promote = ["store_prepare","store_submit","appstore_prepare","appstore_submit"].include?(deploy_mode)
submit = ["store_submit","appstore_submit"].include?(deploy_mode)
# Step1: internal ํธ๋ ์
๋ก๋ (completed)
upload_to_play_store(package_name: 'com.alom.romrom', track: 'internal',
aab: ENV["AAB_PATH"], json_key: ENV["GOOGLE_PLAY_JSON_KEY"], release_status: 'completed', ...)
next unless promote # store_only๋ฉด ์ฌ๊ธฐ์ ์ข
๋ฃ
# Step2: production ์น๊ธ โ submit์ด๋ฉด completed(์ฌ์ฌ ์๋๋ฑ๋ก), prepare๋ฉด draft
promote_internal_to_production(promote_status: submit ? 'completed' : 'draft')lane :promote_internal_to_production do |options|
upload_to_play_store(
track: 'internal', track_promote_to: 'production',
track_promote_release_status: options[:promote_status] || 'completed',
rollout: '1.0',
changes_not_sent_for_review: false, # ๋ณ๊ฒฝ์ ์ฌ์ฌ๋ก ์ ์ก
rescue_changes_not_sent_for_review: false # [๊ฑฐ์ง์ฑ๊ณต ์ฐจ๋จ] ์คํจ ์ ์ํฌํ๋ก์ฐ ๋นจ๊ฐ๋ถ
)
end๊ฑฐ์ง ์ฑ๊ณต์ ์ ์ฒด: fastlane supply์
rescue_changes_not_sent_for_review๊ธฐ๋ณธ๊ฐ์ดtrue๋ผ, Google API๊ฐ "changesNotSentForReview๋ฅผ true๋ก ์ค์ ํ๋ผ" ์๋ฌ๋ฅผ ๋ด๋ฉด fastlane์ด ์๋ rescueํด ์ฌ์ฌ ์ ๋ณด๋ด๊ณ ์ ์ฅ๋ง ํ ์ฑ success ๋ฐํํ๋ค.false๋ก ๋ช ์ํด์ผ ์คํจ๊ฐ ํ๋ฉดํ๋๋ค.
โ ๏ธ 6. ํธ๋ฌ๋ธ์ํ
(์ค์ ๊ฒช์ 6๊ฐ ์์ธ)
| ์ฆ์(๋ก๊ทธ) | ์์ธ | ํด๊ฒฐ |
|---|---|---|
Could not infer your App's Bundle Identifier |
deliver์ app_identifier ์์(๋น๋ํํ) | app_identifier ๋ช
์ |
Cannot submit for review - could not find an editable version |
App Store์ ํด๋น ๋ฒ์ ํ์ด์ง ์์ | app_version ๋ช
์(์๋ ์์ฑ) |
missing required attribute 'whatsNew' |
What's New ๋ฉํ ๋ฏธ๋ฐ์(๊ฒฝ๋ก ์ด๊ธ๋จ) | metadata_path ์ ๋๊ฒฝ๋ก |
Unsupported directory name(s) ... ko-KR |
fastlane ๋ฏธ์ง์ ๋ก์ผ์ผ ์ฝ๋ | ko๋ง ์ฌ์ฉ(ko-KR ๊ธ์ง) |
| 1.10.x ๋ฒ์ ์ ์๋ฑํ ์ด์ ๋น๋๊ฐ ๋ถ์ | skip_metadata:false๋ฉด deliver๊ฐ ๋น๋ ์๋์ ํ ๊ฑด๋๋ |
build_number ๋ช
์ |
| Notes์ ์ ๊ฑฐ์ ์๋ช ์๊ฐ ๋งค๋ฒ ๋จ์ | ASC๊ฐ ์ด์ ๋ฒ์ Notes ์๋ ์น๊ณ | review_information/notes.txt ๋น ํ์ผ |
Apple ๋น๋ ์ฒ๋ฆฌ(Processing) ๋ณ๋ชฉ โ ์ฝ๋ ๋ฌธ์ ์๋
store_prepare/store_submit์ deliver๊ฐ "๋ฐฉ๊ธ ์ฌ๋ฆฐ ๊ทธ ๋น๋"(๊ฐ์ ๋ฒ์ ์ ํฉ์ฑ)๋ฅผ ๊ณจ๋ผ์ผ ํ๋ฏ๋ก pilot์ด ๊ทธ ๋น๋์ Processing โ Complete๋ฅผ ๊ธฐ๋ค๋ฆฐ๋ค. ์ด ์๊ฐ์ Apple ์๋ฒ ์ฌ์ (๋ณดํต 5~15๋ถ, ๋๋๋ก 1์๊ฐ+, ํต์ ๋ถ๊ฐ). CI๊ฐ "TestFlight ๋ฐฐํฌ" step์์ ์ค๋ ๋ฉ์ถฐ๋ hang์ด ์๋๋ค. ๊ธํ๋ฉด store_only ์ฌ์ฉ. ๋จ๊ธฐ๊ฐ ์ฐ์ ์
๋ก๋๋ Processing ํ๋ฅผ ๋ฐ๋ฆฌ๊ฒ ํ๋ฏ๋ก ์์ .
โ 7. ์ด์ ์ฒดํฌ๋ฆฌ์คํธ
- main ๋ณ๊ฒฝ ํ deploy ๋ธ๋์น์ ๋จธ์งํด์ผ ๋ฐฐํฌ์ ๋ฐ์๋จ (
ref: deploy) - ํ์ ๊ธฐ๋ณธ: iOS
store_prepare(์ ์ถ ์ง์ , ์์ ) / Androidstore_submit(์๋ ์ฌ์ฌ) - ์ฌ์ฌ๊น์ง ์๋ ์ํ๋ฉด: iOS๋
IOS_DEPLOY_MODE=store_submit๋๋ dispatch์์ ์ ํ - ์ธ๋ผ์ธ Fastfile(์ด์) ์์ ์ wizard ํ ํ๋ฆฟ๋ ๋๊ธฐํ
- iOS ๋ก์ผ์ผ์ ํญ์
ko(ko-KR ๊ธ์ง), ๋ค๋ฅธ ๋ก์ผ์ผ์DELIVER_LOCALESENV๋ก ์ถ๊ฐ - ๊ฐ์ธ์ ๋ณด(์ฐ๋ฝ์ฒยท๋ฐ๋ชจ๊ณ์ )๋ ASC์์ ๊ด๋ฆฌ, ์ฝ๋์ ํ๋์ฝ๋ฉ ๊ธ์ง
๐โโ๏ธ ๋ด๋น์
- ํ๋ก ํธ์๋: @Cassiiopeia
Activity
SeoHyun1024 commentedon Jun 22, 2026
Guide by SUH-LAB
๋ธ๋์น
์ปค๋ฐ ๋ฉ์์ง
[-]๐[๊ธฐ๋ฅ๊ฐ์ ][CICD] AppStore/PlayStore ๋ฐฐํฌ ์ง์ ๋ํญ ๊ฐ์ โ ์ฌ์ฌ ์๋ ์ ์ถ + ๋ฐฐํฌ ๋ชจ๋ 3๋จ๊ณ ํต์ผ (์๋ฃ ๋ฌธ์)[/-][+]๐[๊ธฐ๋ฅ๊ฐ์ ][CICD] AppStore/PlayStore ๋ฐฐํฌ ์ง์ ๋ํญ ๊ฐ์ โ ์ฌ์ฌ ์๋ ์ ์ถ + ๋ฐฐํฌ ๋ชจ๋ 3๋จ๊ณ ํต์ผ[/+][-]๐[๊ธฐ๋ฅ๊ฐ์ ][CICD] AppStore/PlayStore ๋ฐฐํฌ ์ง์ ๋ํญ ๊ฐ์ โ ์ฌ์ฌ ์๋ ์ ์ถ + ๋ฐฐํฌ ๋ชจ๋ 3๋จ๊ณ ํต์ผ[/-][+]๐[๊ธฐ๋ฅ๊ฐ์ ][CICD] AppStore/PlayStore ๋ฐฐํฌ ์ง์ ๋ํญ ๊ฐ์ > ์ฌ์ฌ ์๋ ์ ์ถ + ๋ฐฐํฌ ๋ชจ๋ 3๋จ๊ณ ํต์ผ[/+]