# CI/CD iOS
## ๐ workflow ํ๋ฆ
- tag๋ฅผ ๋ฌ๊ณ release branch์ push โ workflow ์คํ
- ํ๊ฒฝ ๋ณ์ ์ค์
- ์ธ์ฆ์(certificate & provisioning) ๋ณตํธํ ๋ฐ ์ ์ฅ
- fastlane ์ด์ฉํ์ฌ ๋น๋ ํ testflight์ ์
๋ก๋
- slack์ ์๋ฆผ
## ๐ iOS ๊ฐ๋ฐ์๊ฐ ์ค๋นํด์ผ ํ๋ ๊ฒ (key ๊ด๋ฆฌ)
### [Apple Developer](https://developer.apple.com/account/resources/certificates/list) ์ฌ์ดํธ์์ **certificate** & **provisioning profile** ์์ฑ ํ ์ ์ฅ
- ํค์ฒด์ธ์์ `Apple Distribution` ์ผ๋ก ๊ฒ์ํ์ฌ ๊ฐ์ธํค์ ์ธ์ฆ์ ๋์ ์ ํ ํ ๋ด๋ณด๋ด๊ธฐ (.p12)
- ๋ด๋ณด๋ด๊ธฐ ํ ๋ ์ฌ์ฉํ ๋น๋ฐ๋ฒํธ setting > secrets > actions์ ์ ์ฅ
- .p12 & .mobileprovision โ .gpg ๋ก ์ํธํ (gnupg ํด ์ฌ์ฉ)
- ์ํธํํ ํ์ผ repository์ .github/.secrets ๊ฒฝ๋ก์ ์
๋ก๋
- ์ํธํํ ๋ ์ฌ์ฉํ ๋น๋ฐ๋ฒํธ setting > secrets > actions์ ์ ์ฅ
### [App Store Connect](https://appstoreconnect.apple.com/access/api) ์ฌ์ดํธ์์ **key id** & **issuer id** & **private api key** ์์ฑ ํ ์ ์ฅ
- key id & issuer id โ setting > secrets > actions์ ์ ์ฅ
- private api key (AuthKey_***.p8)
```bash
-----BEGIN PRIVATE KEY-----
abcabc
abc
abcde
-----END PRIVATE KEY-----
```
ํ์์ ํ์ผ์
```bash
-----BEGIN PRIVATE KEY-----
abcabcabcabcde
-----END PRIVATE KEY-----
```
์ ๊ฐ์ ํ์์ผ๋ก ๋ฐ๊พธ์ด setting > secrets > actions์ ์ ์ฅ
```
[testflight์ ์
๋ก๋ ํ๊ธฐ ์ํ ์ธ์ฆ ๋ฐฉ์]
1. apple id, password๋ฅผ ์ด์ฉํ ์ธ์ฆ ๋ฐฉ์
2. api key๋ฅผ ์ด์ฉํ ์ธ์ฆ ๋ฐฉ์
โ ๋ณด์์์ ์ด์ ๋ก api key๋ฅผ ์ฌ์ฉํ์ฌ ์ธ์ฆํ๋ ๋ฐฉ์์ ์ฑํํจ
```
### Project setting
- Info.plist์ ๋ค์๊ณผ ๊ฐ์ด ์ถ๊ฐ

- Singning & Capablities > Automatically manage signing ํด์
- provisioning profile์ ๋ค์ด ๋ฐ์ ๊ฑธ๋ก ์ค์
- project๋ฅผ export ์ฉ์ผ๋ก archive ํ์ฌ .plist ์ถ์ถ
- project์ .plist๋ฅผ repository์ push
## ๐ ์คํฌ๋ฆฝํธ A to Z
### 1. github ํด๋ ์์ ์ํธํ๋ certificate & provisioning profile์ ๋ด์ secretsํด๋์ workflow๋ฅผ ๋ฃ์ workflows ํด๋ ์์ฑ.

