24/7 twenty-four seven

iOS/OS X application programing topics.

Travis CIでビルドごとにiTunes ConnectのValidationを自動的に実行する

コマンドラインからiOSアプリケーションをiTunes Connectにアップロードする - 24/7 twenty-four seven

↑ こちらの記事で書いたように、コマンドラインからiTunes Connectへのアップロードや、バリデーションができるのを利用して、Travis CIを使ってビルドするたびに自動的にバリデーションを実行するようにしました。

これにより、プライベートAPIを利用していたり、必須なサイズのアイコンやLaunchImageが無いなどの理由でバリデーションエラーになってアップロードが失敗するということが未然に防げます。

ARCを使う場合、ヘッダに載っていないメソッドを呼ぶのはコンパイルエラーになるので、知らずにプライベートAPIを使ってしまうようなことは現在はほぼありません。 しかし、iTunes Connectのバリデーションはあまり賢くないので(おそらく単純な文字列のマッチング?)たまたまプライベートAPIと同名のメソッドを全然関係ないクラスに定義してしまった場合などでもエラーになることがあります。

通常なら申請する段階になって初めてわかるので、エラーの原因がサードパーティのライブラリにある場合など、すぐには直せずリリースを延期せざるを得ないこともあります。

バリデーションはAdHocの署名では失敗するようなので、AppStoreの署名でビルドしてバリデーションをするタスクを追加することにしました。 実際のコマンドは下記になります。

script:
  - bundle exec rake $(ACTION)
env:
  matrix:
    - ACTION="profile:install certificate:add distribute:CONFIG certificate:remove"
    - ACTION="profile:install certificate:add version:bump:patch validate certificate:remove"
    - ACTION=test

バリデーションのタスクは上から2番目のこのコマンドです。

profile:install certificate:add version:bump:patch validate certificate:remove

プロビジョニングプロファイルに署名をする必要があるので証明書を追加と、既存のバイナリとバージョン番号が異なっている必要があるのでバージョン番号の末尾を自動的にあげる処理を前処理として入れています。

実際のバリデーションを行うコマンドは下記になります。

task :validate do
  build_xcarchive(configuration: "Release")
  clean_ipa
  sh %[xcodebuild -exportArchive -exportFormat IPA -archivePath "#{ARCHIVE_FILE}" -exportPath "#{IPA_FILE}" -exportProvisioningProfile "Ubiregi App Store" | xcpretty -c; exit ${PIPESTATUS[0]}]
  validate_ipa(IPA_FILE)
end
def validate_ipa(ipa_file)
  sh %['/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Support/altool' --validate-app --file "#{ipa_file}" --username ENV["DEV_APPLE_ID"] --password ENV["DEV_PASSWORD"]]
end

xcarchiveからipa形式にエクスポートして、そのipaファイルをaltoolに指定しています。

以下はわざとプライベートAPIを使うようにして失敗させた時のTravis CIのログです。

