Help us understand the problem. What is going on with this article?

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

元ラーメン屋店長プログラマのObjective-ひろC(@hirothings)です🍜
GitHub Actionsで個人アプリのデプロイを自動化したのでその手順を書きます。

目次

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

GitHub Actionsとは?

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

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

特徴

GitHub Marketplaceに、actionがたくさんあり、自分のワークフローの一部に使用可能です。(詳しくはここ)

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

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

..etc(詳しくはここ)

なぜ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分/月となります。(公式ドキュメントより)
image.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にインストールされているソフトウェアは公式ページで確認できます。

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がぶら下がる構成になります。
公式ドキュメントより

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は並列処理も直列処理も実行可能(詳しくはここ)

Hello Worldまで

Actionsタブをクリックして、右上のSet up workflow yourselfボタンをクリックすると、最初のworkflowの設定画面になります。
01.png
ワークフロー名(yamlのファイル名)を指定。(公式の例に合わせてハイフン区切りで命名)
そのままファイルをGitHub上で編集することもできます。(補完なども効く専用のエディタ)
02.png
このまま、コミットすればHello Worldまで実行できます。

デプロイ自動化の実装の方針

GitHub ActionsでiOSのビルド環境を構築するには、fastlaneを使うのがオススメと中の人が回答しているのでfastlaneで実装します。

実装

ReleaseブランチにPRがマージされたら、デプロイのActionを実行する例は下記です。(yaml)
記法については公式ドキュメントを読んでください。
上から読んでいけば何となく何をやっているか分かると思います。最後のstepでfastlaneを実行して実際のデプロイ処理を行っています。分かりづらい箇所を補足します。

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が用意されているので、それを使います。
marketplaceで用意されているworkflowを使うときは、 {owner}/{repo}@{ref} or {owner}/{repo}/{path}@{ref}. の記法で参照します。 (詳しくはここ)

steps:
    - uses: actions/checkout@v1 

2: キャッシュの利用

cocoapodsやcarthageのキャッシュが可能なactionを公式が提供しています
ifキーワードを組み合わせて、キャッシュがない場合のみインストールを実行するよう制御することが可能です。

- 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以外の情報は渡されません。(詳しくはここ)

指定はsettings/Secrets
スクリーンショット 2019-12-31 5.42.00.png
参照は下記の記法

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を書いて対応しています(参考)

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

参考: Circle CI で Github に write access 可能な Deploy key を設定する

デプロイの実行

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

- 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を実行してます。

fastfile
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

実行!! :rocket:

04.png
05.png

slackへの投稿もfastlaneで行っています。
ワークフロー名やCommit SHAは、GitHub Actionsのデフォルトの環境変数として利用可能です。
(デフォルトで利用できる環境変数について詳しくはここ)

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

無料枠の制限を超えそうになると?

Actionの実装でデバッグを繰り返したところ、1ヶ月の無料枠の制限を超えそうになってしまいました。無料枠の75%を超えたタイミングでGitHubからメールが来ました。
支出の制限の変更はユーザーだとhttps://github.com/settings/billing から指定できるので必要に応じて設定しましょう。詳しくはここ
image.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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした