こんにちは、 iOS エンジニアの伊藤です。
今回は、 iOS アプリの配布を自動化を GitHub のサービス 『 GitHub Actions (Self-hosted runners) 』 と Google のサービス 『 Firebase App Distribution 』 を活用して行ったので、その情報を共有いたします。
 


自動化の経緯


Braveridge では、 BLE 製品の検査に iOS アプリを使用することが多々あります。
もちろん製品によって確認したい項目や、性能も違う為、製品の数だけアプリを作ることになります...

iOS アプリを配布するということは、 Provisioning Profile 作成/更新、アプリや証明書の有効期限、端末登録など管理することが多く大変です。
これからアプリがどんどん増えていくということは見えていますので、何も用意しなければとんでもない事になってしまうことが予想できます。

この課題は早めに解決しなくてはと思い 『 検査アプリ配布の自動化 』 をできるようにしました。
( 検査アプリ作成の自動化 も鋭意作成中です。 )

今回の構成では、 GitHub の GUI で、タグを作成するとワークフローが動作し、 Firebase App Distribution に アップロードされ、 検査端末でアプリがダウンロードできるような仕組みになっています。
 

自動化の構成図


選定サービス


自動化は業務の効率化に必須だと思っています。
ただ、自動化に振り回されてはいけないということは常に考えているので、なるべく小さく構成できるようにしています。
 


CI / CD GitHub Actions
ビルド fastlane
デプロイ先 Firebase App Distributon
通知 teams


iOS アプリの自動化といえば、 Bitrise や CircleCI などが、よく聞くサービスだと思います。
ただ、これらのサービスは、macOS でのビルドに関して、無料枠では制限が大きく、今回は選択を見送りました。

そこで、GitHub Actions の Self-hoted runners に目をつけています。
Self-hoted runners を利用すれば、使用していない Mac は必要にはなりますが、無料枠の制限を気にすることなく、ビルドと配布を行えます。

fastlane は、モバイルアプリビルドの自動化ツールです。
なんでも出来すぎるので、やりすぎないように注意しなければいけないところが難しいところです。

Firebase App Distributon は Firebase の機能の一つで、モバイルアプリの配布が出来るサービスです。
ただ現在、beta という表記のため、今後のことを考えると少し不安ではあります。
 


自動化の流れ


実質、アプリ配布に必要なユーザーの作業は、 Git Push と タグ作成のみです。

ビルドに必要な firebase_token 等の環境変数設定や、証明書管理は、管理者(私)が手動で行っています。
この辺りも自動化できるところはコツコツやっていく予定です。
 


導入前に 〜 ① 自動化ファイルをリポジトリに追加する


管理者は、最終的に以下のファイルをリポジトリに追加していきます。
 



 - /.github/workflows/my-workflow.yml
 - /fastlane 
  - Appfile
  - Fastfile
  - Matchfile
  - Pluginfile
  - README.md
  - report.xml
 - Gemfile
 - /{repo_name}/GoogleService-info.plist



必要な作業
 - ".bundle/" を .gitignore に追記。
 - match で作成された Procisioning Profile を設定。
 


導入前に 〜 ② fastlane match 用の準備


必要なものは、
 - Provisioning Profile、開発者証明書/秘密鍵 をアップロードしておく用の private repository
 - 証明書を扱うことのできる 管理者用 AppleID
 - (Provisioning Profileが作成できる開発者証明書/秘密鍵) ※ fastlane match の自動作成で成功する場合は必要なし

設定時の話ですが、もしも証明書でエラーになってしまう場合は、 Distribution鍵作成の上限になっている(最大 3個)になっていることが考えられます。
その為、既存の AppleDeveloper にて証明書を削除するか、 fastlane match import を使って、既存の開発者証明書/秘密鍵 を private repository にセットします。



 # 証明書作成不可のエラーコード 
 Could not create another certificate, reached the maximum number of available certificates. 



導入方法 〜 fastlane


まずは、 fastlane の設定です。
ポイントは、 以下の 3点です。

 - rbenv に切り替えておく
 - bundler でバージョンを管理する
 - fastlane match が動作するように、開発用 Mac に、開発者証明書/秘密鍵をインストールする
※ match を利用したくない場合は、 CI/CD用の Mac に直接 Provisioning Profile と 開発者証明書/秘密鍵 をインストールすれば良いと思います。


rbenv は、 ruby が予期せぬ動作をしないように導入しておきます。
 


# rbenv のインストール
$ brew install rbenv ruby-build

# インストール可能なリスト
$ rbenv install -l

# 最新の安定版を指定して、 インストール
$ rbenv install 3.0.1

