
안녕하세요,
iOS에서 Continuous Deployment (CD)의 핵심은 개발이 완료된 후 Signing → Archiving & Exporting → Uploading하는 일련의 과정을 자동화하는 것입니다.
CD를 도입하기 전에 가장 먼저 고려해야 할 점은 "프로젝트의 장기성 "입니다. CD를 구축하는 것 또한 리소스가 필요한 작업이기 때문에 프로젝트의 유지보수가 필요하지 않은 경우 CD의 효과가 제한될 수 있습니다.
그러나 iOS 개발을 진행하면서 테스트 플라이트에 배포하는 작업에 어려움을 겪는다면, 혹은 GitHub에 코드를 푸시하면 자동으로 특정 작업이 수행되길 바란다면 이 글을 참고하시면 도움이 될 것입니다.
2024년 3월 현재, iOS CD를 구축하는 데 선택할 수 있는 대안으로는 Xcode Cloud와 GitHub Actions가 있습니다. Xcode Cloud는 2022년에 베타 버전이 출시되었으며, 이후로 점차 사용자들이 늘어나고 있습니다.
Xcode Cloud의 주요 장점은 First-Party 툴임에도 불구하고 베타 OS를 사용할 수 있다는 것입니다. 반면, GitHub Actions는 YAML 스크립트를 사용하여 더 유연한 작업 프로세스를 구현할 수 있습니다. 저희 팀은 특히 SFTP를 통한 앱 파일 업로드 등과 같은 다양한 작업을 쉽게 연동할 수 있는 GitHub Actions를 선택하였습니다. 또한 GitHub Actions에는 이미 다양한 Action들이 제공되어 있어 우리의 요구사항에 쉽게 대응할 수 있었습니다.
또한, GitHub Actions를 사용함으로써 다른 플랫폼에서도 일관된 CD를 구축할 수 있으므로 유연성과 효율성을 높일 수 있습니다.
CD의 목표는 다음과 같습니다:
결론적으로, 원하는 브랜치에 코드가 올라간 후 위 작업들이 수행되고 슬랙으로 알림이 오는 데 약 3분이 소요되었습니다. 이는 프로젝트 규모와 IPA 파일의 크기에 따라 다를 수 있습니다.
위 CD 파이프라인을 통해 개발 및 배포 과정을 자동화하면 개발자들은 더 많은 시간을 코드 작성과 개선에 집중할 수 있으며, 릴리스 프로세스도 훨씬 빠르고 안정적으로 이루어질 수 있습니다. 이러한 결과는 팀의 생산성 향상과 소프트웨어 품질 향상에 도움이 됩니다.


