LoginSignup
11
7

GitHub Actions セルフホストランナーでiOSアプリをストアアップロードする(ビルド・署名・ストアアップロード)

Last updated at Posted at 2023-10-25

はじめに

GitHub Actions セルフホストランナーでiOSアプリを「ビルド」「署名」「ストアアップロード」する手法についてお話しします。
ビルドやストアアップロードは難しくないのですが、署名は一筋縄ではいかないです。
署名と聞くだけで頭が痛くなる私ですがトライアンドエラーの末なんとか解決できました。
感無量でした。この記事が同じように困っている方のヒントになればと思い執筆しました。

GitHub Actions セルフホストランナー とは

公式ページ : https://docs.github.com/actions

説明するまでもなく色々な人が記事を書いているので触りだけ。
GitHub Actionsは基本従量課金なんですが、自前でCIマシンを用意すればここの課金コストを抑えられます。
特にMacOSはLinuxの10倍の時間を食うので洒落にならないです。大食いキャラです。
アプリのビルドを実験していて、約2日で無料枠分を食い潰した経験あります。ごめんなさい。

リポジトリの設定でやること

ランナーの登録

リポジトリの設定で使用するMacをセルフホストランナーとして登録します。
公式サイトの通りやっていくだけでOKです。

ラベルの登録

マシンにラベルをつけることで特定の端末だけでワークフローを実行できるようになるので、必要に応じてラベルを設定しておくと便利です。
公式サイト

image.png

リポジトリシークレットの登録

リポジトリシークレットに署名で必要な情報を登録していきます。
今回はストア用のワークフローで解説するのでストア用に必要なシークレット例を列挙します。

シークレット名の例 補足
DISTRIBUTION_CERTIFICATE_BASE64 配布証明書を文字列化1したもの ipaの署名に必要。
DISTRIBUTION_CERTIFICATE_BASE64_NAME 配布証明書の名前 テンポラリーキーチェーン2から削除するために必要。
DEVELOPMENT_CERTIFICATE_BASE64 開発者証明書を文字列化1したもの ipaの署名に必要。
DEVELOPMENT_CERTIFICATE_BASE64_NAME 開発者証明書の名前 テンポラリーキーチェーン2から削除するために必要。
WWDR_CERTIFICATE_BASE64 中間証明書を文字列化1したもの 証明書を信頼する為に必要。本来は自動で入るものだがテンポラリキーチェーン2には自動で入らないので必要。
WWDR_CERTIFICATE_BASE64_NAME 中間証明書の名前 テンポラリーキーチェーン2から削除するために必要。
DISTRIBUTION_CERTIFICATE_PASSWORD 配布証明書のパスワード テンポラリーキーチェーン2に証明書を登録するときに必要
DEVELOPMENT_CERTIFICATE_PASSWORD 開発者証明書のパスワード テンポラリーリキーチェーン2に証明書を登録するときに必要
TEMPORARY_KEYCHAIN_NAME テンポラリーリキーチェーン2の名前 一時的なキーチェーンファイルの名前。CIで作り直すことになるのでなんでもOK。
TEMPORARY_KEYCHAIN_PASSWORD テンポラリーリキーチェーン2にアクセスするためのパスワード 一時的なキーチェーンにアクセスするためのパスワード。CIで作り直すことになるのでなんでもOK。
STORE_CONNECT_AUTH_KEY store connect apiで使用するAUTH_KEY(P8ファイル)を文字列化1したもの App store connect>ユーザとアクセス>キー>App store connect APIでキーを作成するとP8ファイルがダウンロードできます。Apple公式サイト
STORE_CONNECT_KEY_ID store connect apiで使用するKEY_ID AUTH_KEY(P8ファイル)を作成するとキーIDが発行されます。
STORE_CONNECT_ISSUER_ID store connect ISSUER_ID App store connect>ユーザとアクセス>キー>App store connect APIのページ載っているISSUER_IDです。

