GitHubActionsでUnityのiOSビルドを走らせてXcodeビルドをしてApp Centerに配信するまで

pushしたら自動でUnityビルドが走る人権環境を手に入れるの続編です。

はじめに

詳細を書くのが面倒なので、GitHub ActionsやFastlaneを分かってる人向け記事です。

ポイント

match不使用

今回はmatchを使用せず、証明書(.p12)とmobileprovisionを手動で生成して使用するフローです。
これは好みが分かれるところだと思いますが、自分の趣味です。

すぐLinuxに切り替える

わざわざubuntuでUnityビルド、macOSでXcodeビルド、またubuntuに戻してAppCenterにアップロードと面倒なことをしています。
理由は簡単で、XcodeビルドにはmacOSを利用する必要がありますが、Linuxに比べて値段が10倍もします。
(GitHub Actionsの支払いについて)
ArtifactのUpload/Downloadは結構早いので、Linuxで出来ることはLinuxでやる方針にしています。

Artifactは忘れず消す

Job間を跨いでファイルを共有するためにArtifactを利用していますが、Artifactも課金対象です。
(自動で課金されることはなく、上限を超えると古いものから消えていきます。)
そのため、job間のファイル共有に使ったArtifactは忘れず消えるようなコードにしましょう。
Jobの途中でエラーになったときも正しく削除されるよう気をつけましょう。

development-build.yml

ここからは直接コードにコメントを入れながら解説していきます。

name: Development build

on:
  push:
    branches:
      - main

jobs:
  build_unity:
    name: Build ${{ matrix.targetPlatform }}
    runs-on: ubuntu-latest

    strategy:
      fail-fast: false
      matrix:
        targetPlatform:
          - iOS

    steps:
      - name: Checkout repository
        uses: actions/checkout@v2

      - uses: actions/cache@v2
        with:
          path: Unity/Library
          key: Library-${{ matrix.targetPlatform }}
          restore-keys: |
            Library-

      - name: Build
        uses: game-ci/unity-builder@v2
        env:
          UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
        with:
          projectPath: Unity
          targetPlatform: ${{ matrix.targetPlatform }}
          versioning: Custom
          version: 0.1.${{ github.run_number }} # ここでバージョンを入れておくと出来たROMとGitHubActionsの対応付けが出来て便利

      - uses: actions/upload-artifact@v2
        with:
          name: Build-${{ matrix.targetPlatform }}
          path: build/${{ matrix.targetPlatform }}

  build_xcode:
    name: Build xcode project
    runs-on: macos-latest
    needs: build_unity

    env:
      IOS_APP_ID: dev.kyubuns.test
      IOS_BUILD_PATH: ${{ format('{0}/build/iOS', github.workspace) }}
      CERTIFICATES_PATH: ${{ format('{0}/Certificates.p12', github.workspace) }}
      MOBILEPROVISION_PATH: ${{ format('{0}/Target.mobileprovision', github.workspace) }}
      IPA_OUTPUT_PATH: ${{ format('{0}/Build.ipa', github.workspace) }}
      CODESIGNING_IDENTITY: ${{ secrets.CODESIGNING_IDENTITY }}
      CERTIFICATES_PASSWORD: ${{ secrets.CERTIFICATES_PASSWORD }}

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2

      - name: Download Artifact
        uses: actions/download-artifact@v2
        with:
          name: Build-iOS
          path: build/iOS

      - name: Fix File Permissions
        run: |
          find $IOS_BUILD_PATH -type f -name "**.sh" -exec chmod +x {} \;

      - uses: actions/cache@v2
        with:
          path: vendor/bundle
          key: ${{ runner.os }}-${{ hashFiles('**/Gemfile.lock') }}

      - name: Write Key Files
        env:
          CERTIFICATES_P12: ${{ secrets.CERTIFICATES_P12 }}
          MOBILEPROVISION: ${{ secrets.MOBILEPROVISION }}
        run: |
          echo "$CERTIFICATES_P12" | base64 --decode > $CERTIFICATES_PATH
          echo "$MOBILEPROVISION" | base64 --decode > $MOBILEPROVISION_PATH

      - name: Build Xcode
        uses: maierj/fastlane-action@v2.0.0
        with:
          lane: 'ios development'

      - uses: actions/upload-artifact@v2
        with:
          name: Build-iOS-ipa
          path: ${{ env.IPA_OUTPUT_PATH }}

      - name: Cleanup
        if: always()
        uses: geekyeggo/delete-artifact@v1
        with:
          name: Build-iOS

  # FastlaneでAppCenterにアップロードするところまで行っても良かったものの、macOSの稼働時間を減らすためにlinuxで行っている
  upload_appcenter:
    name: Upload ipa to AppCenter
    runs-on: ubuntu-latest
    needs: build_xcode

    steps:
      - name: Download Artifact
        uses: actions/download-artifact@v2
        with:
          name: Build-iOS-ipa

      - name: Upload to App Center
        uses: wzieba/AppCenter-Github-Action@v1
        with:
          appName: kyubuns/Test
          token: ${{ secrets.APPCENTER_API_TOKEN }}
          file: Build.ipa
          group: Internal

      - name: Cleanup
        if: always()
        uses: geekyeggo/delete-artifact@v1
        with:
          name: Build-iOS-ipa

