こんにちは、そしてはじめまして、今年2月にAndroidエンジニアとして入社したばかりのMeilCliです。先日プロジェクトマネージャーのtummyがユーザー視点でのクラシルAndroidアプリの改善を紹介しましたが、今回は自分が入社してから改善されてきた開発者視点でのクラシルAndroidアプリについてご紹介できればと思います
Detektの導入
クラシルAndroidアプリの開発にはKotlinコードを静的解析するツールとして今までktlintが使用されてきました。類似ツールとしてdetektというものがあり、このdetektにはktlintの解析ルールが収録されているため、解析ルールベースで考えると上位互換にあたります。そのため、ktlintからdetektへ移行することにしました
detektにはGradle Pluginが用意されているためAndroidプロジェクトへの導入はスムーズにできますが、クラシルAndroidのプロジェクトは現在マルチモジュールプロジェクトへの移行をしている最中なのでモジュールが複数存在しています。ktlintを運用しているときはマルチモジュールに対応した設定をしていなかったため、アプリケーションモジュールとなるapp
モジュールのみにktlintを実行している形になっていたのでdetektへの移行と同時にマルチモジュールに対応した設定を行うことにしました
各モジュールへの設定をいちいち書くのは面倒であり管理コストが増えてしまうので、今回はbuildSrc
にGradle Pluginを作成し、各モジュールのbuild.gradle
で作成したGradle Pluginを適用するだけでよい形にしました
まずプロジェクトのルート直下にbuildSrc/build.gradle.kts
を作成し、buildSrcのための設定を記載します
plugins { `kotlin-dsl` } repositories { jcenter() google() maven("https://plugins.gradle.org/m2/") } dependencies { implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.6.0") }
そうしたらbuildSrc
がモジュールのように識別されるのでDetekt用のGradle Pluginのソースコードを追加していきます
buildSrc/src/main/kotlin/com/kurashiru/gradle/plugins/DetektConfigPlugin.kt:
package com.kurashiru.gradle.plugins import io.gitlab.arturbosch.detekt.extensions.DetektExtension import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.dependencies import java.io.File class DetektConfigPlugin : Plugin<Project> { override fun apply(project: Project) { project.extensions.findByType(DetektExtension::class.java)?.apply { toolVersion = "1.6.0" buildUponDefaultConfig = true parallel = true config = project.files("${project.rootProject.rootDir}/detekt.yml") reports.apply { xml.enabled = true val name = project.projectDir.toRelativeString(project.rootProject.rootDir).replace("/", "_") xml.destination = File("${project.rootProject.rootDir}/reports/detekt/${name}.xml") } } project.dependencies { add("detektPlugins", "io.gitlab.arturbosch.detekt:detekt-formatting:1.6.0") } } }
buildSrc/src/main/resources/META-INF/gradle-plugins/DetektConfigPlugin.properties:
implementation-class=com.kurashiru.gradle.plugins.DetektConfigPlugin
このGradle Pluginではルート直下のdetekt.yml
をconfigとして指定し、CI用にルート直下のreports/detekt
フォルダーにレポートを書き出すようにしています
あとは通常と同じようにdetektのGradle Pluginの依存関係をルート直下のbuild.gradle
に書いて、detektを適用するモジュールでは
apply plugin: 'io.gitlab.arturbosch.detekt' apply plugin: 'DetektConfigPlugin'
この2行を追加すれば良いという形になりました
detektはホームページでたくさんのRuleSetsが紹介されるほど細かいところまで静的解析し、問題を検知・報告してくれるツールです。便利な反面、RuleSetsの設定をチューニングしなければ不要な警告を出してしまいます。そのため今後はクラシルAndroidアプリ開発に適したRuleSetsのチューニングをしていこうと考えています、あと最新版のdetektにバージョンアップしないとですね笑
GitHub Actionsの導入
クラシルAndroidアプリの開発では以前からAmazonのCodeBuildを使用してきました。CodeBuildではPullRequestでのビルドチェックやストアリリース時のリリースビルドを行っていましたが、それ以外でもCIで自動化してみたいよねということで、OSSでは無料でありプライベートリポジトリーでも無料枠の多いGitHub Actionsを部分的にですが導入していくことになりました
クラシルAndroidでは様々なGitHub Actionsのワークフローを運用していますが、そのうちの何点かご紹介します
PullRequestのタイトルチェック
クラシルAndroidでは社内的なリリースノート作成のためにPullRequestのタイトルに対して一定の規則を設けています。具体的には[improvement] タイトル
のようにタグをprefixとして付けるだけですが、開発者がこの規則通りにPullRequestを作成できているかを人の手でチェックするのは大変なため、GitHub Actionsで自動化しました
name: プルリクのタイトルチェック on: pull_request: types: [opened, edited, reopened] jobs: check: runs-on: ubuntu-latest steps: - uses: octokit/request-action@v2.x # 条件式は長いので省略しています if: startsWith(github.event.pull_request.title, '[improvement]') == false with: route: POST /repos/:repository/pulls/:pull_number/reviews repository: ${{ github.repository }} pull_number: ${{ github.event.pull_request.number }} event: "COMMENT" body: "PRのタイトルは`[タグ] .+`という形式にしてください" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
このワークフローではoctokit/request-actionを使用しています。これを使うことによってワークフローのステップにてGitHubのREST APIを楽に呼び出すことができるので、「こういう場合にこういうコメントを付ける」といったワークフローを作るときに便利です
PullRequestのマイルストーンチェック
タイトルと同じように、マイルストーンも社内的なリリースノート作成のために設定をルール化しています
name: プルリクのマイルストーンチェック on: issues: types: [demilestoned] pull_request: types: [opened] jobs: check: runs-on: ubuntu-latest steps: - uses: octokit/request-action@v2.x name: Check PR Demilestoned if: github.event_name == 'issues' && github.event.action == 'demilestoned' && github.event.issue.pull_request != null with: route: POST /repos/:repository/pulls/:pull_number/reviews repository: ${{ github.repository }} pull_number: ${{ github.event.issue.number }} event: "COMMENT" body: "PRには必ずマイルストーンを付けてください" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: octokit/request-action@v2.x name: Check PR Opened if: github.event_name == 'pull_request' && github.event.action == 'opened' && github.event.pull_request.milestone == null with: route: POST /repos/:repository/pulls/:pull_number/reviews repository: ${{ github.repository }} pull_number: ${{ github.event.pull_request.number }} event: "COMMENT" body: "PRには必ずマイルストーンを付けてください" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
先にワークフローファイルをお見せするとこのような形で運用しています。octokit/request-actionを使ってコメントで通知するのはタイトルチェックと同じですね
さて、このワークフローではトリガーとなるイベントにissues
のdemilestoned
を使用しています。デフォルトブランチに直Pushする開発体系ならばこういったワークフローの作成を難なくできるかと思いますが、クラシルAndroidプロジェクトではPullRequestを介してデフォルトブランチにpushする形式を取っています
このワークフローを開発していたときのGitHub Actionsの挙動では、PullRequestで変更されたワークフローファイルはPullRequest上のCheckではpull_request
のイベントしかトリガーを引いてくれないという仕様になっている様子でした。そのため最初このワークフローを作成するときにissues
のdemilestoned
に対する動作を確認できなくてつまづきました。「マイルストーンが外された」や「マイルストーンが付いていない状態」をトリガーにするワークフローを作成したいときは是非参考にしていただければと思います
PullRequestに限らずIssueについても動作するサンプルをMeilCli/actionsに置いていますのでそちらもよければどうぞ
ちなみにですが、GitHub上でマイルストーンを貼り替える作業をするとdemilestoned
のイベントが発火するらしく、このワークフローではそのままコメントを付けてしまうのでdelayをかけてAPIから情報を取ってくるなどの改善点があります。そのうち直そうと考えていますm(__)m
Detekt
さて、最初に紹介したDetektですが、PullRequestで自動でチェックしてくれなければ意味がありません。そこでGitHub ActionsでDetektを実行し、Dangerを使ってDetektの結果をコメントで通知するようにしました
クラシルAndroidではCodeBuildのほうでもDangerを運用しているためすでにGemfile
やDangerfile
が存在していました。GitHub Actionsではそれらのファイルと共用するのは避けたかったため、.github
配下にそれらのファイルを配置することにしました。Gemの解決にBundlerを使用している都合上、そのような配置をすると依存関係を解決できなくなるという状態に陥ったため、過去に自分が勉強がてらに作成したMeilCli/danger-actionをGemfile
の位置にとらわれないように改修して使用しています
.github/Gemfile:
# frozen_string_literal: true source "https://rubygems.org" git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } # gem "rails" gem 'danger' gem 'danger-checkstyle_format', '~> 0.0.1'
.github/Dangerfile:
github.dismiss_out_of_range_messages checkstyle_format.base_path = Dir.pwd report_files = Dir.glob("reports/detekt/**") for report_file in report_files do checkstyle_format.report report_file end
.github/workflows/ci.yml:
name: CI on: [pull_request] jobs: detekt: runs-on: ubuntu-latest env: GITHUB_USER: "github-bot" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.action == 'ready_for_review' steps: - uses: actions/checkout@v2 - uses: actions/setup-ruby@v1 with: ruby-version: '2.6' - uses: actions/setup-java@v1 with: java-version: 1.8 - uses: actions/cache@v1 name: Cache Gradle with: path: ~/.gradle/caches key: ${{ runner.os }}-gradle-${{ hashFiles('buildSrc/build.gradle.kts') }}-${{ hashFiles('buildSrc/src/**') }} restore-keys: | ${{ runner.os }}-gradle-${{ hashFiles('buildSrc/build.gradle.kts') }}- ${{ runner.os }}-gradle- - uses: actions/cache@v1 name: Cache Bundler with: path: vendor/bundle key: ${{ runner.os }}-gems-${{ hashFiles('.github/Gemfile') }} restore-keys: | ${{ runner.os }}-gems- - name: Grant permission run: chmod +x gradlew - name: Run Detekt run: ./gradlew detekt - uses: MeilCli/danger-action@v4 name: Run Danger with: plugins_file: '.github/Gemfile' install_path: 'vendor/bundle' danger_file: '.github/Dangerfile' danger_id: 'danger-detekt' env: DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
さて、GitHub ActionsやDangerを運用されている方々にとってはあまり目新しいワークフローではないかもしれませんが、Detektを導入するにいたって、開発者がPullRequestに変更を加えてから静的解析のコードレビューがされるまでの時間を減らす点でこだわることにしました
具体的にはactions/cacheを使って依存関係やタスクの実行結果などをキャッシュすることによって、キャッシュが存在する場合にキャッシュを利用することで早く動作させるようにしました
(参考数値としてktlintの頃はいろいろな処理を待機していたために約8分ほどかかっていました)
今後について
開発者体験という視点で言えばクラシルAndroidプロジェクトはたくさんの課題があります。その中で直近でやろうと考えてるものをご紹介し、これを次回予告にします
- 長年放置し続けてきたlintの対応、不要になったリソースの削除
- 使用しているOSSなどのライセンス表記のための集計ツールをMeilCli/Librarianに置き換える
- これによってライブラリーやライセンス通知の集計精度が向上する見込みです
- ライブラリーアップデートの自動検出・通知
- モジュールの
build.gradle
の共通な宣言部分をbuildSrc
へまとめる
最後にdelyではエンジニアを大募集しています。ランチ会などを定期的に開催してるので、中の人と話してみたい!という風に興味がある方は是非お声がけください