【入門】Flutter プロジェクトに GitHubActions（CI/CD） を導入する

## ■ 概要

### 全体図

```mermaid
graph TD
    A[develop → main へ <br/> プルリクを作成] --> B[CI/CDが走る]
    B --> C{テスト成功}
    C -- Yes --> D[レビュー / 承認]
    C -- No --> E[失敗通知を Slack に送信]
    D -->|承認| F[main にマージ]
    D -->|却下| G[処理を中止]
```

### 【事前準備】

Slack に GitHub を追加する

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3282990/d2f61634-a70b-8965-8557-2ff6cfa63b00.png)
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3282990/6e326867-0588-da2a-f5f6-e8fcbd3819b8.png)

[こちらのURL](https://w1625424953-rox450381.slack.com/apps/A0F7XDUAZ--incoming-webhook-?tab=more_info) から Incoming Webhook を Slack に追加し、通知を受信したい channel を選択 → インテグレーションを追加する

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3282990/5b9293cd-537a-7097-8e38-90b12ac83009.png)
![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3282990/915083d1-3e27-03cc-ee6d-2bc96cdd80bb.png)

下記の Webhook URL を取得しておく

![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3282990/ff0a8c40-9aa9-ae15-d72c-515c86928f25.png)

シークレット情報をそのまま記載しておくのはセキュリティ的によろしくないので、リモートリポジトリに環境変数を設定する。（SupabaseのプロジェクトURL / anonキー / Slack の Webhook URL を登録するため）

対象のリポジトリを選択 → Settings → secrets and variables → Actions にて New repository secret を押下しプロジェクトURLやanonキーを記載し Add secret を追加。
下記の様に3つシークレットを作成している状態となる。

![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3282990/f37fde24-4b84-8ed4-28de-952768aec8f4.png)

### ワークフローのセットアップ
以下のコマンドでプロジェクト配下に GitHub Action 用ディレクトリとファイルを作成（ファイル名は好きな名前で可能）

```bash
% mkdir -p .github/workflows
% touch .github/workflows/flutter_analyze_flutter_test.yaml
```

ディレクトリ & ファイルが出来た事を確認

![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3282990/228c81b6-3de4-8751-0815-205ef7eabea2.png)

ワークフローファイルの内容

```yaml
# ワークフローの名前
name: flutter_analyze_flutter_test

# トリガー（mainブランチにプルリクが作成されたら起動）
on:
  pull_request:
    branches:
      - main
    types: [opened]
  workflow_dispatch:

env:
  SLACK_USERNAME: DeployBot
  SLACK_ICON: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
  SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

jobs:
  flutter_analyze:
    runs-on: ubuntu-latest
    concurrency:  # 同時実行を防止
      group: flutter_analyze
      cancel-in-progress: true  # 実行中のジョブをキャンセル

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2

      - name: Install and Cache Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: "3.24.3"
          channel: "stable"
          cache: true
          cache-key: flutter-${{ runner.os }}-3.24.3

      - name: Show Flutter Version
        run: flutter --version

      - name: Get Flutter Packages
        run: flutter pub get
        working-directory: .

      - name: Analyze Flutter Code
        env:
          SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
          SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
        run: flutter analyze

      - name: Run Flutter Tests
        run: flutter test

  slack_notification:  # Slack通知を別ジョブに分離
    runs-on: ubuntu-latest
    needs: flutter_analyze
    if: ${{ failure() }}  # flutter_analyze または flutter test が失敗した場合に実行
    steps:
      - name: Send Slack Notification on Failure
        uses: rtCamp/action-slack-notify@v2
        env:
          SLACK_TITLE: flutter_analyze_flutter_test / Failure
          SLACK_COLOR: danger
          SLACK_MESSAGE: CI/CDが失敗しました
```

### 静的解析（flutter analyze）

- ルートディレクトリにある analysis_options.yaml はこんな感じ。

```yaml
include: package:flutter_lints/flutter.yaml

linter:
  rules:
    avoid_print: true # print文があると警告が出る
    prefer_single_quotes: true # シングルクオートでないと警告が出る
    always_use_package_imports: true # 相対パスでインポートすると警告が出る。
    avoid_annotating_with_dynamic: true # dynamicで型をつけると警告が出る。
```

- 参考

https://zenn.dev/joo_hashi/articles/a4576f96002dae

### テスト（flutter test）

- testフォルダにテストコードを記載

一旦ここでは、テストが確実に成功するため、初期画面に「ログイン」というテキストが表示されることを確認するためのシンプルなテストコードにする。
各々変えてもらえらばと！

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hogehoge/main.dart';

void main() {
  testWidgets('アプリが正しく起動するか確認する', (WidgetTester tester) async {
    // ProviderScopeでアプリをラップ
    await tester.pumpWidget(
      const ProviderScope(
        child: MyApp(),
      ),
    );

    // 初期画面に「ログイン」というテキストが表示されていることを確認
    expect(find.text('ログイン'), findsOneWidget);
  });
}
```


### CI/CD の動作確認

今回トリガーは下記の通り main ブランチへのプルリクを作る事なので適当に作ります。プルリクが作成されると自動的に CI/CD が走ります。

```yaml
# トリガー（mainブランチにプルリクが作成されたら起動）
on:
  pull_request:
    branches:
      - main
```

チェック中（ Details を押下すると今どの段階か詳細がわかる）

![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3282990/3bf7b584-3aaa-1c9c-5d3f-ca55910e95bf.png)

全てのチェックが完了し成功したパターン

![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3282990/99e98750-d61c-277f-822a-7a895721d964.png)

失敗した時はこんな感じで Slack に通知が来る

![10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3282990/8a65de6c-5b01-e058-2e42-b70394702981.png)


以上。