🤖

[Android] 静的解析の結果を reviewdog でコメント

2024/03/18に公開

はじめに

先日に次のようなスライドを作り LT をしました。

https://speakerdeck.com/hkusu/android-nojing-de-jie-xi-niokeru-sarif-huairunohuo-yong

上記のスライドは GitHub code scanning を使う内容になっているのですが、private リポジトリでは GitHub Advanced Security のライセンス(有料)が必要となり、導入するには少し敷居があります。よって今回は GitHub code scanning を使わないパターンとして、各種静的解析ツールの SARIF 形式のレポートファイルと reviewdog を使用してプルリクエストへコメントする方法について説明してみようと思います。

Android Lint の場合

事前準備

ルートの app モジュールの Lint ブロックで、次のように SARIF 形式でのレポート出力を有効化します。

app/build.gradle.kts
android {
    // ...
    lint {
        // ...
        sarifReport = true
        checkDependencies = true
    }

マルチモジュール構成の場合は、上記のように checkDependencies = true を指定しておけばサブモジュールの結果も app モジュールのレポートに含まれるようになります。

ワークフロー

コードのチェックアウトや Java のセットアップ等は省き、Android Lint のところだけ書くと次のようになります。

- run: ./gradlew app:lintDebug --continue
  continue-on-error: true
- uses: reviewdog/action-setup@v1
- env:
    REVIEWDOG_GITHUB_API_TOKEN: ${{ github.token }}
  run: |
    find . -regex '^.*/build/reports/lint-results.*\.sarif$' -type f | while read file_path; do
      reviewdog -f=sarif -reporter=github-pr-review < "$file_path"
    done

※ ワークフローの job の permissions: には pull-requests: write が必要です(プルリクへレビューコメントする為)。

もしプロダクトフレーバーが存在するなら app:hogeDebug のようにしてください。今回は1モジュールのみを対象にしているので Gradle の --continue オプション(あるモジュールまたはバリアントの lint タスクで問題検出&異常終了しても次を継続)は本来は不要なのですが、今回のようにレポートを目的としている CI の場合は、何も考えず指定しておいていいんじゃないかなと思います。

continue-on-error: true で、lint の問題検出による異常終了(0以外の終了コード)でも step が fail しないようにしています。ここは Android Lint の設定で、もしくは ./gradlew app:lintDebug --continue || true のようにして、異常終了を握り潰すことも出来ます(が、一般論として握りつぶしは避けた方がよいとは思います)。

ワークフローを動かすと、プルリクで変更した行に Android Lint の問題が見つかればレビューコメントされます。



変更した行よりも範囲を広げたり、レビューコメントでなく GitHub Checks API を用いた annotation にすることもできるので、いろいろ試してみてもよいと思います。ただ annotation だとプルリク画面上で人間による返信できなかったり(この指摘は◯◯という理由で今回はそのままにします的なコメントをしたいことはよくある)、suggestion(修正コードの提案) がされなかったりするので、個人的には今回のようなレビューコメントにするのが使い勝手がよいかなと思います。

※ suggestion されるのは、今回紹介する静的解析ツールでは Android Lint のみです。出力された SARIF ファイルに suggestion に関するデータが含まれているは Android Lint のみである為。

今回のレポートは app モジュールのもの1ファイルのみになるので for 文で複数のレポートファイルを拾う実装は本来は必要はありませんが一応、レポートがあれば全て拾うような実装にしています。

ktlint の場合

事前準備

SARIF 形式でレポートが出力されるようにします。仮に ktlint を プラグイン無しで導入 している場合は、次のようになるかと思います。もしプラグインを利用している場合はプラグインでの設定方法に従ってください。

build.gradle.kts
args(
    "--reporter=sarif,output=${buildDir}/reports/ktlint-results.sarif",
    "**/src/**/*.kt",
    "**.kts",
    "!**/build/**",
)

参考:ktlint のレポーターの説明

ここでは具体的な設定方法については記載しませんが、マルチモジュール構成の場合は全てのモジュールのレポートが出力されるような設定を行ってください。どのような方法であっても、恐らく Android Lint の場合と違ってレポートは1つにまとめることは出来ず、モジュール毎にレポートファイルは別になると思います。

ワークフロー

- run: ./gradlew ktlintCheck --continue
  continue-on-error: true
- uses: reviewdog/action-setup@v1
- env:
    REVIEWDOG_GITHUB_API_TOKEN: ${{ github.token }}
  run: |
    find . -regex '^.*/build/reports/ktlint-results\.sarif$' -type f | while read file_path; do
      reviewdog -f=sarif -reporter=github-pr-review < "$file_path"
    done

上記の例の Gradle の ktlintCheck タスクはプラグインによってタスク名が異なると思うので、プラグインを利用している場合はタスク名を置き換えてください。もしかしてプラグインによっては不要かもですが、マルチモジュール構成の場合は Gradle の --continue オプションを忘れずに。

そのほかの細かい説明は Android Lint の場合と同様なので、ここでは割愛します。

ワークフローを動かすと、次のようにレビューコメントされます。



ktlint でも suggestion させたい場合

Android Lint の項でも触れたのですが、SARIF ファイルに suggestion に関するデータが含まれているのは今回紹介する静的解析ツールの中では Android のみである為、ktlint の場合は suggestion されません。

ktlint でも suggestion させたいという場合は、ktlint のフォーマットと action-suggester の step を追記します。

- run: ./gradlew ktlintCheck --continue
  continue-on-error: true
- uses: reviewdog/action-setup@v1
- env:
    REVIEWDOG_GITHUB_API_TOKEN: ${{ github.token }}
  run: |
    find . -regex '^.*/build/reports/ktlint-results\.sarif$' -type f | while read file_path; do
      reviewdog -f=sarif -reporter=github-pr-review < "$file_path"
    done
# 以下の step を追記
- run: ./gradlew ktlintFormat --continue
  continue-on-error: true
- uses: reviewdog/action-suggester@v1
  with:
    tool_name: 'ktlint' # ツール名の表示だけなので何てもよい
    filter_mode: 'added' # 変更した行を対象に(SARIFでのコメントと範囲を統一)


もちろん suggestion できるのは ktlint のフォーマッタで自動修正できる場合のみです。また上図のようにコメントが2件になってしまいますが、そんなに不自然でもないかなと思います。

mobsfscan

mobsfscan については弊社でも導入効果の検証中の段階ではあるのですが、ワークフローの紹介をしておきます。

事前準備

必須で準備することはありませんが、もし build ディレクトリを検査対象から外す場合は、リポジトリ直下に下記のような .mobsf ファイルを配置します。

.mobsf
---
- ignore-paths:
    - build

その他、除外したい検査ルールがあれば上記のファイルへ設定してください。.mobsf ファイルの配置は必須ではないので、まずは .mobsf ファイル無しでワークフローを動かしてもよいです。

ワークフロー

mobsfscan の場合、Gradle タスクは実行しませんので Java のセットアップの step は不要です。

Android Lint や ktlint と同様にレビューコメントでもいいのですが、脆弱性のチェックという性質上、プルリクの変更ファイルに関係なく GitHub Checks API を用いた annotation の形でコメントさせます。

- uses: MobSF/mobsfscan@0.3.6
  with:
    args: . --sarif --output '${{ runner.temp }}/mobsfscan-results.sarif'
  continue-on-error: true
- uses: reviewdog/action-setup@v1
- env:
    REVIEWDOG_GITHUB_API_TOKEN: ${{ github.token }}
  run: reviewdog -f=sarif -reporter=github-check -name='mobsfscan' -filter-mode=nofilter < '${{ runner.temp }}/mobsfscan-results.sarif'

※ ワークフローの job の permissions: には checks: write が必要です。

-name='mobsfscan' は checks 名を指定しています(指定しないとレポーター名 sarif となり、プルリク画面上でもそれが表示されてしまう為)。

ワークフローを動かすと、次のように annotation が表示されます。



Android Lint/ktlint/mobsfscan 全部入りワークフロー

実際は単体テストやビルドチェック等のプルリクの CI と組み合わせることになると思うので、今回の静的解析 単体のワークフローを作る機会は少ないとは思いますが、仮に静的解析だけするワークフローを組むとすると次のようになるかと思います。

ワークフロー
.github/workflows/analysis-review.yml
name: Analysis Review

on: pull_request

concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref }}
  cancel-in-progress: true

