ラクマ Androidアプリ開発でのCIを利用した自動化施策

# はじめに
皆様こんにちは！ラクマでAndroidアプリの開発をしている @tarumzu です。
ラクマAndroidチームでは、開発に専念できるように自動化出来るところは可能な限り自動化しよう！を合言葉に様々なタスクをGithub ActionsやBitriseを使って自動化しています。本章ではそれら自動化施策をご紹介します。

# 自動化施策一覧
まず、どのようなことを行ってきたのかを表にしました。
表の **本章の対象** に○がついてある項目について順番に説明していきます。また、本章で紹介する施策はすべてGithub Actionsを利用したものになります。

※ 「Firebase App Distribution、Google Playへのaabアップロード」に関してはRAKUMA TECH BOOK vol1 「ラクマのiOSアプリを支えるCI/CD環境」で詳細に説明しています。基本的流れは同様のため本章では説明省きます。気になる方はぜひそちら読んでみてください。

| トリガー | タスク | 本章の対象 |
|:--|:--|:--|
| プルリクエストの作成・更新 | ・ktlint実行 <br> ・AndroidLint実行 <br> ・unit test実行 <br> ・オープンソースライセンスチェック <br> ・Milestoneが設定されているかチェック | ○ |
| プルリクエストのオープン | ・Assignees設定 | ○ |
| release, hotfixブランチの作成・更新 | ・master, developブランチへのプルリクエストを作成　<br> （Git-flowで運用しているため） | ○ |
| release, hotfixブランチを作成 | ・ブランチ名のバージョンからVersionNameを更新するプルリクを作成する |  |
| releaseブランチに対してのプルリクエストマージ | ・紐付けられたGithub Issuesをクローズ  　<br> （Issuesに対するCloseキーワードはデフォルトブランチに対してのみ動作するため、デフォルトブランチ以外でもクローズできるようにしている） | ○ |
| develop, master, releaseブランチへのマージ | ・ktlint実行 <br> ・AndroidLint実行 <br> ・unit test実行 |  |
| SlackからBitriseワークフロー実行コマンドを叩く | ・Firebase App Distribution、Google Playへのaabアップロード  |  |

# 自動化施策
## プルリクエストの作成・更新時

ここでは一つのワークフローの中でktlint実行 、AndroidLint実行 、unit test実行、オープンソースライセンスの表記漏れがチェックを行っているのですが長いので各Jobに分けて説明します。

### ktlint

まずは皆さんよく使っているであろうktlintです。プルリクエストを作成したらコードスタイルのチェックを行い、違反している箇所をコメントで指摘してくれるという流れになります。
ここではDanger用のActionとして `MeilCli/danger-action` を利用してKtlintに必要なGemのみに絞ったGemfile、Dangerfile使うことで処理時間の最適化を図っています。微々たるものですがこういう積み重ねが大事！

```yml:.github/workflows/check_pr.yml
name: check pr
on:
  pull_request:
    branches:
      - 'master'
      - 'develop'
      - 'feature/**'
      - 'release/**'
jobs:
  ktlint_and_other:
    runs-on: ubuntu-latest
    timeout-minutes: 30 # デフォルトは360分となっていて長すぎるので30分を設定
    strategy:
      matrix:
        ruby: [ '2.7' ]
    steps:
      # 連続でpushした際などに前回のジョブが残っていたら停止させる
      - name: Cancel Previous Runs
        uses: styfle/cancel-workflow-action@0.9.1
        with:
          access_token: ${{ github.token }}
      - uses: actions/checkout@v2
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: ${{ matrix.ruby }}
      - name: Cache Gradle
        uses: actions/cache@v2
        id: cache_gradle
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
          restore-keys: |
            ${{ runner.os }}-gradle-
      - uses: actions/cache@v1
        name: Cache Bundler
        with:
          path: vendor/bundle
          key: ${{ runner.os }}-${{ matrix.ruby }}-gems-${{ hashFiles('.github/danger/Gemfile') }}
          restore-keys: |
            ${{ runner.os }}-${{ matrix.ruby }}-gems-
      - name: Run ktlintCheck
        run: ./gradlew --no-daemon ktlintCheck
      # MeilCli/danger-action使うことでGemfile、Dangerfileを分けることが出来る
      - uses: MeilCli/danger-action@v5
        if: always()
        name: Run Danger
        with:
          plugins_file: '.github/danger/Gemfile'
          install_path: 'vendor/bundle'
          danger_file: '.github/danger/ktlint_and_other.Dangerfile'
          danger_id: 'danger-ktlint'
        env:
          DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}

...

```