# 使用するバージョンを指定
$ rbenv global 3.0.1

# rbenv が動作するように記述を追加
$ touch ~/.bash_profile
$ echo '# rbenv' >> ~/.bash_profile
$ echo 'export PATH=~/.rbenv/bin:$PATH' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ touch ~/.bashrc
$ source ~/.bash_profile



次からの作業は、証明書を作成/管理したい mac だけで良いです。
 
認証情報を管理する必要があるので、これからの作業は home ディレクトリでやると良いと思います。
(プロジェクトごとに fastlane 設定しても良いが、ログイン認証が都度になってしまうため。)
 
fastlane match を利用する為、  bundler と fastlane をインストールします。
 



# home
$ cd ~

# bundler install
$ sudo gem install bundler

# fastlane install
$ sudo gem install fastlane --verbose



fastlane match をセットアップします。
 



# fastlane match 有効化
$ fastlane match init



{user}/.fastlane/Matchfile が作成されているかと思いますので、
これを以下のように編集します。
 



git_url("{ 証明書アップロード用の GitHub リポジトリ }")
username("{ 証明書作成権限のある AppleID }")
storage_mode("git")
type("adhoc") 
team_id("{ developer_team_id }")
team_name("{ developer_team_name }")



そして証明書作成コマンドを打っていきます。
 



# apple developer appid regiter
$ fastlane produce -a { app_identifier }

# apple developer provisioningProfile regiter
$ fastlane match adhoc

# app_identifier を聞かれるので、
# -> { app_identifier }



初めの起動時には、 GitHub と証明書の作成の為にログイン認証が入ると思います。
設定に問題がなければ、 { match - Adhoc - { app_identifer } } の名前の Provisioning Profile が作成され、 設定を行ったリポジトリにアップロードされています。

作成した Provisioning Profile はプロジェクトに 『 manual sign 』 にしてセットしてから、 GitHub に Push しておきましょう。
 


導入方法 〜 Firebase App Distribution


最後に Firebase の設定です。
ポイントは、 以下の 3点です。

 - GoogleService-info.plist をプロジェクトに導入する ( pod install は特に必要なし )
 - プロジェクトが一つでも、複数の AppID を取得できるので、プロジェクトは一つでも良い
 - firebase login:ci でトークンを出力する
 


前提として、 Firebase のアカウントは作成済ということにさせてください。
 


はじめに、 配布用のプロジェクトを作成し、 iOS アプリを Firebase に登録してください。
続いて、ダウンロードした GoogleService-info.plist をリポジトリに追加します。
 


追加した iOS アプリの App Distribution を有効化します。
リリースとモニタリング -> App Distribution を選択し、 開始を押してください。
 


有効化した後、 AppID を探しにいきます。
プロジェクト概要の右のギアアイコン -> プロジェクトを設定の中に、マイアプリ情報があります。

そこに記述されている アプリID が今回必要な FIREBASE_APPID になります。 
 


最後に Firebase に自動でログインできるように、認証トークンを発行しておきます。
 



$ firebase login:ci



自動でブラウザが開かれ、認証が行われると思います。
認証後、発行されたトークンがターミナルに表示されるので、リポジトリの FIREBASE_TOKEN へセットします。
 


導入方法 〜 GitHub Actions (Self-hosted runners)


次に、GitHub Actions の設定です。
ポイントは、 以下の2点です。

 - repository ではなく、 organization にて Self-hosted runners のマシンを設定する。(チームの場合)
 - secret を、repository ごとに設定する。
 


こちらも、CI/CD 用の MAC で操作していきます。

GitHub -> Organization -> Setting -> Actions と選択し、 Actions permissions の画面を表示します。
下部に、 Add New の緑のボタンがあるので選択し、 画面に表示された指示通り、 macOS と GitHub Actions を紐付けます。
 


表記通りに進めれば、 『 Listening for Jobs 』 と表示されているはずです。
この状態でリポジトリを監視している状態となります。

監視状態は、mac をシャットダウン、 スリープになるまで動作します。
コマンドの control + C でも止めることが可能です。
 


次に secret を repository に追加します。
 


New repository secret から下記の様な、 secret をリポジトリごとに設定します。
 

APP_NAME scheme 名 & xcodeproj 名を設定
FIREBASE_APPID Firebase App Distribution から得られるアプリ用のID
FIREBASE_TOKEN firebase login:ci から得られる認証トークン
TEAMS_UPLOAD_URL teams 通知用の URL


GitHub GUI での設定項目は以上になります。

Self-hosted の設定は、各リポジトリに、マシンを紐づける場合は、毎回行う必要がある上に、
ターミナルを複数起動しておかなければならなくなってしまいます。
 
