【Flutter-Android】【GithubActions】flutterのCD環境を整備してみた

# はじめに
普段仕事ではiOSアプリを開発していますが、
昨年5月頃からFlutterに興味を持ち始め、趣味の時間でFlutterアプリの勉強を始めました。
アプリの配信は毎回手間に感じていて自動化できないか？と調査をしていた内容を共有します。

今回はAndroid編です。iOS編は[こちら](https://qiita.com/SUGYKEN/items/b654eb65478e51554936)。

# 目標
Githubにコードがpushされたときに、AndroidアプリをStoreにビルド配信する。

# Storeにビルド配信する。
では作業を開始します。

## 最終的なフォルダ構成
```
.
├── .github
│   └── workflows
│       ├── cd_aos.yml・・・・Android用githubActionsの設定ファイル
├── android
│   └── fastlane
│       ├── Appfile・・・・Store関係の設定が書かれたファイル
│       └── Fastfile・・・・fastlaneの設定ファイル
└── 以下省略
```

## 注意！！~今回説明しないもの~
申し訳ないのですが、今回は以下の環境設定方法については省略します。
- GooglePlayStore（deveploper)の登録や設定方法
- Githubの登録や設定方法
- GithubActionsの知識

## 作業の流れ
1. fastlaneを用意する
1. Storeに配信するための設定
1. Fastfileを設定する
1. github actionsを準備
1. あとはコミット！！

## 1. fastlaneを用意する
※作業ディレクトリは、/android/ で実施します。
今回は署名の管理や面倒なビルドコマンドの省略のためにfastlaneを使用しました。
（xcodebuildやgradleなどのコマンド、flutterのコマンドからでもビルドが作成できれば、それでも良いと思います）

fastlaneには多くの便利な"アクション"が定義されています。アプリのビルドであったり、配信であったり、単体テストなども、fastlaneに定義されているアクションを利用可能です。

### 1-1. fastlaneを準備する
fastlaneは、iOSアプリ開発 及び Androidアプリ開発をサポートするツールです。
https://docs.fastlane.tools/ 

fastlaneの導入は以下のコマンドより実行します。

```
xcode-select --install
sudo gem install fastlane -NV
fastlane init
```

fastlane/にファイルが作成されたでしょうか？
作成されたAppfileやFastfileに設定を書き加えていきます。

## 2. Storeに配信するための設定
いよいよfastlaneを使って、ビルドから配信を行うまでの設定を行います、fastlaneの細かな設定はFastfileで行います。
と、、その前に更にいくつかの設定を行います。Storeに配信するための設定です。

### 2-1. fastlane supplyの準備
Android側では、[fastlane supply](https://docs.fastlane.tools/actions/supply/)を使ってAndroidのStore配信を行います。

その準備として、iOSと同じようにStoreアップロード用にAPIなどを用意します。

iOSと比べて、こちらは少し面倒です。。。

#### 2-1-1. 認証用のjsonファイルを作成
[Google Play Console](https://play.google.com/console/developers)を開いて、メニューから「アカウントの詳細」を開いて「ディベロッパーアカウントID」をメモしておきます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782672/b63b1d0d-9f06-cac6-8be3-3342966dbeb7.png)

次に、メニューから「設定」→「APIアクセス」を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782672/fd2c6f0a-6f78-ae3d-e991-9f8749979b98.png)

「リンクするプロジェクトを選択」を押下すると、次のような画面となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782672/cb1989a2-8dac-0e10-6b12-4013c2370b02.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782672/1456711c-f7ab-3ab6-2dee-3120d5fbcda7.png)

そこで「新しいサービスアカウントを作成」を押下します。ダイアログが開くため指示に従っていきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782672/689b03e0-07e1-d1b9-e3f2-afa6b175085b.png)

GCPを開きます。メニューから「サービスアカウント」を開きます。アカウントがない場合は次の画像のような画面が開きます。そこで「＋サービスアカウントを作成」を選びます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782672/e99a8dca-a1a3-dcbc-29f6-74be1af1b7c5.png)

サービスアカウントの作成では以下のような画面になります。サービスアカウント名を入力します。その後に「@...」の部分に、手順2でメモした「ディベロッパーアカウントID」があることを確認して下さい。また、「.iam.gserviceaccount.com」があることも確認して下さい。問題なければ「作成して続行」を押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782672/6bbdd63a-d3df-943d-a8d8-5c7ea0b08120.png)

次のステップではアカウントにロールを設定します。ここでは「サービスアカウントユーザー」を設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782672/228d8c24-8bac-c0f9-d64e-514f35933256.png)

次のステップは特に何もせずに完了とします。
GCPの画面では下記のような状態になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782672/2ae564c0-5eed-9383-0b2c-2801cde50e15.png)