jobs:
  review:
    runs-on: ubuntu-latest
    permissions:
      contents: read # for checkout
      checks: write # for mobsfscan checks
      pull-requests: write # for review comment
    env:
      REVIEWDOG_GITHUB_API_TOKEN: ${{ github.token }} # jobへ定義してしまう
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: 'zulu'
          java-version: 17
      - uses: reviewdog/action-setup@v1
      - run: ./gradlew app:lintDebug --continue
        continue-on-error: true
      - run: |
          find . -regex '^.*/build/reports/lint-results.*\.sarif$' -type f | while read file_path; do
            reviewdog -f=sarif -reporter=github-pr-review < "$file_path"
          done
      - run: ./gradlew ktlintCheck --continue
        continue-on-error: true
      - run: |
          find . -regex '^.*/build/reports/ktlint-results\.sarif$' -type f | while read file_path; do
            reviewdog -f=sarif -reporter=github-pr-review < "$file_path"
          done
      - uses: MobSF/mobsfscan@0.3.6
        with:
          args: . --sarif --output '${{ runner.temp }}/mobsfscan-results.sarif'
        continue-on-error: true
      - run: reviewdog -f=sarif -reporter=github-check -name='mobsfscan' -filter-mode=nofilter < '${{ runner.temp }}/mobsfscan-results.sarif'