### 2. workflows ํด๋ ์์ .yml ํ์ผ ์์ฑ ๋ฐ ์คํฌ๋ฆฝํธ ์์ฑ.
์ฌ์ฉํ ๋๋ ์ฃผ์ ์ ๊ฑฐํ๊ณ ์ฌ์ฉ!
~~~yml
# workflow ์ด๋ฆ
name: Deploy
# work flow๊ฐ ํธ๋ฆฌ๊ฑฐ ๋๋ ์กฐ๊ฑด.
# git action (๊ณต์๋ฌธ์)https://docs.github.com/en/actions/using-workflows/triggering-a-workflow ์ฐธ๊ณ .
on:
push:
branches: [test-sello]
jobs:
deploy:
# ์คํํ๊ฒฝ
runs-on: macos-latest
env:
XC_SCHEME: ${{ 'TestSwiftUI' }}
# certificate
ENCRYPTED_CERT_FILE_PATH: ${{ '.github/secrets/certs.p12.gpg' }}
DECRYPTED_CERT_FILE_PATH: ${{ '.github/secrets/certs.p12' }}
CERT_ENCRYPTION_KEY: ${{ secrets.CERTS_ENCRYPTO_PWD }} # gpg๋ก ํ์ผ ์ํธํํ ๋ ์ฌ์ฉํ ์ํธ
# provisioning
ENCRYPTED_PROVISION_FILE_PATH: ${{ '.github/secrets/Klip_Test_New_App_Store.mobileprovision.gpg' }}
DECRYPTED_PROVISION_FILE_PATH: ${{ '.github/secrets/Klip_Test_New_App_Store.mobileprovision' }}
PROVISIONING_ENCRYPTION_KEY: ${{ secrets.PROFILES_ENCRYPTO_PWD }} # gpg๋ก ํ์ผ ์ํธํํ ๋ ์ฌ์ฉํ ์ํธ
# certification export key
CERT_EXPORT_KEY: ${{ secrets.CERTS_EXPORT_PWD }}
# ํค์ฒด์ธ ๋ณ์
KEYCHAIN: ${{ 'test.keychain' }}
steps:
- name: checkout
uses: actions/checkout@v3
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '13.1'
# ์์ ํค์ฒด์ธ ์ค์ (์ธ์ฆ์๋ฅผ ๊ฐ์ ธ์ฌ ๋ ์ฌ์ฉ)
- 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
run: |
# certification๊ณผ provisionfile์ ๋ค์ ๋ณตํธํ
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"
# keychain์ผ๋ก import
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์์ provisiong file์ ์ฐพ์ ์ ์๊ฒ ๋๋ ํ ๋ฆฌ ์์ฑ ํ rename ๋ฐ ๋ณต์ฌ
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
echo `ls .github/secrets/*.mobileprovision`
for PROVISION in `ls .github/secrets/*.mobileprovision`
do
UUID=`/usr/libexec/PlistBuddy -c 'Print :UUID' /dev/stdin <<< $(security cms -D -i ./$PROVISION)`
cp "./$PROVISION" "$HOME/Library/MobileDevice/Provisioning Profiles/$UUID.mobileprovision"
done
# fastlane Code
# build -> archive -> upload
# api key๋ฅผ ํตํด testflight์ uploadํจ.
- uses: maierj/fastlane-action@v2.2.0
with:
lane: 'flight'
# ํ๊ฒฝ๋ณ์.
# fastfile์์ ENV๋ก ์ค์ ํ ๋ณ์๋ฅผ ์ฌ๊ธฐ์ git secret๋ฅผ ์ด์ฉํด ์ ์
# ex) fastfile: ENV["Something"]
# workflow: Somthing = ${{ secrets.Something }}
env:
DEVELOPER_APP_IDENTIFIER: ${{ secrets.DEVELOPER_APP_IDENTIFIER }}
API_KEY: ${{ secrets.API_KEY }}
API_ISSUER : ${{ secrets.API_ISSUER }}
PRIVATE_API_KEY: ${{ secrets.PRIVATE_API_KEY }}
SCHEME: ${{ env.XC_SCHEME }}
# slack ์ค์
slack:
runs-on: ubuntu-20.04
needs: [ deploy ]
if: |
( needs.deploy.result=='success' )
steps:
- name: Send custom JSON data to Slack workflow
id: slack
uses: slackapi/slack-github-action@v1.18.0
with:
payload: |
{
"attachments": [
{
"fallback": "Success Upload TestFlight .",
"color": "#3AA3E3",
"text": "Success Upload TestFlight",
"fields": [
{
"title": "Branch",
"value": "${{ github.ref_name }}",
"short": true
},
{
"title": "Project",
"value": "${{ github.repository }}",
"short": true
},
{
"title": "Workflow name",
"value": "${{ github.workflow }}",
"short": true
},
{
"title": "Actor",
"value": "${{ github.actor }}",
"short": true
}
]
}
]
}
env:
SLACK_WEBHOOK_URL: ""
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
~~~
### 3. fastlane ์ค์
fastlane ํด๋ ์์ Appfile & Fastfile ์์ฑ.

**Appfile**
~~~ruby
app_identifier(ENV["DEVELOPER_APP_IDENTIFIER"]) # The bundle identifier of your app
~~~
**Fastfile**
~~~ruby
default_platform(:ios)
lane :flight do
# derived ํด๋ ์ ๊ฑฐ(Optional)
clear_derived_data
# api key ๋ง๋ค๊ธฐ
api_key = app_store_connect_api_key(
key_id: ENV["API_KEY"],
issuer_id: ENV["API_ISSUER"],
key_content: ENV["PRIVATE_API_KEY"],
duration: 1200,
in_house: false
)
# build number 1์ฉ ์ฆ๊ฐ
# https://docs.fastlane.tools/actions/increment_build_number/
increment_build_number(
build_number: latest_testflight_build_number + 1
)
# ipa ํ์ผ ์ถ์ถ.
# https://docs.fastlane.tools/actions/gym/
gym(
scheme: ENV["SCHEME"],
export_method: "app-store",
clean: true,
)
# api_key๋ฅผ ์ด์ฉ upload
pilot(api_key: api_key)
end
~~~
## ๐ง๐ปโโ๏ธ ์ฐธ๊ณ ๋งํฌ
- [naljin - [CI/CD] Github Actions ๋ฅผ ์ด์ฉํ TestFlight ์
๋ก๋ ์๋ํ](https://sujinnaljin.medium.com/ci-cd-github-actions-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-testflight-%EC%97%85%EB%A1%9C%EB%93%9C-%EC%9E%90%EB%8F%99%ED%99%94-8ecdbeb227a3)
- [fastlane with GitAction](https://litoarias.medium.com/continuous-delivery-for-ios-using-fastlane-and-github-actions-edf62ee68ecc)
- [Code Signing](https://songtaehwan.github.io/code-sigining/)
- [gym & pilot ์ฌ์ฉ ์์](https://www.runway.team/blog/how-to-build-the-perfect-fastlane-pipeline-for-ios#using-app-store-connect-api-key)