wheatandcatの開発ブログ

技術系記事を投稿してます

GitHub ActionsでiOSアプリをビルドしてFirebase App Distribution にデプロイする

作っているアプリで実機検証をやりやすくすためにCIで自動でデプロイできるようにしたの記事化
今回はiOSアプリのみ、次回Androidアプリのデプロイ方法も記事にする予定

PR

github.com

使用

実装

①. Apple Developer で証明書を作成

まず、iOSアプリを配布するためにApple Developerでアプリの証明書を作成する必要があるので、以下のコマンドでprivate.keycsr.pemを作成

$ openssl genrsa -out private.key 2048
$ openssl req -new -key private.key -out csr.pem

key作成時の入力は以下みたいな感じで自身の設定を入力

Country Name (2 letter code) [AU]:JP
State or Province Name (full name) [Some-State]:Tokyo
Locality Name (eg, city) []:Tokyo
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Your Company Name
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:Your Name
Email Address []:your-email@example.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

Apple DeveloperにログインしてCertificatesを画面を開いて、+をクリックして以下の通りに操作

  • Apple Distributionを選択
  • 以下の画像の画面でcsr.pemをアップロード
  • 証明書をダウンロードしてdistribution.cerの名前で保存

②. p12 ファイルを作成 & インポート

以下のコマンドで①でダウンロードしたdistribution.cerを元にp12ファイルを作成

$ openssl x509 -in distribution.cer -inform der -out distribution.pem
$ openssl pkcs12 -legacy -export -out distribution.p12 -inkey private.key -in distribution.pem

※ 以下の記事の通り、legacyのオプションを付けないと mac でパスワード認証ができないので注意

以下でパスワードを入力

Enter Export Password:
Verifying - Enter Export Password:

作成されたdistribution.p12をダブルクリックし、mac のキーチェーンにインストール
パスワードを求められるので、上記で入力したパスワードを入力、これで証明書をインポートできる

③. 配信用の証明書をプロビジョニングプロファイルを作成

Apple Developer にログインして、配信用の証明書をプロビジョニングプロファイルを作成

  • Profiles にアクセスして、+をクリック
  • Ad Hocを選択
  • App ID を選択
  • Certificates で①で作成した証明書を選択
  • プロビジョニングプロファイルをダウンロードしてadhok.mobileprovisionの名前で保存

④. Xcodeでプロビジョニングプロファイルが正しく作成されたか確認

以下のコマンドでFlutterアプリをXcodeで開く

$ open ios/Runner.xcworkspace
  • Xcode でプロジェクトを開き、Signing & Capabilitiesを選択
  • Automatically manage signingにチェックがついている場合は外す
  • Provisioning Profileで③でダウンロードしたadhok.mobileprovisionを選択
  • 以下の画像の通り表示されていればOK(認証できていない場合はエラーが表示される)

⑤. 作成したファイルをGitHub Actions の secrets に設定

②と③で作成した証明書とプロビジョニングプロファイルはデプロイ時に使用するので以下のコマンドでbase 64に変換してGitHub Actions の secrets に設定

$ base64 -i distribution.p12 | pbcopy
$ base64 -i adhok.mobileprovision | pbcopy

⑥. fastlaneで配布用のビルドを作成 & Firebase App Distribution にデプロイ

以下のfastlaneのファイルを作成

dart/app/ios/fastlane/Fastfile

# Fastfile

default_platform(:ios)

platform :ios do
  desc "Build and distribute iOS app for Adhoc"
  lane :adhoc do    
    p12_base64 = ENV['CERTIFICATES']
    p12_password = ENV['CERTIFICATES_PASSWORD']
    File.write("certificate.p12", Base64.decode64(p12_base64)) 
    
    create_keychain(
      name: "build.keychain",
      password: "keeper",
      default_keychain: true,
      unlock: true,
      timeout: 36000,
      lock_when_sleeps: false
    )

   `curl -OL https://www.apple.com/certificateauthority/AppleWWDRCAG3.cer`

    import_certificate(
      keychain_name: "build.keychain",
      keychain_password: "keeper",
      certificate_path: "fastlane/AppleWWDRCAG3.cer",
    )

    import_certificate(
      keychain_name: "build.keychain",
      keychain_password: "keeper",
      certificate_path: "fastlane/certificate.p12",
      certificate_password: p12_password
    )

    current_time = Time.now.to_i

    increment_build_number(
      build_number: current_time,
      xcodeproj: "Runner.xcodeproj"
    )


    build_app(
      workspace: "Runner.xcworkspace",
      scheme: "Runner",
      export_options: {
        method: "ad-hoc",
        provisioningProfiles: {
          "com.unicorn.stockkeeper" => "keeper-adhoc"
        },
        signingStyle: "manual"
      },
      clean: true,
      output_name: "app.ipa",
      output_directory: "build"
    )

    # Firebase App Distributionにアップロード
    firebase_app_distribution(
      app: ENV['REVIEW_IOS_FIREBASE_APP_ID'],
      firebase_cli_token: ENV['REVIEW_FIREBASE_CLI_TOKEN'],
      ipa_path: "build/app.ipa",
      groups: "testers"
    ) 

    delete_keychain(name: "build.keychain")

  end
end

以下の部分で証明書作成 & キーチェーンにインポート

    p12_base64 = ENV['CERTIFICATES']
    p12_password = ENV['CERTIFICATES_PASSWORD']
    File.write("certificate.p12", Base64.decode64(p12_base64)) 
    
    create_keychain(
      name: "build.keychain",
      password: "keeper",
      default_keychain: true,
      unlock: true,
      timeout: 36000,
      lock_when_sleeps: false
    )

   `curl -OL https://www.apple.com/certificateauthority/AppleWWDRCAG3.cer`

    import_certificate(
      keychain_name: "build.keychain",
      keychain_password: "keeper",
      certificate_path: "fastlane/AppleWWDRCAG3.cer",
    )

    import_certificate(
      keychain_name: "build.keychain",
      keychain_password: "keeper",
      certificate_path: "fastlane/certificate.p12",
      certificate_password: p12_password
    )

