Dangerでプルリクエストのチェックを自動化する

こんにちは。

リサーチ・アンド・イノベーションAndroidエンジニアをしている jageishi です。

今回は、弊社で利用している Danger の簡単な説明と導入事例を紹介します。

Dangerとは

Dangerはプルリクエストの形式化や機械的なチェックを自動化するのに使える便利なツールです。

以下のような特徴があります。

  • Dangerfileにルールを記述する
  • ルールはプラグインとして公開されているものがある
  • 様々なコードホスティングサービスをサポートしている
  • Ruby版の他にJavaScript、Kotlin版等が存在する

Dangerの導入

今回はRuby版を使ってGitHub ActionsのワークフローでDangerを実行して、プルリクエストにコメントする方法を例に説明します。

Gemfile

公式からもBundlerの利用が推奨されているため、Gemfileを用意します。

# frozen_string_literal: true

source "https://rubygems.org"

gem "danger"

Dangerfile

次に、Dangerfileをプロジェクト直下のディレクトリに作成します。

message("メッセージ")

DangerfileはRuby DSLで記述します。

今回はメッセージをコメントするだけですが、他にも様々な機能が用意されているため細かいチェックを行うことが可能です。

使用できるAPIについては こちら をご覧ください。

GitHub Actions

pull_requestイベントをトリガーとして実行されるワークフローを作成します。

name: Danger

on:
  pull_request:

jobs:
  run:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.0'
          bundler-cache: true
      - run: bundle exec danger
        env:
          DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Rubyを使用するために ruby/setup-ruby を利用しています。 こちらのアクションはデフォルトでBundlerもインストールされるため、別途gem install bundlerを実行する必要はありません。

また、bundler-cache: true を設定することで併せて bundle installが実行され、gemがキャッシュされるようになります。

最後に環境変数 DANGER_GITHUB_API_TOKEN にアクセストークンを設定して bundle exec danger を実行すればOKです。

プルリクエス

以上の設定を行うことで、プルリクエストの作成時に以下のようにコメントされるようになります。

プルリクエストのコメント

このように導入のハードルが低く、手軽に試すことができます。

弊社の導入事例

ここからは弊社での導入事例を紹介します。

CODEのAndroidアプリ開発で利用しているDangerfileの構成は以下の通りです。

danger.import_plugin("danger/plugins/*.rb")

# Android Lint
android_lint.gradle_task = "app:lintFlavorDebug"
android_lint.report_file = "app/build/reports/lint-results-flavorDebug.xml"
android_lint.filtering = true
android_lint.lint

# 画面密度毎に画像リソースが存在するか確認する
image_resource_checker.check

# Roomマイグレーションの有無を確認する
schema_changes_checker.check

かなりシンプルな内容ですが、以下のような流れになっています。

  1. プラグインの読み込み
  2. Android Lintの実行
  3. 画面密度毎の画像リソースのチェック
  4. Roomマイグレーション有無のチェック

1. プラグインの読み込み

danger.import_plugin("danger/plugins/*.rb")

Dangerはプラグインとしてルールを配布できますが、私達はDangerfileの肥大化を防ぐ目的でローカルにプラグインを作成してファイルを分離しています。

こちらはそれらのプラグインを読み込んでDangerfile内で参照できるようにするものです。

プラグインの作成方法については こちら をご覧ください。

2. Android Lintの実行

android_lint.gradle_task = "app:lintFlavorDebug"
android_lint.report_file = "app/build/reports/lint-results-flavorDebug.xml"
android_lint.filtering = true
android_lint.lint

Android Lintの実行には loadsmart/danger-android_lint を利用しています。Androidアプリ開発者にはおなじみかもしれません。

Android版CODEはマルチモジュール構成になっているため、checkDependencies true を設定することで全てのモジュールに対してチェックが行われるようにしています。

android {
    lintOptions {
        checkDependencies true
    }
}

前もって各モジュールに対してLintを実行しておいて、出力された複数のレポートを処理する方法もよく見かけますね。

