🚗

iOSのXCUITestをCI上で実機向けビルドする

2024/04/02に公開

タイトルの通りのことをするときに、署名で無駄にハマったので、メモしておきます。

前提として、

  • fastlane matchで証明書やプロファイルの管理をしている。
  • ローカルではシミュレータ向けでも実機向けでもビルド可能。
  • CIではシミュレータ向けにはビルド・実行可能

結論

  1. #{app_identifier}.#{test_target_scheme}.xctrunnerをindentifierに登録
  2. fastlane match development -a #{app_identifier}.#{test_target_scheme}.xctrunner
    • development向けプロファイルをmatch管理下で作成する。
  3. Xcodeで、test用ターゲットのmanual signingを有効化し、matchしたプロファイルを設定
    • エラーが出ても無視
  4. ci上でfastlane match development --readonly && fastlane scan --build_for_testing

ごく当たり前の話+αですが、いくつかのポイントでハマってしまいました。
(fastlaneのコマンドはlaneやMatchfile/Scanfile等を作って実行してるので不足があります)

起きるエラー

下記のように、テスト用のビルドを行うときにプロファイルがない、というエラーが起きました。

[04:04:41]: ▸ /Users/runner/work/my-repo/my-repo/MyApp.xcodeproj: error: No profiles for 'net.wasnot.sample.MyApp-UITests.xctrunner' were found: Xcode couldn't find any iOS App Development provisioning profiles matching 'net.wasnot.sample.MyApp-UITests.xctrunner'. Automatic signing is disabled and unable to generate a profile. To enable automatic signing, pass -allowProvisioningUpdates to xcodebuild. (in target 'MyApp_UITests' from project 'MyApp')
[04:04:41]: ▸ 2024-03-26 04:04:41.480 xcodebuild[7012:36009] Writing error result bundle to /var/folders/f8/xn9w26457_v2fc5qzhbrlyj00000gn/T/ResultBundle_2024-26-03_04-04-0041.xcresult
[04:04:42]: ▸ xcodebuild: error: Failed writing xctestrun file: The folder “MyApp_iphoneos17.0-arm64.xctestrun” doesn’t exist..

ここではMyAppというのがプロジェクト名や、アプリターゲット・scheme名で、MyApp_UITestsというのがUIテスト用のターゲット・scheme名です。
MyApp用のprofileは存在して、fastlane matchでインストールできているのに、MyApp_UITests用のものが存在しないのが問題でした。

ハマり1: XCTestでも署名は必要

ローカルでの実機インストールではwildcardプロファイルが使われる?

手元でXCTestを実機インストールしている際には、プロファイル等は特に不要で、dev portal上にも特にidentifier/profileは追加されていなかったため、不要と勘違いしてしまいました。

よくあることかと思いますが、ローカル環境だと無料プランでもXcodeにappleアカウントでログインしていると、XC wildcardのようなidentifierが適用されて、profileも一時的なものが作られて?、わざわざidentifyやprofileを用意しなくても動くようです。

XCTest用のRunnerも署名されていた

そして、XCTestRunnerは通常はテスト内容を示すxmlファイルだけだったりするので、わざわざ署名しないでも動いている、と誤解してしまっていました。

XCTestを実行するためのRunnerも通常のアプリのようにパッケージ・署名され、実機にインストールされる必要があるようです。

また、Test用ターゲットでは署名がAutomatically設定のままで、ローカルではエラー等も出ていないため、気付くのが遅れてしまいました。

その点は一応、CI上ではautomatically signingが動かないので、-allowProvisioningUpdatesをつけなさい、と言う警告が出ていました。
この警告でももっと素早く気付くことができたかなと思います。

参考

証明書関連、CIやテスター文脈ではよくある話で、automatically signingだとidentifier/profileがプロジェクト作成時にしか作成されないため、ダミープロジェクトを作るべし、としているドキュメントがいくつかありました。
今回は目的と異なりますが、参考にはなりました。

ハマり2: identifierに勝手にxctrunnerが付与される

これが本題ですが、Xcode 11ころからXCUITest用のtargetではidentifierに暗黙で.xctrunnerというsuffixが付与されるようです。

それだけならいいのですが、XcodeのTarget編集画面ではxctrunnerは見えず、証明書もxctrunner付きのidentifierのものだと、エラーとして表示されてしまうようです。

