---
# System prepended metadata

title: CI/CD iOS

---

# 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)