もし error レベルの問題の検出時に CI を落としたいという場合は ステータスバッジを README に表示する場合 のように判定してワークフローを失敗させるとよいと思います。

(おまけ)Qodana の場合

Qodana 自体に、プルリクの変更行へ annotation する機能があるので、reviewdog と連携する意味があまり無いのですが一応、連携もできるよということで紹介しておきます。

事前準備

リポジトリ直下に qodana.yaml ファイルを配置します(拡張子は .yml でもよいです)。下記はあくまで例なので、プロファイルや JDK のバージョンは適宜、変更してください。

qodana.yaml
version: "1.0"
linter: jetbrains/qodana-jvm-android:2023.3
profile:
  name: qodana.recommended
projectJDK: 17

ワークフロー

mobsfscan と同様、Gradle タスクは実行しませんので Java のセットアップの step は不要です。

- uses: JetBrains/qodana-action@v2023.3
  with:
    # reviewdog に任せるので Qodana のプルリク連携機能は無効化
    post-pr-comment: false
    use-annotations: false
    pr-mode: false
- uses: reviewdog/action-setup@v1
- env:
    REVIEWDOG_GITHUB_API_TOKEN: ${{ github.token }}
  run: reviewdog -f=sarif -reporter=github-pr-review < '${{ runner.temp }}/qodana/results/qodana.sarif.json'

※ ワークフローの job の permissions: には pull-requests: write が必要です(reviewdog でプルリクへレビューコメントする為)。

紹介はしましたが、仮に Qodana を reviewdog と連携せず単体で利用するとしても、次の理由により、現時点では導入するには未だ検討の余地があるかなと個人的には思っています。

  • Android Lint や ktlint を既に導入している場合、重複するレビューコメントにより煩雑になる
    • まず先に検査ルールの精査をした方がよさそう
    • (とはいえ Android Lint と ktlint も重複しているところありますが..)
  • 作成される GitHub Actions の cache のサイズが一つあたり 1G 超と大きすぎる
    • キャッシュ運用を考えないと幾つでも作られる
    • cache はリポジトリ全体で 10G までなので、別の用途の cache を追い出してしまう可能性
  • cache にヒットするか否かで検出件数が変わる謎の挙動

解析結果全件のレポートを見たい

これまでプルリクに対する問題検出について説明してきましたが、リポジトリ全体でどれだけ問題があるか?というのは気になるところです。手元にコードをチェックアウトして Android Lint や ktlint を実行してもよいのですが、毎回それをするのは面倒です。また、チーム全員が見れるところにレポートがあった方がミーティング時に便利そうです。

そのような場合は、次のようなワークフローを用意しておくとよいかと思います。

ワークフロー
.github/workflows/analysis-report.yml
name: Analysis Report

on:
  push:
    branches: [main] # 検査したいブランチ

