Github ActionsでiOSアプリのデプロイを自動化する

元ラーメン屋店長プログラマのObjective-ひろC([@hirothings](https://twitter.com/hirothings))です🍜
GitHub Actionsで[個人アプリ](https://apps.apple.com/jp/app/id1460380962)のデプロイを自動化したのでその手順を書きます。

目次

1. GitHub Actionsとは？
2. なぜGitHub Actionsをデプロイ環境に選んだか？
2. 実装の流れ

## GitHub Actionsとは？
「GitHub上で動作するサーバレス実行環境」で、GitHub関連のイベントをトリガーにCI/CD様々な処理を実行できます。
[中の人がGitHubの概要について説明しているわかりやすい記事](https://codezine.jp/article/detail/11450)があるので、それを読むのがオススメです。
以下、抜粋。

>
* Actionの実体はDockerコンテナ
* Actionを組み合わせて独自のワークフローを構築できる
* コンテナ実行環境がコンパクトで重い処理の実行には向いていない

### 特徴
[GitHub Marketplace](https://github.com/marketplace?type=actions)に、actionがたくさんあり、自分のワークフローの一部に使用可能です。([詳しくはここ](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/using-actions-from-github-marketplace-in-your-workflow))  

GitHubの各種操作に対するイベントのサポートが他CIより手厚く、様々なトリガーをもとに処理を実行できます。

* PR
  * assigned
  * opened
  * closed .. etc
* push
* release
* project
* cron

..etc([詳しくはここ](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows))

## なぜGitHub Actionsをデプロイ環境に選んだか？
BitriseもCircle CIもiOSの個人開発で利用しようとすると有料プランの利用が必須になってきます。

* Bitrise
    * ビルド時間の制約が10min以内
* Circle CI
    * freeプランにmacOSの実行環境がない

GitHub ActionsはOSSだと利用が無料。Privateリポジトリは従量課金制ですが、1か月あたり2,000分(Proなら3,000分)まで無料という緩めの制約で、デプロイに利用するくらいであれば無料枠で利用できます。

**そのため個人アプリ開発者のお財布事情に優しく、ヘビーユースしなければ無料でCI/CD実行環境が手に入ります:rocket:**

:warning: 追記: macOSで処理を実行する場合、実行可能時間が10倍の速度で消費されます。つまり最短で200分/月となります。([公式ドキュメントより](https://help.github.com/en/github/setting-up-and-managing-billing-and-payments-on-github/about-billing-for-github-actions#about-billing-for-github-actions))
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30423/3946317a-6b05-5867-5659-fe2069cc8257.png)


## サポートされている環境
iOSのビルド周りに必要な、主要なツールはプリインストールされています。
Xcodeに関してはBitriseは19/12/28現在、12/3に出た11.3がサポートされているので、アプリに特化したCIサービスに比べると少しサポート遅めです。

* macOS 10.15(Catalina)
* Xcode11.2.1(19/12/28現在)
* fastlane
* iOS simulator (19/12/28現在: 13系のみ)
* Bundler
* Carthage
* CocoaPods
* Homebrew

GitHub Actionsにインストールされているソフトウェアは[公式ページで確認できます。](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/software-installed-on-github-hosted-runners)

## GitHub Actionsの構成
### ディレクトリ構造
リポジトリの`.github/workflows/`をrootに複数のワークフロー(yamlで定義)がぶら下がる階層構造になります。

```
|-- hello-world (repository)
|   |__ .github
|       └── workflows
|           └── my-first-workflow.yml
|       └── actions
|           |__ hello-world-action
|               └── action.yml
```

### ワークフロー内の構造
`{some-workflow}.yml`の中に最低1つのjobがあり、複数のstepがぶら下がる構成になります。
[公式ドキュメントより](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/configuring-a-workflow#referencing-an-action-in-the-same-repository-where-a-workflow-file-uses-the-action)

``` yaml
name: CI

on: [push]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v1
    - name: Run a one-line script
      run: echo Hello, world!
    - name: Run a multi-line script
      run: |
        echo Add other actions to build,
        echo test, and deploy your project.
```

jobは並列処理も直列処理も実行可能([詳しくはここ](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/core-concepts-for-github-actions#job))

## Hello Worldまで
Actionsタブをクリックして、右上の`Set up workflow yourself`ボタンをクリックすると、最初のworkflowの設定画面になります。
<img width="1440" alt="01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30423/06037a7f-a796-8f11-4d5b-632fdf2a17ec.png">
ワークフロー名(yamlのファイル名)を指定。(公式の例に合わせてハイフン区切りで命名)
そのままファイルをGitHub上で編集することもできます。(補完なども効く専用のエディタ)
<img width="1440" alt="02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30423/2b11b532-a257-f3f3-731d-08cbf7ff6d40.png">
このまま、コミットすればHello Worldまで実行できます。

## デプロイ自動化の実装の方針
GitHub ActionsでiOSのビルド環境を構築するには、fastlaneを使うのがオススメと[中の人が回答している](https://github.community/t5/GitHub-Actions/iOS-Code-Signing-amp-Provisioning/td-p/35187)のでfastlaneで実装します。

## 実装
ReleaseブランチにPRがマージされたら、デプロイのActionを実行する例は下記です。(yaml)
記法については[公式ドキュメント](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/configuring-workflows)を読んでください。
上から読んでいけば何となく何をやっているか分かると思います。最後のstepでfastlaneを実行して実際のデプロイ処理を行っています。分かりづらい箇所を補足します。

``` yaml
name: Deploy to AppStore Connect

on:
  push:
    branches:
      - release

jobs:
  build:

    runs-on: macos-latest

    steps:
    - uses: actions/checkout@v1 # 1: MarketplaceのActionの利用
    - name: Select Xcode
      run: sudo xcode-select -s '/Applications/Xcode_11.2.1.app/Contents/Developer'
    # 2: キャッシュを使う
    - uses: actions/cache@v1
      with:
        path: Pods
        key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
        restore-keys: |
          ${{ runner.os }}-pods-
    - name: Pod Install
      if: steps.cache-cocoapods.outputs.cache-hit != 'true'
      run: pod install
    # 4: fastlaneで必要なため、SSHキーのセットアップをする
    - name: Setup SSH Keys and known_hosts for fastlane match
      # Copied from https://github.com/maddox/actions/blob/master/ssh/entrypoint.sh
      run: |
        SSH_PATH="$HOME/.ssh"

        mkdir -p "$SSH_PATH"
        touch "$SSH_PATH/known_hosts"

        echo "$PRIVATE_KEY" > "$SSH_PATH/id_rsa"

        chmod 700 "$SSH_PATH"
        ssh-keyscan github.com >> ~/.ssh/known_hosts
        chmod 600 "$SSH_PATH/known_hosts"
        chmod 600 "$SSH_PATH/id_rsa"

        eval $(ssh-agent)
        ssh-add "$SSH_PATH/id_rsa"
      env:
        # 3: 秘匿情報の受け渡し
        PRIVATE_KEY: ${{ secrets.GITHUB_ACTIONS_SSH_PRIVATE_KEY }}

    - name: Run Fastlane - Deploy to AppStore Connect
      run: fastlane appstore_release
      env:
        TEAM_ID: ${{ secrets.TEAM_ID }}
        APPLE_ID: ${{ secrets.APPLE_ID }}
        MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
        FASTLANE_USER: ${{ secrets.FASTLANE_USER }}
        FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
```

## 補足

### 1: MarketplaceのActionの利用
リポジトリのCheckoutなど主要な処理はGitHub謹製のActionが[用意されている](https://github.com/marketplace/actions/checkout
)ので、それを使います。
marketplaceで用意されているworkflowを使うときは、 `{owner}/{repo}@{ref}` or `{owner}/{repo}/{path}@{ref}.` の記法で参照します。 ([詳しくはここ](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/configuring-a-workflow#referencing-actions-in-your-workflow))

``` yaml
steps:
    - uses: actions/checkout@v1 
```

### 2: キャッシュの利用
cocoapodsやcarthageのキャッシュが可能なactionを[公式が提供しています](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/caching-dependencies-to-speed-up-workflows)。
`if`キーワードを組み合わせて、キャッシュがない場合のみインストールを実行するよう制御することが可能です。

```yaml
- uses: actions/cache@v1
  with:
    path: Pods
    key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
    restore-keys: |
      ${{ runner.os }}-pods-
- name: Pod Install
  if: steps.cache-cocoapods.outputs.cache-hit != 'true'
  run: pod install
```

### 3: 秘匿情報の受け渡し
秘匿情報は、リポジトリの`settings/Secrets`で指定でき、内部で暗号化され、アクションでのみ使用できます。
ワークフローがforkedリポジトリからトリガーされた場合、GITHUB_TOKEN以外の情報は渡されません。([詳しくはここ](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets))

指定は`settings/Secrets`
<img width="1023" alt="スクリーンショット 2019-12-31 5.42.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30423/d7cf79af-ec6a-89d9-b5e7-02d2872a006d.png">
参照は下記の記法

``` yaml
env:
  PRIVATE_KEY: ${{ secrets.GITHUB_ACTIONS_SSH_PRIVATE_KEY }}
```

### 4: fastlaneで必要なため、SSHキーのセットアップをする
自分のケースだと、fastlaneの`match`を使用しているため、SSHの設定が必要でした。
fastlaneのstepより先のstepでセットする必要があります。

2019.12現在、GitHub Actions公式のSSH設定関連のactionがなく、野良actionはあるのですが、悪用される可能性が0ではないので、scriptを書いて対応しています([参考](https://github.community/t5/GitHub-Actions/iOS-Code-Signing-amp-Provisioning/m-p/36314#M2524))

秘密鍵の登録自体は、`settings/keys`画面でDeploy Key登録しました。(Read権限のみ)

参考: [Circle CI で Github に write access 可能な Deploy key を設定する](https://qiita.com/boushi-bird@github/items/6b6eb1d1ed6f6d3341e4)

## デプロイの実行
デプロイの実行自体は、stepの最後でfastlaneのlaneを叩くことで実行しています。
`TEAM_ID` などfastlaneに必要な秘匿情報は`env` or `with`キーワードを使って環境変数経由で渡します。

``` yaml
- name: Run Fastlane - Deploy to AppStore Connect
  run: fastlane appstore_release
  env:
    TEAM_ID: ${{ secrets.TEAM_ID }}
    APPLE_ID: ${{ secrets.APPLE_ID }}
    MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
    FASTLANE_USER: ${{ secrets.FASTLANE_USER }}
    FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
```

fastlaneの記事ではないので参考程度ですが、こんなlaneを実行してます。

<details><summary>fastfile</summary><div>

``` ruby
default_platform(:ios)

platform :ios do

  desc "Push a new release build to the App Store"
  lane :appstore_release do
    begin
      bump_build_version_with_commit
      build_to_release
      deliver
      post_message_to_slack(message: "デプロイしたゴねー")
    rescue => exception
      post_errormessage_to_slack(message: "デプロイに失敗したゴねー", exception: exception)
      raise exception
    end
  end


  # private

  private_lane :build_to_release do
    create_keychain_for_CI if is_ci
    match(
      type: "appstore",
      readonly: is_ci,
      keychain_name: "KEYCHAIN_NAME",
      keychain_password: ENV["MATCH_PASSWORD"],
    )
    disable_automatic_code_signing(
      use_automatic_signing: false,
      targets: ["PROJECT_NAME"],
      code_sign_identity: "iPhone Distribution",
      team_id: ENV["TEAM_ID"],
      profile_name: "PROFILE_NAME"
    )
    gym(
      workspace: "PROJECT_NAME.xcworkspace",
      configuration: "Release",
      scheme: "SOME_SCHEME",
      export_method: "app-store",
    )
  end

  private_lane :bump_build_version_with_commit do
    increment_build_number(
      xcodeproj: "PROJECT_NAME.xcodeproj"
    )
    git_commit(path: ".", message: "Version Bump")
  end

  private_lane :create_keychain_for_CI do
    create_keychain(
      name: "KEYCHAIN_NAME",
      password: ENV["MATCH_PASSWORD"],
      default_keychain: true,
      unlock: true,
      timeout: 3600,
      lock_when_sleeps: false
    )
  end

  def post_message_to_slack(message:)
    webhook_url = "webhookのURL"
    slack(
      message: message,
      success: true,
      slack_url: webhook_url,
      default_payloads: [:lane, :last_git_commit],
      attachment_properties: {
        fields: [
          {
            title: "Workflow",
            value: ENV["GITHUB_WORKFLOW"]
          },
          {
            title: "URL",
            value: "https://github.com/hirothings/{Repository名}/commit/" + ENV["GITHUB_SHA"] + "/checks"
          },
          {
            title: "AppStore Connect",
            value: "AppStore ConnectのURL"
          }
        ]
      },
    )
  end

  def post_errormessage_to_slack(message:, exception:)
    webhook_url = "webhookのURL"
    slack(
      message: message,
      success: false,
      slack_url: webhook_url,
      default_payloads: [:lane, :last_git_commit],
      attachment_properties: {
        fields: [
          {
            title: "Error message",
            value: exception
          },
          {
            title: "Workflow",
            value: ENV["GITHUB_WORKFLOW"]
          },
          {
            title: "URL",
            value: "https://github.com/hirothings/{Repository名}/commit/" + ENV["GITHUB_SHA"] + "/checks"
          }
        ]
      },
    )
  end

end
```

</div></details>

## 実行!! :rocket:
<img width="1027" alt="04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30423/df10314e-1d9d-db00-853b-0a95ba20e30b.png">
<img width="630" alt="05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30423/c69cabf9-575e-aa44-5a2d-5a174ca8bc96.png">

slackへの投稿もfastlaneで行っています。
ワークフロー名やCommit SHAは、GitHub Actionsのデフォルトの環境変数として利用可能です。
(デフォルトで利用できる環境変数について[詳しくはここ](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/using-environment-variables))

実行したActionのURLは下記のパターンで生成されるので、
`https://github.com/{ユーザー or 組織}/{リポジトリ}/commit/{Commit SHA}/checks`
GitHub Actionsのデフォルトの環境変数`GITHUB_SHA`を使えば、slackなど実行結果にリンクを貼ることが可能です。

## 無料枠の制限を超えそうになると？
Actionの実装でデバッグを繰り返したところ、1ヶ月の無料枠の制限を超えそうになってしまいました。無料枠の75%を超えたタイミングでGitHubからメールが来ました。
支出の制限の変更はユーザーだとhttps://github.com/settings/billing から指定できるので必要に応じて設定しましょう。[詳しくはここ](https://help.github.com/github/setting-up-and-managing-billing-and-payments-on-github/managing-your-spending-limit-for-github-actions)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30423/9812b9ea-34fb-0c7d-7e99-655b99860509.png)


## 参考記事
公式ドキュメント
https://help.github.com/en/actions

Github actions + Fastlane + iOS = ❤
https://medium.com/well-red/github-actions-fastlane-ios-1f6d43cce726

Github Actions で Xcode プロジェクトをビルドしてみる
https://qiita.com/koogawa/items/ddf86b56d6b82ae11473