### AndroidLint

続いてはAndroidLintです。やっていることはKtlintとほぼ同じですが、最後にJobの結果を `notify-result-action` というアクションを使ってプルリクエストにコメントで通知するようにしています。今までは起票者がCIでエラーになったことに気づきにくく、コードの修正が遅れることが多かったための施策です。
`notify-result-action` に関しては次で説明します。

```yml:.github/workflows/check_pr.yml
jobs:

...

  android_lint:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    strategy:
      matrix:
        ruby: [ '2.7' ]
    steps:
      - name: Cancel Previous Runs
        uses: styfle/cancel-workflow-action@0.9.1
        with:
          access_token: ${{ github.token }}
      - uses: actions/checkout@v2
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: ${{ matrix.ruby }}
      - name: Cache Gradle
        uses: actions/cache@v2
        id: cache_gradle
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
          restore-keys: |
            ${{ runner.os }}-gradle-
      - uses: actions/cache@v1
        name: Cache Bundler
        with:
          path: vendor/bundle
          key: ${{ runner.os }}-${{ matrix.ruby }}-gems-${{ hashFiles('.github/danger/android_lint.Gemfile') }}
          restore-keys: |
            ${{ runner.os }}-${{ matrix.ruby }}-gems-
      - name: run android lint
        run: ./gradlew --no-daemon :app:lintProductionRelease
      - uses: MeilCli/danger-action@v5
        if: always()
        name: Run Danger
        with:
          plugins_file: '.github/danger/android_lint.Gemfile'
          install_path: 'vendor/bundle'
          danger_file: '.github/danger/android_lint.Dangerfile'
          danger_id: 'danger-android-lint'
        env:
          DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - name: notify result
        if: always()
        uses: ./.github/workflows/composite/notify-result-action
        with:
          job-title: 'Android Lint'
          github-token: ${{secrets.GITHUB_TOKEN}}

...

```

### Composite Action `notify-result-action` について
AndroidLint, UnitTest, ライセンスチェック結果についてはプルリクエストにコメントとして通知するようにしています。ここではComposite Actionを利用して処理を再利用可能なstepにしています。
2021/08にusesが利用できるようになったことでようやく使えるレベルになったと思います。

```yml:.github/workflows/composite/notify-result-action/action.yml
name: "Notify result"
inputs: # 呼び出し元から値を受け取れるようにするため変数を定義
  job-title:
    required: true # 必須の変数にするためにrequiredはtrueに
  github-token:
    required: true
runs:
  using: "composite"
  steps:
    # body-includesに指定したコメントを探す
    - name: Find Comment
      uses: peter-evans/find-comment@v1
      id: comment
      with:
        issue-number: ${{ github.event.pull_request.number }}
        comment-author: 'github-actions[bot]'
        body-includes: ${{ inputs.job-title }} results
    # すでに実行結果がコメントされている場合はコメントを削除＆新規作成することで起票者に通知させる
    - uses: actions/github-script@v5
      with:
        github-token: ${{ inputs.github-token }}
        script: |
          if (process.env.COMMENT_ID === '') {
            return;
          }
          github.rest.issues.deleteComment({
            issue_number: ${{ github.event.pull_request.number }},
            owner: context.repo.owner,
            repo: context.repo.repo,
            comment_id: process.env.COMMENT_ID
          })
      env:
        COMMENT_ID: ${{ steps.comment.outputs.comment-id }}
    # job.statusをプルリクエストにコメントする
    - name: ${{ inputs.job-title }} results
      uses: peter-evans/create-or-update-comment@v1
      with:
        issue-number: ${{ github.event.pull_request.number }}
        body: |
          ${{ inputs.job-title }} results: ${{ job.status }}!
        edit-mode: append
```

`notify-result-action` の処理の流れは次のようになっています。
1. `peter-evans/find-comment` を使ってすでにコメントしてあるか確認
2. すでにコメントされていたら `actions/github-script` を使ってコメントを削除[^1]
3. `peter-evans/create-or-update-comment` を使ってコメントを追加