name: Action Test
on:
push:
branches: ["action-test"]
# pull_request:
# branches: [ "action-test" ]
jobs:
build:
name: Build and Deploy Appknot Portal
runs-on: macos-14 #가상머신 OS 버전 선택, 24.03. 기준 stable latest는 12이나 빌드가 되지 않는 문제로 14로 지정
env:
XC_ARCHIVE: ${{ 'AppknotPortal-iOS.xcarchive' }}
XC_EXPORT_PATH: ${{ './artifacts' }}
KEYCHAIN: ${{ 'test.keychain' }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
# certificate
ENCRYPTED_CERT_FILE_PATH: ${{ '.github/secrets/Appknot-iOS-Distribution.p12.gpg' }}
DECRYPTED_CERT_FILE_PATH: ${{ '.github/secrets/Appknot-iOS-Distribution.p12' }}
CERT_ENCRYPTION_KEY: ${{ secrets.CERTS_ENCRYPTION_PWD }} # gpg로 파일 암호화할 때 사용한 암호
# provisioning
ENCRYPTED_PROVISION_FILE_PATH: ${{ '.github/secrets/Appknot_Portal_Distribution_Unlisted.mobileprovision.gpg' }}
DECRYPTED_PROVISION_FILE_PATH: ${{ '.github/secrets/Appknot_Portal_Distribution_Unlisted.mobileprovision' }}
#appclip provisioning
ENCRYPTED_CLIP_PROVISION_FILE_PATH: ${{ '.github/secrets/Appknot_Portal_App_Clip_Appstore.mobileprovision.gpg' }}
DECRYPTED_CLIP_PROVISION_FILE_PATH: ${{ '.github/secrets/Appknot_Portal_App_Clip_Appstore.mobileprovision' }}
PROVISIONING_ENCRYPTION_KEY: ${{ secrets.PROVISION_ENCRYPTION_PWD }} # gpg로 파일 암호화할 때 사용한 암호.
# certification export key
# 인증서 내보내기 할 때 사용한 암호
CERT_EXPORT_KEY: ${{ secrets.CERT_EXPORT_PWD }}
steps:
#최신 xcode 선택
- name: Select latest Xcode
run: "sudo xcode-select -s /Applications/Xcode.app"
# 러너가 레포 체크아웃
- name: Checkout
uses: actions/checkout@v3
# 키체인 초기화 - 임시 키체인 생성
- name: Configure Keychain
run: |
security create-keychain -p "" "$KEYCHAIN"
security list-keychains -s "$KEYCHAIN"
security default-keychain -s "$KEYCHAIN"
security unlock-keychain -p "" "$KEYCHAIN"
security set-keychain-settings
# 인증서 복호화 및 설치
- name: Configure Code Signing # Corrected indentation
run: |
gpg -d -o "$DECRYPTED_CERT_FILE_PATH" --pinentry-mode=loopback --passphrase "$CERT_ENCRYPTION_KEY" "$ENCRYPTED_CERT_FILE_PATH"
gpg -d -o "$DECRYPTED_PROVISION_FILE_PATH" --pinentry-mode=loopback --passphrase "$PROVISIONING_ENCRYPTION_KEY" "$ENCRYPTED_PROVISION_FILE_PATH"
# Appclip
gpg -d -o "$DECRYPTED_CLIP_PROVISION_FILE_PATH" --pinentry-mode=loopback --passphrase "$PROVISIONING_ENCRYPTION_KEY" "$ENCRYPTED_CLIP_PROVISION_FILE_PATH"
security import "$DECRYPTED_CERT_FILE_PATH" -k "$KEYCHAIN" -P "$CERT_EXPORT_KEY" -A
security set-key-partition-list -S apple-tool:,apple: -s -k "" "$KEYCHAIN"
# Xcode에서 찾을 수 있는 프로비저닝 프로필 설치하기 위해 우선 프로비저닝 디렉토리를 생성
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
# 모든 프로비저닝 프로파일을 rename 하고 위에서 만든 디렉토리로 복사하는 과정
for PROVISION in `ls .github/secrets/*.mobileprovision`
do
echo ALVIN PRIVISION DEBUG
UUID=`/usr/libexec/PlistBuddy -c 'Print :UUID' /dev/stdin <<< $(security cms -D -i ./$PROVISION)`
cp "./$PROVISION" "$HOME/Library/MobileDevice/Provisioning Profiles/$UUID.mobileprovision"
done
#Archive & Export
- name: Archive & Export
run: |
pod install
xcodebuild archive -workspace AppknotPortal.xcworkspace -scheme AppknotPortal-iOS -configuration release -archivePath $XC_ARCHIVE
xcodebuild -exportArchive -archivePath $XC_ARCHIVE -exportOptionsPlist ExportOptions.plist -exportPath "$XC_EXPORT_PATH" -allowProvisioningUpdates
#Artifact 업로드
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: Artifacts
path: ./artifacts
#TestFlight 업로드
- name: Upload app to TestFlight
uses: apple-actions/upload-testflight-build@v1
with:
app-path: "./artifacts/AppknotPortal.ipa"
issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }}
api-private-key: ${{ secrets.APPSTORE_API_PRIVATE_KEY }}
#Slack 알림
- name: Notify to Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,message,commit,author,action,eventName,ref,workflow,job,took,pullRequest # selectable (default: repo,message)
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # required
if: always() # Pick up events even if the job fails or is canceled.
YAML 파일에서는 space와 tab의 위치를 정확히 지정해야 합니다. 잘못된 들여쓰기는 원치 않는 동작을 초래할 수 있습니다.main 브렌치에 yml 파일이 생성됩니다. 원하는 파일이름을 적으시고 저는 iOS라고 지었습니다.