image.png

今回プロビジョニングプロファイルはfastlaneで最新をダウンロードする予定なのでシークレットには登録しないです。
にしても多いですよね。もっと簡潔に署名できるようになったら嬉しいですね。
ともあれこのようにリポジトリシークレットに登録することで簡単にCI端末を追加できるようになります。

Macでやること

CIマシンとして使うMacで作業します。

GitHub Actionsのランナーをインストールする

リポジトリの設定でやることのランナーの登録でやっているので割愛します。

ランナーのサービス化

CI端末は定期的に再起動などするので、サービスとして起動時にランナーが実行されるようにサービス化を推奨します。
公式サイト

Xcodeをインストールする

DeveloperのサイトやXcodesを使って対象のXcodeをインストールしておきます。
ここでインストールしたパスをワークフローの環境変数に設定します。

(CIでテストを実行する場合)テストで使うシミュレータをインストールする

テストで必要な場合はシミュレータを起動できる状態にしておきます。

(fastlaneを使用する場合)fastlaneをインストールしておく

ここはワークフローに含めるのがあるべき姿ですが今回は妥協して端末に入れています。

GitHub Actionsのワークフローファイル作成

いよいよ本題のワークフローファイルです。

プロジェクト配下の.github/workflowsにおく必要があるのでそこにymlを作成します。

ワークフローの名前

name: AppStoreにipaをアップロードする

ここにワークフローの名前を記入します。
リポジトリのActionsタブから一覧で見るときにここの名前が出てくるので分かりやす名前にしておきましょう。

トリガーの設定

on:
  push:
    branches:
      - 'main'

今回はmainブランチへプッシュで動作させます。
もし手動でも実行したい場合はworkflow_dispatchも追記すると良いです。

コンカレンシー設定

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

なくてもいいですが、同じブランチに複数アクションが流れてきたら後続の処理を最優先に実行してくれます。(古いアクションはキャンセルされる)

ワークフロー内で使用する環境変数の設定

env:
  DEVELOPER_DIR: /Applications/Xcode-14.2.0.app # 実行する環境によって変える

CI端末にインストールされているXcodeの名前をフルパスで記入します。複数CI端末がある場合は名前を同じにしておく必要があります。

jobの指定

jobでCI端末として動かすランナーを指定します。

jobs:
  build:
    runs-on: ランナーのラベル

runs-onに動かしたいランナーを記載します。

ステップの記入

jobs:
  build:
    runs-on: ランナーのラベル
    steps:

ステップのところに実際に処理を追記していきます。

チェックアウト

    - name: チェックアウト
      uses: actions/checkout@v3
      with:
        token: ${{ secrets.あなたのリポジトリへのアクセストークン }} # プライペートリポジトリでアクセストークン使う場合必要
        submodules: 'true' # サブモジュールがある場合は必要

アクションに紐づいているリポジトリをチェックアウトします。

