Skip to content

Merge remote-tracking branch 'origin/main' into deploy #8

Merge remote-tracking branch 'origin/main' into deploy

Merge remote-tracking branch 'origin/main' into deploy #8

# ===================================================================
# Flutter iOS TestFlight 자동 배포 워크플로우
# ===================================================================
#
# 이 워크플로우는 Flutter iOS 앱을 빌드하여 TestFlight에 자동 배포합니다.
#
# ★ 마법사 우선 아키텍처 ★
# - 빌드에 필요한 설정 파일들은 웹 마법사가 생성합니다
# - 워크플로우는 마법사가 생성한 파일들을 그대로 사용합니다
# - 마법사 경로: .github/util/flutter/ios-testflight-setup-wizard/index.html
# (브라우저에서 열어서 사용)
#
# 빌드 파이프라인:
# 1. flutter build ios --no-codesign (Flutter 빌드)
# 2. xcodebuild archive (Xcode 아카이브 생성)
# 3. xcodebuild -exportArchive (IPA 생성, ExportOptions.plist 사용)
# 4. fastlane upload_testflight (마법사 생성 Fastfile 사용)
#
# ===================================================================
# 📋 필요한 GitHub Secrets
# ===================================================================
#
# 🔐 Apple 인증서 & 프로비저닝 프로파일 (필수):
# - APPLE_CERTIFICATE_BASE64 : .p12 인증서 (base64 인코딩)
# - APPLE_CERTIFICATE_PASSWORD : .p12 인증서 비밀번호
# - APPLE_PROVISIONING_PROFILE_BASE64 : .mobileprovision 파일 (base64 인코딩)
# - IOS_PROVISIONING_PROFILE_NAME : 프로비저닝 프로파일 이름 (예: "MyApp Distribution")
#
# 🔑 App Store Connect API (필수):
# - APP_STORE_CONNECT_API_KEY_ID : API Key ID (10자리, 예: ABC123DEF4)
# - APP_STORE_CONNECT_ISSUER_ID : Issuer ID (UUID 형식)
# - APP_STORE_CONNECT_API_KEY_BASE64: AuthKey_XXXXXX.p8 파일 (base64 인코딩)
#
# 📝 환경 설정 (선택):
# - ENV_FILE (또는 ENV) : .env 파일 내용 (앱에서 사용하는 환경변수)
# - SECRETS_XCCONFIG : ios/Flutter/Secrets.xcconfig 내용 (선택)
# iOS 네이티브 빌드 시 필요한 API 키 등
# (예: GOOGLE_MAPS_API_KEY=xxx)
#
# ===================================================================
# 🛠️ 초기 설정 방법
# ===================================================================
#
# 1. 웹 마법사 실행:
# 브라우저에서 .github/util/flutter/ios-testflight-setup-wizard/index.html 열기
# → 필요한 정보 입력 후 설정 파일 다운로드
#
# 2. GitHub Secrets 설정 (위 목록 참고)
#
# 3. 생성된 파일 커밋:
# git add ios/
# git commit -m "chore: iOS TestFlight 배포 설정"
#
# 4. deploy 브랜치로 푸시하여 배포 시작
#
# ===================================================================
name: PROJECT-iOS-TestFlight-Deploy
on:
push:
branches: ["deploy"]
workflow_run:
workflows: ["CHANGELOG 자동 업데이트"]
types: [completed]
branches: [main]
workflow_dispatch:
# ============================================
# 🔧 프로젝트별 설정 (아래 값들을 수정하세요)
# ============================================
env:
FLUTTER_VERSION: "3.35.5"
XCODE_VERSION: "16.4"
PROJECT_TYPE: "flutter"
# ============================================
# 📝 환경 파일 설정
# ============================================
# .env 파일 생성 경로 (프로젝트 루트 기준)
ENV_FILE_PATH: ".env"
# ============================================
# 📝 TestFlight Changelog 설정
# ============================================
SKIP_WAITING_FOR_BUILD_PROCESSING: "false"
jobs:
prepare-build:
name: 환경 설정 및 준비
runs-on: macos-15
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name != 'workflow_run' }}
outputs:
version: ${{ steps.current_version.outputs.version }}
build_number: ${{ steps.current_version.outputs.build_number }}
project_type: ${{ steps.current_version.outputs.project_type }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: deploy
fetch-depth: 0
- name: Pull latest changes
run: git pull origin deploy
- name: Select Xcode version
run: sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Developer
- name: Create .env file
run: |
cat << 'EOF' > ${{ env.ENV_FILE_PATH }}
${{ secrets.ENV_FILE || secrets.ENV }}
EOF
echo "✅ ${{ env.ENV_FILE_PATH }} file created"
- name: Create Secrets.xcconfig file
run: |
mkdir -p ios/Flutter
if [ -n "${{ secrets.SECRETS_XCCONFIG }}" ]; then
echo "${{ secrets.SECRETS_XCCONFIG }}" > ios/Flutter/Secrets.xcconfig
echo "Secrets.xcconfig created"
else
echo "// No secrets provided" > ios/Flutter/Secrets.xcconfig
fi
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Cache Flutter dependencies
uses: actions/cache@v4
with:
path: ~/.pub-cache
key: ${{ runner.os }}-flutter-pub-${{ hashFiles('**/pubspec.lock') }}
restore-keys: ${{ runner.os }}-flutter-pub-
- name: Install dependencies
run: flutter pub get
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.1"
bundler-cache: true
- name: Install CocoaPods
run: |
gem install cocoapods
cd ios && pod install
- name: 버전 관리 스크립트 권한 설정
run: |
chmod +x ./.github/scripts/version_manager.sh
chmod +x ./.github/scripts/changelog_manager.py
- name: 현재 버전 정보 가져오기
id: current_version
run: |
VERSION=$(./.github/scripts/version_manager.sh get | tail -n 1)
BUILD_NUMBER=$(./.github/scripts/version_manager.sh get-code | tail -n 1)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT
echo "project_type=${{ env.PROJECT_TYPE }}" >> $GITHUB_OUTPUT
echo "📋 버전: $VERSION (빌드번호: $BUILD_NUMBER)"
- name: 릴리즈 노트 생성
id: release_notes
run: |
VERSION="${{ steps.current_version.outputs.version }}"
if [ -f "CHANGELOG.json" ]; then
python3 ./.github/scripts/changelog_manager.py generate-md
python3 ./.github/scripts/changelog_manager.py export --version $VERSION --output final_release_notes.txt
if [ -s final_release_notes.txt ]; then
echo "RELEASE_NOTES_FOUND=true" >> $GITHUB_ENV
else
echo "RELEASE_NOTES_FOUND=false" >> $GITHUB_ENV
echo "v$VERSION 업데이트" > final_release_notes.txt
fi
else
echo "RELEASE_NOTES_FOUND=false" >> $GITHUB_ENV
echo "v$VERSION 업데이트" > final_release_notes.txt
fi
- name: Upload release notes
uses: actions/upload-artifact@v4
with:
name: release-notes
path: final_release_notes.txt
retention-days: 1
- name: Upload project files
uses: actions/upload-artifact@v4
with:
name: project-files
path: |
${{ env.ENV_FILE_PATH }}
ios/Flutter/Secrets.xcconfig
pubspec.yaml
lib/
assets/
retention-days: 1
# ============================================
# iOS 빌드 (xcodebuild 직접 사용)
# ============================================
build-ios:
name: iOS 앱 빌드
runs-on: macos-15
needs: prepare-build
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: deploy
fetch-depth: 0
- name: Pull latest changes
run: git pull origin deploy
- name: Select Xcode version
run: sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Developer
- name: Download project files
uses: actions/download-artifact@v4
with:
name: project-files
path: .
- name: Ensure .env file exists
run: |
if [ ! -f "${{ env.ENV_FILE_PATH }}" ]; then
cat << 'EOF' > ${{ env.ENV_FILE_PATH }}
${{ secrets.ENV_FILE || secrets.ENV }}
EOF
echo "✅ ${{ env.ENV_FILE_PATH }} file created (fallback)"
fi
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Cache Flutter dependencies
uses: actions/cache@v4
with:
path: ~/.pub-cache
key: ${{ runner.os }}-flutter-pub-${{ hashFiles('**/pubspec.lock') }}
restore-keys: ${{ runner.os }}-flutter-pub-
- name: Install dependencies
run: flutter pub get
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.1"
bundler-cache: true
- name: Install CocoaPods
run: |
gem install cocoapods
cd ios && pod install
- name: Import Code-Signing Certificates
uses: Apple-Actions/import-codesign-certs@v2
with:
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
- name: Install Provisioning Profiles
id: install_profile
run: |
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
# 메인 앱 프로파일 설치
echo "${{ secrets.APPLE_PROVISIONING_PROFILE_BASE64 }}" | base64 --decode > profile.mobileprovision
cp profile.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
# UUID 추출
uuid=$(grep -A1 -a "UUID" profile.mobileprovision | grep string | sed -e "s/<string>//" -e "s/<\/string>//" -e "s/[[:space:]]//g")
cp profile.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/$uuid.mobileprovision
# 프로파일 이름 추출 (동적)
profile_name=$(security cms -D -i profile.mobileprovision | plutil -extract Name xml1 -o - - | sed -n 's/.*<string>\(.*\)<\/string>.*/\1/p')
echo "✅ Main App Provisioning Profile installed"
echo " UUID: $uuid"
echo " Name: $profile_name"
# 출력 저장
echo "profile_uuid=$uuid" >> $GITHUB_OUTPUT
echo "profile_name=$profile_name" >> $GITHUB_OUTPUT
# Share Extension 프로파일 설치 (선택적)
if [ -n "${{ secrets.APPLE_PROVISIONING_PROFILE_SHARE_BASE64 }}" ]; then
echo "${{ secrets.APPLE_PROVISIONING_PROFILE_SHARE_BASE64 }}" | base64 --decode > share_extension_profile.mobileprovision
cp share_extension_profile.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
# Share Extension UUID 추출
share_uuid=$(grep -A1 -a "UUID" share_extension_profile.mobileprovision | grep string | sed -e "s/<string>//" -e "s/<\/string>//" -e "s/[[:space:]]//g")
cp share_extension_profile.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/$share_uuid.mobileprovision
# Share Extension 프로파일 이름 추출 (동적)
share_profile_name=$(security cms -D -i share_extension_profile.mobileprovision | plutil -extract Name xml1 -o - - | sed -n 's/.*<string>\(.*\)<\/string>.*/\1/p')
echo "✅ Share Extension Provisioning Profile installed"
echo " UUID: $share_uuid"
echo " Name: $share_profile_name"
# 출력 저장
echo "share_profile_uuid=$share_uuid" >> $GITHUB_OUTPUT
echo "share_profile_name=$share_profile_name" >> $GITHUB_OUTPUT
fi
- name: Create ExportOptions.plist
run: |
cd ios
# Share Extension 프로파일이 있는지 확인
if [ -n "${{ steps.install_profile.outputs.share_profile_name }}" ]; then
# Share Extension 포함
cat > ExportOptions.plist << EOF
<?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>app-store</string>
<key>teamID</key>
<string>${{ secrets.APPLE_TEAM_ID }}</string>
<key>provisioningProfiles</key>
<dict>
<key>com.elipair.mapsy</key>
<string>${{ steps.install_profile.outputs.profile_name }}</string>
<key>com.elipair.mapsy.share</key>
<string>${{ steps.install_profile.outputs.share_profile_name }}</string>
</dict>
<key>signingStyle</key>
<string>manual</string>
<key>signingCertificate</key>
<string>Apple Distribution</string>
<key>stripSwiftSymbols</key>
<true/>
<key>uploadBitcode</key>
<false/>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>
EOF
echo "✅ ExportOptions.plist 생성 완료 (Share Extension 포함)"
echo " Main Profile: ${{ steps.install_profile.outputs.profile_name }}"
echo " Share Extension Profile: ${{ steps.install_profile.outputs.share_profile_name }}"
else
# 메인 앱만
cat > ExportOptions.plist << EOF
<?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>app-store</string>
<key>teamID</key>
<string>${{ secrets.APPLE_TEAM_ID }}</string>
<key>provisioningProfiles</key>
<dict>
<key>com.elipair.mapsy</key>
<string>${{ steps.install_profile.outputs.profile_name }}</string>
</dict>
<key>signingStyle</key>
<string>manual</string>
<key>signingCertificate</key>
<string>Apple Distribution</string>
<key>stripSwiftSymbols</key>
<true/>
<key>uploadBitcode</key>
<false/>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>
EOF
echo "✅ ExportOptions.plist 생성 완료 (메인 앱만)"
echo " Main Profile: ${{ steps.install_profile.outputs.profile_name }}"
fi
- name: Flutter build (no codesign)
run: |
flutter build ios --release --no-codesign \
--build-name="${{ needs.prepare-build.outputs.version }}" \
--build-number="${{ needs.prepare-build.outputs.build_number }}"
- name: Configure Code Signing in Project
run: |
cd ios
# xcodeproj gem 설치
gem install xcodeproj
# Ruby 스크립트로 프로젝트 설정 수정
ruby <<RUBY_SCRIPT
require 'xcodeproj'
project = Xcodeproj::Project.open('Runner.xcodeproj')
# Runner 타겟 설정
runner_target = project.targets.find { |t| t.name == 'Runner' }
if runner_target
runner_target.build_configurations.each do |config|
if config.name == 'Release'
config.build_settings['CODE_SIGN_STYLE'] = 'Manual'
config.build_settings['CODE_SIGN_IDENTITY'] = 'Apple Distribution'
config.build_settings['PROVISIONING_PROFILE_SPECIFIER'] = '${{ steps.install_profile.outputs.profile_name }}'
puts "✅ Runner target configured with profile: ${{ steps.install_profile.outputs.profile_name }}"
end
end
end
# Share Extension 타겟 설정 (있는 경우)
share_profile = '${{ steps.install_profile.outputs.share_profile_name }}'
if !share_profile.empty?
share_target = project.targets.find { |t| t.name == 'com.elipair.mapsy.share' }
if share_target
share_target.build_configurations.each do |config|
if config.name == 'Release'
config.build_settings['CODE_SIGN_STYLE'] = 'Manual'
config.build_settings['CODE_SIGN_IDENTITY'] = 'Apple Distribution'
config.build_settings['PROVISIONING_PROFILE_SPECIFIER'] = share_profile
puts "✅ com.elipair.mapsy.share target configured with profile: #{share_profile}"
end
end
end
end
project.save
puts "✅ Project signing configuration saved"
RUBY_SCRIPT
- name: Create Archive
run: |
cd ios
echo "📦 Creating Archive with Manual Signing"
xcodebuild -workspace Runner.xcworkspace \
-scheme Runner \
-configuration Release \
-archivePath build/Runner.xcarchive \
-destination 'generic/platform=iOS' \
archive
- name: Export IPA
run: |
cd ios
xcodebuild -exportArchive \
-archivePath build/Runner.xcarchive \
-exportPath build/ipa \
-exportOptionsPlist ExportOptions.plist
- name: Upload IPA artifact
uses: actions/upload-artifact@v4
with:
name: ios-ipa
path: ios/build/ipa/*.ipa
retention-days: 1
# ============================================
# TestFlight 배포 (마법사 생성 Fastfile 사용)
# ============================================
deploy-testflight:
name: TestFlight 배포
runs-on: macos-15
needs: [prepare-build, build-ios]
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: deploy
fetch-depth: 0
- name: Setup App Store Connect API Key
run: |
mkdir -p ~/.appstoreconnect/private_keys
echo "${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }}" | base64 --decode > ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}.p8
- name: Download IPA artifact
uses: actions/download-artifact@v4
with:
name: ios-ipa
path: ios/build/ipa/
- name: Download release notes
uses: actions/download-artifact@v4
with:
name: release-notes
path: .
- name: Verify Fastfile exists
run: |
if [ ! -f "ios/fastlane/Fastfile" ]; then
echo "❌ ios/fastlane/Fastfile이 없습니다!"
echo "웹 마법사를 실행하여 설정 파일을 생성하세요:"
echo " 브라우저에서 .github/util/flutter/ios-testflight-setup-wizard/index.html 열기"
exit 1
fi
echo "✅ Fastfile 확인됨: ios/fastlane/Fastfile"
- name: Install Fastlane
run: gem install fastlane
- name: Upload to TestFlight
env:
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
SKIP_WAITING_FOR_BUILD_PROCESSING: ${{ env.SKIP_WAITING_FOR_BUILD_PROCESSING }}
run: |
# IPA 파일 찾기
IPA_PATH=$(find $GITHUB_WORKSPACE/ios/build/ipa -name "*.ipa" | head -1)
echo "📦 Found IPA at: $IPA_PATH"
if [ ! -f "$IPA_PATH" ]; then
echo "❌ IPA 파일을 찾을 수 없습니다!"
ls -la ios/build/ipa/
exit 1
fi
# Release notes 준비
if [ -f "final_release_notes.txt" ]; then
RELEASE_NOTES=$(cat final_release_notes.txt)
echo "📝 Release Notes:"
echo "$RELEASE_NOTES"
else
RELEASE_NOTES="v${{ needs.prepare-build.outputs.version }} 업데이트"
echo "📝 기본 Release Notes: $RELEASE_NOTES"
fi
# 환경변수 설정 (Fastfile에서 사용)
export API_KEY_PATH="$HOME/.appstoreconnect/private_keys/AuthKey_${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}.p8"
export IPA_PATH="$IPA_PATH"
export RELEASE_NOTES="$RELEASE_NOTES"
# 디버깅 정보
echo "🔧 Working directory: $(pwd)"
echo "🔧 IPA_PATH: $IPA_PATH"
echo "🔧 API_KEY_PATH: $API_KEY_PATH"
# Fastlane 실행 (마법사가 생성한 Fastfile 사용)
cd ios
fastlane upload_testflight
- name: Notify TestFlight Upload Success
if: success()
run: |
echo "✅ TestFlight 업로드 성공!"
echo "📱 버전: ${{ needs.prepare-build.outputs.version }}"
echo "🔢 빌드번호: ${{ needs.prepare-build.outputs.build_number }}"
echo "📝 커밋: ${{ github.sha }}"
- name: Notify on Failure
if: failure()
run: |
echo "❌ TestFlight 업로드 실패!"
echo "로그를 확인해주세요."