GitHub Actions+Firebase App DistributionでiOSアプリをAd hoc配布するための構成例

この記事は[Ateam Brides Inc. Advent Calendar 2019](https://qiita.com/advent-calendar/2019/a-t-brides)　13日目の記事です。

本日は最近 Firebase によるサーバーレス構成に心酔中の @okoshi が担当いたします。
今年10月にローンチされたFirebase App Distributionと、11月に正式ローンチされたGitHub Actionsを取り上げてみたいと思います。

# 概要

GitHub ActionsでFirebaseを使用したiOSアプリのCDを実現するための構成例です。

この構成例のFastlaneでは、ビルドしたiOSアプリをFirebase App Distributionで配布してChatworkに通知を行います。

証明書とプロビジョニングファイルは、環境変数にBASE64エンコードしてあるものをデコードして使用します。

個人的な理由ですが、証明書がリニューアルされるのは困るので、fastlane matchを使わない方法にしました。

# 環境変数の準備

環境変数が多くなってしまいましたが、勘違いしないようにお願いしたいのは、GitHub Actionsを使用するのにこれだけの環境変数が必要というわけではありません。Fastlaneの設定ファイルやワークフローファイルに必要な設定の変数を切り出して使い回せるようにしたところ、これだけの環境変数が必要になってしまいました。

環境変数はGitHubのSettingsから設定します。

「Add a new secret」を押して以下のように環境変数を追加してください。

一度設定した環境変数の値は見ることができません。更新するときは、Add a new secretのName覧を同じ名前で登録するか、Removeボタンを押してから新しく環境変数を追加してください。

![ss1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64294/dd479d88-f12d-4220-852c-cd918b91d7f5.png)

ここに追加した環境変数をワークフローファイル(yamlファイル)から参照するには、secretsオブジェクトを経由します。例えば、SCHEME_NAMEの値を参照するならsecrets.SCHEME_NAMEとなります。

## SCHEME_NAME

スキーム名を指定します。Scheme名はXcodeのメインメニューから「Project」-「Scheme」から確認できます。多くのケースではプロジェクト名と同じかと思いますが、ワークスペースでプロジェクトを複数持っている場合は適切なものを選択してください。

![ss5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64294/190391ca-6376-6766-97a9-7b887b6bceac.png)

※Xcode11の場合です。

## PROJECT_NAME

プロジェクト名を指定します。

![ss6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64294/a61731f1-1c59-fdac-d116-a3340925fd3d.png)

## BUNDLE_ID

バンドルIDを指定します。

![ss7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64294/c5d0c291-65f6-1015-4b9e-57063e9a414b.png)

## PLIST_PATH

Info.plistのパスを指定します。
プロジェクト名がMyAppであれば、多くの場合以下になります。

```
MyApp/Info.plist
```

debugビルド用(Info-debug.plist等)とreleaseビルド用(Info-release.plist等)に分けている方は、Info-debug.plistとかに書き換えてください。

## CERTIFICATES

BASE64テキスト化された証明書を指定します。証明書はバイナリなのでBASE64でテキスト化したものにします。

後述のワークフローファイル内で、BASE64をデコードして証明書を復元します。

最初にキーチェーンの分類から「自分の証明書」を選択して「Apple Distribution: XXXXX」を書き出します。配布するのでApple Distribution:で始まる証明書になります。Apple Development:で始まる証明書では配布ができません。

![ss8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64294/91804541-b02a-4e8b-d69e-acdb15db756a.png)

書き出すフォーマットは「個人情報交換 (.p12)」です。

書き出すときにファイル名をつけますが、このファイル名はなんでもかまいません。なぜなら中身をBASE64化するためファイル名は関係ないためです。

書き出したファイルが仮に「証明書.p12」だったとすると以下のコマンドで証明書をBASE64テキストにできます。

```bash
$ cat 証明書.p12 | base64 | pbcopy
```

非常に長いテキストファイルになるため、コピーが大変だと思うのでpbcopyコマンドを併用してクリップボードにコピーしています。

## PROVISIONING_PROFILES

Apple DeveloperからダウンロードしたプロビジョニングファイルをBASE64テキスト化したものを指定します。証明書はバイナリなのでBASE64でテキスト化したものにします。
Ad hoc配布するので、このプロビジョニングファイルのTypeは当然Ad hocである必要があります。これを間違えるとFastlaneの実行でエラーになります。

後述のワークフローファイル内で、BASE64をデコードして証明書を復元します。

ダウンロードしたプロビジョニングファイルの拡張子はmobileprovisionになっています。

例えばjpcomyhostMyApp.mobileprovisionというファイル名であった場合は、以下のコマンドで証明書をBASE64テキストにできます。

```bash
$ cat jpcomyhostMyApp.mobileprovision | base64 | pbcopy
```

非常に長いテキストファイルになるため、コピーが大変だと思うのでpbcopyコマンドを併用してクリップボードにコピーしています。

## PROVISIONING_PROFILE_SPECIFIER

プロビジョニングファイルのプロファイル名を指定します。
Apple Developerでプロビジョニングファイルを作成したときにつけた名前になります。
Xcodeに取り込むと、赤枠の部分に表示されます。

![ss11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64294/b9eaf894-91d0-ee5a-0069-6b27552f7d1e.png)

## PROVISIONING_PROFILE_NAME

ダウンロードしたプロビジョニングファイルをXcodeに取り込んだときディレクトリー「~/Library/MobileDevice/Provisioning Profiles/」にできたファイルのファイル名から拡張子を取り除いたものを指定します。

同ディレクトリーにあるプロビジョニングファイルのファイル名のほとんどはランダムな英数字がハイフンで繋がれたものになっています。

<img width="431" alt="ss9.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64294/9d142ec7-b78a-ef2b-fa6e-91c61799503c.png">

どれが取り込んだファイルなのかわかりづらいため、生成された時間で判断するか、あるいは一旦すべてのプロビジョニングファイルを削除してXcodeにプロビジョニングファイルを取り込み直すという作業が必要です。

※Xcodeを起動した状態でプロビジョニングファイルを削除するとXcodeが落ちる可能性があります。
※Webでこの辺りの記事を見ると、この作業をやっていないものが多いので、以前は不要だったのかもしれません。

## CODE_SIGN_IDENTITY

証明書の識別子を指定します。

キーチェーンに表示されていたものと同じ内容にします。

![ss10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64294/65b21cb3-6c9f-15f1-8c8f-52d66090a9ad.png)

## CHATWORK_API_TOKEN

ChatworkのAPI設定からAPI Tokenを取得して指定します。

![ss4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64294/d412aa26-e12d-1d45-427d-ff42d166fb8c.png)

## CHATWORK_ROOM_ID

ChatworkのルームIDを指定します。投稿先のチャットワークルームを開き、URLから取得しましょう。

URLの#!ridに続く数字の部分です。

![ss2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64294/7655199a-8941-0ce3-1d3f-f54aa9fd331d.png)

## FIREBASE_TOKEN

Firebaseのログインするためのトークンを指定します。

Firebaseの機能を使うにはログインが必要です。当然GitHub Actionsでビルドする環境でもログインが必要です。しかし、自動で実行したいのに対話型で入力を促されていては、自動で実行することができません。そのためFirebaseにはCIツールからログインするためのトークンを発効してくれる機能があります。トークンの発効はローカルで実行します。

```bash
firebase login:ci
```

Webブラウザーが開くため、ログインするアカウントを選択すると、ターミナルにトークンが表示されます。

```txt
$ firebase login:ci

Visit this URL on this device to log in:
https://accounts.google.com/o/oauth2/auth?client_id=123456789012-aaaabbbbccccddddeeeeffffgggghhhh.apps.googleusercontent.com&scope=email%20openid%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloudplatformprojects.readonly%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Ffirebase%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&response_type=code&state=123456789&redirect_uri=http%3A%2F%2Flocalhost%3A9005

Waiting for authentication...

✔  Success! Use this token to login on a CI server:

1//xxxx_yyyy000_XXXXYYYYZZZZaaa-XXXXbb-ZZZZYYYYXXXX00001111222233_aaaabbbbccccddddeeeeffffgggghhhhiiiij

Example: firebase deploy --token "$FIREBASE_TOKEN"
```

Success! Use this token to login on a CI server:と書かれた部分の次の行にでてくる文字列がトークンです。

表示されたトークンをGitHubに指定します。

## FIREBASE_TESTERS

配信先のメールアドレスを指定します。複数ある場合はカンマ区切りで指定します。

このメールアドレスでメールを受け取り、アプリをインストールするiOSデバイスは、Apple Developerにデバイス登録されたものでなければなりません。

```:例
foo@mydomain.co.jp,bar@mydomain.co.jp,foobar@mydomain.co.jp
```

## FIREBASE_APP_ID

Firebase上で管理されているiOSアプリのアプリIDを指定します。

FirebaseコンソールのProject Overviewの右側にある歯車アイコンから、「プロジェクトの設定」を開きます。

「全般」タブの「マイアプリ」セクションにアプリIDがあります。

![ss3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64294/96031ada-16e4-55a2-73a6-2054a54b92ed.png)

# ワークフローファイル

```yaml:(プロジェクトルート)/.github/workflows/swift.yml
name: Swift

on:
  push:
    branches:
      - deploy

jobs:
  build:
    runs-on: macOS-latest

    steps:
    - uses: actions/checkout@v1
    - name: ブランチ名を取得
      id: extract_branch
      run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
    - name: Rubyのバージョンを表示
      run: ruby -v
    - name: 環境変数をBASE64デコードして証明書を復元
      env:
        CERTIFICATES: ${{ secrets.CERTIFICATES }}
      run: echo $CERTIFICATES | base64 --decode > apple-distribution-mycompany-inc.p12 && echo $PROVISIONING_PROFILES | base64 --decode
    - name: プロビジョニングプロファイルを格納するディレクトリーを作成
      run: mkdir -pv ~/Library/MobileDevice/Provisioning\ Profiles/
    - name: 環境変数をBASE64デコードしてプロビジョニングプロファイルを復元
      env:
        PROVISIONING_PROFILES: ${{ secrets.PROVISIONING_PROFILES }}
      run: echo $PROVISIONING_PROFILES | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/${{ secrets.PROVISIONING_PROFILE_SPECIFIER }}.mobileprovision
    - name: Bundlerをアップデート
      run: gem update bundler && bundle -v && which bundle
    - uses: actions/cache@v1
      with:
        path: vendor/bundle
        key: ${{ runner.os }}-vendor/bundle-${{ hashFiles('**/Gemfile.lock') }}
        restore-keys: |
          ${{ runner.os }}-vendor/bundle-
    - name: Bundleをインストール
      env:
        BUNDLE_JOBS: 4
        BUNDLE_RETRY: 3
      run: bundle check --path vendor/bundle || bundle install --path vendor/bundle
    - name: Firebase Toolsをインストール
      run: npm i firebase-tools
    - uses: actions/cache@v1
      with:
        path: Pods
        key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
        restore-keys: |
          ${{ runner.os }}-pods-
    - name: Fastlaneのセットアップ、Cocoapodsをインストール
      env:
        BUNDLE_JOBS: 4
        BUNDLE_RETRY: 3
      run: bundle exec fastlane ios setup

    - name: Fastlaneを実行
      env:
        CIRCLE_BRANCH: ${{ steps.extract_branch.outputs.branch }}
        FASTLANE_LANE: fad
        FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
        SCHEME_NAME: ${{ secrets.SCHEME_NAME }}
        PROJECT_NAME: ${{ secrets.PROJECT_NAME }}
        PLIST_PATH: ${{ secrets.PLIST_PATH }}
        BUNDLE_ID: ${{ secrets.BUNDLE_ID }}
        PROVISIONING_PROFILE_SPECIFIER: ${{ secrets.PROVISIONING_PROFILE_SPECIFIER }}
        PROVISIONING_PROFILE_NAME: ${{ secrets.PROVISIONING_PROFILE_NAME }}
        CODE_SIGN_IDENTITY: ${{ secrets.CODE_SIGN_IDENTITY }}
        FIREBASE_APP_ID: ${{ secrets.FIREBASE_APP_ID }}
        FIREBASE_TESTERS: ${{ secrets.FIREBASE_TESTERS }}
        CHATWORK_API_TOKEN: ${{ secrets.CHATWORK_API_TOKEN }}
        CHATWORK_ROOM_ID: ${{ secrets.CHATWORK_ROOM_ID }}
      run: bundle exec fastlane ios $FASTLANE_LANE
```

# Fastlaneファイル

CircleCIでも動作するようにしてあります。

```ruby:(プロジェクトルート)/fastlane/Fastfile
fastlane_version "2.134.0"

default_platform(:ios)

platform :ios do
  before_all do
    ENV["FL_OUTPUT_DIR"] = 'temp'

    clear_derived_data(derived_data_path: "./DerivedData")
    sh "rm -rf ../build"
  end

  desc "Runs setup"
  lane :setup do
    unless File.exist?("../Pods/Manifest.lock") && FileUtils.cmp("../Podfile.lock", "../Pods/Manifest.lock") then
      cocoapods(verbose: true)
    end
  end

  # fadレーン 
  desc "アプリをFirebase App Distributionで配布します"
  lane :fad do
   
    if is_ci?
      setup_circle_ci
      import_certificate(
        certificate_path: "apple-distribution-mycompany-inc.p12",
        certificate_password: "",
        keychain_name: ENV["MATCH_KEYCHAIN_NAME"],
        keychain_password: ENV["MATCH_KEYCHAIN_PASSWORD"]
      )
    end

    automatic_code_signing(
      targets: ENV["PROJECT_NAME"],
      use_automatic_signing: false
    )

    update_app_identifier(
      xcodeproj: ENV["PROJECT_NAME"] + ".xcodeproj",
      plist_path: ENV["PLIST_PATH"],
      app_identifier: ENV["BUNDLE_ID"]
    )

    # アプリをビルドし、ipaファイルを作成する
    gym(
      configuration: "Release",
      derived_data_path: "./DerivedData",
      clean: true,
      verbose: true,
      scheme: ENV["SCHEME_NAME"],
      export_method: "ad-hoc",
      output_directory: "./build/ipa/" + Time.new.strftime("%Y/%m/%d/%H%M"),
      output_name: ENV["PROJECT_NAME"] + ".ipa",
      include_bitcode: false,
      xcargs: "OTHER_SWIFT_FLAGS='$(inherited) -DSTGING' PROVISIONING_PROFILE='" + ENV["PROVISIONING_PROFILE_NAME"] + "' PROVISIONING_PROFILE_SPECIFIER='" + ENV["PROVISIONING_PROFILE_SPECIFIER"] + "' CODE_SIGN_IDENTITY='" + ENV["CODE_SIGN_IDENTITY"] + "'",
      export_xcargs: "-allowProvisioningUpdates",
      export_options: {
        method: "ad-hoc",
        compileBitcode: false,
        uploadBitcode: false,
        provisioningProfiles: {
          ENV["BUNDLE_ID"] => ENV["PROVISIONING_PROFILE_SPECIFIER"]
        }
      }
    )
 
    # Firebase App Distribution にアプリをアップロードする
    firebase_app_distribution(
      app: ENV["FIREBASE_APP_ID"],
      testers: ENV["FIREBASE_TESTERS"],
      release_notes: "Firebase App Distributionからの配信 " + Time.new.strftime("%Y/%m/%d %H:%M") + "版リリース(" + ENV["CIRCLE_BRANCH"] + ")",
      firebase_cli_path: "./node_modules/.bin/firebase"
    )
 
    # 処理完了メッセージをChatworkに投稿する
    chatwork(
      message: "PROJECTNAME(" + ENV["CIRCLE_BRANCH"] + ")をFirebase App Distributionにデプロイしました。",
      roomid: ENV["CHATWORK_ROOM_ID"],
      success: true,
      api_token: ENV["CHATWORK_API_TOKEN"]
    )
  end
end
```

# Fastfileをデバッグする

Fastlaneの設定はローカルでも動かせるようにしてあります。

```bash
fastlane ios fad
```

dotenvを導入していつでもすぐに実行できるようにするのもありですね。
※XXXXXXXXXXXXXXXXXXの部分はGitHubのSettingsのSecretsに指定した値と同じものでOKです。
.envファイルを誤ってgitの管理化に置かないようにしましょう。GitHubにPUSHしてしまったら、なんのためにGitHubのSettingsのSecretsに設定したのかってことになりますよね。

```:.env
SCHEME_NAME=XXXXXXXXXXXXXXXXXX
PROJECT_NAME=XXXXXXXXXXXXXXXXXX
BUNDLE_ID=XXXXXXXXXXXXXXXXXX
PLIST_PATH=XXXXXXXXXXXXXXXXXX
CERTIFICATES=XXXXXXXXXXXXXXXXXX
PROVISIONING_PROFILES=XXXXXXXXXXXXXXXXXX
PROVISIONING_PROFILE_SPECIFIER=XXXXXXXXXXXXXXXXXX
PROVISIONING_PROFILE_NAME=XXXXXXXXXXXXXXXXXX
CODE_SIGN_IDENTITY=XXXXXXXXXXXXXXXXXX
CHATWORK_API_TOKEN=XXXXXXXXXXXXXXXXXX
CHATWORK_ROOM_ID=XXXXXXXXXXXXXXXXXX
FIREBASE_TOKEN=XXXXXXXXXXXXXXXXXX
FIREBASE_TESTERS=XXXXXXXXXXXXXXXXXX
FIREBASE_APP_ID=XXXXXXXXXXXXXXXXXX
```

dotenvを使用した起動方法はこちら。

```bash
dotenv fastlane ios fad
```

# 注意

## iOSアプリのビルドでは1分=10分

プライベートリポジトリーで開発している方に限った話になります。
GitHub Actionsはプランによって無料枠が決まっていますが、使用する環境によって1分の単位が異なります。(1分の単位が異なるという表現は分という単位を使用しているのに変ですが。)

- Linuxのケース：現実での1分＝GitHub Actionsでの1分 
- macOSのケース：現実での1分=GitHub Actionsでの10分
- Windowsのケース：現実での1分=GitHub Actionsでの2分

Teamプランですと無料枠は10000分ですが、macOSを使うケースでは現実での1000分ということになります。
これを知った上で使用しないと、Teamプランで「わーい10000分もある！ビルドしまくるぜ！」->「あれ、気づかなかったけど、今月はよく働いたな」ってなります。

参考：https://help.github.com/ja/github/setting-up-and-managing-billing-and-payments-on-github/about-billing-for-github-actions

## 新しいサービスなので不具合があるかも

GitHub Actionsは2019年11月にリリースされたばかりの機能です。無料枠は月に一度リセットされます。私が使用していたときは、なぜかリセットされませんでした。そこでGitHubに問い合わせたらリセットされたため、不具合だった模様です。

同じような症状の方はあきらめないで問い合わせましょう。

## デプロイに使うブランチにmasterは避けましょう

TestFlightにアップロードするようにした場合や、管理のためにバージョン番号やビルド番号の更新をFastlane内で自動でやりたい場合がでてきます。

そんなときは、increment_build_numberやincrement_version_numberを使用いただければと思いますが、変更したらgitにpushしないとバージョン番号やビルド番号が増えていきません。もちろんgitにcommitしてpushする方法もあって、commit_version_bumpやpush_to_git_remoteを使えばいいのですが、pushする先のブランチはデフォルトがmasterです。masterブランチに対して実行してしまうと、masterにpush->GitHub Actions反応->ビルド番号変更->masterにpush->GitHub Actions反応...という感じで、永久ループに陥り、すぐに無料枠を使い切ります。

導入当初は覚えているかもしれませんが、しばらくしたら忘れていると思います。やらかさないようにご注意ください。

## プロビジョニングファイルはAutomatically manage signingのものは使えない

Apple Developerでプロビジョニングファイルを作るのが面倒だからAutomatically manage signingで作られたプロビジョニングファイルを使おうと思った方もいらっしゃるかもしれませんが、ビルドの途中でエラーが発生するのでManualでサインしましょう。

## 定期的に証明書とプロビジョニングファイルの更新が必要

概要で、「個人的な理由ですが、証明書がリニューアルされるのは困るので、fastlane matchを使わない方法にしました。」と書きましたが、fastlane matchを使った場合は証明書をリニューアルしてくれます。それはそれで便利なんですが、更新前の証明書はREVOKEされるので、企業内で使うとき証明書を共有している場合や、社内のルールで管理台帳による管理がされている場合に困ります。
そういったケースでは、本ページで紹介したやりかたを実践していただけると良いと思いますが、デメリットがあります。証明書やプロビジョニングファイルは期限があるので、期限がくる前に更新しなければなりません。
GitHubのSettingsのSecretsが有効期限切れにならないようにご注意ください。

fastlane matchの設定例はWeb検索するとたくさんでてくるので、うちは別に問題無いよって方は検索してみてください。

# 終わりに

いかがでしたでしょうか。GitHub Actionsは１年間のベータ期間があったとはいえ、正式リリースから間もないサービスですし、Firebase App Distributionもリリースされたばかりのサービスなので、ちょっと様子見の方もいらっしゃるかもしれません。
本ページの内容で使用感を確かめていただくなんて使い方をしてもらえるとうれしく思います。

# 私たちのチームで働きませんか？
![alt](https://brides.a-tm.co.jp/wp-content/uploads/2019/11/ATEAM_BRIDES.jpg)
[エイチーム](https://www.a-tm.co.jp/)は、インターネットを使った多様な技術を駆使し、幅広いビジネスの領域に挑戦し続ける**名古屋の総合IT企業**です。
そのグループ会社である**株式会社エイチームブライズ**では、一緒に働く仲間を募集しています！

- [ハナユメ サイト開発グループ](https://jobs.qiita.com/employers/brides-a-tm/development_teams/138)
- [HIMARI開発チーム](https://jobs.qiita.com/employers/brides-a-tm/development_teams/147)
- [技術開発部](https://jobs.qiita.com/employers/brides-a-tm/development_teams/148)

上記求人をご覧いただき、少しでも興味を持っていただけた方は、まずは**チャットでざっくばらんに話**をしましょう。
技術的な話だけでなく、私たちが大切にしていることや、お任せしたいお仕事についてなどを詳しくお伝えいたします！

[Qiita Jobs](https://jobs.qiita.com/)よりメッセージお待ちしております！