BitriseでAppStoreConnectからdSYMを安定してダウンロードする

Bitriseワークフローの中で
AppStoreConnectからdSYMをダウンロードするには
いくつかの課題があります。

この記事では、それら課題に対しての私なりのアプローチを書きます。
前提として、fastlaneを主に利用します。

### fastlaneを利用したdSYMのダウンロード

このActionでdSYMのダウンロードが可能です。
[download_dsyms](http://docs.fastlane.tools/actions/download_dsyms/#download_dsyms)

# 2ファクタ認証を解決する
Bitriseで2ファクタ認証を解決する方法が2つあります。

- FASTLANE_SESSIONを利用するセッションベース
- API Keyを利用したAppStoreConnect API

AppStoreConnect APIはdSYMダウンロードの機能を提供していません。
よって、fastlaneの `download_dsyms` を利用する際はセッションベースの方法で2ファクタ認証を解決するしかありません。

[これら2つの方法の詳細はこちらの記事にまとめてあります。](https://qiita.com/watanave/items/6e1982dfb9631172895d)


# bitcode再コンパイルを待たなければならない

#### ipaのアップロードと同時にアプリ審査をする場合
fastlaneのAction [deliver](http://docs.fastlane.tools/actions/deliver/#deliver) を利用し、パラメータ `submit_for_review` をtrueにします。
こうすることで [deliver](http://docs.fastlane.tools/actions/deliver/#deliver) Actionが bitcodeの再コンパイルを待機し、完了後にアプリ審査へサブミットされます。
[deliver](http://docs.fastlane.tools/actions/deliver/#deliver) の直後に[download_dsyms](http://docs.fastlane.tools/actions/download_dsyms/#download_dsyms)を呼び出せばdSYMをダウンロードすることが可能です。

#### ipaのアップロードと同時にアプリ審査をしない場合

このケースが最も事態が複雑です。

###### パラメータ`wait_for_dsym_processing`は絶対ではない

[download_dsyms](http://docs.fastlane.tools/actions/download_dsyms/#download_dsyms) Actionにも再コンパイルを待機する機能があります。
パラメータ `wait_for_dsym_processing` がそれにあたります。
下の画像のリストにアップロードしたバージョンが表示されると、この`wait_for_dsym_processing`は有効に作用します。
![2020-12-16 20.59のイメージ.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14646/254c79a7-bf2c-7415-7e65-6c76ca855ad6.jpeg)

しかし、ipaをアップロードしてからこのリストに表示されるまで若干のラグがあるため
アップロード直後に[download_dsyms](http://docs.fastlane.tools/actions/download_dsyms/#download_dsyms) Actionを実行しても、再コンパイルを待機してくれない場合があります。
しかも、この場合はfastlaneは正常終了と判断します。
(bitcodeなしビルドのケースを考慮してのこと。とfastlaneのコードにコメントが書かれています)

そのため、dSYMダウンロードの成否は **dSYMファイル (zipファイル)の有無で判定する** のが確実です。
この時、[download_dsyms](http://docs.fastlane.tools/actions/download_dsyms/#download_dsyms)のパラメータ `build_number` `version` は必ず指定する必要があります。
[download_dsyms](http://docs.fastlane.tools/actions/download_dsyms/#download_dsyms)は、`build_number`と`version`を指定しなければ、すべてのバージョンのdSYMをダウンロードしてしまうからです。
また、事前に[clean_build_artifacts](http://docs.fastlane.tools/actions/clean_build_artifacts/#clean_build_artifacts)を利用し、Bitriseでビルドした際に出力されるdSYMファイルを削除しておく必要もあるので注意が必要です。

###### 再コンパイル待ちワークフローが長時間bitriseを占有してしまう。

再コンパイル完了まで、数十分かかることもあります。

- Bitriseの契約プランのビルド時間の上限を迎える
- 他のワークフローをブロックしてしまう

可能性があります。

そこで、dSYMファイルのダウンロードに失敗したら (ファイルがなかったら) Bitriseのワークフローを一旦終了し、新たにBitriseにビルドを要求させます。

[download_dsyms](http://docs.fastlane.tools/actions/download_dsyms/#download_dsyms) に
`wait_timeout` というパラメータがあります。デフォルトは5分です。
5分経過後、dSYMがダウンロードできなくとも`fastlane`は正常終了します。
終了後、dSYMファイル(zipファイル)の有無を判定し、ファイルがなければbitriseのRestful　APIを利用して、再びdSYMをダウンロードするビルドを要求します。

こうすることで、長時間bitriseを占有する問題を回避できます。

この時、`build_number`にBitriseの `Set Xcode Project Build Number` を利用していると注意が必要です。
アプリをビルドした時の`build_number`と、dSYMダウンロード時の`build_number`が異なってしまうからです。
これは、BitriseAPIをリクエストする際に`build_number`をパラメータにすることで解決できます。

# サンプル

```ruby
    lane :deploy_store do
        match(type: "appstore", readonly: true)
        gym()
        deliver()
        clean_build_artifacts() # ⚠️これを入れないとCIビルド時のdSYMをアップロードしてしまう⚠️
        upload_dsym()
    end

    lane :upload_dsym do
        version = get_version_number(
            xcodeproj: "DeploySample.xcodeproj",
            target: "DeploySample"
        )
        # `Set Xcode Project Build Number`を使わない場合
        # build_number = get_build_number(
        #     xcodeproj: "DeploySample.xcodeproj"
        # )
        build_number = ENV["DSYM_BUILD_NUMBER"] || ENV["XCODE_BUNDLE_VERSION"]
        puts "build_number = #{build_number}"
        download_dsyms(
            app_identifier: "アプリのBundleID",
            wait_for_dsym_processing: true,
            wait_timeout: 180, # 再コンパイルを3分待つ
            version: version,
            build_number: build_number
        )

        # dSYMファイルの有無を判定
        if Dir.glob("#{ENV['PWD']}/*.dSYM.zip").empty?
            puts "No dsym files"
            trigger_upload_dsym_workflow()
            next
        end

        upload_symbols_to_crashlytics(
            gsp_path: "./DeploySample/GoogleService-Info.plist",
            binary_path: "./Pods/FirebaseCrashlytics/upload-symbols",
        )
    end

    lane :trigger_upload_dsym_workflow do
        uri = URI.parse("https://api.bitrise.io/v0.1/apps/#{ENV["BITRISE_APP_SLUG"]}/builds")
        params = { 
            build_params: {
                branch: ENV["BITRISE_GIT_BRANCH"],
                commit_hash: ENV["BITRISE_GIT_COMMIT"],
                workflow_id: "upload_dsym", # `fastlane upload_dsym` を呼ぶだけのworkflowを事前に用意する。このパラメータはそのworkflowの名前。
                environments: [
                    {
                      # `build_number`を環境変数に渡す
                      is_expand: true,
                      mapped_to: "DSYM_BUILD_NUMBER",
                      value: ENV["DSYM_BUILD_NUMBER"] || ENV["XCODE_BUNDLE_VERSION"]
                    }
                ],    
            },
            hook_info: {
                type: "bitrise"
            }
        }
        headers = { 
            "Content-Type" => "application/json",
            "Authorization" => ENV["BITRISE_API_ACCESS_TOKEN"]
        }
        uri.query = URI.encode_www_form(params)
    
        http = Net::HTTP.new(uri.host, uri.port)
        http.use_ssl = true
        http.verify_mode = OpenSSL::SSL::VERIFY_NONE
        
        request = Net::HTTP::Post.new(uri.request_uri)
        request["Content-Type"] = "application/json"
        request["Authorization"] = ENV["BITRISE_API_ACCESS_TOKEN"]
        request.body = params.to_json

        response = http.request(request)
        response.value
    end
```