▸ Compiling UBDashboardCheckoutDetailCell.xib
▸ Compiling UBDataTransferViewController.xib
▸ Processing Ubiregi2-Info.plist
▸ Touching Ubiregi2.app
▸ Signing /Users/travis/Library/Developer/Xcode/DerivedData/Ubiregi2-gavhqalfhdytsueqelwxtefkbbio/Build/Intermediates/ArchiveIntermediates/Ubiregi2-Release/InstallationBuildProductsLocation/Applications/Ubiregi2.app
▸ Touching Ubiregi2.app.dSYM
xcodebuild -exportArchive -exportFormat IPA -archivePath "/Users/travis/build/ubiregiinc/ubiregi-client/build/Ubiregi2.xcarchive" -exportPath "/Users/travis/build/ubiregiinc/ubiregi-client/build/Ubiregi2.ipa" -exportProvisioningProfile "Ubiregi App Store" | xcpretty -c; exit ${PIPESTATUS[0]}
'/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Support/altool' --validate-app --file "/Users/travis/build/ubiregiinc/ubiregi-client/build/Ubiregi2.ipa" --username [DEV_APPLE_ID] --password [DEV_PASSWORD]
2014-11-02 11:16:00.289 altool[4121:d07] *** Error: (
    "Error Domain=ITunesConnectionOperationErrorDomain Code=1350 \"Your app contains non-public API usage. Please review the errors, correct them, and resubmit your application.\" UserInfo=0x7fd788f56f40 {NSLocalizedRecoverySuggestion=Your app contains non-public API usage. Please review the errors, correct them, and resubmit your application., NSLocalizedDescription=Your app contains non-public API usage. Please review the errors, correct them, and resubmit your application., NSLocalizedFailureReason=iTunes Store operation failed.}",
    "Error Domain=ITunesConnectionOperationErrorDomain Code=50 \"The app references non-public selectors in Payload/Ubiregi2.app/Ubiregi2: _terminateWithStatus:\" UserInfo=0x7fd788f39310 {NSLocalizedRecoverySuggestion=The app references non-public selectors in Payload/Ubiregi2.app/Ubiregi2: _terminateWithStatus:, NSLocalizedDescription=The app references non-public selectors in Payload/Ubiregi2.app/Ubiregi2: _terminateWithStatus:, NSLocalizedFailureReason=iTunes Store operation failed.}",
    "Error Domain=ITunesConnectionOperationErrorDomain Code=-19000 \"If you think this message was sent in error and that you have only used Apple-published APIs in accordance with the guidelines, send the app's nine-digit Apple ID, along with detailed information about why you believe the above APIs were incorrectly flagged, to appreview@apple.com. For further information, visit the Technical Support Information page at http://developer.apple.com/support/technical/.\" UserInfo=0x7fd788d88170 {NSLocalizedRecoverySuggestion=If you think this message was sent in error and that you have only used Apple-published APIs in accordance with the guidelines, send the app's nine-digit Apple ID, along with detailed information about why you believe the above APIs were incorrectly flagged, to appreview@apple.com. For further information, visit the Technical Support Information page at http://developer.apple.com/support/technical/., NSLocalizedDescription=If you think this message was sent in error and that you have only used Apple-published APIs in accordance with the guidelines, send the app's nine-digit Apple ID, along with detailed information about why you believe the above APIs were incorrectly flagged, to appreview@apple.com. For further information, visit the Technical Support Information page at http://developer.apple.com/support/technical/., NSLocalizedFailureReason=iTunes Store operation failed.}"
)
rake aborted!
Command failed with status (3): ['/Applications/Xcode.app/Contents/Applicat...]
/Users/travis/build/ubiregiinc/ubiregi-client/Rakefile:202:in `validate_ipa'
/Users/travis/build/ubiregiinc/ubiregi-client/Rakefile:198:in `block in <top (required)>'
Tasks: TOP => validate
(See full trace by running task with --trace)

The command "bundle exec rake $(echo ${ACTION} | sed -e "s/CONFIG/${CONFIG}/g")" exited with 1.

Done. Your build exited with 1.

エラーが3つ返ってきていますが、いずれも非公開のAPIを使用していることについてのエラーです。 エラーのうちのこの部分に直接の原因が書いてあります。

The app references non-public selectors in Payload/Ubiregi2.app/Ubiregi2: _terminateWithStatus:

_terminateWithStatusUIApplicationで定義されているメソッドですが、iTunes Connectはあまり賢くないので、同じ名前のメソッドをうっかり他のクラスに定義してしまったとしても同様のエラーでバリデーションが失敗します。

このように「ついうっかり」プライベートAPIとして検出されるメソッドやプロパティを定義してしまっていたり、サードパーティのライブラリがエラーとして検出されてしまう、などという問題が、申請時になって発覚するのは大変なことなので、事前にわかるのはかなり便利ではないかと思います。