その辺にいるITインフラ屋さん

気が向いた時に適当な記事を書いたりする個人の備忘録です。個人で開発しているアプリの事やインフラについてなどの記事を書いたりすると思います。更新頻度は少なめ

GitHub Actions と faslaneを使ってiOSアプリのリリース準備を自動化する

先日、MoveLogというiOSアプリをリリースしました。機能追加したいとは思いつつも、最近はテストコード書いたり足回りを整えたりしています。その中でも、アプリのリリース作業というものはとても面倒な作業で、普通にやろうとすると下記のような手作業で実施するかと思います。

  1. App Store Connect で新しいバージョンの提出準備をする
  2. XcodeでアプリをBuidして、ビルドしたアプリをアップロードする
  3. App Store Connectでメタ情報を修正してリリースする

App Store Connect とXcodeを行き来しないといけないし、Xcode で Buidを実行すると10分ほどかかって、その後に画面でぽちぽちしてアップロードしないといけないしで面倒ですよね。ビルド中はMacが悲鳴をあげるという厳しさもあります。

仕事でインフラ周りの整備をやってる身ということもあり、この作業を効率化しないといけないという使命感にかられたので、GitHub Actions と faslaneを使って部分的に自動化するようにしました。心が折れそうになるくらいハマったので備忘録として残します。主に証明書や2段階認証周りの設定がハマり尽くしました。



全体像

今回はアプリのビルドからApp Store Connectへのビルドファイルのアップロードをするだけとなります。 スクリーンショット取得やアプリの紹介情報の更新もfastlaneで出来るようですが、そこまでは実施していないです。

  1. アプリのコードをGitHubリポジトリにPushする
  2. Release ブランチにPRがマージされると GitHub Actionsが発火する
  3. GitHub Actions の中でfastlaneを実行する
  4. fastlane match で ビルド用の証明書情報を取得する
  5. fastlane gymでアプリをビルドする
  6. faslane deliver でApp Store Connectにビルドしたアプリをアップロードする

fastlaneについて

fastlane はiOS,Android アプリのデプロイを自動化するツールです。

fastlane - App automation done right

今回は App Store Connectにビルドしたものをアップロードするまでが目的なので、 APP STORE DEPLOYMENTCODE SIGNINGのみを利用します。元々は APP STORE DEPLOYMENT のみを利用しようと思ってましたが、CI環境を動かそうとすると証明書をどうするか問題があったので、証明書の管理も fastlane で実施する事にしました。

fastlane は actionsの組み合わせによって自動化出来る範囲を増やすことが出来ます。

Available Actions - fastlane docs

fastlane の 設定(fastfile)は下記のような感じになり、lane の中で指定されたアクションを定義していくといったものですね。

lane :release do
  build_app(scheme: "MyApp")
end

この記事では、fastlane actions の下記のアクションを利用します。

fastlane match を使った証明書の管理

公式のドキュメントはこちら

match - fastlane docs

元々は複数の開発者のMacでそれぞれ証明書の管理をする事が大変という事で、中央集権的な管理をするためのものらしいですね。個人開発者からすると若干オーバースペックな気はしますが、一旦は気にしない。

Code Signing Guide for Teams

下記の流れで初期化をする事が出来ました。(2020/6/14時点)

  1. アプリとは異なる証明書管理専用の新しいリポジトリを作成する。
  2. fastlane match init を実行して初期化する
  3. fastlane match development を実行して、開発用の証明書(Development)を作成する
  4. fastlane match appstoreを実行して、リリース用の証明書(Distribution)を作成する

初期化をした後には XcodeAutomatically manage signingのチェックを外して matchで作成した開発用の証明書を指定すれば良いです。 f:id:mayuki123:20200614102911p:plain

f:id:mayuki123:20200614103352p:plain

fastlane gym でのアプリのビルド

公式のドキュメントはこちら。内部的には xcodebuild を利用しているようです。

gym - fastlane docs

設定したものは特に複雑な事はしていないかと思います。ライブラリの管理にPodを利用しているので .xcworkspaceを指定しています。

gym(
  workspace: "XXXX.xcworkspace",
  configuration: "Release",
  scheme: "XXXXX",
  export_method: "app-store",
)

fastlane deliver でのリリース準備

公式のドキュメントはこちら。

deliver - fastlane docs

App Store Connectとの連携をするアクションですね。今回は新しいバージョンの作成とビルドしたもののアップデートをしたいので、下記設定をしました。何も設定をしない場合は対話形となるため、CIで利用するためには force: true の設定が必要です。

deliver(
   force: true,
   skip_metadata: true,
   skip_screenshots: true
)

GitHub Actionsでの設定

職場ではCircle CI を 使っているので、GitHub Actionsは始めて触りました。macOS の実行環境が無料で利用できるのはとても良いですね。単純に言えばLinuxの10倍お金かかるので、無料プランのビルド時間だとテストも常に回すようにすると無料だと厳しそうですね。 個人開発なら程よいくらいかも。

github.co.jp

アプリのリポジトリ/.github/workflows/xxxx.yml のような構造で設定ファイルを配置すれば利用できるようになるので楽ですね。ライブラリのキャッシュにも対応したのでとても便利になりました。

設定自体は下記のようになりました。timeout-minutes を設定しない場合はデフォルト値が6時間なので、処理が固まった時にひたすら利用時間が消費されるので気をつけた方が良さそうです。あとはPodのライブラリはキャッシュするようにしてるので、2回目以降の実行は少し早くなるはずです。

name: iOSBuild

on:
  push:
    branches:
      - release

jobs:
  build:
    runs-on: macos-latest
    timeout-minutes: 900
    steps:
    - uses: actions/checkout@v2
    - name: Cache Pods
      uses: actions/cache@v2
      with:
        path: Pods
        key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
    - name: Pod Install
      if: steps.cache-cocoapods.outputs.cache-hit != 'true'
      run: pod install
    - name: Release
      run: fastlane release --verbose
      env:
        FASTLANE_USER: ${{ secrets.FASTLANE_USER }}
        FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
        MATCH_PASSWORD: ${{secrets.MATCH_PASSWORD}}
        MATCH_GIT_BASIC_AUTHORIZATION: ${{secrets.MATCH_GIT_BASIC_AUTHORIZATION}}
        FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD}}
        FASTLANE_SESSION: ${{secrets.FASTLANE_SESSION}}

環境変数の詳細については後述しますが、GitHub ActionsではリポジトリのSecretsに秘匿情報を設定すれば環境変数として使う事が出来ます。 f:id:mayuki123:20200614112548p:plain

GitHub Actions 上で利用する証明書の設定

ローカルのMacで実行する分にはXcodeで指定している証明書が利用されるので、あまり気にしなくてよいのですが、CI環境で実行させようとするとかなりハマりました。 ビルドするための証明書を fastlane matchで設定したリポジトリから取得します。 sync_code_signing のアクションを実行する事で取得する事が出来ます。

# Fastfile

sync_code_signing(
  git_url: "<fastlane matchの設定で作成したリポジトリ>",
  git_basic_authorization: ENV["MATCH_GIT_BASIC_AUTHORIZATION"],
  type: "appstore",
  readonly: true
)

環境変数 MATCH_PASSWORDfastlane match で証明書を作成した際に設定したパスワードを設定します。 また、環境変数 MATCH_GIT_BASIC_AUTHORIZATION がないと怒られます。

https://docs.fastlane.tools/actions/match/#git-storage-on-github

エラーが発生した時の内容

Exit status: 128
Error cloning certificates repo, please make sure you have read access to the repository you want to use
Run the following command manually to make sure you're properly authenticated:

GitHub Actions は実行されたリポジトリへのアクセストークンは保有していますが、異なるリポジトリへのアクセスは出来ないので、認証情報を設定する必要があるようです。

また、この認証情報は <ユーザ名>:<GitHub Personal access tokens>base64 フォーマットしたものを利用する必要があります。 そのため、下記のようなコマンドを実行して生成しました。私は echo で改行コード含んだ状態でbase64にしてしまって動かないという初歩ミスして時間を溶かしました。
echo -n <ユーザ名>:<GitHub Personal access tokens> | base64

sync_code_signing の設定をしただけだと、Xcodeで設定している証明書ファイル(match Development XXXX)が指定されてしまってビルドでこけるという問題も発生しました。