証明書をシークレットからデコードしてテンポラリキーチェーンにインポートする

    - name: 証明書をシークレットからデコードしてテンポラリキーチェーンにインポートする
      run: |
        # 変数用意
        DISTRIBUTION_CERTIFICATE_PATH=$RUNNER_TEMP/distribution_certificate.p12
        DEVELOPMENT_CERTIFICATE_PATH=$RUNNER_TEMP/development_certificate.p12
        WWDR_CERTIFICATE_PATH=$RUNNER_TEMP/wwdr_certificate.cer
        # シークレットから取得した証明書をデコード
        echo -n "$DISTRIBUTION_CERTIFICATE_BASE64" | base64 --decode -o $DISTRIBUTION_CERTIFICATE_PATH
        echo -n "$DEVELOPMENT_CERTIFICATE_BASE64" | base64 --decode -o $DEVELOPMENT_CERTIFICATE_PATH
        echo -n "$WWDR_CERTIFICATE_BASE64" | base64 --decode -o $WWDR_CERTIFICATE_PATH
        # キーチェーン作成
        sh RunScripts/preparing_keychain.sh "$TEMPORARY_KEYCHAIN_NAME" "$TEMPORARY_KEYCHAIN_PASSWORD"
        # 配布証明書インポート
        sh RunScripts/import_certificate.sh "$TEMPORARY_KEYCHAIN_NAME" "$TEMPORARY_KEYCHAIN_PASSWORD" "$DISTRIBUTION_CERTIFICATE_PATH" "$DISTRIBUTION_CERTIFICATE_PASSWORD"
        # 開発者証明書インポート
        sh RunScripts/import_certificate.sh "$TEMPORARY_KEYCHAIN_NAME" "$TEMPORARY_KEYCHAIN_PASSWORD" "$DEVELOPMENT_CERTIFICATE_PATH" "$DEVELOPMENT_CERTIFICATE_PASSWORD"
        # 中間証明書インポート
        sh RunScripts/import_certificate.sh "$TEMPORARY_KEYCHAIN_NAME" "$TEMPORARY_KEYCHAIN_PASSWORD" "$WWDR_CERTIFICATE_PATH"
      env:
        DISTRIBUTION_CERTIFICATE_BASE64: ${{ secrets.DISTRIBUTION_CERTIFICATE_BASE64 }}
        DEVELOPMENT_CERTIFICATE_BASE64: ${{ secrets.DEVELOPMENT_CERTIFICATE_BASE64 }}
        WWDR_CERTIFICATE_BASE64: ${{ secrets.WWDR_CERTIFICATE_BASE64 }}
        DISTRIBUTION_CERTIFICATE_PASSWORD: ${{ secrets.DISTRIBUTION_CERTIFICATE_PASSWORD }}
        DEVELOPMENT_CERTIFICATE_PASSWORD: ${{ secrets.DEVELOPMENT_CERTIFICATE_PASSWORD }}
        TEMPORARY_KEYCHAIN_NAME: ${{ secrets.TEMPORARY_KEYCHAIN_NAME }}
        TEMPORARY_KEYCHAIN_PASSWORD: ${{ secrets.TEMPORARY_KEYCHAIN_PASSWORD }}

キーチェーンの作成は色々方法ありますが、私はsecurityコマンドを用いたshを採用しました。

preparing_keychain.sh
(この名前にしているのはfastlaneのcreate_keychainと競合するから)

