🫶

GitHub ActionsでFlutter macOSアプリをTestFlightにのせる

2024/04/01に公開

はじめに

Fluterで『Tokeru』という名前のmacOSのプロダクトを開発しています。
GitHub ActionsからTestFlightにアップロードするところまでを取り入れてみたので記事に残しておこうかなと思います。
どハマりして1日悩んだ箇所もあるので、参考になると嬉しいです。

環境変数を設定すればあとはコピペで行けるんじゃないかなと思います。

このActionsはTokeruのリポジトリで使っています。

https://github.com/imajoriri/tokeru/blob/main/.github/workflows/deploy.yml

Tokeruは絶賛Beta版です!
誰でも触ることができるので、ぜひTestFlightからインストールしてみてください。

https://github.com/imajoriri/tokeru

環境

  • Flutter 3.16.9
  • Flutterのバージョン管理にはFVMを使用

ローカルでの準備

pip3 install codemagic-cli-tools

今回はCodemagicのCLIツールを使うため、ローカルでもpipでインストールしておきます。

https://github.com/codemagic-ci-cd/cli-tools

pip3 install codemagic-cli-tools

秘密鍵の作成

以下をコマンドで実行するとcert_keyというファイルができるので、後にSecretに入れるため残しておきます。
<certificate_name>.p12はDistribution用のp12ファイルをキーチェーンからexportたものに置き換えてください。

openssl pkcs12 -in <certificate_name>.p12 -nodes -nocerts | openssl rsa -out cert_key

Mac インストーラー配布証明書の作成

作成したcert_keを使ってMac インストーラー配布証明書を作成します。
ローカルの~/Library/MobileDevice/Certificatesにも証明書が作成されます。
GitHub Actions上で作成することもできますが、実行のたびに増えていってしまうので、1度ローカルで作ってしまってActions上では取得するだけにします。

app-store-connect certificates create \
            --type MAC_INSTALLER_DISTRIBUTION \
            --certificate-key=@file:./cert_key \
            --save

GitHubのSecretを設定する

Actions内で必要なSecretをGitHubに設定してきます。
「Settings > secrets and variables > Actions」で設定できます。

CERT_KEY

先ほど作成したcert_keyの中身をそのまま作成します。

BUNDLE ID: APP_BUNDLE_ID

アプリのbundle idを設定します。

APPLE ID: APPLE_APP_ID

App Store ConnectからApple idを取得し、設定します。

App Store Connect APIまわり

App Store ConnectからAPI Keyを作成し設定します。
権限はApp Manger以上で作成します

  • APP_STORE_CONNECT_ISSUER_ID: Issuer ID
  • APP_STORE_CONNECT_KEY_IDENTIFIER: Key ID
  • APP_STORE_CONNECT_PRIVATE_KEY: p8ファイルの中身

Secretは以上です。
次は実際のymlファイルを見ていきます。

GitHub Actionsの中身

忙しい人のために最終的なymlファイルを先に載せておきます。
この後にそれぞれの説明を書いていこうと思います。

name: Build and Release macOS App

on:
  workflow_dispatch:

env:
  BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
  P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
  APP_STORE_CONNECT_KEY_IDENTIFIER: ${{ secrets.APP_STORE_CONNECT_KEY_IDENTIFIER }}
  APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
  APP_STORE_CONNECT_PRIVATE_KEY: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }}
  APP_BUNDLE_ID: ${{ secrets.APP_BUNDLE_ID }}
  APPLE_APP_ID: ${{ secrets.APPLE_APP_ID }}

