Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
415e74a
chore: Android/iOS 릴리즈 빌드 CI 자동화 (AAB/IPA 산출물 추출)(#312)
jeonbinggu Jul 2, 2026
f6a13a6
chore: 리뷰 사항 반영(#312)
jeonbinggu Jul 4, 2026
7301648
fix: CI 리뷰 반영(#312)
MlNTYS Jul 4, 2026
567b0f6
chore: release CI에 google-services.json 주입 스텝 추가(#312)
jeonbinggu Jul 12, 2026
9ef26cc
Merge branch 'develop' of https://github.com/Finders-Official/Web int…
jeonbinggu Aug 8, 2026
d7a6441
chore: release CI에 GoogleService-Info.plist 주입 스텝 추가(#312)
jeonbinggu Aug 8, 2026
f461d43
fix: 릴리즈 빌드 번호 기준값을 배포된 versionCode에 맞춤(#312)
jeonbinggu Aug 8, 2026
5a3bf81
chore: 머지 전 CI 검증용 push 트리거 임시 추가(#312)
jeonbinggu Aug 8, 2026
4812211
fix: release CI의 Node 버전을 22로 올림(#312)
jeonbinggu Aug 8, 2026
d19ac47
chore: 서명 Secret 디코딩 결과를 검증하는 가드 추가(#312)
jeonbinggu Aug 9, 2026
64abcf0
chore: iOS 서명 Secret 재검증용 빈 커밋(#312)
jeonbinggu Aug 9, 2026
1e789dd
chore: 서명 파일 매직 바이트를 로그에 남김(#312)
jeonbinggu Aug 9, 2026
0d777ae
chore: 서명 스텝에 명령 단위 마커 추가(#312)
jeonbinggu Aug 9, 2026
65626f1
fix: 프로비저닝 프로파일 디코딩을 openssl로 교체(#312)
jeonbinggu Aug 9, 2026
d378730
fix: 서명 설정을 App 타깃으로 옮겨 Pods 아카이브 실패 해결(#312)
jeonbinggu Aug 9, 2026
858364c
chore: CI 검증용 임시 트리거와 디버그 마커 제거(#312)
jeonbinggu Aug 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
255 changes: 255 additions & 0 deletions .github/workflows/release-build.yml

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MUST: 이거 github secrests로 빌드 하려면 env: 으로 명시 참조해야 주입됩니당

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SHOULD: 이거 하면서 android 부분에 빌드 버전 자동으로 올리는 것도 같이 하면 좋을 거 같아요!

Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
name: Release Build (AAB / IPA)

on:
push:
branches: [main]
workflow_dispatch:

concurrency:
group: release-build-${{ github.ref }}
cancel-in-progress: true

env:
# 빌드 번호 = VERSION_CODE_BASE + github.run_number (Android versionCode / iOS CFBundleVersion 공용).
# 이미 배포된 versionCode(4) 이상이어야 Play Console이 업로드를 받는다.
VERSION_CODE_BASE: 4

jobs:
android-build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Enable Corepack (pnpm)
run: |
corepack enable
pnpm -v

- name: Setup Node (with pnpm cache)
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- name: Install dependencies
run: pnpm install

- name: Build web assets
env:
VITE_PUBLIC_API_URL: ${{ secrets.VITE_PUBLIC_API_URL }}
VITE_PUBLIC_KAKAO_REDIRECT_URI: ${{ secrets.VITE_PUBLIC_KAKAO_REDIRECT_URI }}
VITE_PUBLIC_KAKAO_JS_KEY: ${{ secrets.VITE_PUBLIC_KAKAO_JS_KEY }}
VITE_PUBLIC_KAKAO_REST_API_KEY: ${{ secrets.VITE_PUBLIC_KAKAO_REST_API_KEY }}
VITE_PUBLIC_KAKAO_NATIVE_APP_KEY: ${{ secrets.VITE_PUBLIC_KAKAO_NATIVE_APP_KEY }}
VITE_PUBLIC_APPLE_CLIENT_ID: ${{ secrets.VITE_PUBLIC_APPLE_CLIENT_ID }}
VITE_PUBLIC_APPLE_REDIRECT_URI: ${{ secrets.VITE_PUBLIC_APPLE_REDIRECT_URI }}
VITE_PUBLIC_PORTONE_STORE_ID: ${{ secrets.VITE_PUBLIC_PORTONE_STORE_ID }}
VITE_PUBLIC_PORTONE_CHANNEL_KEY: ${{ secrets.VITE_PUBLIC_PORTONE_CHANNEL_KEY }}
run: pnpm build

- name: Sync Capacitor (android)
run: npx cap sync android

- name: Setup JDK
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"

- name: Setup Android SDK
uses: android-actions/setup-android@v3

- name: Write google-services.json
env:
ANDROID_GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.ANDROID_GOOGLE_SERVICES_JSON_BASE64 }}
run: |
echo "$ANDROID_GOOGLE_SERVICES_JSON_BASE64" | base64 -d > android/app/google-services.json

- name: Decode keystore
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: |
echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > android/app/release.keystore

- name: Write keystore.properties
env:
ANDROID_KEYSTORE_STORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_STORE_PASSWORD }}
ANDROID_KEYSTORE_KEY_ALIAS: ${{ secrets.ANDROID_KEYSTORE_KEY_ALIAS }}
ANDROID_KEYSTORE_KEY_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_KEY_PASSWORD }}
run: |
cat <<EOF > android/keystore.properties
storeFile=release.keystore
storePassword=$ANDROID_KEYSTORE_STORE_PASSWORD
keyAlias=$ANDROID_KEYSTORE_KEY_ALIAS
keyPassword=$ANDROID_KEYSTORE_KEY_PASSWORD
EOF

- name: Build release AAB
working-directory: android
run: |
chmod +x gradlew
VERSION_CODE=$((VERSION_CODE_BASE + GITHUB_RUN_NUMBER))
echo "versionCode: $VERSION_CODE"
./gradlew bundleRelease --no-daemon -PversionCode=$VERSION_CODE

- name: Upload AAB artifact
uses: actions/upload-artifact@v4
with:
name: app-release-aab
path: android/app/build/outputs/bundle/release/*.aab
retention-days: 14

ios-build:
runs-on: macos-latest
env:
TEAM_ID: YC7R5A6449
# TestFlight 내부 테스트 배포용. TestFlight는 app-store 방식 IPA를 사용하며, 내부 테스터 배포는 App Review 없이 가능.
EXPORT_METHOD: app-store
BUNDLE_ID: com.finders.app
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Enable Corepack (pnpm)
run: |
corepack enable
pnpm -v

- name: Setup Node (with pnpm cache)
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- name: Install dependencies
run: pnpm install

- name: Build web assets
env:
VITE_PUBLIC_API_URL: ${{ secrets.VITE_PUBLIC_API_URL }}
VITE_PUBLIC_KAKAO_REDIRECT_URI: ${{ secrets.VITE_PUBLIC_KAKAO_REDIRECT_URI }}
VITE_PUBLIC_KAKAO_JS_KEY: ${{ secrets.VITE_PUBLIC_KAKAO_JS_KEY }}
VITE_PUBLIC_KAKAO_REST_API_KEY: ${{ secrets.VITE_PUBLIC_KAKAO_REST_API_KEY }}
VITE_PUBLIC_KAKAO_NATIVE_APP_KEY: ${{ secrets.VITE_PUBLIC_KAKAO_NATIVE_APP_KEY }}
VITE_PUBLIC_APPLE_CLIENT_ID: ${{ secrets.VITE_PUBLIC_APPLE_CLIENT_ID }}
VITE_PUBLIC_APPLE_REDIRECT_URI: ${{ secrets.VITE_PUBLIC_APPLE_REDIRECT_URI }}
VITE_PUBLIC_PORTONE_STORE_ID: ${{ secrets.VITE_PUBLIC_PORTONE_STORE_ID }}
VITE_PUBLIC_PORTONE_CHANNEL_KEY: ${{ secrets.VITE_PUBLIC_PORTONE_CHANNEL_KEY }}
run: pnpm build

- name: Sync Capacitor (ios)
run: npx cap sync ios

# Xcode 프로젝트에 리소스로 등록되어 있어 파일이 없으면 Archive가 바로 실패한다
# (Android와 달리 존재 여부를 체크하는 경로가 없음). cap sync 뒤에 써야 덮이지 않는다
- name: Write GoogleService-Info.plist
env:
IOS_GOOGLE_SERVICE_INFO_PLIST_BASE64: ${{ secrets.IOS_GOOGLE_SERVICE_INFO_PLIST_BASE64 }}
run: |
echo "$IOS_GOOGLE_SERVICE_INFO_PLIST_BASE64" | base64 -d -o ios/App/App/GoogleService-Info.plist

- name: Import signing certificate & provisioning profile
env:
CERTIFICATE_BASE64: ${{ secrets.IOS_DIST_CERTIFICATE_P12_BASE64 }}
CERTIFICATE_PASSWORD: ${{ secrets.IOS_DIST_CERTIFICATE_PASSWORD }}
PROFILE_BASE64: ${{ secrets.IOS_PROVISION_PROFILE_BASE64 }}
run: |
KEYCHAIN_PASSWORD=$(openssl rand -base64 24)
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
CERT_PATH="$RUNNER_TEMP/certificate.p12"
PROFILE_PATH="$RUNNER_TEMP/profile.mobileprovision"

echo "$CERTIFICATE_BASE64" | base64 -d -o "$CERT_PATH"
echo "$PROFILE_BASE64" | base64 -d -o "$PROFILE_PATH"

# Secret이 비었거나 base64가 아니면 여기서 잡는다.
# (안 잡으면 뒤의 security cms가 "Cannot parse a NULL or zero-length data"로만 죽어 원인이 안 보인다)
for f in "$CERT_PATH" "$PROFILE_PATH"; do
SIZE=$(stat -f%z "$f" 2>/dev/null || echo 0)
# 컨테이너 형식 판별용 매직 바이트. DER(30 82…)가 아니면 인코딩이 잘못된 것이다
# (3c3f786d = "<?xml" 서명 안 된 plist, 그 외 ASCII면 base64 이중 인코딩)
echo "$(basename "$f"): ${SIZE} bytes, head=$(head -c 4 "$f" | xxd -p)"
if [ "$SIZE" -lt 100 ]; then
echo "::error::$(basename "$f") 디코딩 결과가 ${SIZE} bytes다. 해당 Secret 값을 다시 등록할 것"
exit 1
fi
done

security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -P "$CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | sed 's/"//g')

mkdir -p ~/Library/MobileDevice/"Provisioning Profiles"
cp "$PROFILE_PATH" ~/Library/MobileDevice/"Provisioning Profiles"/profile.mobileprovision

# security cms는 정상 DER 프로파일에도 "Cannot parse a NULL or zero-length data"로
# 실패한다(macos-latest 기준). openssl로 CMS 페이로드만 꺼낸다.
# -noverify: 서명 체인 검증은 불필요하고 payload plist만 있으면 된다
PROFILE_PLIST="$RUNNER_TEMP/profile.plist"
openssl smime -inform der -verify -noverify -in "$PROFILE_PATH" -out "$PROFILE_PLIST"

PROFILE_UUID=$(/usr/libexec/PlistBuddy -c "Print UUID" "$PROFILE_PLIST")
PROFILE_NAME=$(/usr/libexec/PlistBuddy -c "Print Name" "$PROFILE_PLIST")
echo "profile: $PROFILE_NAME ($PROFILE_UUID)"

echo "PROFILE_UUID=$PROFILE_UUID" >> "$GITHUB_ENV"
echo "PROFILE_NAME=$PROFILE_NAME" >> "$GITHUB_ENV"
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"

- name: Archive
working-directory: ios/App
run: |
BUILD_NUMBER=$((VERSION_CODE_BASE + GITHUB_RUN_NUMBER))
echo "build number: $BUILD_NUMBER"
xcodebuild -workspace App.xcworkspace \
-scheme App \
-configuration Release \
-archivePath "$RUNNER_TEMP/App.xcarchive" \
-destination "generic/platform=iOS" \
OTHER_CODE_SIGN_FLAGS="--keychain $KEYCHAIN_PATH" \
CURRENT_PROJECT_VERSION=$BUILD_NUMBER \
archive

- name: Create ExportOptions.plist
run: |
cat <<EOF > "$RUNNER_TEMP/ExportOptions.plist"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>$EXPORT_METHOD</string>
<key>teamID</key>
<string>$TEAM_ID</string>
<key>signingStyle</key>
<string>manual</string>
<key>provisioningProfiles</key>
<dict>
<key>$BUNDLE_ID</key>
<string>$PROFILE_NAME</string>
</dict>
</dict>
</plist>
EOF

- name: Export IPA
run: |
xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/App.xcarchive" \
-exportPath "$RUNNER_TEMP/build" \
-exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist"

- name: Upload IPA artifact
uses: actions/upload-artifact@v4
with:
name: app-release-ipa
path: ${{ runner.temp }}/build/*.ipa
retention-days: 14

- name: Cleanup keychain
if: always()
run: security delete-keychain "$KEYCHAIN_PATH" || true
2 changes: 1 addition & 1 deletion android/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ keystore.properties
.cxx/

# Google Services (e.g. APIs or Firebase)
# google-services.json
google-services.json

# Freeline
freeline.py
Expand Down
3 changes: 2 additions & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ android {
applicationId "com.finders"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 4
// CI가 -PversionCode로 주입한다. 폴백은 로컬 빌드용 (develop 기준 최신 값)
versionCode project.hasProperty('versionCode') ? (project.property('versionCode') as Integer) : 4
versionName "0.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
Expand Down
3 changes: 3 additions & 0 deletions ios/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ capacitor-cordova-ios-plugins
# Generated Config files
App/App/capacitor.config.json
App/App/config.xml

# Google Services (e.g. APIs or Firebase)
App/App/GoogleService-Info.plist
4 changes: 3 additions & 1 deletion ios/App/App.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -393,14 +393,16 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CODE_SIGN_IDENTITY = "Apple Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = YC7R5A6449;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.1.6;
PRODUCT_BUNDLE_IDENTIFIER = com.finders.app;
PROVISIONING_PROFILE_SPECIFIER = "Finders App Store";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
SWIFT_VERSION = 5.0;
Expand Down
Loading