name: Action Test
action-test 브랜치에 코드가 푸시될 때만 트리거가 작동하도록 설정했습니다. 또한 PR 등 다양한 트리거 조건을 지정할 수 있습니다.name: Action Test
on:
push:
branches: [ "action-test" ]
# pull_request:
# branches: [ "action-test" ]
Triggering a workflow - GitHub Docs
runs-on 에서 가상머신의 OS와 버전을 지정합니다. 현 시점에서 macos-latest 옵션은 MacOS 12를 사용하게 됩니다. 저 같은 경우 빌드가 되지 않는 문제가 있어 MacOS 14로 명시하였습니다.jobs:
build:
name: Build and Deploy Appknot Portal
runs-on: macos-14 #가상머신 OS 버전 선택, 24.03. 기준 stable latest는 12이나 빌드가 되지 않는 문제로 14로 지정

name: Build and Deploy Appknot Portal

The components of GitHub Actions - GitHub Docs
About GitHub-hosted runners - GitHub Docs



brew install gnupg2

gpg -c myCertificate.p12
gpg -c myProfile.mobileprovision




YAML 파일 내에서 script로 암호에 접근할 수 있습니다.

env:
XC_ARCHIVE: ${{ 'AppknotPortal-iOS.xcarchive' }}
XC_EXPORT_PATH: ${{ './artifacts' }}
KEYCHAIN: ${{ 'test.keychain' }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
# certificate
ENCRYPTED_CERT_FILE_PATH: ${{ '.github/secrets/Appknot-iOS-Distribution.p12.gpg' }}
DECRYPTED_CERT_FILE_PATH: ${{ '.github/secrets/Appknot-iOS-Distribution.p12' }}
CERT_ENCRYPTION_KEY: ${{ secrets.CERTS_ENCRYPTION_PWD }} # gpg로 파일 암호화할 때 사용한 암호
# provisioning
ENCRYPTED_PROVISION_FILE_PATH: ${{ '.github/secrets/Appknot_Portal_Distribution_Unlisted.mobileprovision.gpg' }}
DECRYPTED_PROVISION_FILE_PATH: ${{ '.github/secrets/Appknot_Portal_Distribution_Unlisted.mobileprovision' }}
#appclip provisioning
ENCRYPTED_CLIP_PROVISION_FILE_PATH: ${{ '.github/secrets/Appknot_Portal_App_Clip_Appstore.mobileprovision.gpg' }}
DECRYPTED_CLIP_PROVISION_FILE_PATH: ${{ '.github/secrets/Appknot_Portal_App_Clip_Appstore.mobileprovision' }}
PROVISIONING_ENCRYPTION_KEY: ${{ secrets.PROVISION_ENCRYPTION_PWD }} # gpg로 파일 암호화할 때 사용한 암호.
# certification export key
# 인증서 내보내기 할 때 사용한 암호
CERT_EXPORT_KEY: ${{ secrets.CERT_EXPORT_PWD }}
steps:
#최신 xcode 선택
- name: Select latest Xcode
run: "sudo xcode-select -s /Applications/Xcode.app"
# 러너가 레포 체크아웃
- name: Checkout
uses: actions/checkout@v3
# 키체인 초기화 - 임시 키체인 생성
- name: Configure Keychain
run: |
security create-keychain -p "" "$KEYCHAIN"
security list-keychains -s "$KEYCHAIN"
security default-keychain -s "$KEYCHAIN"
security unlock-keychain -p "" "$KEYCHAIN"
security set-keychain-settings
$HOME/Library/MobileDevice/Provisioning Profiles)를 생성합니다..github/secrets/ 디렉토리에 저장된 모든 .mobileprovision 파일을 순환합니다./usr/libexec/PlistBuddy를 사용하여 UUID를 추출하고 UUID를 파일 이름으로 하는 지정된 디렉터리에 프로비저닝 프로필을 복사합니다. # 인증서 복호화 및 설치
- name: Configure Code Signing # Corrected indentation
run: |
gpg -d -o "$DECRYPTED_CERT_FILE_PATH" --pinentry-mode=loopback --passphrase "$CERT_ENCRYPTION_KEY" "$ENCRYPTED_CERT_FILE_PATH"
gpg -d -o "$DECRYPTED_PROVISION_FILE_PATH" --pinentry-mode=loopback --passphrase "$PROVISIONING_ENCRYPTION_KEY" "$ENCRYPTED_PROVISION_FILE_PATH"
# Appclip
gpg -d -o "$DECRYPTED_CLIP_PROVISION_FILE_PATH" --pinentry-mode=loopback --passphrase "$PROVISIONING_ENCRYPTION_KEY" "$ENCRYPTED_CLIP_PROVISION_FILE_PATH"
security import "$DECRYPTED_CERT_FILE_PATH" -k "$KEYCHAIN" -P "$CERT_EXPORT_KEY" -A
security set-key-partition-list -S apple-tool:,apple: -s -k "" "$KEYCHAIN"
# Xcode에서 찾을 수 있는 프로비저닝 프로필 설치하기 위해 우선 프로비저닝 디렉토리를 생성
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
# 모든 프로비저닝 프로파일을 rename 하고 위에서 만든 디렉토리로 복사하는 과정
for PROVISION in `ls .github/secrets/*.mobileprovision`
do
echo ALVIN PRIVISION DEBUG
UUID=`/usr/libexec/PlistBuddy -c 'Print :UUID' /dev/stdin <<< $(security cms -D -i ./$PROVISION)`
cp "./$PROVISION" "$HOME/Library/MobileDevice/Provisioning Profiles/$UUID.mobileprovision"
done