同じbuild_numberのアプリはデプロイできないので以下のコードで現在日時で更新

    current_time = Time.now.to_i

    increment_build_number(
      build_number: current_time,
      xcodeproj: "Runner.xcodeproj"
    )

以下のコードでプロビジョニングプロファイルを指定してiOSアプリをビルド

    build_app(
      workspace: "Runner.xcworkspace",
      scheme: "Runner",
      export_options: {
        method: "ad-hoc",
        provisioningProfiles: {
          "com.unicorn.stockkeeper" => "keeper-adhoc"
        },
        signingStyle: "manual"
      },
      clean: true,
      output_name: "app.ipa",
      output_directory: "build"
    )

最後に以下のコードでFirebase App Distributionにアップロード

    firebase_app_distribution(
      app: ENV['REVIEW_IOS_FIREBASE_APP_ID'],
      firebase_cli_token: ENV['REVIEW_FIREBASE_CLI_TOKEN'],
      ipa_path: "build/app.ipa",
      groups: "testers"
    ) 

appfirebase_cli_tokenの値は以下のページを参考に取得

firebase.google.com

⑥. GitHub Actionsでデプロイできるようにする

以下のyamlファイルを作成することでCI上でデプロイできる

.github/workflows/depoly_ios_app_review.yaml

name: レビュー環境のiOSアプリをFirebase App Distributionにデプロイする

on:
  push:
    branches:
      - main
    paths:
      - "dart/app/**"
env:
  REVIEW_IOS_FIREBASE_APP_ID: ${{ secrets.REVIEW_IOS_FIREBASE_APP_ID }}
  REVIEW_FIREBASE_CLI_TOKEN: ${{ secrets.REVIEW_FIREBASE_CLI_TOKEN }}
  CERTIFICATES: ${{ secrets.CERTIFICATES }}
  CERTIFICATES_PASSWORD: ${{ secrets.CERTIFICATES_PASSWORD }}
  REVIEW_PROVISIONING_PROFILE: ${{ secrets.REVIEW_PROVISIONING_PROFILE }}
  REVIEW_GOOGLE_SERVICE_INFO_PLIST: ${{ secrets.REVIEW_GOOGLE_SERVICE_INFO_PLIST }}
  REVIEW_DART_FIREBASE_OPTIONS: ${{ secrets.REVIEW_DART_FIREBASE_OPTIONS }}
  REVIEW_RUNNER_INFOPLIST: ${{ secrets.REVIEW_RUNNER_INFOPLIST }}
jobs:
  deploy:
    runs-on: macos-latest
    defaults:
      run:
        working-directory: dart/app

    steps:
      - name: Checkout repository
        uses: "actions/checkout@v4"

      - name: Create GoogleService-Info.plist
        run: echo $REVIEW_GOOGLE_SERVICE_INFO_PLIST | base64 --decode > ./ios/Runner/GoogleService-Info.plist

      - name: Create Provisioning File
        run: |
          mkdir -pv ~/Library/MobileDevice/Provisioning\ Profiles/
          cd ~/Library/MobileDevice/Provisioning\ Profiles/
          echo $REVIEW_PROVISIONING_PROFILE | base64 --decode > ./keeper-adhoc.mobileprovision

      - name: Create lib/firebase_options.dart
        run: echo $REVIEW_DART_FIREBASE_OPTIONS | base64 --decode > ./lib/firebase_options.dart

      - name: Create dart/app/ios/Runner/Info.plist
        run: echo $REVIEW_RUNNER_INFOPLIST | base64 --decode > ./ios/Runner/Info.plist

      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: "2.7"

      - name: Cache Flutter dependencies
        uses: actions/cache@v4
        with:
          path: |
            ${{ env.FLUTTER_HOME }}/.pub-cache
            **/.flutter-plugins
            **/.flutter-plugin-dependencies
            **/.dart_tool/package_config.json
          key: ${{ runner.os }}-flutter-${{ hashFiles('dart/app/pubspec.lock') }}
          restore-keys: |
            ${{ runner.os }}-flutter-

      - name: Cache Bundler dependencies
        uses: actions/cache@v4
        with:
          path: vendor/bundle
          key: ${{ runner.os }}-bundler-${{ hashFiles('dart/app/ios/Gemfile.lock') }}
          restore-keys: |
            ${{ runner.os }}-bundler-

      - name: Cache CocoaPods
        uses: actions/cache@v4
        with:
          path: |
            dart/app/ios/Pods
          key: ${{ runner.os }}-pods-${{ hashFiles('dart/app/ios/Podfile.lock') }}
          restore-keys: |
            ${{ runner.os }}-pods-

      - name: Set up Flutter
        uses: subosito/flutter-action@v2
        with:
          channel: stable
          cache: true

      - name: Install Flutter dependencies
        run: flutter pub get

      - name: Install dependencies
        run: |
          cd ios
          bundle install

      - name: Install CocoaPods dependencies
        run: |
          cd ios
          pod install

      - name: Build and distribute with Fastlane
        run: |
          cd ios
          bundle exec fastlane adhoc

.gitignoreしているファイルが結構あるので、除外しているファイルをGitHub Actions の secrets 経由で生成してCIで実行

CIが正常に動作すれば以下みたいな感じでFirebase App Distributionからアプリをダウンロードできるようになる