INFO [2020-06-14 05:01:18.70]: $ set -o pipefail && xcodebuild -workspace XXXX.xcworkspace -scheme XXXX -configuration Release -destination 'generic/platform=iOS' -archivePath /Users/runner/Library/Developer/Xcode/Archives/2020-06-14/XXXX\ 2020-06-14\ 05.01.18.xcarchive archive | tee /Users/runner/Library/Logs/gym/XXXX-XXXX.log | xcpretty
INFO [2020-06-14 05:01:25.61]: ▸ ❌  error: No profile for team 'XXXX' matching 'match Development XXXX' found: Xcode couldn't find any provisioning profiles matching 'XXXX/match Development XXXX. Install the profile (by dragging and dropping it onto Xcode's dock item) or select a different one in the Signing & Capabilities tab of the target editor. (in target 'XXXX' from project 'XXXX')
INFO [2020-06-14 05:01:25.61]: ▸ ** ARCHIVE FAILED **

下記のように証明書の設定をアップデートする事で通るようにはなりました。match で設定してたら自動でしてくれそうなものの、出来ないのかなと疑問に思ってるので良い方法を知ってる人がいたら教えてください。

update_code_signing_settings(
      code_sign_identity: "iPhone Distribution",
      profile_name: "match AppStore XXXX"
)

App Store Connect への認証

App Store Connect にビルドしたものをfastlaneでアップロードする際には下記の環境変数の設定が必要となります。

https://docs.fastlane.tools/best-practices/continuous-integration/#environment-variables-to-set

  • FASTLANE_USER : App Store Connectに接続するユーザ
  • FASTLANE_PASSWORD : App Store Connectに接続するユーザのパスワード

また、App Store Connect にログインしようとする際には Apple ID の二段階認証が必須となったかと思います。CIからのログインで二段階認証が突破出来ないと下記のような事象に陥ります。

INFO [2020-06-13 06:10:33.91]: Login to App Store Connect (***)
Reading keychain entry, because either user or password were empty
Two-factor Authentication (6 digits code) is enabled for account '***'
More information about Two-factor Authentication: https://support.apple.com/en-us/HT204915

If you're running this in a non-interactive session (e.g. server or CI)
check out https://github.com/fastlane/fastlane/tree/master/spaceship#2-step-verification

Please enter the 6 digit code you received at +81 •••-••••-••XX:

エラーログ自体はおそらくはStringをパース出来ないとかで、関連性がなさそうなものが出ます。めっちゃハマりました。

➡️  [Swift] undefined method `each' for nil:NilClass - Cannot Create Group Within FastlaneSwiftRunner Project
    https://github.com/fastlane/fastlane/issues/15184 [open] 28 💬
    a week ago
/usr/local/lib/ruby/gems/2.6.0/gems/highline-1.7.10/lib/highline/question.rb:413:in `remove_whitespace': [!] undefined method `strip' for nil:NilClass (NoMethodError)

二段階認証を突破するためには下記の環境変数の設定が必要です。

  • FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD : Apple IDのApp用パスワード
  • FASTLANE_SESSION : fastlane spaceauth で実行した結果の文字列
    • fastlane spaceauth -u <Apple IDのメールアドレス>

詳細な設定方法はドキュメントをご確認ください。
https://docs.fastlane.tools/best-practices/continuous-integration/#application-specific-passwords
fastlane/spaceship at master · fastlane/fastlane · GitHub

GitHub Actions特有の罠

一通りビルドが終わった後に Running script '[CP] Embed Pods Frameworks' というログの後で動かなくなるという事象がありました。

f:id:mayuki123:20200614122626p:plain

具体的な原因は分かりませんが、setup_ci を 仮に "travis" として設定すれば良いようです。公式でGitHub Actionsに対応するのを期待する。

Fastlane Match and Gym always freeze at Running script '[CP] Embed Pods Frameworks' · Issue #15695 · fastlane/fastlane · GitHub

setup_ci(
      force: true,
      provider: "travis",
)

最終的な 設定ファイル(fastfile)

最終的には fastfileは下記のようになりました。大変だったわりにはシンプルな感じにはなりました。今回はビルド番号を自動でインクリメントするとか、Slack通知をするといった設定は割愛します。

default_platform(:ios)

setup_ci(
  force: true,
  provider: "travis"
)

platform :ios do
  desc "Push a new release build to the App Store"
  lane :release do
    sync_code_signing(
      git_url: "<fastlane matchの設定で作成したリポジトリ>",
      git_basic_authorization: ENV["MATCH_GIT_BASIC_AUTHORIZATION"],
      type: "appstore",
      readonly: true
    )
    update_code_signing_settings(
      code_sign_identity: "iPhone Distribution",
      profile_name: "match AppStore XXXX"
    )
    gym(
      workspace: "XXXX.xcworkspace",
      configuration: "Release",
      scheme: "XXXX",
      export_method: "app-store",
    )
    deliver(
      force: true,
      skip_metadata: true,
      skip_screenshots: true
    )
  end
end

おわりに

分かってしまえばなんて事ない手順なんですが、証明書とか二段階認証を突破するのにかなり苦労しました。試行錯誤しているうちに無料枠も超えてしまったので課金しました。使用量の75% → 90% → 100% とメール通知がくるので、 どのくらい使ったかは分かりやすいですね。個人開発だし手動で良いのでは…と心がおれそうになりました。こんなにハマると思ってなかった

f:id:mayuki123:20200614142050p:plain