top of page
Matt

GitHub Actions 파이프라인을 만들어 iOS 앱을 TestFlight에 배포하는 방법

최종 수정일: 2023년 10월 12일

이전 블로그 글에서 테스트와 통합 자동화의 중요성에 대해 설명한 바 있으며 이는 완전히 자동화된 CICD 파이프라인의 일부로 이루어집니다. 우리는 GitHub Actions 워크플로우를 위한 yml 파일의 코드 스니펫을 공유했습니다.

이 글에서는 이를 어떻게 실현하는지 정확히 설명하겠습니다. 코드를 보려면 여기를 클릭하세요

이 워크플로우는 다음을 수행합니다:

파이프라인 환경 변수

재사용 가능한 변수를 전역 파이프라인 변수로 캡처하는 것이 항상 좋은 실천입니다

코드:

env:
  ARCHIVE_SCHEME: "Your Project Name (e.g. Falling Sky)"
  APP_NAME: "Your App Name (e.g. FallingSky)"
  BUNDLE_ID: "Your Bundle Id (e.g. com.ka.fallingsky)"

작업 공간 정리 (자체 호스트된 러너에 적합)

코드:

- name: Clean Workspace
        run: |
          rm -rf $GITHUB_WORKSPACE/* 
          rm -rf $RUNNER_TEMP/*

만약 당신이 자체 빌드 에이전트를 실행 중이라면 각 빌드가 서로 완벽하게 격리되도록 확인하고 싶을 것입니다. 여기서 우리가 하는 것은 빌드의 일부로 사용되는 두 개의 폴더에서 파일을 단순히 제거하는 것뿐입니다.


코드 체크아웃

코드:

- name: Checkout
        uses: actions/checkout@v3

이것은 GitHub Actions의 체크아웃 단계를 사용합니다. 이 단계는 당신의 코드의 최신 버전을 체크아웃할 것입니다.


기본 스키마 설정

코드:

- name: Set Default Scheme
        run: |
          scheme_list=$(xcodebuild -list -json | tr -d "\n")
          default=$(echo $scheme_list | ruby -e "require 'json'; puts JSON.parse(STDIN.gets)['project']['targets'][0]")
          echo $default | cat >default
          echo Using default scheme: $default

이 단계는 Xcode 프로젝트의 스키마 목록을 검색하고 첫 번째 타겟(기본 스키마로 가정되는)을 추출한 다음 기본 스키마의 이름을 "default"라는 이름의 파일에 저장하면서 사용 중인 기본 스키마를 나타내는 로그 메시지도 제공합니다. 이것은 특정 Xcode 스키마가 기본 스키마로 설정되어 있는 것을 기대하는 미래의 파이프라인 단계에 의존하는 경우 유용합니다.


자동화된 테스트 빌드 및 실행

코드:

- name: Build for test
        env:
          scheme: ${{ 'default' }}
          platform: ${{ 'iOS Simulator' }}
        run: |
          # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959)
          device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"`
          if [ $scheme = default ]; then scheme=$(cat default); fi
          if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi
          file_to_build=`echo $file_to_build | awk '{$1=$1;print}'`

          xcodebuild clean build-for-testing -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=$device"

      - name: Test
        timeout-minutes: 30
        env:
          scheme: ${{ 'default' }}
          platform: ${{ 'iOS Simulator' }}
        run: |
          # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959)
          device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"`
          if [ $scheme = default ]; then scheme=$(cat default); fi
          if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi
          file_to_build=`echo $file_to_build | awk '{$1=$1;print}'`
          xcodebuild test-without-building -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=$device" -test-iterations 3 -retry-tests-on-failure

이는 테스트가 실패할 경우 파이프라인이 중지되도록 보장하는 중요한 단계입니다. 이는 생산 환경에서 재귀적 문제를 방지하기에 이상적입니다.


인증서 및 프로비저닝 프로파일 설치

코드:

- name: Install the Apple certificate and provisioning profile
        env:
          BUILD_CERTIFICATE_BASE64: ${{ secrets.DIST_CERT_BASE64 }}
          P12_PASSWORD: ${{ secrets.DIST_CERT_P12_PASSWORD }}
          BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.PROVISIONING_PROFILE_BASE64 }}
          KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
        run: |
          # create variables
          CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
          PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
          KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db

          # import certificate and provisioning profile from secrets
          echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
          echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH

          # create temporary keychain
          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

          # apply provisioning profile
          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
          cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles

이 단계는 TestFlight로 배포할 앱을 아카이빙하기 위해 필요합니다. 이 단계에는 수행해야 하는 GitHub Repository 수준 비밀이 필요합니다. 이러한 비밀을 설정하는 방법을 자세히 설명하겠습니다. 자세한 내용은 https://docs.github.com/en/actions/deployment/deploying-xcode-applications/installing-an-apple-certificate-on-macos-runners-for-xcode-development를 참조하세요.


DIST_CERT_BASE64 & DIST_CERT_P12_PASSWORD

이것은 p12 빌드 인증서를 Base64로 저장한 것이며, 이를 서명하는 데 사용하는 비밀번호입니다.

한 번 p12 인증서를 다음 단계에서 생성한 후 https://help.apple.com/xcode/mac/current/#/dev154b28f09, 다음을 실행하여 Base64 텍스트를 가져와야 합니다.

base64 -i BUILD_CERTIFICATE.p12 | pbcopy

그런 다음 이것을 리포지토리 비밀로 저장하세요.


PROVISIONING_PROFILE_BASE64

이것은 배포에 필요한 프로비저닝 프로파일입니다. 프로파일을 생성한 후(프로비저닝 프로파일을 https://developer.apple.com에서 생성해야 함), 다음을 실행하여 Base64 텍스트를 가져오려면 다음을 실행합니다.

base64 -i PROVISIONING_PROFILE.mobileprovision | pbcopy

그런 다음 리포지토리 비밀로 저장하세요.


KEYCHAIN_PASSWORD

각 실행마다 임시 키체인이 생성되므로 이는 임의로 생성된 문자열이 될 수 있습니다.


버전 번호 증가

코드:

- uses: yanamura/ios-bump-version@v1
        with:
          version: 1.1.0
          build-number: ${{ github.run_number }}

GitHub 실행 번호를 버전의 "고유" 부분으로 사용합니다. 이는 해당 파일을 업데이트하여 이를 App Store Connect에서 새로운 빌드로 사용할 수 있게 합니다.


앱 빌드 및 아카이빙

코드:

- name: Build and archive app
        run: |
          if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi
          file_to_build=`echo $file_to_build | awk '{$1=$1;print}'`
          xcodebuild archive -"$filetype_parameter" "$file_to_build" -scheme "$ARCHIVE_SCHEME" -archivePath "$APP_NAME" -configuration Release

IPA 파일 생성

코드:

- name: Create IPA file
        env:
          EXPORT_OPTIONS_PLIST: ${{ secrets.EXPORT_OPTIONS_PLIST_BASE64 }}
        run: |
          EXPORT_OPTIONS_PLIST_PATH=$RUNNER_TEMP/ExportOptions.plist
          echo -n "$EXPORT_OPTIONS_PLIST" | base64 -d -o $EXPORT_OPTIONS_PLIST_PATH
          xcodebuild -exportArchive -archivePath $GITHUB_WORKSPACE/$APP_NAME.xcarchive -exportPath $RUNNER_TEMP/build -exportOptionsPlist $EXPORT_OPTIONS_PLIST_PATH
          mv "$RUNNER_TEMP/build/$ARCHIVE_SCHEME.ipa" $RUNNER_TEMP/build/$APP_NAME.ipa

이것은 TestFlight에 업로드할 준비가 된 IPA 파일을 생성합니다.

EXPORT_OPTIONS_PLIST_BASE64 비밀을 사용합니다. 이는 ExportOptions.plist의 Base64입니다. 이 파일은 많은 예제가 온라인에 있으므로 수동으로 만들 수 있습니다. 다음을 실행하여 ExportOptions.plist 파일의 Base64를 캡처하고 리포지토리 비밀로 저장합니다.

base64 -i ExportOptions.plist | pbcopy

TestFlight로 배포

코드:

- name: Upload to TestFlight
        env:
          API_KEY_BASE64: ${{ secrets.APPSTORE_API_PRIVATE_KEY }}
        run: |
          mkdir -p ./private_keys
          echo -n "$API_KEY_BASE64" | base64 --decode -o "./private_keys/AuthKey_${{ secrets.APPSTORE_API_KEY_ID }}.p8"
          xcrun altool --validate-app -f ${{ runner.temp }}/build/${{ env.APP_NAME }}.ipa -t ios --apiKey ${{ secrets.APPSTORE_API_KEY_ID }} --apiIssuer ${{ secrets.APPSTORE_ISSUER_ID }}
          xcrun altool --upload-app -f ${{ runner.temp }}/build/${{ env.APP_NAME }}.ipa -t ios --apiKey ${{ secrets.APPSTORE_API_KEY_ID }} --apiIssuer ${{ secrets.APPSTORE_ISSUER_ID }}

이것은 생성된 IPA 파일을 TestFlight에 업로드하며, 완료되면 Apple이 처리하는데 시간이 걸리지만 디바이스에서 테스트할 준비가 됩니다.

이 단계는 APPSTORE_API_PRIVATE_KEY, APPSTORE_API_KEY_ID 및 APPSTORE_ISSUER_ID 세 가지 비밀이 필요합니다. 이들은 App Store Connect에서 생성한 API 키에서 생성됩니다. 키를 다운로드하고 .p8 파일의 Base64를 캡처하고 APPSTORE_API_PRIVATE_KEY 리포지토리 비밀로 저장합니다. 키 id와 발행자 id는 App Store Connect에서 찾을 수 있습니다.


지원 방법


이 콘텐츠는 항상 무료로 유지될 것이며, 가치 있다고 판단된다면 다른 사람과 공유하는 것을 고려해 주십시오. 게다가, 우리의 게임을 다운로드하고 솔직한 리뷰를 남겨 주시면 저희를 크게 지원해 주실 것입니다. 궁금한 점이나 피드백이 있다면 언제든지 문의해 주시고, 최선을 다해 답변 드리겠습니다.


오늘 Apple App Store에서 Falling Sky를 다운로드하세요: https://apps.apple.com/app/id6446787964


전체 yml 파일 코드는 다음과 같습니다:

name: My Automated CI/CD Pipeline

on:
  push:
    branches: [ "main" ]
  workflow_dispatch:

env:
  ARCHIVE_SCHEME: "Your Project Name (e.g. Falling Sky)"
  APP_NAME: "Your App Name (e.g. FallingSky)"
  BUNDLE_ID: "Your Bundle Id (e.g. com.ka.fallingsky)"

jobs:
  build:
    name: Build and Test default scheme using any available iPhone simulator
    runs-on: self-hosted

    steps:
      - name: Clean Workspace
        run: |
          rm -rf $GITHUB_WORKSPACE/* 
          rm -rf $RUNNER_TEMP/*

      - name: Checkout
        uses: actions/checkout@v3

      - name: Set Default Scheme
        run: |
          scheme_list=$(xcodebuild -list -json | tr -d "\n")
          default=$(echo $scheme_list | ruby -e "require 'json'; puts JSON.parse(STDIN.gets)['project']['targets'][0]")
          echo $default | cat >default
          echo Using default scheme: $default

      - name: Build for test
        env:
          scheme: ${{ 'default' }}
          platform: ${{ 'iOS Simulator' }}
        run: |
          # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959)
          device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"`
          if [ $scheme = default ]; then scheme=$(cat default); fi
          if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi
          file_to_build=`echo $file_to_build | awk '{$1=$1;print}'`

          xcodebuild clean build-for-testing -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=$device"

      - name: Test
        timeout-minutes: 30
        env:
          scheme: ${{ 'default' }}
          platform: ${{ 'iOS Simulator' }}
        run: |
          # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959)
          device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"`
          if [ $scheme = default ]; then scheme=$(cat default); fi
          if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi
          file_to_build=`echo $file_to_build | awk '{$1=$1;print}'`
          xcodebuild test-without-building -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=$device" -test-iterations 3 -retry-tests-on-failure

      - name: Install the Apple certificate and provisioning profile
        env:
          BUILD_CERTIFICATE_BASE64: ${{ secrets.DIST_CERT_BASE64 }}
          P12_PASSWORD: ${{ secrets.DIST_CERT_P12_PASSWORD }}
          BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.PROVISIONING_PROFILE_BASE64 }}
          KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
        run: |
          # create variables
          CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
          PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
          KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db

          # import certificate and provisioning profile from secrets
          echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
          echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH

          # create temporary keychain
          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

          # apply provisioning profile
          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
          cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles

      - uses: yanamura/ios-bump-version@v1
        with:
          version: 1.1.0
          build-number: ${{ github.run_number }}

      - name: Build and archive app
        run: |
          if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi
          file_to_build=`echo $file_to_build | awk '{$1=$1;print}'`
          xcodebuild archive -"$filetype_parameter" "$file_to_build" -scheme "$ARCHIVE_SCHEME" -archivePath "$APP_NAME" -configuration Release

      - name: Create IPA file
        env:
          EXPORT_OPTIONS_PLIST: ${{ secrets.EXPORT_OPTIONS_PLIST_BASE64 }}
        run: |
          EXPORT_OPTIONS_PLIST_PATH=$RUNNER_TEMP/ExportOptions.plist
          echo -n "$EXPORT_OPTIONS_PLIST" | base64 -d -o $EXPORT_OPTIONS_PLIST_PATH
          xcodebuild -exportArchive -archivePath $GITHUB_WORKSPACE/$APP_NAME.xcarchive -exportPath $RUNNER_TEMP/build -exportOptionsPlist $EXPORT_OPTIONS_PLIST_PATH
          mv "$RUNNER_TEMP/build/$ARCHIVE_SCHEME.ipa" $RUNNER_TEMP/build/$APP_NAME.ipa

      - name: Upload to TestFlight
        env:
          API_KEY_BASE64: ${{ secrets.APPSTORE_API_PRIVATE_KEY }}
        run: |
          mkdir -p ./private_keys

          echo -n "$API_KEY_BASE64" | base64 --decode -o "./private_keys/AuthKey_${{ secrets.APPSTORE_API_KEY_ID }}.p8"

          xcrun altool --validate-app -f ${{ runner.temp }}/build/${{ env.APP_NAME }}.ipa -t ios --apiKey ${{ secrets.APPSTORE_API_KEY_ID }} --apiIssuer ${{ secrets.APPSTORE_ISSUER_ID }}

          xcrun altool --upload-app -f ${{ runner.temp }}/build/${{ env.APP_NAME }}.ipa -t ios --apiKey ${{ secrets.APPSTORE_API_KEY_ID }} --apiIssuer ${{ secrets.APPSTORE_ISSUER_ID }}




조회수 103회
bottom of page