そのため、可能であれば、 organization で設定できると楽です。
 


実装ワークフローファイル


実際に使用したワークフローファイルを紹介いたします。

ワークフローの内容のポイントとしては、
 

  • タグを切った時にワークフローが動作するようにしている、 今回は ' FactoryDevelop_v* ' でタグを作成した時、 ワークフローが動作するようになっている

on:
  push:
    tags:
      - 'FactoryDevelop_v*'


  • ワークフロー起動 OS を Self-hosted runners に指定している (指定しないと、 容量が自動で使用されます...)

jobs: 
  build: 
    runs-on: [self-hosted, macos, x64, iOS] 


  •  cocoapods などを version 管理できるように bundle でインストールしている


    - name: bundle Install
      run: bundle install --path .bundle

    - name: pod Install
      if: steps.cache-cocoapods.outputs.cache-hit != 'true'
      run: bundle exec pod install
     
    


こちらがワークフローファイルコードの全体です。
 


name: Deploy to Firebase App Distribution

on:
  push:
    tags:
      - 'FactoryDevelop_v*'

jobs:
  build:
    runs-on: [self-hosted, macos, x64, iOS]

    steps:
    - uses: actions/checkout@v1
    - uses: actions/setup-node@v1
      with:
        node-version: '10.x'
    
    - name: show Xcode version
      run: xcodebuild -version

    - name: before cache
      run: |
        mkdir -p ~/pods/mod
        ls ~/pods
      
    - uses: actions/cache@v1
      id: cache-pods
      with:
        path: ~/pods/mod
        key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
        restore-keys: |
          ${{ runner.os }}-pods-
    
    - name: npm Iinstall
      run: npm install -g firebase-tools

    - name: bundle Install
      run: bundle install --path .bundle

    - name: pod Install
      if: steps.cache-cocoapods.outputs.cache-hit != 'true'
      run: bundle exec pod install
     
    - name: adhoc
      env:
        FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
        TEAMS_URL: ${{ secrets.TEAMS_UPLOAD_URL }}
        FIREBASE_APPID: ${{ secrets.FIREBASE_APPID }}
        APP_NAME: ${{ secrets.APP_NAME }}
      run: bundle exec fastlane upfirebase


fastlane の Fastfile も中身を書きすぎないように注意しています。

動作としては、version 取得 → Provisioning Profile 取得 → ビルド → firebase deploy → teams 通知 となっています。 

下の Fastfile ではテストの部分、ビルド番号を増分する箇所などは省いています。
 



default_platform(:ios)

platform :ios do

    lane :upfirebase do 
    app_version = get_version_number(xcodeproj: ENV["APP_NAME"] + ".xcodeproj", target: ENV["APP_NAME"])
    display_name = get_info_plist_value(path: ENV["APP_NAME"] + "/Info.plist", key: "CFBundleDisplayName")
    
    match(
        type: "adhoc", 
        readonly: is_ci
    )

    gym(
        scheme: ENV["APP_NAME"],
        configuration: "Debug",
        export_method: "ad-hoc"
    )
    
    firebase_app_distribution(
         app: ENV["FIREBASE_APPID"],
         testers: "[testuser@email]",
         release_notes: "test release",
         firebase_cli_path: `which firebase`.strip()
    )

    teams_url = ENV["TEAMS_URL"]
    teams(
        title: "GitHub Actions 終了",
        message: "アプリのアップロードが完了しました。",
        facts:[
            {
                "name"=>"Message",
                "value"=>"#{display_name}_v#{app_version} の配布が開始されました。"
            },
            {
                "name"=>"URL",
                "value"=>"https://appdistribution.firebase.google.com/testerapps"
            }
        ],
        teams_url: teams_url
    )
    end
  
  error do |lane, exception|
  end

end



実装してみて


やはり、アプリの作成から配布までのラグが大きく短縮されるのが良いです。

アプリをビルドする必要が出てくるとどうしても時間を取られがちです。
こういった自動化を組んで、ミスを少なく、確実に作業できる環境を整えていくのも大切な作業ですよね。
 


さいごに


世の中のサービスもたくさん増えてきて、選択するだけでも苦労することがあると思います。

そんな中でも、情報を発信してくれる先駆者の方々のおかげでとても助かっています。

この記事も、アプリ配布を自動化しようと思ってるけど...と困っている様な方々に届けば嬉しいです。

それでは次の更新まで ノシ
 

参考


 ・ Documents
  - fastlane documents
  - Firebase documents
  - GitHub Actions documents

 ・ blog
  - rbenv 導入 ( Qiita )

 ・ 図作成
  - diaframs.net
 

SNS SHARE