はじめに
こんにちは。今日が誕生日のishihayaと申します。自分、誕生日おめでとうございます。
今回はFlutterでテスト環境へのデプロイ、およびテスターへアプリを配布するフローを自動化したいと思います。
コードだけ見たいという人は右の目次っぽいとこから、サンプルコードまで飛んでください。
自分はネイティブアプリの経験は浅く1、2ヶ月というところでまだ試行錯誤中ですが、
基本的にデプロイフローは(テスト環境や本番に限らず)自動化もしくはフロー化しておいた方が良いと思っています。
今回実装した理由としては、
例えば自分だけがデプロイフローを知っていて手元でデプロイしていた場合、急にいなくなったらできる人がおらず苦労することが見えているからです。
ワークフローとしておくことで、pushなどのトリガーや自分で実行したい時にボタン1つでリリースすることができると、基本的に引き継ぎすることなくデプロイをし続けられます。
他にもリモートからpushで自動でデプロイすることで同じフローを踏むのでヒューマンエラーがなくなる等あります。
それらを踏まえて、以下のサービスを使用して今回はテスターへの配布またはステージング環境などへのデプロイを自動化したいと思います。
GitHub Actions
CIサービスです。
GitHub Actionsを採用した理由としては単純に自分が慣れているという理由と、GitHubでCIも管理して使うサービス自体を減らす(新しいサービスを使用して管理しない)意図があります。
(注意点)
GitHub Actionsは個人だと2000分/月無料(Orgだと3000分?)なので自分の場合は使い切ったことはありません。
ただし、macOSをランタイムに指定すると10倍の時間消費されるので、この記事では自動化と謳っているものの、
実際に自分が案件等でmacOSを使用するワークフローのときは必要な時だけボタンを押してリリースする形、すなわちworkflow_dispatch
のみ指定する形を取っています。
今はボタンを押すだけの形でも困ってはいないのでこれでも問題ないのですが、実際自動化するなら料金によっては別サービス(FlutterならBitriseやCodeMagic等)の方がいい場合もあります。
とはいえその2つも時間数制限あるので使いすぎると普通に課金になります。
また、pushが少ないブランチで定期的に使用するとかだと普通に自動化になるので、個人的にはGitHub Actionsでも良いかなとは思いますが、その辺りは運用方法と技術選定をチームで話し合うのが良さそうです(良いソリューションがあれば教えてください)
Firebase App Distribution
テスト環境へのアプリのリリースを容易にするFirebaseのサービスです。
バックエンドでGCPのプロジェクトの使用、また認証でFirebaseを使用したりするので今回はFirebase App Distributionを使用するとスムーズにリリースできるかなと思い採用しました。
今回の実装は本番環境ではなくテスト環境へのデプロイのみを行いますが、ワークフローの環境変数と、リリース先をFirebase App Distribution経由ではなくアプリのストアの方に直接するように変えれば適宜本番環境へも対応可能かと思われます(まだやってはいないです。。)
すなわち、最後のデプロイ先以外は本番でも真似できると思われますので、ぜひみて行ってください。
次に、iOSのサンプルコードを示します。解説はその後に載せてますので、読むかどうかはコードを先に見てから判断してください。
iOSのサンプルコード
name: Deploy dev ios
on:
# mainブランチへのマージでデプロイしたい場合 publicリポジトリだと無料なのでこれでいいと思う
push:
branches:
- main
# macOSはGitHubActionsの時間数を食うのでプライベートリポジトリの場合は自分はしたい時だけ手動でdev環境にデプロイしてます(できればmainブランチpushでトリガーしたい)
workflow_dispatch:
jobs:
build_ios:
runs-on: macos-latest
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.IOS_P12_BASE64 }}
P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }}
BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_PROVISION_PROFILE_BASE64 }}
KEYCHAIN_PASSWORD: ${{ secrets.IOS_KEYCHAIN_PASSWORD }}
APPLICATION_NAME: exampleApp # bundleIDの最後の要素のアプリ名
TEAM_NAME: exampleTeam # プロジェクトのチームや会社名
steps:
- uses: actions/checkout@v2
- name: cache flutter dependencies
uses: actions/cache@v2
with:
path: /Users/runner/hostedtoolcache/flutter
key: ${{ runner.os }}-flutter
- name: install flutter
uses: subosito/flutter-action@v1
with:
flutter-version: '2.5.0'
- name: set up environment
run: |
cat ios/envs/dev/Release.xcconfig >> ios/Flutter/Release.xcconfig
cp -f ios/envs/dev/ExportOptions.plist ios/
cp -f ios/envs/dev/GoogleService-Info.plist ios/
- name: set up provisioning profile
# https://docs.github.com/ja/actions/deployment/deploying-xcode-applications/installing-an-apple-certificate-on-macos-runners-for-xcode-development
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/$TEAM_NAME.p12
PP_PATH=$RUNNER_TEMP/$APPLICATION_NAME.mobileprovision
KEYCHAIN_PATH=$RUNNER_TEMP/$TEAM_NAME.keychain-db
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode --output $CERTIFICATE_PATH
echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode --output $PP_PATH
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
- name: install dependencies
run: |
flutter pub get
find . -name "Podfile" -execdir pod install \;
- name: build ios app
run: |
flutter build ipa \
--release \
--export-options-plist=ios/ExportOptions.plist \
- name: upload artifact
uses: actions/upload-artifact@v2
with:
name: ios-build
path: build/ios/ipa/example_app.ipa
- name: clean up keychain and provisioning profile
if: ${{ always() }}
run: |
security delete-keychain $RUNNER_TEMP/$TEAM_NAME.keychain-db
rm ~/Library/MobileDevice/Provisioning\ Profiles/$APPLICATION_NAME.mobileprovision
deploy_ios:
runs-on: macos-latest
needs: build_ios
env:
GCP_PROJECT_ID: example-app
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY_BASE64 }}
APP_ID: example-app-id # Firebaseのやつ
RELEASE_BINARY_PATH: ios/example_app.ipa
RELEASE_NOTES_PATH: ios/release-notes.txt
TESTER_GROUPS: example-app-group
steps:
- uses: actions/checkout@v2
- name: download artifact
uses: actions/download-artifact@master
with:
name: ios-build
path: ios
- name: set up cloud sdk
uses: google-github-actions/setup-gcloud@master
with:
project_id: ${{ env.GCP_PROJECT_ID }}
service_account_key: ${{ env.GCP_SA_KEY }}
export_default_credentials: true
- name: install firebase
run: curl -sL https://firebase.tools | bash
- name: deploy
run: firebase appdistribution:distribute $RELEASE_BINARY_PATH --app $APP_ID --release-notes-file "$RELEASE_NOTES_PATH" --groups "$TESTER_GROUPS"
iOSの実装手順
それでは、上記のコードの解説に入ります。
以下、テスト環境をデプロイするまでのフローを記述します
また、テスト環境のことをdev環境と呼びますが、stg(ステージング)でもtestでも自分のチームで使用する環境名でOKです。なお、本番環境のテストリリースなどでもFirebase App Distributionが使えたはずなので、その場合はprdに置き換えて読んでもらって大丈夫です。
早速ワークフローを書いていきたいところなんですが、
ワークフローを書く前にいくつか準備をする必要があります
自分はデプロイから逆算してエラーを出していったのでここに書いていくフローは基本的に省略できない最短距離だと思っています(他に良い書き方があれば教えてください)
なお、今回は環境が複数ある前提で作業していますが、個人開発だから本番しか使わない、など単一環境の場合は環境を分けるSTEPに関しては適宜読み飛ばしてもらっても問題ありません。
STEP1 App Store Connectでアプリを作成する
dev環境のアプリを作成します
アプリのbundleIDがわかっていてコンソール上に表示されていればOKです
(詳細省略)
STEP2 Firebaseでアプリ作成
FirebaseのAppIDがわかっていてコンソール上に表示されていればOKです
アプリ登録時にGoogleService-Info.plist
が入手できると思いますが次のステップで使用するのでダウンロードだけしておいてください
Firebase App Distribution有効化は、確か最初に1度だけやっておく必要があったはずです
また、Firebaseをimportするためにswiftに数行コードを書く必要があったはずで、そちらもFirebaseのステップの中で出てくるはずなので、それも対応必要です。
(詳細省略)
STEP3 GoogleService-Info.plist
をアプリに入れる
このSTEP3からSTEP5までのフローで実現したいことを先に書きます。
...
steps:
# 省略
- name: set up environment
run: |
cat ios/envs/dev/Release.xcconfig >> ios/Flutter/Release.xcconfig
cp -f ios/envs/dev/ExportOptions.plist ios/
cp -f ios/envs/dev/GoogleService-Info.plist ios/
ここでやりたいことは、環境ごとに設定ファイルを分けてワークフローで一気に読み込むことをしたいです。
上記を踏まえ、今から、ios/envs/環境/
配下(今回だとios/envs/dev/
)にそれぞれの環境ごとの設定ファイルを用意していきます。
それでは、GoogleService-Info.plist
をアプリに入れます。
まず、ios/envs/dev/GoogleService-Info.plist
に置きます。
次に、ios/GoogleService-Info.plist
にも同じファイルを置きます。
理由はbuildがこけるからです。どの環境のファイルでも良いです。
同じ名前のファイルがあればcp -f ...
でワークフローの際に上書きされます。
注意点)
XCodeでios直下でGoogleService-Info.plist
を認識させないとビルドエラーになります。
自分はネイティブアプリ開発経験が浅いのでこの辺りの知識に乏しいのですが、いい感じでコードでできないかな・・・と思ってます(project.pbxprojをいじればなんとかなる?)
STEP4 Build時の設定ファイルExportOptions.plist
を用意する
build時に設定ファイルとして読み込むためのファイルです。
XCodeでアーカイブしてダウンロードすると中にExportOptions.plist
が入っています。
なお、アーカイブしてダウンロードしなくても、以下コピってきて各種設定すれば使いまわせますので、(個人的には)そちらをお勧めします。
なお、Provisioning Profileやアプリ名やチームIDは仮のものを記述しています。
次以降のSTEPでは、以下のBundleIDやファイル名で進めていきます。
配置先: ios/envs/dev/ExportOptions.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>compileBitcode</key>
<true/>
<key>destination</key>
<string>export</string>
<key>method</key>
<string>ad-hoc</string> // 配布形式 本番ならapp-storeとかになる
<key>provisioningProfiles</key>
<dict>
<key>com.exampleTeam.exampleAppDev</key> // Apple Bundle ID
<string>exampleAppDev</string> // Provisioning Profile ファイル名
</dict>
<key>signingCertificate</key>
<string>Apple Distribution</string>
<key>signingStyle</key>
<string>manual</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>ADZK6RM6CL</string> // チームID
<key>thinning</key>
<string><none></string>
</dict>
</plist>
STEP5 Build時に使用する設定ファイルRelease.xcconfig
を用意する
こちらは環境変数などを定義していくファイルになります
適宜この辺りは変えてもらってOKです
DISPLAY_APPLICATION_NAME=exampleAppDev # 表示するアプリ名
BUNDLE_ID_SUFFIX=exampleAppDev # BundleIDの最後のやつ
PROVISIONING_PROFILE_NAME=exampleAppDev # Provisioning Profile ファイル名
上記ファイルをワークフローの時に
ios/Flutter/Release.xcconfig
に追記されるようにします
また、デバッグ時用に
ios/Flutter/Debug.xcconfig
に以下を追記してください。テスト環境のもので良いです。
(PROVISIONING_PROFILE_NAME
は自分はdebugの時は設定していません)
DISPLAY_APPLICATION_NAME=exampleAppDev
BUNDLE_ID_SUFFIX=exampleAppDev
STEP6 STEP5で設定した環境変数を適用できるようにする
STEP5でOSごとのディレクトリで使用する環境変数を定義したので、ソースコード上でいくつか適用したい箇所があります。
ios/Runner.xcodeproj/project.pbxproj
のなかで
PRODUCT_BUNDLE_IDENTIFIER
とコード検索をかけるとBundle IDを登録しているっぽい記述が3つほど(debug, profile, releaseの設定をしているらしきコード)見つかるので、BundleIDを環境ごとで更新してください。
PRODUCT_BUNDLE_IDENTIFIER = com.exampleTeam.$(BUNDLE_ID_SUFFIX);
exampleTeamはチームまたは会社名が入ると思います。
環境変数を使うと、上記のようになると思います。
また、Provisioning Profileもここで読み込んでおきます。
アルファベット順ぽいので2行下くらいに、
PROVISIONING_PROFILE_SPECIFIER = "$(PROVISIONING_PROFILE_NAME)";
としてください
大体こんな感じなります
XXXXXXXXXXXXXXX / * Profile * / = {
...
PRODUCT_BUNDLE_IDENTIFIER = "com.exampleTeam.$(BUNDLE_ID_SUFFIX)";
PRODUCT_NAME = xxxxxxxxxxx;
PROVISIONING_PROFILE_SPECIFIER = "$(PROVISIONING_PROFILE_NAME)";
...
}
XXXXXXXXXXXXXXX / * Debug * / = {
...
PRODUCT_BUNDLE_IDENTIFIER = "com.exampleTeam.$(BUNDLE_ID_SUFFIX)";
PRODUCT_NAME = xxxxxxxxxxx;
PROVISIONING_PROFILE_SPECIFIER = "$(PROVISIONING_PROFILE_NAME)";
...
}
XXXXXXXXXXXXXXX / * Release * / = {
...
PRODUCT_BUNDLE_IDENTIFIER = "com.exampleTeam.$(BUNDLE_ID_SUFFIX)";
PRODUCT_NAME = xxxxxxxxxxx;
PROVISIONING_PROFILE_SPECIFIER = "$(PROVISIONING_PROFILE_NAME)";
...
}
ios/Runner/Info.plist
<dict>
<!-- 省略 -->
<key>CFBundleName</key>
<string>$(DISPLAY_APPLICATION_NAME)</string>
<key>CFBundleDisplayName</key>
<string>$(DISPLAY_APPLICATION_NAME)</string>
<!-- 省略 -->
</dict>
上記のKeyがあれば更新、なければ新規で定義しましょう。
STEP7 証明書設定
いくつかファイルの準備が必要です。
- p12証明書
- provisioning profile
上記の用意をしましょう。
証明書
Provisioning Profile
もろに上記の2つのサイトを参考にしました。
ファイルはBASE64エンコードしてGitHub Secretsに入れましょう。
また、p12作成時にパスワードの作成を求められますのでそちらもメモしつつ、同様にGitHub Secretsに入れましょう。
なお、上記の証明書等をワークフロー上で適用する方法ですが、
最終的に以下のサイトのコピペで適宜変更で落ち着きました。
ソースコード自体はサンプルコードでも参照できますが、ほぼそのまま使ってます。使えてます。
actionsの外部ライブラリを使用することも検討しましたが、サードパーティ製のライブラリにp12やprovisioning profileなどの情報を渡したくなかったり、個人開発で作られたライブラリだとメンテナンスがされなかったりするので、自分は公式のやつとかスター数がかなり多いもの以外のライブラリの使用は見送っています。
なお、GitHub Secretsに設定するKEYCHAIN_PASSWORDに関しては、任意に設定してOKです。
STEP8 アプリアイコンの設定
自分はdev環境の場合アイコンを別に分けたかったので、
を使用しました。
環境ごとにymlファイルを作成して、ワークフローで呼び分けます。
以下はflutter pub getの後に挟む処理です
- name: import app icon
run: flutter pub run flutter_launcher_icons:main -f flutter_icons-dev.yml
flutter_icons-環境.ymlをプロジェクトルートに環境数分配置します。
# https://pub.dev/packages/flutter_launcher_icons
# flutter pub run flutter_launcher_icons:main -f flutter_icons-dev.yml
flutter_icons:
android: true
ios: true
image_path: "assets/launcher_icon/icon-dev.png"
assets/launcheer_icon/icon-dev.png
にdev環境のアイコンを配置します。
STEP9 ビルド
こんな感じです。dart defineを使用している場合は以下で環境変数を定義します。
jobs:
build_ios:
# jobs配下のenvならrunnerの環境変数として$~~で読み込み可能
env:
EXAMPLE_ENV: example-env-value
...
- name: build ios app
run: |
flutter build ipa \
--release \
--export-options-plist=ios/ExportOptions.plist \
--dart-define=EXAMPLE_ENV=$EXAMPLE_ENV
STEP10 ビルドされたバイナリをアップロードしておく
- name: upload artifact
uses: actions/upload-artifact@v2
with:
name: ios-build
path: build/ios/ipa/exampleApp.ipa # DISPLAY_APPLICATION_NAME.ipa
上記のようにしておくことで、.ipaファイルを入手できます。
自分が触ったところだとまずipaファイルを初回だけアップロードしないといけなかった気がするので、その際にも使えそうです。
また、こちらはdeploy時にも使用します。
STEP11 バイナリダウンロード
buildとdeployのjobを分けているので、build jobで作成したバイナリをダウンロードします。
- name: download artifact
uses: actions/download-artifact@master
with:
name: ios-build
path: ios
STEP12 GCP認証
FIREBASE_TOKENでの認証でも可能ですが、ユーザー依存になるので、
自分達のチームでは、GCPプロジェクトでサービスアカウントを作成して認証するようにしています。(GCPをそもそも使っているのもある)
それを気にしなければ、Firebase login:ci
でトークンもらってそれでCI回してもOKです
Firebase cli tokenの場合
https://firebase.google.com/docs/cli?hl=ja#cli-ci-systems
この辺り参考にしつつ、TOKENをGitHub Secretsに入れたらあとはSTEP14で--token
を指定すればできるはずです
GCPの場合
- name: set up cloud sdk
uses: google-github-actions/setup-gcloud@master
with:
project_id: ${{ env.GCP_PROJECT_ID }}
service_account_key: ${{ env.GCP_SA_KEY }}
export_default_credentials: true
STEP13 Firebase App Distribution使ってデプロイ
上記を参考にしました。
CLIの方では書いてないけどコンソールでのデプロイで書いていることもちょくちょくあるので、両方見つつデプロイしてみるのがいいと思います。
APP_ID: xxxxxxxxxxxxx # Firebaseでアプリ作成したときののApp ID
RELEASE_BINARY_PATH: ios/exampleApp.ipa # DISPLAY_APPLICATION_NAME.ipa
RELEASE_NOTES_PATH: ios/release-notes.txt # このファイル作っておいたらrelease notes変更できる
TESTER_GROUPS: example-team # testerは firebase コンソール上から作成可能。ない場合エラー出る。
...
- name: install firebase
run: curl -sL https://firebase.tools | bash
- name: deploy
run: firebase appdistribution:distribute $RELEASE_BINARY_PATH --app $APP_ID --release-notes-file "$RELEASE_NOTES_PATH" --groups "$TESTER_GROUPS"
firebase tokenを使用している場合、デプロイ時に以下の引数が使用可能です。(ドキュメントにも記載されている)
--token=${{ secrets.FIREBASE_TOKEN }}
STEP14 ワークフロー記述
解説が必要そうなステップは全て完了したので、上記を踏まえてワークフローを書きましょう。
GitHub Actionsの基本的な構文については以下を参照してください。
https://docs.github.com/ja/actions/learn-github-actions/workflow-syntax-for-github-actions
解説が必要そうな箇所は上のSTEPで触れていますので、あとはサンプルコードを参考に実装してみてもらえると!!!(投げやり)
随時更新しますので不明点やご指摘あればお願いいたします。
Androidのサンプルコード
name: Deploy dev android
on:
# androidはmainブランチにマージされた時点でdev環境にデプロイされる
push:
branches:
- main
workflow_dispatch:
jobs:
build_android:
runs-on: ubuntu-latest
env:
# NOTE: android/app/build.gradleで下記STORE_FILE_PATHを直接指定している
STORE_FILE_PATH: android/release.keystore
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
APPLICATION_NAME: example_app # applicationIdの最後の要素のアプリ名
steps:
- uses: actions/checkout@v2
- name: Cache Flutter dependencies
uses: actions/cache@v2
with:
path: /opt/hostedtoolcache/flutter
key: ${{ runner.os }}-flutter
- name: install flutter
uses: subosito/flutter-action@v1
with:
flutter-version: '2.5.0'
- name: set up config
run: cp android/envs/dev/google-services.json android/app/
- name: create store file for sign
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > $STORE_FILE_PATH
- name: install dependencies
run: flutter pub get
- name: build android app
run: |
flutter build appbundle \
--release \
- name: upload artifact
uses: actions/upload-artifact@v2
with:
name: android-build
path: build/app/outputs/bundle/release/app-release.aab
deploy_android:
runs-on: ubuntu-latest
needs: build_android
env:
GCP_PROJECT_ID: example-app
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY_BASE64 }}
APP_ID: example-app-id # Firebaseのやつ
RELEASE_BINARY_PATH: android/app-release.aab
RELEASE_NOTES_PATH: android/release-notes.txt
TESTER_GROUPS: example-app-group
steps:
- uses: actions/checkout@v2
- name: download artifact
uses: actions/download-artifact@master
with:
name: android-build
path: android
- name: set up cloud sdk
uses: google-github-actions/setup-gcloud@master
with:
project_id: ${{ env.GCP_PROJECT_ID }}
service_account_key: ${{ env.GCP_SA_KEY }}
export_default_credentials: true
- name: install firebase
run: curl -sL https://firebase.tools | bash
- name: deploy
run: firebase appdistribution:distribute $RELEASE_BINARY_PATH --app $APP_ID --release-notes-file "$RELEASE_NOTES_PATH" --groups "$TESTER_GROUPS"
Androidの実装手順
すみません、
まだ書けてません・・・(iOS書けたからいいかという甘えが出ました)
随時更新すると思います。
納期は絶対に守りますので、ぜひ仕事は任せてください!!!本当にお願いします!!!
おわりに
以上になります。
「はじめに」があるので、「おわりに」を作ってみましたが、特に書くことはありませんでした。
ありがとうございました。
全力で参考にさせていただいた記事