① 배운 것
이 블로그들이 없었다면 절대 해내지 못했을것..
👇 내 워크플로우 코드
name: "[DEV] Build and Publish iOS"
on:
workflow_dispatch:
push:
branches:
- develop_test
permissions:
contents: write
jobs:
build:
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Apple certificate and provisioning profile
- name: Install the Apple certificate and provisioning profile
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.APPSTORE_CERT_BASE64 }}
P12_PASSWORD: ${{ secrets.APPSTORE_CERT_PASSWORD }}
BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.MOBILEPROVISION_BASE64 }}
BUILD_PROVISION_NOTI_PROFILE_BASE64: ${{ secrets.NOTI_MOBILEPROVISION_BASE64 }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
# 변수 생성
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
PP_PATH=$RUNNER_TEMP/github_action_2.mobileprovision
PP_NOTI_PATH=$RUNNER_TEMP/github_action_noti_2.mobileprovision
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
#secrets으로 부터 인증서와 프로비저닝 프로파일 가져오기
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode --output $CERTIFICATE_PATH
echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode --output $PP_PATH
echo -n "$BUILD_PROVISION_NOTI_PROFILE_BASE64" | base64 --decode --output $PP_NOTI_PATH
# 키체인 초기화 - 임시 키체인 생성
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# import certificate to keychain
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
# provisioning profile 적용
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_NOTI_PATH ~/Library/MobileDevice/Provisioning\ Profiles
# 디버깅
echo "Listing provisioning profiles:"
ls -l "$HOME/Library/MobileDevice/Provisioning Profiles/"
- name: Extract Version
run: |
VERSION_NAME=$(grep 'version:' pubspec.yaml | sed -e 's/version: //' -e 's/+.*//')
VERSION_CODE=$(grep 'version:' pubspec.yaml | sed -e 's/version: //' -e 's/.*+//')
echo "VERSION_NAME=$VERSION_NAME" >> $GITHUB_ENV
echo "VERSION_CODE=$VERSION_CODE" >> $GITHUB_ENV
id: extract_version
# Install flutter
- name: Flutter get
uses: subosito/flutter-action@v1
with:
channel: 'stable'
- name: Configure Git
run: |
git config --global url."https://${{ vars.USER_NAME }}:${{ secrets.TOKEN_GITHUB }}@github.com/".insteadOf "https://github.com/"
# Install your project's dependencies
- run: flutter pub get
# Make config.dart
- name: Generate config
run: |
echo '${{ secrets.CONFIG }}' | base64 --d >> ./lib/common/config.dart
# Build and sign the ipa using a single flutter command
- name: Building IPA
run: flutter build ipa --export-options-plist=ios/Runner/ExportOptions.plist --flavor dev
# Collect the file and upload as artifact
- name: collect ipa artifacts
uses: actions/upload-artifact@v4
with:
name: debug-ipa
path: build/ios/ipa/*.ipa
if-no-files-found: error
# Important! Cleanup: remove the certificate and provisioning profile from the runner!
- name: Clean up keychain and provisioning profile
if: ${{ always() }}
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
rm ~/Library/MobileDevice/Provisioning\ Profiles/github_action_2.mobileprovision
rm ~/Library/MobileDevice/Provisioning\ Profiles/github_action_noti_2.mobileprovision
- name: Create Github Release
uses: softprops/action-gh-release@v1
with:
tag_name: "${{ env.VERSION_NAME }}(${{ env.VERSION_CODE }})"
release_name: "${{ env.VERSION_NAME }}(${{ env.VERSION_CODE }})"
generate_release_notes: true
files: |
build/ios/ipa/*.ipa
# Release job, upload the ipa to Firebase App Distribution
# Container action is only supported on Linux
release:
name: Release ipa to Firebase
needs: [ build ]
runs-on: ubuntu-latest
steps:
# Retrieve ipa file from GitHub artifacts
- uses: actions/checkout@v4
- name: Get release-ipa from artifacts
uses: actions/download-artifact@v4
with:
name: debug-ipa
# Upload ipa file to Firebase app distribution.
- name: Upload artifact to Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{secrets.FIREBASE_APP_ID_IOS_DEV}}
serviceCredentialsFileContent: ${{ secrets.CREDENTIAL_FILE_CONTENT }}
groups: "testers"
file: 앱이름.ipa
이건 내가 ios의 certification, provisioning profile 개념을 잘 몰라서 발생한 일이다.
일단 결론부터 이야기하자면 나는 디버깅 모드로 ipa를 추출해서 앱 디스트리뷰션에 올리기 때문에 'development'타입의 certification과 provision이 필요했다. (distribution 타입이 아님)
👆 나는 보통 위의 과정을 통해 ipa를 추출해 앱 디스트리뷰션에 올린다.
근데 디버깅 모드로 앱을 추출하기위해서는 distribution 타입의 certification은 아예 선택을 할수가 없다. 그래서 development 타입의 certification과 그 certification이 포함된 provisioning profile이 필요함.
[certifications type 종류]

[provisioning profile type 종류]
여기서 ios App Development를 선택하고 다음 화면의 포함될 certification 선택하는 부분에서 위에서 만든 Development 타입의 certification을 포함하면됨 
[development type vs distribution type]
Development 인증서
- 용도:
- 개발 및 테스트 단계에서 사용
- 실제 기기에서 앱 테스트 가능
- Xcode에서 디버깅 가능
- 특징:
- 개발자당 여러 개 발급 가능 (최대 100개)
- UDID가 등록된 테스트 기기에서만 실행 가능
- 디버깅 정보 포함
- 유효기간: 1년
- 사용 시나리오:
- 개발 중인 앱 테스트
- 실제 디바이스에서 디버깅
- 테스트 빌드 배포
Distribution 인증서
- 용도:
- 앱스토어 배포용
- TestFlight 배포
- In-House 기업용 배포
- Ad-Hoc 배포
- 특징:
- 하나의 팀당 2개만 발급 가능
- 실제 배포용으로 최적화된 빌드 생성
- 디버깅 정보 미포함
- 유효기간: 1년
- 배포 방식:
- App Store: 앱스토어 공개 배포
- Ad Hoc: 제한된 디바이스에 배포
- Enterprise: 사내 배포
- TestFlight: 베타 테스트 배포
주요 차이점 요약:
1. 목적
- Development: 개발 및 테스트
- Distribution: 실제 배포 및 출시
- 발급 수량
- Development: 개발자당 최대 100개
- Distribution: 팀당 최대 2개
- 사용 범위
- Development: 등록된 테스트 기기만
- Distribution: 용도에 따라 다름 (전체 공개/제한적 배포)
- 기능
- Development: 디버깅 가능
- Distribution: 디버깅 불가, 최적화된 배포
- 보안
- Development: 개발용이라 상대적으로 덜 엄격
- Distribution: 더 엄격한 보안 요구사항
이 파일을 추출하기 위해서는 Xcode에서 아카이브를 하면된다.
평소에는 signing&capabilies에 automatically manage signing에 체크해두고 자동으로 signing을 관리했겠지만, 깃헙액션용 exportOptions.plist를 뽑아내기 위해서는 해당 체크박스에 체크 해지를 하고 위에서 만들어둔 provisioning profile을 선택해야한다.
그리고 아카이브를 돌리고 원하는 방식(나같은 경우는 위에서 보여준 디버깅 타입)으로 ipa를 추출하면 exportOptions.plist가 만들어진다.

👇 나의 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>compileBitcode</key>
<false/>
<key>destination</key>
<string>export</string>
<key>method</key>
<string>debugging</string>
<key>provisioningProfiles</key>
<dict>
<key>{bundle id}</key>
<string>github_action_2</string>
<key>{bundle id}.NotificationServiceExtension</key>
<string>github_action_noti_2</string>
</dict>
<key>signingCertificate</key>
<string>{certification id}</string>
<key>signingStyle</key>
<string>manual</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>{team id}</string>
<key>thinning</key>
<string><none></string>
</dict>
</plist>
내 프로젝트 같은 경우는 flavor로 빌드 타입을 나눠서 관리하고 있기 때문에 마지막에 --flavor dev를 붙여서 dev flavor로 빌드되도록 했다.
flutter build ipa --export-options-plist=ios/Runner/ExportOptions.plist --flavor dev
scheme manage에서 해당 scheme의 share에 체크하지 않으면 이 scheme이 공유가 안되서(이 scheme에 대한 설정 파일이 로컬에만있고 깃헙에 올라가지않는 너낌) flavor을 찾을 수 없다(scheme을 찾을 수 없다)는 에러가 나온다.

평소에는 automatically manage signing을 체크해두었지만 manual하게 signing하기 위해 해당 체크를 풀고 아까 만든 provisioning profile을 선택하면 project.pbxproj파일 코드가 변경된다. 수정된 버전의 파일을 푸시해줘야했음. 그렇지 않으면 필요한 profile을 찾지 못한다는 에러가 발생함 (이 부분은 내가 평소에 쓰던 provisioning profile 말고 다른 프로필을 써서 나만 필요한 단계인지는 정확히 모르겠음)

이 과정은 docker을 이용해 진행되는데, 리눅스 os에서만 가능하기 때문에 (도커의 특성상 메인 os[리눅스]랑 다른 os[맥]를 사용할 수 없어서 그런듯?? 잘 모르겟슴) ipa를 추출하는 job을 mac에서 했다면 이 과정은 리눅스를 이용하는 다른 job을 만들어서 실행해야한다.
그리고 이 job은 ipa를 추출하는 앞의 job이 성공해야 진행할 수 있기 때문에 need 키워드를 이용해 앞의 job이 성공하면 실행하게 코드를 짜야한다.

base64 -i certificate.p12 -o 아웃풋파일이름
② 회고 (restropective)
사실 IOS 빌드과정, certification, profile 같은 개념을 잘 몰라서 엄청 삽질을 했다.
거의 5일내내 삽질을 한 것 같다.
개념을 먼저 알고 시도하는게 아니라 일단 해보면서 감을 (개념도 아니고 감을 ㅋㅋ) 찾아가는 식으로 시도함
예전같았으면 이런일을 해야할때 엄청 막막하기만 했을텐데 이런일을 몇번 겪어보니 일단 암것도 모르고 시작해도 하다보면 알게되고 나중엔 어떻게든 해내게된다는 것이 학습되었다. 그래서 이번에도 물론 좀 막막하긴 했지만 예전만큼 막 울면서 하진않고(뻥임 예전에도 울진않았음 그냥 그런마음이였다는거) 하루하루 삽질할때마다 오늘은 이만큼 알게됐으니까 내일은 이걸 이용해서 다음 단계로 나가면된다 이런 생각으로 계속 시도했던 것 같다.
그리고 삽질한 시간들도 결국 다 내 지식이되고 피와 살이 되기 때문에 삽질한 시간도 의미없거나 아깝다는 생각이 들지 않게된것 같다.
그리고 진짜 결국 성공해벌임✌️
뿌듯허다... 🍀

다음엔 fastlane을 이용해서 스토어에 올리는것을츄라이츄라이 😵
③ 개선을 위한 방법
1. 깃헙액션 플로우에서 플러터를 다운받고 캐시에 저장해두어서 다음에 다시 다운 받지 않아도 되게 개선
2. 지금은 안드로이드랑 ios랑 yml파일이 따로 되어있는데 한번에 합쳐야함