クラシル開発ブログ

クラシル開発ブログ

クラシルAndroidプロジェクトの開発者体験の向上を頑張ってます!

こんにちは、そしてはじめまして、今年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を使ってコメントで通知するのはタイトルチェックと同じですね

さて、このワークフローではトリガーとなるイベントにissuesdemilestonedを使用しています。デフォルトブランチに直Pushする開発体系ならばこういったワークフローの作成を難なくできるかと思いますが、クラシルAndroidプロジェクトではPullRequestを介してデフォルトブランチにpushする形式を取っています
このワークフローを開発していたときのGitHub Actionsの挙動では、PullRequestで変更されたワークフローファイルはPullRequest上のCheckではpull_requestのイベントしかトリガーを引いてくれないという仕様になっている様子でした。そのため最初このワークフローを作成するときにissuesdemilestonedに対する動作を確認できなくてつまづきました。「マイルストーンが外された」や「マイルストーンが付いていない状態」をトリガーにするワークフローを作成したいときは是非参考にしていただければと思います

PullRequestに限らずIssueについても動作するサンプルをMeilCli/actionsに置いていますのでそちらもよければどうぞ

ちなみにですが、GitHub上でマイルストーンを貼り替える作業をするとdemilestonedのイベントが発火するらしく、このワークフローではそのままコメントを付けてしまうのでdelayをかけてAPIから情報を取ってくるなどの改善点があります。そのうち直そうと考えていますm(__)m

Detekt

さて、最初に紹介したDetektですが、PullRequestで自動でチェックしてくれなければ意味がありません。そこでGitHub ActionsでDetektを実行し、Dangerを使ってDetektの結果をコメントで通知するようにしました

クラシルAndroidではCodeBuildのほうでもDangerを運用しているためすでにGemfileDangerfileが存在していました。GitHub Actionsではそれらのファイルと共用するのは避けたかったため、.github配下にそれらのファイルを配置することにしました。Gemの解決にBundlerを使用している都合上、そのような配置をすると依存関係を解決できなくなるという状態に陥ったため、過去に自分が勉強がてらに作成したMeilCli/danger-actionGemfileの位置にとらわれないように改修して使用しています

.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を使って依存関係やタスクの実行結果などをキャッシュすることによって、キャッシュが存在する場合にキャッシュを利用することで早く動作させるようにしました

f:id:meilcli:20200617170748p:plain
GitHub ActionsのCIワークフローの実行一覧
結果としては遅ければ5分程度、早ければ2分程度、平均3~4分で実行が完了するようになったためキャッシュを利用してワークフローを高速化する試みは成功だったかなと思います
(参考数値としてktlintの頃はいろいろな処理を待機していたために約8分ほどかかっていました)

今後について

開発者体験という視点で言えばクラシルAndroidプロジェクトはたくさんの課題があります。その中で直近でやろうと考えてるものをご紹介し、これを次回予告にします

  • 長年放置し続けてきたlintの対応、不要になったリソースの削除
  • 使用しているOSSなどのライセンス表記のための集計ツールをMeilCli/Librarianに置き換える
    • これによってライブラリーやライセンス通知の集計精度が向上する見込みです
  • ライブラリーアップデートの自動検出・通知
  • モジュールのbuild.gradleの共通な宣言部分をbuildSrcへまとめる

note.com

最後にdelyではエンジニアを大募集しています。ランチ会などを定期的に開催してるので、中の人と話してみたい!という風に興味がある方は是非お声がけください