3. 画面密度毎の画像リソースのチェック

image_resource_checker.check
module Danger
  class ImageResourceChecker < Plugin
    attr_accessor :target_densities, :target_extensions
    def target_densities
      return @target_densities || ["mdpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"]
    end
    def target_extensions
      return @target_extensions || ["png", "jpg"]
    end
    def check
      renamed_files = git.renamed_files
      target_files = git.added_files \
                       + git.modified_files \
                       + renamed_files.map { |f| f[:after] } \
                       - git.deleted_files \
                       - renamed_files.map { |f| f[:before] }
      regex = /drawable-(#{target_densities.join("|") })\/.*\.(#{target_extensions.join("|")})$/
      target_image_files = target_files.filter { |f| regex.match?(f) }
      checked_files = []
      missing_density_files = []
      target_image_files.each do |f|
        next if checked_files.include?(f)
        target_densities.each do |d|
          image_file = f.gsub(/drawable-(#{target_densities.join("|")})/, "drawable-#{d}")
          missing_density_files << image_file unless File.exists?(image_file)
          checked_files << image_file
        end
      end
      unless missing_density_files.empty?
        header = "### 画像ファイルを追加してください:pray:\n"
        header << "| ファイル |\n"
        header << "| --- |\n"
        message = missing_density_files.map { |f| "| `#{f}` |\n" }.join
        markdown(header + message)
      end
    end
  end
end

こちらでは画像リソースに変更が加えられた際に、画面密度毎に用意されているかをチェックしています。

Android LintにIconDensitiesというissue idがあり、まさに同じようなチェックを行ってくれるのですが、以下の理由から自作することにしました。

  • ディレクトリ毎に検出されるためdanger-android_lintでandroid_lint.filtering = trueを指定して対象を追加、および変更されたファイルに絞っている場合はプルリクエストにコメントされない
  • xxxhdpiがチェック対象外になっている

4. Roomマイグレーション有無のチェック

schema_changes_checker.check
module Danger
  class SchemaChangesChecker < Plugin
    def check
      schema_json_path = "<Your schema location>"
      has_changed_schema = (git.added_files + git.modified_files).filter { |f| f.start_with?(schema_json_path) }.any?
      if has_changed_schema
        markdown_text = "### マイグレーションが必要です\n"
        markdown_text << "- [ ] マイグレーション処理を実装済み\n"
        markdown_text << "- [ ] Google Play公開バージョンからのマイグレーションをテスト済み\n"
        markdown(markdown_text)
      end
    end
  end
end

弊社ではRoomを利用しており、スキーマファイルを出力してリポジトリに含める運用になっています。

こちらはスキーマファイルに変更が加えられた際に、マイグレーションが必要である旨をコメントして確認を促すようなものです。

スキーマが変更されたもののマイグレーション処理が抜けた状態でメインのブランチにマージされてしまい、アプリをアップデートした際にエラーが発生してしまう状態になっているのをリリース前のテストまで気づけなかったことがありました。 そういった経緯もあり、対応漏れを未然に防ぐためにチェックを行っています。

出力内容

出力は以下のようになります。

出力内容

ちなみに、同じプルリクエストでDangerが再度実行された場合は、すでにDangerによって投稿されたコメントが更新されるような挙動になっており、プルリクエストがコメントで溢れかえってしまうようなことはありません。

こういった部分にも配慮されているため、Dangerはとても使い勝手の良いツールだと思います。

まとめ

Dangerと弊社での導入事例について紹介しました。

チェックが自動化されることで、問題の早期発展に繋がったり、コードレビューの負担を減らせていると感じています。

導入済みの方も多いと思いますが、まだ導入していないという方は是非お試しください。その際にこの記事が少しでも参考になれば幸いです!

便利な機能は他にもたくさんありますので、詳しくは 公式ホームページ のガイドやリファレンスをご覧ください。


リサーチ・アンド・イノベーションではエンジニアを募集中です。

興味を持っていただけた方はお気軽にご連絡ください!

採用情報