はじめに

AWA Androidチームの向井です AndroidチームではCI/CDによって日々の作業を自動化しています

この記事ではAWA Androidチームの開発で運用しているCI/CDについて紹介していこうと思います

基本的にAndroid開発の話なので具体的な内容についてはAndroid前提となってしまうのですが、どういった作業を自動化しているのかという観点ではAndroidに限らず活用できる部分もあると思います

CI

KtLint、Lint、Unit Test

CIではktLint、Android Lint、Unit Testを実行しています

最初はこれらのタスクを実行するだけで運用していたのですが、コードベースが大きくなり次第に実行時間が長くかかるようになってしまいました

これらのタスクはGithub Actionsを使って実行していますが、並列数も多くはなく、PRを出すたびにCIを実行しているためCIの時間が開発のボトルネックになってしまうリスクがあります

そこで、Dropbox社が公開しているAffectedModuleDetector というOSSのGradle pluginを活用してCI時間を短くする工夫をしています

前提としてプロジェクトがマルチモジュールであること、Gitで管理されていることが必要となります

AffectedModuleDetectorはGitの変更履歴から差分を分析してどのモジュールに対して変更があったのかを検知してくれます

さらにモジュール間の依存関係をもとにして、あるモジュールの変更が他のどのモジュールに影響を与え得るかを検出してくれます

これを活用して、Unit Testでは変更があったモジュールとそのモジュールに依存しているモジュールのみ、ktLintとAndroid Lintでは変更があったモジュールのみを実行対象とするカスタムのGradleタスクを作りました

下記はLintの実行時間の推移ですが、2023/01頃にAffectedModuleDetectorを導入して20〜30%ほど実行時間が改善されています

AffectedModuleDetectorによるLint時間削減の様子

それ以前は実行時間が毎回一定の時間で推移していたのが、PRによって変更されているモジュールが異なるため実行時間が一定ではなくなっているのがわかります

AWAのマルチモジュール化はまだ途上で、モジュールによっては1モジュールあたりのコード量が多いところもあり、コード量が多いモジュールをよく変更する開発内容だと効果が薄まってしまう課題があります

これに対してはよりマルチモジュール化を進めていけばこういったケースが減り実行時間も改善されると見込んでいます

CD

AWAでは内部向けに継続的な最新アプリの配信、リリース作業をGithub Actionsを使って自動化しています

ここではそれらのworkflowについて紹介していきます

共通workflow

まず、CD系ではほとんどの処理が共通しています

  • ブランチを指定する
  • 環境を指定する
  • ビルドする
  • slackに通知する

というのがすべてについて共通していることです

それに加えて、workflowによっては成果物をどこかにアップロードするという処理が含まれます

workflowごとにこれらすべてを各々記述していると重複が多くメンテナンスコストも高くなってしまうので共通化したworkflowを用意して運用しています

すべて載せると長くなってしまうので一部抜粋したものがこちらです

on:
  workflow_call:
    inputs:
      branch:
        description: build branch
        type: string
        required: false
      environment:
        description: build environment
        type: string
        required: true
        default: develop
        # - develop
        # - staging
        # - product
      application: # ビルドするアプリケーションのgradleモジュール名
        description: build application
        type: string
        required: true
        default: app
      deploy-app-distribution: # true: app distributionにアップロードする
        description: deploy to app distribution
        type: boolean
        required: true
        default: false
      deploy-google-play: # true: google playにアップロードする
        description: deploy to google play
        type: boolean
        required: true
        default: false
jobs:
  deploy:
    env:
      〜
      〜
    steps:
      - name: inputsから受け取ったbranchをcheckout
        〜
      - name: inputsから受け取ったアプリ、環境を指定してビルド
        〜
      - name: githubのartifactへのアップロード
        〜
      - name: google playへのアップロード
        if: inputs.deploy-google-play
        〜
      - name: firebase app distributionへのアップロード
        if: inputs.deploy-app-distribution
        〜
      - name: slackへの通知
        〜

このworkflowはworkflow_callとして作ることで、他のworkflowから呼び出せるようになっています

workflowごとに異なる処理についてはworkflowのinputsとして与えられるようにしています

inputsではブランチ、環境、ビルド対象アプリケーション、各配布先へのアップロードの有無を設定できるようにしています

リリース

AWAのAndroidアプリでは複数のフォームファクターに対応しており、mobile、TV、Wear、Automotive向けのアプリをリリースしています

これらのコードベースはシングルリポジトリ、マルチモジュールで管理しているためどのフォームファクター用のアプリのビルドでも同じリポジトリを対象にビルドすればいいようになっています

ただし、ビルド対象のアプリケーションは都度指定する必要があるため先ほどの共通workflowではビルド対象のアプリを指定できるようにしています

さらに成果物のaabをPlay Consoleにアップロードする際に手動でアップロードすると意図しないファイルをアップロードしてしまうリスクがあるため、ビルドからPlay Consoleへのアップロードまでを自動化しています

また、リリースの記録をGithubのreleaseに残るようにしたかったため、Githubのreleaseを作る際にタグをつくり、そのタグをトリガーにしてworkflowを呼び出す運用にしています

具体的なworkflowはこのようになっています

on:
  push:
    tags:
      - app/v[0-9]+.[0-9]+.[0-9]+
jobs:
  release-app:
    uses: ./.github/workflows/reusable_deploy.yml
    secrets: inherit
    with:
      environment: product
      application: app
      deploy-app-distribution: false
      deploy-google-play: true
    concurrency:
      group: ${{ github.workflow }}-${{ github.ref }}
      cancel-in-progress: true</pre>