fastlane/Fastfile

fastlane init してGemfileとかは作ってください。

keychain_name = "temporary_keychain"
keychain_password = SecureRandom.base64

platform :ios do
  lane :development do
    certificates
    provisining_uuid = sh("grep UUID -A1 -a #{ENV["MOBILEPROVISION_PATH"]} | grep -io \"[-A-Z0-9]\\{36\\}\"").strip

    build_options = {}
    build_options[:scheme] = "Unity-iPhone"
    build_options[:output_name] = File.basename(ENV['IPA_OUTPUT_PATH'])
    build_options[:output_directory] = File.dirname(ENV['IPA_OUTPUT_PATH'])
    build_options[:configuration] = "Debug"
    build_options[:project] = "#{ENV['IOS_BUILD_PATH']}/iOS/Unity-iPhone.xcodeproj"
    build_options[:export_method] = "ad-hoc"
    build_options[:codesigning_identity] = ENV['CODESIGNING_IDENTITY']
    build_options[:export_options] = {
      signingStyle: "manual",
      compileBitcode: false,
      provisioningProfiles: {
        "#{ENV["IOS_APP_ID"]}": "#{provisining_uuid}"
      },
    }
    build_options[:skip_profile_detection] = true

    update_app_identifier(
      xcodeproj: build_options[:project],
      plist_path: "Info.plist",
      app_identifier: ENV["IOS_APP_ID"],
    )

    update_project_provisioning(
      xcodeproj: build_options[:project],
      target_filter: build_options[:scheme],
      profile: ENV["MOBILEPROVISION_PATH"],
      code_signing_identity: build_options[:codesigning_identity]
    )

    build_ios_app(build_options)
  end

  lane :certificates do
    cleanup_keychain
    create_keychain(
      name: keychain_name,
      password: keychain_password,
      default_keychain: true,
      lock_when_sleeps: true,
      timeout: 3600,
      unlock: true
    )
    import_certificate(
      certificate_path: ENV["CERTIFICATES_PATH"],
      certificate_password: ENV["CERTIFICATES_PASSWORD"],
      keychain_name: keychain_name,
      keychain_password: keychain_password
    )
    install_provisioning_profile(path: ENV["MOBILEPROVISION_PATH"])
  end

  lane :cleanup_keychain do
    if File.exist?(File.expand_path("~/Library/Keychains/#{keychain_name}-db"))
      delete_keychain(name: keychain_name)
    end
  end

  after_all do
    if File.exist?(File.expand_path("~/Library/Keychains/#{keychain_name}-db"))
      delete_keychain(name: keychain_name)
    end
  end
end

参考

GAME.CI - Deploy to the App Store https://game.ci/docs/github/deployment/ios