# 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์— ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ถ”๊ฐ€ ![](https://i.imgur.com/ubwRBoI.png) - 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 ํด๋” ์ƒ์„ฑ. ![](https://i.imgur.com/PBlEXWs.png) ### 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 ์ƒ์„ฑ. ![](https://i.imgur.com/0WSGkN7.png) **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)