./.github/workflows/reusable_deploy.yml というのが共通のworkflowで、リリースのworkflowではほぼこれを呼び出しているだけです

これはリリースのworkflowなので、firebaseにはアップロードせずにGoogle Playにはアップロードするという設定で呼び出しています

同様に、TVアプリであればapplicationの部分をtv、Automotiveであればautomtiveにしているだけのworkflowをそれぞれ用意しています

日次配信

常に最新版のアプリの動作を確認できるように、日次でメインのブランチに対してビルドしてapp distributionを通じて配信しています

加えて、リリース前のテスト期間ではリリース用のブランチに対しても日次配信をしています

Github Actionsでは定期的に決まった時間に決まったworkflowを実行するためにschedule機能を使うことができます

ただし、scheduleを使って自動で実行する場合はworkflow_dispatchを使って手動で実行するときのように任意のブランチを指定して実行することができません

もちろんworkflowの内容を変更すれば実行するブランチを変更することができますが、リリースのたびに変更を加えることは手間がかかってしまいます

そこで、Githubのvariables機能を使うことにしました

variablesはGithubのSetting → Actions secrets and variables のページで任意の名前の変数を設定することができます

たとえばこのように複数個のブランチを改行区切りで指定しておきます

Variables機能による設定例

このVariableを使ってビルドしたい任意のブランチを指定して、workflowから変数を読み出すことで動的に設定できる任意のブランチに対して日次配信をすることができます

これらのブランチに対して複数の環境でアプリを作成したかったので Github Actionsのmatrix機能を使ってブランチと環境の組み合わせに対して先程の共通workflowを使ってビルド、配信を実行しています

workflowのmatrix: に対して値を渡すときは [ “xxx”, “yyy” ] の形式で渡す必要があるため、Variableから読み出した変数を少々泥臭くshellで変換しています

on:
  schedule:
    # JSTの2時に実行. GMTでいうと17時.
    - cron: "0 17 * * *"
  workflow_dispatch:

jobs:
  setup_build_branches:
    runs-on: ubuntu-latest
    outputs:
      build_branch_list: ${{ steps.set_build_branch_list.outputs.build_branch_list }}
    steps:
      - name: setup build branch list
        id: set_build_branch_list
        run: |
          # 改行区切りの変数をjson配列に変換するため、一度ファイルに書き込んでから ["release/xxx","feature/xxx"] のような形式に変換する
          echo -n "${{ vars.SCHEDULED_BUILD_BRANCHES }}" > /tmp/branchs.txt
          branch_list=$(cat /tmp/branchs.txt | tr -d "\r" | tr "\n" "," | sed 's/^/["/g;s/,/\",\"/g;s/$/"]/g')
          echo "build_branch_list=${branch_list}" >> $GITHUB_OUTPUT
  deploy:
    needs: setup_build_branches
    # 任意のブランチをdevelopとproductで定期配信する
    strategy:
      fail-fast: false
      matrix:
        environment: [ develop, product ]
        branch: ${{fromJson(needs.setup_build_branches.outputs.build_branch_list)}}
    uses: ./.github/workflows/reusable_deploy.yml
    secrets: inherit
    with:
      branch: ${{ matrix.branch }}
      environment: ${{ matrix.environment }}
      application: app
      deploy-app-distribution: true
      deploy-google-play: false
    concurrency:
      group: ${{ github.workflow }}-${{ matrix.branch }}-${{ matrix.environment }}
      cancel-in-progress: true</pre>

複数の任意のブランチに対して日次配信する仕組みを整えたことによって、リリース前のテスト期間中のうっかりビルド忘れを防ぎ、テスト開始が遅れるリスクもなくせました

matrix、variable、共通workflowがいい感じに連携できて個人的に気に入っているworkflowです

apk/aab作成

mobileのアプリ以外はapp distributionで配信することができません

そのため、wearやTVで実機確認をするときのためにapkをつくるだけのworkflowを用意しています

していることは共通workflowを呼び出してどこにも配信しない設定にしているだけです

番外編

リリース用ブランチからメインのブランチへのPRの自動作成

Gitのブランチの運用として、メインのブランチからリリース用のブランチを作るようにしています

そしてそのリリースブランチに対してテストを実施しています

そのため、もしテストで不具合が発覚した場合はリリース用のブランチに対して修正を加えていっています

しかし、そうするとメインのブランチとの乖離が大きくなりコンフリクトの可能性が増えてしまうためリリース用ブランチに変更が入ったらできるだけすぐにメインのブランチにも同じ差分を取り込むようにしています

このときに手動でPull Requestを作る運用だと手間がかかってしまうのと、うっかり忘れてしまうリスクがあります

これを避けるためにリリース用のブランチに変更が入ったときに自動でメインのブランチに対してPRを出すworkflowを用意しています

やり方としてはリリース用のブランチのに対して変更が入ったときにgithubのAPIを実行してPRを作っているだけです

これのおかげで乖離が起こっている時間の短縮と手間の削減、うっかり忘れの防止ができています

おわりに

AWAのAndroidチームで運用しているCI/CDについて紹介しました

Lintや日次配信などはおそらく多くのチームで実施されていることだと思いますが、差分のみを実行対象にしたり動的に実行ブランチを指定できるようにしたりとより便利になる工夫を加えています

まだやりたいことができていないところもあるので引き続き効率的、安全な開発環境の構築をめざしていきます