この記事は「Medley(メドレー) Advent Calendar 2025」の5日目の記事です🎅🏻
株式会社メドレーでモバイルアプリエンジニアをやっている奥澤です。メドレーでは、Flutterを使ってモバイルアプリを開発しています。
最近、GitHub ActionsとFirebase App Distributionを使ってFlutterアプリのベータ版配信のContinuous Delivery(CD)ワークフローを整備しました。CDのワークフロー自体はこの人生で何度目かわからないくらい書いてますが、ベストプラクティスは常に変わっていきます。今回もいくつか新たな発見があったので、アドベントカレンダーの記事としてまとめます。
共通化
開発環境向け・ステージング環境向けなどの環境ごとに異なるFirebaseのプロジェクトを使いたいことがあると思います。このような場合、それぞれの環境でほぼ同じGitHub Actionsのワークフローとなることが多いです。ワークフローをそれぞれの環境向けに書いてしまうと複数のファイルをメンテナンスする必要が生じて余分なコストとなるので、ワークフローを極力共通化したいところです。
そこで、ワークフロー自体とそのワークフローに必要なパラメータを分離し、ワークフローを共通化・再利用できるようにします。これには、 workflow_call が使えます。共通化されたワークフローを呼び出す方のワークフローから、 input や secrets でパラメータを渡します。
以下は、共通化されたワークフロー(呼び出される側)のサンプルです。
on:
workflow_call:
inputs:
release_notes:
description: "ベータ版のリリースノート"
required: true
type: string
secrets:
GOOGLE_SERVICES_INFO:
description: "base64エンコードされたGoogleService-Info.plist"
required: true
以下は、共通化されたワークフローを呼び出す側のサンプルです。サンプルとして with や secrets を使っています。
jobs:
prepare:
# 省略
call-distribute:
needs: "prepare"
uses: "./.github/workflows/shared_workflow.yaml"
with:
release_notes: ${{ needs.prepare.outputs.release_notes }}
secrets:
GOOGLE_SERVICES_INFO: ${{ secrets.GOOGLE_SERVICES_INFO_DEV }}
GitHubのリポジトリシークレットにbase64エンコードしたファイルを置き、GitHub Actions上でデコードし、ワークフロー内でのみ利用できるファイルとして配置するのはよく使うテクニックです。開発環境やステージング環境ごとにリポジトリシークレットを作成し、パラメータとして workflow_call にシークレットを渡します。
workflow_call の詳しい使い方はGitHub Actionsのドキュメントを参照してください。
--dart-define-from-file
環境ごとに変数を切り替えたい場合があると思います。例えばサーバーのURLを開発環境やステージング環境で切り替える、というような場合です。我々のプロジェクトではローカルでアプリをビルドする時にFlutterの --dart-define-from-file を使っているので、GitHub Actions上でも同じ仕組みを使うようにします。
前述した内容と重複しますが、シークレットで環境ごとの .env ファイルを、inputsで環境名をそれぞれ受け取り、いい感じに echo すれば環境ごとの .env ファイルを配置できます。
- name: "setup .env"
run: |
echo "${{ secrets.ENV_FILE }}" > ./env/${{ inputs.environment }}.env
このステップの後、 --dart-define-from-file を用いて flutter build コマンドを実行します。この時、Firebase App Distributionはビルド番号が同じアプリを上書きして配信するので、配信ごとにビルド番号をインクリメントすると便利です。GitHub Actionsではワークフローが実行されるたびにインクリメントされる github.run_number が用意されているので、これを使うのが手軽です。
- name: "build iOS app with Flutter"
run: |
flutter build ios --release --dart-define-from-file=env/${{ inputs.environment }}.env --no-codesign --build-number=$((100 + ${{ github.run_number }}))
ただし、GitHubのワークフローのファイル名を変更すると整数が1まで戻ってしまうので、ファイル名を変更したら整数値を補正してあげないと訳のわからないことになります(罠を踏みました)。 $((100 + ${{ github.run_number }}) の部分が整数値の補正処理です。
署名
ビルド後、 xcodebuild を使ってアーカイブと署名を行います。App Store Connect APIを使うとプロビジョニングプロファイルのダウンロードもしてくれるので便利です。App Store Connectのキーは、App Store Connect > ユーザとアクセス > 統合 > キー > App Store Connect APIから発行します。なお、Adminで発行しないと権限が不足します。Issuer IDもApp Store Connectから入手できます。
- name: "create Xcode archive"
run: |
xcodebuild archive \
-workspace ./ios/Runner.xcworkspace \
-scheme Runner \
-archivePath ./build/ios/archive/Runner.xcarchive \
-configuration Release \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO
- name: "export signed IPA"
run: |
xcodebuild -exportArchive \
-archivePath ./build/ios/archive/Runner.xcarchive \
-exportPath ./build/ios/ipa \
-exportOptionsPlist ./ios/export_options/ExportOptions.plist \
-allowProvisioningUpdates \
-authenticationKeyID "${{ secrets.APP_STORE_CONNECT_KEY_ID }}" \
-authenticationKeyIssuerID "${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}" \
-authenticationKeyPath "${API_KEY_PATH}"
Fastlaneを用いたアップロード
最後に、署名したIPAファイルをFirebase App Distributionにアップロードします。ここではFastlaneを利用しました。ここでも、パラメータとして受け取った環境を利用して、実行する lane を切り替えることができます。つまり、開発環境やステージング環境用の lane を用意しておいて、GitHub Actions上から環境に応じて lane を切り替えることができる、ということです。
- name: "upload ipa to Firebase App Distribution"
run: |
cd ios
bundle exec fastlane ios upload_to_firebase_${{ inputs.environment }}
env:
# 省略
最後に
メドレーの人材プラットフォーム本部では、Flutterエンジニアを積極採用中です。Flutterを活用した医療ヘルスケア領域の課題解決に興味がある方は、ぜひカジュアル面談にお越しください!
Medley(メドレー) Advent Calendar 2025の6日目は、 @yuporon で「React Hook Form の getValues で UI が更新されなかった話」です。お楽しみに!