はじめに
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%ほど実行時間が改善されています
それ以前は実行時間が毎回一定の時間で推移していたのが、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 のページで任意の名前の変数を設定することができます
たとえばこのように複数個のブランチを改行区切りで指定しておきます
この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や日次配信などはおそらく多くのチームで実施されていることだと思いますが、差分のみを実行対象にしたり動的に実行ブランチを指定できるようにしたりとより便利になる工夫を加えています
まだやりたいことができていないところもあるので引き続き効率的、安全な開発環境の構築をめざしていきます