jobs:
  deploy_macos_app:
    runs-on: macos-latest
    steps:
      - name: "Checkout"
        uses: actions/checkout@v4

      - name: "setup java"
        uses: actions/setup-java@v3
        with:
          distribution: "zulu"
          java-version: "12.x"

      - name: "Read flutter version from fvm config"
        id: flutter_info
        run: |
          FLUTTER_VERSION=$(jq -r '.flutterSdkVersion' ./.fvm/fvm_config.json)
          echo "FLUTTER_VERSION=$FLUTTER_VERSION" >> $GITHUB_ENV
        shell: bash

      - name: "Setup Flutter"
        uses: subosito/flutter-action@v2
        with:
          flutter-version: ${{ env.FLUTTER_VERSION }}
          cache: true

      - name: "Install codemagic-cli-tools"
        run: |
          pip3 install codemagic-cli-tools

      - name: "Install the Apple certificate"
        run: |
          echo -n "$CERT_KEY" >> $RUNNER_TEMP/cert_key

      - name: "Install the Apple provisioning profile(MAC_APP_STORE)"
        run: |
          app-store-connect fetch-signing-files "$APP_BUNDLE_ID" \
            --platform MAC_OS \
            --type MAC_APP_STORE \
            --certificate-key=@file:$RUNNER_TEMP/cert_key \
            --create

      - name: "Install the Certificates(MAC_INSTALLER_DISTRIBUTION)"
        run: |
          app-store-connect certificates list \
            --type MAC_INSTALLER_DISTRIBUTION \
            --certificate-key=@file:$RUNNER_TEMP/cert_key \
            --save

      - name: "keychain initialize"
        run: |
          keychain delete 
          keychain initialize

      - name: "keychain add-certificates"
        run: |
          keychain add-certificates

      - name: "xcode-project use-profiles"
        run: |
          /usr/bin/plutil -replace CFBundleIdentifier -string $APP_BUNDLE_ID ios/Runner/Info.plist
          find **/*.xcodeproj -type f | xargs sed -i "" -E 's/PRODUCT_BUNDLE_IDENTIFIER = ".+";//g'
          xcode-project use-profiles

      - name: Build macOS app
        run: |
          build_number=$(app-store-connect get-latest-build-number $APPLE_APP_ID)
          new_build_number=$((build_number + 1))
          flutter build macos --dart-define-from-file=dart_defines/prod.json --build-number=10039

      - name: "Package macOS app"
        run: |
          APP_NAME=$(find $(pwd) -name "*.app")
          PACKAGE_NAME=$(basename "$APP_NAME" .app).pkg
          echo "APP_NAME=$APP_NAME" >> $GITHUB_ENV
          echo "PACKAGE_NAME=$PACKAGE_NAME" >> $GITHUB_ENV
          xcrun productbuild --component "$APP_NAME" /Applications/ unsigned.pkg
          INSTALLER_CERT_NAME=$(keychain list-certificates \
                    | jq '[.[]
                      | select(.common_name
                      | contains("Mac Developer Installer"))
                      | .common_name][0]' \
                    | xargs)
          xcrun productsign --sign "$INSTALLER_CERT_NAME" unsigned.pkg "$PACKAGE_NAME"
          rm -f unsigned.pkg

      - name: "Upload TestFlight"
        run: |
          app-store-connect publish --path "$PACKAGE_NAME" --testflight --beta-group=group01

Actionsの説明

それぞれを説明していきたいと思います。

トリガー

トリガーをworkflow_dispatchにしています。
正直ここはプロジェクトによるので深掘りしません。お好きなトリガーを設定してあげてください。

on:
  workflow_dispatch:

runs-on

当たり前なのですが、macOSアプリをビルドするのでubuntuではなくmacosを使います。

runs-on: macos-latest

Setup Flutterまで

TokeruはFVMでバージョン管理しているので、fvm_config.jsonからバージョンを取得し、flutterをインストールしています。
ここはご自身の環境によって変えてください。(flutter analyzeやflutter testを実行してたらすでにあるとは思いますが)

- name: "Read flutter version from fvm config"
  id: flutter_info
  run: |
    FLUTTER_VERSION=$(jq -r '.flutterSdkVersion' ./.fvm/fvm_config.json)
    echo "FLUTTER_VERSION=$FLUTTER_VERSION" >> $GITHUB_ENV
    shell: bash

- name: "Setup Flutter"
  uses: subosito/flutter-action@v2
  with:
    flutter-version: ${{ env.FLUTTER_VERSION }}
    cache: true

Install codemagic-cli-tools

GitHub Actions内でもCodemagicのCLIツールを使うため、pipでインストールします。

https://github.com/codemagic-ci-cd/cli-tools

- name: "Install codemagic-cli-tools"
  run: pip3 install codemagic-cli-tools

Install the Apple certificate

secret変数に設定したcert_keyをファイルとして書き出します。

- name: "Install the Apple certificate"
        run: |
          echo -n "$CERT_KEY" >> $RUNNER_TEMP/cert_key

Install the Apple provisioning profile(MAC_APP_STORE)

MAC_APP_STOREのプロファイルを取得します。--createオプションは、もしなければ作成してくれます。

- name: "Install the Apple provisioning profile"
  run: |
    app-store-connect fetch-signing-files "$APP_BUNDLE_ID" \
      --platform MAC_OS \
      --type MAC_APP_STORE \
      --certificate-key=@file:$RUNNER_TEMP/cert_key \
      --create

Install the Certificates(MAC_INSTALLER_DISTRIBUTION)

ローカルで作成した証明書を取得します。
こちらは無くても作成してくれないので、ローカルで先ほど作成しておきました。

- name: "Install the Certificates(MAC_INSTALLER_DISTRIBUTION)"
    app-store-connect certificates list \
      --type MAC_INSTALLER_DISTRIBUTION \
      --certificate-key=@file:$RUNNER_TEMP/cert_key \
      --save

xcode-project use-profiles

xcode-project use-profilesは取得したプロファイル、証明書を参考にproject.pbxprojファイルを更新してくれるコマンドです。
ここで注意なのが、xcodeの設定でbundle idをcom.example.${DEFINE_BUILD_ENV}のような環境ごとに変えるようにしているとxcode-project use-profilesはbundle idをうまく認識しせず、Did not find matching provisioning profiles for code signing!となってしまいます。
困ったことにこのケースはfailedにならず、Actionsも最後まで通るのでかなりハマりました。

なので、Info.plistAPP_BUNDLE_IDを直接書き換えています。

- name: "xcode-project use-profiles"
  run: |
    /usr/bin/plutil -replace CFBundleIdentifier -string $APP_BUNDLE_ID ios/Runner/Info.plist
    find **/*.xcodeproj -type f | xargs sed -i "" -E 's/PRODUCT_BUNDLE_IDENTIFIER = ".+";//g'
    xcode-project use-profiles