#아카이브 & IPA생성
- name: Archive & Export
run: |
pod install
xcodebuild archive -workspace AppknotPortal.xcworkspace -scheme AppknotPortal-iOS -configuration release -archivePath $XC_ARCHIVE
xcodebuild -exportArchive -archivePath $XC_ARCHIVE -exportOptionsPlist ExportOptions.plist -exportPath "$XC_EXPORT_PATH" -allowProvisioningUpdates
#Artifact 업로드
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: Artifacts
path: ./artifacts


#TestFlight 업로드
- name: Upload app to TestFlight
uses: apple-actions/upload-testflight-build@v1
with:
app-path: "./artifacts/AppknotPortal.ipa"
issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }}
api-private-key: ${{ secrets.APPSTORE_API_PRIVATE_KEY }} Upload app to TestFlight - GitHub Marketplace



#Slack Notification
- name: Post to Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,message,commit,author,action,eventName,ref,workflow,job,took,pullRequest # selectable (default: repo,message)
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # required
if: always() # Pick up events even if the job fails or is canceled.

워크스페이스에 깃헙 앱 추가
Slack API에서 앱을 추가합니다.
Slack API
Incoming Webhooks에서 URL을 GitHub Secret에 등록합니다.



이렇게 GitHub Actions를 사용하여 iOS에서 기본적인 Testflight CD를 도입하는 방법에 대해 알아봤습니다. 깃헙 액션의 최대 장점은 커뮤니티와 그것을 기반한 Marketplace라고 생각합니다. 또한 깃헙이라는 플랫폼 안에서 iOS를 넘어 거의 모든 분야에서 사용할 수 있기 때문에 포텐셜이 매우 높다고 생각합니다.
이어서는 Match를 사용한 Certificate & Profile관리에 대해 알아보겠습니다.
긴 글 읽어주셔서 감사합니다 😃
GitHub Actions 이해 - GitHub Docs