MY_KEYCHAIN=$1
MY_KEYCHAIN_PASSWORD=$2
# ログインを読み込むキーチェーンリストに追加
security list-keychains -d user -s login.keychain
# キーチェーンを作成
security create-keychain -p "$MY_KEYCHAIN_PASSWORD" "$MY_KEYCHAIN"
# ユーザードメインに一時キーチェーンを追加
security list-keychains -d user -s "$MY_KEYCHAIN" $(security list-keychains -d user | sed s/\"//g)
# 再ロックのタイムアウトを削除
security set-keychain-settings "$MY_KEYCHAIN"
# キーチェーンのロックを解除
security unlock-keychain -p "$MY_KEYCHAIN_PASSWORD" "$MY_KEYCHAIN"

import_certificate.sh

MY_KEYCHAIN=$1
MY_KEYCHAIN_PASSWORD=$2
CERT_PATH=$3
CERT_PASSWORD=$4
# キーチェーンに証明書を追加
security import $CERT_PATH -k "$MY_KEYCHAIN" -P "$CERT_PASSWORD" -A -T "/usr/bin/codesign" -T "/usr/bin/productsign"
# インタラクティブシェルからのコード署名を有効にする
security set-key-partition-list -S apple-tool:,apple:, -s -k $MY_KEYCHAIN_PASSWORD -t private $MY_KEYCHAIN
# キーチェーンのロックを解除
security unlock-keychain -p "$MY_KEYCHAIN_PASSWORD" "$MY_KEYCHAIN"

fastlaneでApp StoreにipaをUploadする

    - name: Fastlaneを使ってStoreにipaをアップロードする
      run: |
        # 変数用意
        AUTH_KEY_PATH=$RUNNER_TEMP/auth_key.p8
        # シークレットから取得した証明書をデコード
        echo -n "$STORE_CONNECT_AUTH_KEY" | base64 --decode -o $AUTH_KEY_PATH
        fastlane store_upload auth_key:$AUTH_KEY_PATH
      env:
        STORE_CONNECT_KEY_ID: ${{ secrets.STORE_CONNECT_KEY_ID }}
        STORE_CONNECT_ISSUER_ID: ${{ secrets.STORE_CONNECT_ISSUER_ID }}
        STORE_CONNECT_AUTH_KEY: ${{ secrets.STORE_CONNECT_AUTH_KEY }}

fastlaneで用意してるのでそのまま実行します。
今回はfastlaneの中でプロビジョニングプロファイルを取得し、引数の証明書を使って署名しています。
※faslaneの処理については後述します。

fastlaneでの成果物を削除する

    - name: fastlaneでの成果物を削除する
      run: |
        fastlane run clean_build_artifacts
        rm -r ~/Library/Developer/Xcode/Archives/*

dsymやipaを削除します。これをやっとかないとストレージを圧迫します。
ipaはサイズが大きいので残しておきたい場合は削除スクリプトなど用意してローテーションさせるなどが必要ですね。

テンポラリキーチェーンを削除する

    - name: テンポラリキーチェーンを削除する
      if: ${{ always() }}
      run: |
        # 配布証明書削除
        sh RunScripts/delete_certificate.sh "$TEMPORARY_KEYCHAIN_NAME" "$DISTRIBUTION_CERTIFICATE_NAME"
        # 開発者証明書削除
        sh RunScripts/delete_certificate.sh "$TEMPORARY_KEYCHAIN_NAME" "$DEVELOPMENT_CERTIFICATE_NAME"
        # 中間証明書削除
        sh RunScripts/delete_certificate.sh "$TEMPORARY_KEYCHAIN_NAME" "$WWDR_CERTIFICATE_NAME"
        # キーチェーン削除
        sh RunScripts/clean_up_keychains.sh "$TEMPORARY_KEYCHAIN_NAME"
      env:
        DISTRIBUTION_CERTIFICATE_NAME: ${{ secrets.DISTRIBUTION_CERTIFICATE_NAME }}
        DEVELOPMENT_CERTIFICATE_NAME: ${{ secrets.DEVELOPMENT_CERTIFICATE_NAME }}
        WWDR_CERTIFICATE_NAME: ${{ secrets.WWDR_CERTIFICATE_NAME }}
        TEMPORARY_KEYCHAIN_NAME: ${{ secrets.TEMPORARY_KEYCHAIN_NAME }}

CIがエラーになっても常に削除するように「if: ${{ always() }}」を追記しています。
残っていると次のCIアクションのときにインポートが失敗します。
作成の時と同様にsecurityコマンドを用いたshを作成しました。

delete_certificate.sh

MY_KEYCHAIN=$1
CERT_PATH=$2
# キーチェーンの証明書を削除
security delete-certificate -c "$CERT_PATH" "$MY_KEYCHAIN"

clean_up_keychains.sh
(この名前にしているのはfastlaneのdelete_keychainと競合するから)

MY_KEYCHAIN=$1
# 一時的なキーチェーンを削除する
security delete-keychain "$MY_KEYCHAIN"
# ユーザーログインキーチェーンをデフォルトに戻す
security list-keychains -d user -s login.keychain

プロビジョニングプロファイルを削除する

    - name: プロビジョニングプロファイルを削除する
      if: ${{ always() }}
      run: rm ~/Library/MobileDevice/Provisioning\ Profiles/*.mobileprovision

CIがエラーになっても常に削除するように「if: ${{ always() }}」を追記しています。
古いプロビジョニングプロファイルが残っていると署名に失敗する可能性があります。

fastlaneでやること

fastlaneで署名する時ちょっとしたコツがあります。

Xcodeで「自動(Automaticaliy manage siging)」を使用している状態でCIビルドすると端末によって署名に失敗する可能性があります。
なぜならテンポラリーキーチェーン以外のキーチェーンに証明書が入っている場合はそっちをXcodeが参照してしまうことがあるからです。
なので、fastlaneから署名するとき「手動」で署名する様にlaneを組みます。

lane :store_upload do |options|

    app_identifier = "あなたのアプリのidentifier"
    team_id = "チームID"

    # storeにapi_key認証で接続
    api_key = app_store_connect_api_key(
      key_id: ENV["STORE_CONNECT_KEY_ID"],
      issuer_id: ENV["STORE_CONNECT_ISSUER_ID"],
      key_filepath: options[:auth_key],
      duration: 1200,
      in_house: false
    )

    # プロビジョニングプロファイルをダウンロード
    sigh(
      api_key: api_key,
      force: true,
      app_identifier: app_identifier,
      skip_certificate_verification: true,
    )
    app_profile = lane_context[SharedValues::SIGH_PROFILE_PATH]

    # 手動署名に設定変更
    update_code_signing_settings(
      use_automatic_signing: false,
      path: "あなたのアプリ.xcodeproj",
      team_id: team_id,
      code_sign_identity: "iPhone Distribution",
      sdk: "iphoneos*",
    )

    # 署名
    update_project_provisioning(
      target_filter: "あなたのアプリ",
      build_configuration: "Release",
      profile: app_profile,
      code_signing_identity: "iPhone Distribution"
    )

    # ipa作成
    gym(
      scheme: "あなたのアプリ",
      clean: true,
      configuration: "Release",
      export_method: "app-store",
      xcargs: "DEVELOPMENT_TEAM=" + team_id,
      export_options: {
        signingStyle: "manual",
        compileBitcode: false,
        uploadSymbols: false,
        provisioningProfiles: {
          app_identifier => app_profile,
        },
      },
    )

    # storeにupload
    deliver(
      api_key: api_key,
      force: true,
      skip_screenshots: true,
      skip_metadata: true,
      precheck_include_in_app_purchases: false,
    )

    # profileとipaファイルが一時ディレクトリに残ってるので削除
    sh("rm " + app_profile)
    sh("rm " + lane_context[SharedValues::IPA_OUTPUT_PATH])
  end

手動で署名することでXcodeや端末のキーチェーンに依存することがなくなります。
つまり全く別のアプリの証明書をCI端末に入れていてもCIが失敗することはないです。

実行してみる

実際に実行(ブランチにpush)してみるとActionsのタブで進行状況が確認できます。
ワークフロー名をクリックすると詳細な状況やエラーになったときのログも確認が可能です。
actions_capture.png

おわりに

はじめての構築作業ということもあり、トライアンドエラーの繰り返しで(合間ので作業だったというのもありますが)1ヶ月近くかかりました。
とても苦労しましたが、できた時の達成感もその分大きかったので本当にやってよかったなと実感しています。
コストも抑えられるのでセルフホストランナーは是非皆様にも使っていただきたいですね。

  1. 文字列化
    そのまま登録できないので文字列に変換が必要です。
    公式ドキュメントにあるとおりbase64を使用します。 2 3 4

  2. テンポラリーキーチェーン
    CIからアクセスするための一時的なキーチェーンファイルです。
    署名が終わったら不要になるので一時的に作成して削除します。
    事前にCI端末に証明書を入れておくと更新の度に入れ直さないといけなくなったり、複数台のCI端末を用意する場合は管理が煩雑になるので、テンポラリーキーチェーンを強く推奨します。 2 3 4 5 6 7 8

11
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
7