~~Composite Actionを実際に使ってみて１点だけ残念な点がありました。それはif構文 `jobs.<job_id>.steps[*].if` が使えないという点です。今回、すでにコメントがあったら削除するという判定をscript内に入れているのはそのためです。
if構文使えるようになれば最高なんですけどね〜。Githubさん対応してくれないかな？|ω･)ﾁﾗ~~

if構文にも対応したそうです。
https://github.blog/changelog/2021-11-09-github-actions-conditional-execution-of-steps-in-actions/

[^1]: `peter-evans/create-or-update-comment` はコメントの更新にも対応していますが都度メンションしたいので、コメントを削除、追記するようにしています。

### Licenseチェック

cookpadさんのライブラリであるLicenseToolsPluginを使ってオープンソースライセンス表記の記載漏れがないかをチェックしています。ライブラリは使わせてもらっている立場なので抜けもれなく表記したいところです。

```yml:.github/workflows/check_pr.yml
jobs:

...

  check_licence:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Cancel Previous Runs
        uses: styfle/cancel-workflow-action@0.9.1
        with:
          access_token: ${{ github.token }}
      - uses: actions/checkout@v2
      - name: Cache Gradle
        uses: actions/cache@v2
        id: cache_gradle
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
          restore-keys: |
            ${{ runner.os }}-gradle-
      - name: check license
        run: ./gradlew --no-daemon checkLicenses
      - name: notify result
        if: always()
        uses: ./.github/workflows/composite/notify-result-action
        with:
          job-title: 'Check License'
          github-token: ${{secrets.GITHUB_TOKEN}}

...

```

### UnitTest

ここでは各モジュールのテストを行ったあとに `mikepenz/action-junit-report` を利用してテストのエラーケースをハイライトするようにしています。どこがエラーになっているかがひと目で分かるので便利です。

```yml:.github/workflows/check_pr.yml
jobs:

...

  unit_test:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Check out
        uses: actions/checkout@v2
      - name: Set up JDK
        uses: actions/setup-java@v2
        with:
          distribution: 'zulu'
          java-version: 11
      - name: Run unit test
        run: |
          ./gradlew --no-daemon app:testProductionReleaseUnitTest \
            network:testReleaseUnitTest \
            data:repository:testReleaseUnitTest \
            model:testReleaseUnitTest
      - name: Publish Test Report
        if: cancelled() != true
        uses: mikepenz/action-junit-report@v2
        with:
          report_paths: '**/build/test-results/*/TEST-*.xml'
      - name: notify result
        if: always()
        uses: ./.github/workflows/composite/notify-result-action
        with:
          job-title: 'Unit Test'
          github-token: ${{secrets.GITHUB_TOKEN}}
```


## プルリクエストのオープン・レビュー準備完了時

プルリクエストのオープン・レビュー準備完了時はAssigneesやReviewersを自動的に設定したいですよね。
塵も積もれば山となるということで、ここでは`hkusu/review-assign-action` を使って Assigneesにプルリクエストの作成者を自動設定するようにしています。
※ Reviewersの設定に関してはGithubのCODE OWNERSを使って自動設定しているので本章では触れません。

```yml:.github/workflows/review-assign.yml
name: Review Assign

on:
  pull_request:
    types: [opened, ready_for_review]

jobs:
  assign:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: hkusu/review-assign-action@v1.0.0
        with:
          assignees: ${{ github.actor }}
```

## release, hotfixブランチの作成・更新時

release, hotfixブランチを作成したら何をするのか？そうです。master, developブランチへのプルリクエストを作ります。手動で作るのは面倒なのでここもCIで作成しちゃいましょう。
プルリクエストの作成には `git-pr-release` というGemライブラリを利用します。