キーがないため、キーを作成します。3点リーダーでメニューを開き、「鍵を管理」を選びます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782672/db5fd8fa-91bf-eac3-b869-bf5b2879790e.png)

次の画面にて「新しい鍵を作成」します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782672/429ba70c-97e7-0728-f41a-0efae2e599cd.png)

JSONを選択していることを確認して、鍵を作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782672/9ac3587f-7392-f897-1258-d003a956a0b2.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782672/e6138a4a-981d-4fa3-a315-f4e23a17f01f.png)

元の画面（GooglePlayConsole）に戻ります。元表示されていたダイアログを閉じる、再リロードすると作成したサービスアカウントの情報が反映されます。　

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782672/6c329d1c-d861-ee65-2477-ddf3cd251af2.png)

ここでアクセス権を設定します。fastlaneのドキュメント上ではAdminが推奨されていますが、リリース関係のみ許可をして「ユーザーを招待」します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782672/3839dbea-bb51-c6cd-a22c-aa6075c97328.png)

ここまででjsonファイルの作成は完了です。

#### 2-1-2. 作成したjsonファイルの確認
まずは下記のコマンドで動きを確認します。

```
fastlane run validate_play_store_json_key json_key:/path/to/your/downloaded/file.json
```

下記の結果が得られれば成功です。

```
[21:08:37]: ------------------------------------------
[21:08:37]: --- Step: validate_play_store_json_key ---
[21:08:37]: ------------------------------------------
+----------+--------------------------------------+
|    Summary for validate_play_store_json_key     |
+----------+--------------------------------------+
| json_key | /Users/hogetaro/Desktop/pc-api-4XXXX  |
|          | 4YYYYYYYYYYYYYYYYYYYYYYYYYYYYY.json  |
| timeout  | 300                                  |
+----------+--------------------------------------+
[21:08:37]: Successfully established connection to Google Play Store.
```

#### 2-1-3. AppFileを設定
今作成したjsonファイルなどをAppfile内に設定します。

```
json_key_file("path/to/your/play-store-credentials.json") package_name("my.package.name")
```

記載を終えたら、

```
fastlane supply init
```

これでsupplyの準備は完了です。

## 3. Fastfileを設定する
fastlaneでは、Fastfileに自動化タスクを「レーン」として定義することで、1コマンド(fastlane {レーン名})でタスクが実行できるようになります。

### 3-1. 署名済みaabファイルをfastlaneで作成
まずはローカル環境で署名済みaabファイルをfastlaneで作成してみましょう。

```
default_platform(:android)
platform :android do

desc "Deploy a new version to the Google Play"
  lane :deploy do
    gradle(
      task: "clean bundle",    
      build_type: "Release",    
      print_command: false,    
      properties: {    
        "android.injected.signing.store.file" => "{ビルドマシン内のjksファイルのパス}",
        "android.injected.signing.store.password" => "{KeyStoreのパスワード}",        
        "android.injected.signing.key.alias" => "{keyエイリアス}",
        "android.injected.signing.key.password" => "{keyエイリアスのパスワード}",
      }
    )
  end
end
```