Build macOS app

flutter build macosを行うのですが、普通にビルドするだけだと後のアップロードでbuild numberのかぶりで失敗します。
なのでapp-store-connect get-latest-build-numberコマンドで最新のbuild numberを取得して、+1した値をbuild-numberオプションに渡してあげています。

- name: Build macOS app
  run: |
    build_number=$(app-store-connect get-latest-build-number $APPLE_APP_ID) 
    new_build_number=$((build_number + 1))
    flutter build macos --dart-define-from-file=dart_defines/prod.json --build-number=$new_build_number

Package macOS app

.appファイルをApp Store Connectにアップロードできる形の.pkgに変換します。
その後、xcrun productsign --signで署名をします。

- name: "Package macOS app"
  run: |
    APP_NAME=$(find $(pwd) -name "*.app")
    PACKAGE_NAME=$(basename "$APP_NAME" .app).pkg
    xcrun productbuild --component "$APP_NAME" /Applications/ unsigned.pkg
    INSTALLER_CERT_NAME=$(keychain list-certificates \
      | jq '[.[]
      | select(.common_name
      | contains("Mac Developer Installer"))
      | .common_name][0]' \
      | xargs)
    xcrun productsign --sign "$INSTALLER_CERT_NAME" unsigned.pkg "$PACKAGE_NAME"
    rm -f unsigned.pkg

Upload TestFlight

最後にアップロードして終わりです。
--testflightオプションをつけることで外部テスターへの審査を自動で行ってくれます。
また、--beta-groupオプションには追加したい外部テスターグループの名前をつけます。
これで完了です。

- name: "Upload TestFlight"
  run: |
    app-store-connect publish --path "$PACKAGE_NAME" --testflight --beta-group=group01

さいごに

長くなりましたが最後まで読んでいただきありがとうございます。
XでもFlutter、個人開発周りのことを発信しているのでよかったらフォローしてください。

https://twitter.com/imasirooo

参考

https://docs.github.com/ja/actions/deployment/deploying-xcode-applications/installing-an-apple-certificate-on-macos-runners-for-xcode-development

https://qiita.com/takuyaWt/items/300cbb0570814b521732

https://www.electronjs.org/ja/docs/latest/tutorial/mac-app-store-submission-guide

https://takeyuweb.hatenablog.com/entry/2021/01/24/011135

Discussion