```yml:.github/workflows/git-pr-release.yml
name: git-pr-release

on:
  push:
    branches:
      - release/**
      - hotfix/**

jobs:
  create-release-pr:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    strategy:
      matrix:
        ruby: [ '2.7' ]
    steps:
      # 実行中のジョブをキャンセル
      - name: Cancel Previous Runs
        uses: styfle/cancel-workflow-action@0.9.1
        with:
          access_token: ${{ github.token }}
      # リポジトリのチェックアウト
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: ${{ matrix.ruby }}
      # ブランチ名の取得
      - name: get branch name
        id: vars
        run: echo "::set-output name=branch_name::${GITHUB_REF#refs/heads/}"
      # Releaseブランチがプッシュされた場合、Masterブランチに向けたプルリクエストを作成
      - name: Create a release pull request for release branch
        if: contains(steps.vars.outputs.branch_name, 'release')
        env:
          GIT_PR_RELEASE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GIT_PR_RELEASE_BRANCH_PRODUCTION: master
          GIT_PR_RELEASE_BRANCH_STAGING: ${{ steps.vars.outputs.branch_name }}
          GIT_PR_RELEASE_LABELS: release
          GIT_PR_RELEASE_TEMPLATE: .github/.git-pr-release-template
        run: |
          gem install -N git-pr-release -v "1.9.0"
          git-pr-release --no-fetch
      # Hotfixブランチがプッシュされた場合、Masterブランチに向けたプルリクエストを作成
      - name: Create a release pull request for hotfix branch
        if: contains(steps.vars.outputs.branch_name, 'hotfix')
        env:
          GIT_PR_RELEASE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GIT_PR_RELEASE_BRANCH_PRODUCTION: master
          GIT_PR_RELEASE_BRANCH_STAGING: ${{ steps.vars.outputs.branch_name }}
          GIT_PR_RELEASE_LABELS: release
          GIT_PR_RELEASE_TEMPLATE: .github/.git-pr-hotfix-template
        run: |
          gem install -N git-pr-release -v "1.9.0"
          git-pr-release --no-fetch
      # Release, Hotfixそれぞれから、Developブランチに向けたプルリクエストをドラフト状態で作成
      # 差分がなくてもエラーなしで終了する
      - name: Create a develop pull request
        uses: repo-sync/pull-request@v2
        with:
          source_branch: ${{ steps.vars.outputs.branch_name }}
          destination_branch: develop
          pr_title: ${{ steps.vars.outputs.branch_name }} into develop
          pr_body: release branch into develop
          pr_label: release
          pr_allow_empty: true
          pr_draft: true
          github_token: ${{ secrets.GITHUB_TOKEN }}
```

`git-pr-release` はテンプレートファイルを用意してあげることでプルリクエストに記載する内容を定義出来ます。１行目がタイトル、pull_requests.eachでマージ対象のコミットの一覧を出力します。

```:.github/.git-pr-release-template
Release <%= Time.now.strftime("%Y-%m-%d") %>

## コミット対象
各自が担当した修正が下記一覧にあるかチェックする。

<% pull_requests.each do |pr| -%>
<%= pr.to_checklist_item %>
<% end -%>

---

## リリース作業

...

```


## releaseブランチに対してのプルリクエストマージ時

ラクマAndroidチームでは、バグや改善系などのプロジェクトに紐付かないタスクはGithub Issuesで管理しています。
プルリクエストとIssuesをcloseなどのキーワードを紐付けることで自動でクローズしてくれて便利なのですが、デフォルトブランチ以外に対するプルリクエストのマージでは自動クローズはされません。
されないならCIでクローズしちゃおう！ということで下記のワークフローです。

```yml:.github/workflows/close-issue-for-release-branch.yml
name: close issue for release branch

on:
  pull_request:
    types:
      - closed
    branches:
      - 'release/**'

jobs:
  # リリースブランチにマージされた Pull Request に Issue のリンクがあれば、Issue を閉じる。
  close-issue:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    if: github.event.pull_request.merged == true
    steps:
      - name: Closes issue
        uses: ldez/gha-mjolnir@v1.0.3
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

# おわりに

今後もさまざまな処理の自動化を検討しています。例えばGoogle Play Consoleを使った段階公開をCI + slackワークフローの組み合わせでカジュアルに実行することや、テストカバレッジの導入など。

ラクマでは一緒にこれらの施策を行い、Androidアプリ開発してくれる仲間を絶賛募集中です！少しでも興味を持ってくれた方がいましたらTwitter（@tarumzu）などでもお気軽にお声がけください。

# 参考文献
- https://qiita.com/hkusu/items/39eb92dbd4d6db8a14d8
- https://blog.beachside.dev/entry/2021/09/24/220000
- https://github.com/x-motemen/git-pr-release