上記は、laneにdeployを定義しました。
署名つきのaabファイルを作成するfastlaneのアクションを探したところ「gradle」が使えそうなので、
[「gradle」というアクション](https://docs.fastlane.tools/actions/gradle/)を使っています。

propertiesの設定は、ビルド時の署名情報です。リリース時に設定している内容を使います。

こちらをFastflieに記述したら、動かしてみます。

```
fastlane deploy
```

これで以下のフォルダに作成されていればOKです。

```
/build/app/outputs/bundle/release/app-release.aab 
```

ログは以下のような感じです。
```
[20:19:34]: ▸ BUILD SUCCESSFUL in 53s
[20:19:34]: ▸ 154 actionable tasks: 133 executed, 21 up-to-date
[20:19:34]: Couldn't find any new signed apk files...
+------+---------------------+-------------+
|             fastlane summary             |
+------+---------------------+-------------+
| Step | Action              | Time (in s) |
+------+---------------------+-------------+
| 1    | default_platform    | 0           |
| 2    | clean bundleRelease | 53          |
+------+---------------------+-------------+
[20:19:34]: fastlane.tools finished successfully 🎉
```

### 3-2. ローカルで作成した署名済みaabファイルをアップロードする
3-1で作成したFastfileに以下の処理を追加します。

```
    upload_to_play_store(
      aab: "../build/app/outputs/bundle/release/app-release.aab",
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
```

こちらのactionも、play storeに配信するfastlaneのアクションを探して見つけました。
Fastflieに追記したら、動かしてみます。

```
fastlane deploy
```

以下のログを得られたらOKです！

```
[22:37:49]: Preparing aab at path '../build/app/outputs/bundle/release/app-release.aab' for upload...
[22:38:17]: Updating track 'production'...
[22:38:19]: Preparing to upload for language 'ja-JP'...
[22:38:20]: Updating changelog for '4' and language 'ja-JP'...
[22:38:20]: Uploading all changes to Google Play...
[22:38:23]: Successfully finished the upload to Google Play
+------+----------------------+-------------+
|             fastlane summary              |
+------+----------------------+-------------+
| Step | Action               | Time (in s) |
+------+----------------------+-------------+
| 1    | default_platform     | 0           |
| 2    | clean bundleRelease  | 32          |
| 3    | upload_to_play_store | 39          |
+------+----------------------+-------------+
[22:38:23]: fastlane.tools finished successfully 🎉
```

## 4. github actionsを準備
ルートの直下に、以下の構成でファイルを用意します。

```
.github/workflows/{githubActionsの設定ファイル名}.yml
```

バイナリの署名の設定を「app/build.gradle」に移動します。
署名のファイルをリポジトリ管理するようにして、以下のような設定を行います。
※iOSのfastlaneのような署名管理方法があれば教えて欲しいです・・・

```
signingConfigs {
    release {
        storeFile file("../config/KeyStore.jks") //check that the file exists
        storePassword "KeyStorePassword"
        keyAlias "keyAlias"
        keyPassword "keyPassword"
    }
}

buildTypes {
    release {
        signingConfig signingConfigs.release
    }
}
```

署名関係については、
・supplyの設定で作成したjsonファイル
・リリース用のjksファイル
・署名設定を反映したapp/build.gradleファイル
以上が準備できていればOKと思います。

さて、ようやくですが。。yamlファイルの中身は以下を設定しました。

```
name: CD_Android

# Controls when the workflow will run
on:
  # Triggers the workflow on push or pull request events but only for the master branch
  push:
    branches: [ master ]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  aos_distribution:
    runs-on: macos-11
    if: |
      contains(github.event.head_commit.message, '[Release]') == true && contains(github.event.head_commit.message, '[AOS]') == true
    name: Distribution(AOS)
    steps:
    - uses: actions/checkout@v2

    # install fastlane
    - name: Install fastlane
      run: gem install fastlane

    - name: Setup flutter
      uses: subosito/flutter-action@v1
      with:
        flutter-version: '2.5.3'

    - name: Set up tools
      run: |
        flutter pub get
    - name: ExportPath
      run: |
        export PATH=$PATH:${FLUTTER_HOME}/bin/cache/dart-sdk/bin
        export PATH=$PATH:${FLUTTER_HOME}/.pub-cache/bin
    # Build & Deploy
    - name: Run builds(fastlane)
      run: |
        cd android/
        ls
        fastlane deploy
      env:
          JSON_KEY_PATH: ${{ secrets.JSON_KEY_PATH }}
```

GithubSecrets上に環境変数を定義します。

- JSON_KEY_PATH：fastlane supplyの設定で作成したJSONファイルへのパス

### 簡単な説明や補足

#### flutterのリポジトリのため、コミットメッセージに[Release]と[AOS]がある場合にTestFlightへのアップロードを行うこととしました。

```
    if: |
      contains(github.event.head_commit.message, '[Release]') == true && contains(github.event.head_commit.message, '[AOS]') == true
```

#### gradlewに関する以下のエラーが発生した場合は、gradle関連のファイルをコミットします。

```
エラー：　Couldn't find gradlew at path
```

gradle関連のファイルがコミットされていなければコミットします。

```
android/gradle/
android/gradlew
```

#### localPropertiesFileに関する以下のエラーが発生した場合も修正します。

```
エラー：　assert localPropertiesFile.exists()
```

[こちら](https://zenn.dev/endo/books/8de60fbbe786331b6f0f/viewer/example4)を参考にして修正しました。

#### Flutter SDKが見つからないエラーがあったため、SDKのセットアップ処理を追加しました。

```
エラー：　Flutter SDK not found.
```

```
    - name: Set up tools
      run: |
        flutter pub get

    - name: ExportPath
      run: |
        export PATH=$PATH:${FLUTTER_HOME}/bin/cache/dart-sdk/bin
        export PATH=$PATH:${FLUTTER_HOME}/.pub-cache/bin
    - name: Setup flutter
      uses: subosito/flutter-action@v1
      with:
        flutter-version: '2.5.3'
```

## 5. あとはコミット。祈るだけ。

あとはコミットしてみて祈るだけです。

# さいごに
お疲れ様でした。
iOS、Androidの二部構成で記事を作成しています。もしよければiOS側の記事もご覧いただけますと幸いです。Flutterの良さは1コードで2OSに配信できることです。CD環境を整備して、そのリリース速度を加速していければと思います。