Fastlane match for UITest Target · fastlane/fastlane · Discussion #20064

Xcode 15でも変わらず、エラー表示になりますが、ビルドや実行はローカルでも特に問題なくできます。
気持ち悪いですが仕方ないですね。。

参考

Xcodeのバグ?仕様変更を認める問い合わせはいくつかありました。
日本語の記事は見つからなかったので、皆さんはこまっていないかもしれません。

解決法

1. dev portalでidentifierを登録

UITestターゲット用のidentifierをapple developer portal上でapp idとして追加します。

https://developer.apple.com/account/resources/identifiers/add/bundleId

この際、net.wasnot.sample.MyApp-UITestsのように#{app_indentifier}.#{UITest-Target}ではなく、
net.wasnot.sample.MyApp-UITests.xctrunnerのようにsuffixに.xctrunnerをつける必要があります。
これは、上でも書いたようにXcodeが勝手にsuffixを追加するためで、UITestのターゲットで設定変更した場合も追加する必要があるので注意です。

2. fastlane matchを使用してprofileを作成

cliやlaneを用いて、identifierに対応するprofileをmatch管理下で作ります。

CLIの場合

$ fastlane match development -a net.wasnot.sample.MyApp-UITests.xctrunner

Fastfileの場合


lane :test_lane do |options|
    match(
      app_identifier: ["#{app_identifier}.MyApp-UITests.xctrunner"],
      type: 'development',
      readonly: false, # 初回登録時はfalseにするが、ciからはtrueで呼び出す
    )
end

3. Xcodeのsigningに設定する

XcodeでUITestsターゲットの署名設定をautomatically -> manualに変更し、作成したmatch developmentから始まるprofileを選択します。
この際、上でも言及した通り、ターゲット上で設定できるidentifierと異なるため、選択肢に出ない、エラー表示になる、と言う問題がありますが、ビルド時や実機テスト時には問題ないので無視します。

下記のようなエラー表示になりますが、問題ありません。

4. CI上でmatchおよびbuildを行う

あとは自分の行いたい実機向けビルドを行うだけです。
自分の場合の例を示して終わりにします。

desc "Bundle XCTestRun for execute on Firebase Test Lab"
lane :bundle_xctestrun do |options|
    match(
      # 実機向けに必要なすべてのidentifierのプロファイルをmatchする
      app_identifier: [
        app_identifier,
        app_extension_identifier,
        "#{app_identifier}.MyApp-UITests.xctrunner"
      ], 
      type: 'development',
      readonly: true,
    )
    scan(
      scheme: 'MyApp',
      clean: true,
      skip_detect_devices: true,
      build_for_testing: true,
      sdk: 'iphoneos', # 実機向けにビルドする
      should_zip_build_products: true
    )
end

別件: CI上でのmatchエラー

ちなみに今回はGithub Actionsを使用していましたが、そもそもfastlane matchが失敗してしまう問題がありました。

これは下記箇所があったためでした。

  • match設定(Matchfile)のgit_urlにgit schemeを使っていた
  • git_basic_authorizationをruby上で作成、設定していた

修正したら直りました。

git schemeの修正

githubの仕様変更への対応もれの可能性があります。
(circleciやローカルでは動いていたので謎です)

-git_url "git@github.com:xxx/my-certificates.git"
+git_url "https://github.com/xxx/my-certificates.git"

auth headerの作成

こちらは、もっと理由がわからないので、たまたまかもしれないです。
詳しく調べていません、すみません。

ダメだった

username = ENV['USERNAME']
personal_github_access_token = ENV["PERSONAL_GITHUB_ACCESS_TOKEN"]
authorization_token_str = "#{username}:#{personal_github_access_token}"
basic_authorization_token = Base64.strict_encode64(authorization_token_str)

match(
  git_basic_authorization:basic_authorization_token,
  type: "development",
  app_identifier: app_identifiers(),
  readonly: true
)

動いた

- name: calculate env
  run: |
    echo "MATCH_GIT_BASIC_AUTHORIZATION=$(echo -n ${{secrets.GH_USERNAME}}}:${{ secrets.GH_ACCESS_TOKEN }} | base64)" >> $GITHUB_ENV
- name: fastlane match
  env:
    MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
  run: fastlane match development

参考

matchのtokenの設定方法として参考になりました。

Discussion