[Android] 静的解析の結果を reviewdog でコメント
はじめに
先日に次のようなスライドを作り LT をしました。
上記のスライドは GitHub code scanning を使う内容になっているのですが、private リポジトリでは GitHub Advanced Security のライセンス(有料)が必要となり、導入するには少し敷居があります。よって今回は GitHub code scanning を使わないパターンとして、各種静的解析ツールの SARIF 形式のレポートファイルと reviewdog を使用してプルリクエストへコメントする方法について説明してみようと思います。
Android Lint の場合
事前準備
ルートの app
モジュールの Lint ブロックで、次のように SARIF 形式でのレポート出力を有効化します。
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 を プラグイン無しで導入 している場合は、次のようになるかと思います。もしプラグインを利用している場合はプラグインでの設定方法に従ってください。
args(
"--reporter=sarif,output=${buildDir}/reports/ktlint-results.sarif",
"**/src/**/*.kt",
"**.kts",
"!**/build/**",
)
ここでは具体的な設定方法については記載しませんが、マルチモジュール構成の場合は全てのモジュールのレポートが出力されるような設定を行ってください。どのような方法であっても、恐らく 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
ファイルを配置します。
---
- 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 と組み合わせることになると思うので、今回の静的解析 単体のワークフローを作る機会は少ないとは思いますが、仮に静的解析だけするワークフローを組むとすると次のようになるかと思います。
ワークフロー
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 のバージョンは適宜、変更してください。
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 を実行してもよいのですが、毎回それをするのは面倒です。また、チーム全員が見れるところにレポートがあった方がミーティング時に便利そうです。
そのような場合は、次のようなワークフローを用意しておくとよいかと思います。
ワークフロー
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 レポートが作成されるので、こちらの方が見やすいかもしれません。
そのほか紹介
ほかに Android/Kotlin のプロジェクトの CI で導入すると便利そうなものをここで紹介します。
JUnit
先週に書いた投稿なのですが、次のように結果をレポートするとよさそうです。
GitHub custom actions
comoposite actions(複合アクション)という方法で、最近いくつか GitHub の custom action を作り、下記のスライドで紹介させて頂きました。
スライドで紹介している6つの aciton のうち、下記の2つは Android/Kotln プロジェクトの CI で便利だと思いますので、よければ利用してみてください。
-
Gradle Dependency Diff Report
- ライブラリの依存関係の変化をレポート
-
Problem Matchers for Kotlin - Gradle
- Kotlin コンパイル時のワーニング/エラーをレポート
Discussion