jobs:
  report:
    runs-on: ubuntu-latest
    permissions:
      contents: read # for checkout
      checks: write # for reviewdog
    env:
      REVIEWDOG_GITHUB_API_TOKEN: ${{ github.token }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: 'zulu'
          java-version: 17
      - uses: reviewdog/action-setup@v1
      - run: ./gradlew app:lintDebug --continue
        continue-on-error: true
      - run: |
          find . -regex '^.*/build/reports/lint-results.*\.sarif$' -type f | while read file_path; do
            reviewdog -f=sarif -reporter=github-check -name='Android Lint' < "$file_path"
          done
      - run: ./gradlew ktlintCheck --continue
        continue-on-error: true
      - run: |
          find . -regex '^.*/build/reports/ktlint-results\.sarif$' -type f | while read file_path; do
            module="$(echo "$file_path" | awk '{gsub(/^\.\/|\/build\/.*$/,"")}1')"
            reviewdog -f=sarif -reporter=github-check -name="ktlint(${module})" < "$file_path"
          done
      - uses: MobSF/mobsfscan@0.3.6
        with:
          args: . --sarif --output '${{ runner.temp }}/mobsfscan-results.sarif'
        continue-on-error: true
      - run: reviewdog -f=sarif -reporter=github-check -name='mobsfscan' < '${{ runner.temp }}/mobsfscan-results.sarif'

これは Android Lint、ktlint、mobsfscan 3つ場合ですが、全てのツールにおいて、mobsfscan の項でも説明した、GitHub Checks API を用いたレポートを行います。この際、reviewdog の filter-mode の指定は必要ありません(push イベント時はプルリク時のようなフィルタリングはどのみち出来ず、全件検出となる)。

main ブランチにコードが push されるとこのワークフローが動きます。checks の結果は次のように GitHub 上で確認することが出来ます。



checks の詳細画面には reviewdog のレポートが表示されています。



特定のブランチに対して検出した問題の全件が表示されるので、これを元にチームでリファクタリング計画や、余分な検出項目の抑止について話し合うとよいんじゃないかなと思います。

ステータスバッジを README に表示する場合

上述のようにリポジトリの checks の結果は分かるのですが、error レベルの問題が何かしらある旨のステータスバッジを README に表示すると、問題がある旨をより視認しやすいかと思います。その為には、各静的解析の step のどれかが error レベルの問題を検出&異常終了したことを判定(continue-on-error: true 適用前のステータスで判定)し、ワークフロー自体も失敗させるようにします(ステータスバッジはワークフロー単位の成否にしか対応していない為)。

具体的には、各静的解析の step に適当な id を振っておき、

- run: ./gradlew app:lintDebug --continue
  continue-on-error: true
  id: android-lint # id を振る

ワークフローの最後に下記の step を追記します。

- if: contains(steps.*.outcome, 'failure')
  run: echo '::error::Some checks were failure.'; exit 1

id が振られた全ての step の outcome を一括で判定しています。

上記の方法は、各静的解析時の異常終了を握りつぶしてしまっている場合は使えません。その場合は次のように checks の結果から判定するとよいと思います。もしくは jq で SARIF ファイル(JSON 形式)内の error の数を集計してもよいです。

- run: |
    failure_count=$(gh api repos/{owner}/{repo}/commits/${{ github.sha }}/check-runs | jq '.check_runs|map(select(.conclusion=="failure"))|length')
    if [ "$failure_count" != '0' ]; then echo '::error::Some checks were failure.'; exit 1; fi
  env:
    GH_TOKEN: ${{ github.token }}

ステータスバッジのコードは、ワークフローの履歴画面から取得できるので、



そのコードを README.md に貼ります。

状況に応じて、静的解析ツール別のワークフローとステータスバッジを用意してもよいと思います。

ktlint のエラーレベルを変更したい場合

ktlint で検出した問題のエラーレベルは全て「error」です。本来は問題のコードは修正すべきですが、ktlint を初めて導入した際やルールを変更した際に、検出された問題の数が大量にあり直ぐには修正できす、結果として Android Lint や mobsfscan の error レベルの問題が埋もれてしまうことがあるかもしれません。

ここはプロジェクトの状況や考え方次第なのですが、コードのフォーマットの問題がアプリに重大な不具合を引き起こすことはないということで、下記のように jq で SARIF ファイル(JSON 形式)を操作して、エラーレベルを一括で「warning」に変更することができます。

- run: |
    find . -regex '^.*/build/reports/ktlint-results\.sarif$' -type f | while read file_path; do
      module="$(echo "$file_path" | awk '{gsub(/^\.\/|\/build\/.*$/,"")}1')"
      jq '.runs[].results[].level = "warning"' < "$file_path" | \
        reviewdog -f=sarif -reporter=github-check -name="ktlint(${module})"
    done

本当は ktlint の設定でエラーレベルを変更できるとよいのですが、調べた限りできなさそうでした。Android Lint の場合は Android Lint の設定で各検出項目のエラーレベルを変更できるので、そちらで変更するのがよいと思います(もちろん重大な問題を見過ごしてしまうような設定をしないように注意)。

HTML レポートを GitHub Pages へデプロイする場合

これまで checks の結果でレポートする方法を説明してきましたが、替わりに、もしくは併用で、次の投稿のように、HTML レポートを GitHub Pages へデプロイしてもよいと思います。Android Lint と Qodana については綺麗な HTML レポートが作成されるので、こちらの方が見やすいかもしれません。

https://zenn.dev/yumemi_inc/articles/9e8f5e08e8b23f

そのほか紹介

ほかに Android/Kotlin のプロジェクトの CI で導入すると便利そうなものをここで紹介します。

JUnit

先週に書いた投稿なのですが、次のように結果をレポートするとよさそうです。

https://zenn.dev/yumemi_inc/articles/0524d47b5df531

GitHub custom actions

comoposite actions(複合アクション)という方法で、最近いくつか GitHub の custom action を作り、下記のスライドで紹介させて頂きました。

https://speakerdeck.com/hkusu/github-composite-actions

スライドで紹介している6つの aciton のうち、下記の2つは Android/Kotln プロジェクトの CI で便利だと思いますので、よければ利用してみてください。

株式会社ゆめみ

Discussion