[Android] 静的解析の結果を GitHub Pages へデプロイ
はじめに
下記の投稿では Androd Lint、ktlint、mobsfscan、Qodana といった静的解析ツールの SARIF 形式のレポートと reviewdog を連携して、コードの問題をプルリクへコメントさせる方法について書きました。
これらの静的解析ツールは、SARIF 形式以外にも、HTML 形式でのレポート出力にも対応しています。
プルリクはあくまで差分についての関心なので、差分ではなく全解析結果を HTML レポートとして参照できるようにしておくことで、問題の全体把握の役に立ちそうです。チームの定例会等で定期的にレポートを確認するのもよいでしょう。
今回は、特定のブランチに対する解析結果(HTML形式のレポート)を、同リポジトリの GitHub Pages へデプロイするワークフローについて説明します。
※ GitHub Pages でホスティングできるコンテンツは1つのリポジトリあたり1つなので、どれか1つのブランチに対するレポートしかホスティングできません(最新1件のコンテンツで上書きしてしまう)。同様の理由により、今回の静的解析とは別の用途で GitHub Pages を既に利用している場合は、既存のコンテンツが上書きされてしまうので注意してください。
ちなみに、private リポジトリの GitHub Pages のコンテンツは、所定の権限が無いと参照できないので、関係者以外に見られてしまうという心配はありません。ただ GitHub の Free プランだと、private リポジトリでは GitHub Pages は使えません。
事前準備
リポジトリの設定で Pages の source を「GitHub Actions」にしてください。そうしない場合は今回説明するワークフローが動いた時にエラーとなります。
また、Android Lint と ktlint については、レポートの設定を事前にしておく必要があります。
Android Lint のレポートの設定は、冒頭で紹介した投稿での通り、Lint ブロックで行いますが、HTML 形式での出力はデフォルトで有効となっている為、明示的に無効としていない限りは何もしなくてよいです。ただマルチモジュール構成の場合は、レポートは1つにまとまっていた方が見やすいので、もし未だ設定していない場合はルートの app
モジュールで checkDependencies = true
を指定しておきます。今回 説明するワークフローも、1つにまとまっている前提とします。
ktlint のレポートの設定も、冒頭で紹介した投稿での通り で、仮にプラグイン無しの場合は次のようになるかと思います。レポーターは複数指定できるので --reporter=html
の行を追加します。
args(
"--reporter=sarif,output=${buildDir}/reports/ktlint-results.sarif",
"--reporter=html,output=${buildDir}/reports/ktlint-results.html",
"**/src/**/*.kt",
"**.kts",
"!**/build/**",
)
mobsfscan と Qodana については、レポートの設定については特に事前にすることはありません(mobsfscan については CI 上のコマンドで出力形式が指定でき、Qodana については元々 HTML 形式のレポートが出力されている)が、もし未だでしたら .mobsf
ファイルの配置 や qodana.yaml
ファイルの配置 を行ってください。
※ Android Lint と Qodana については HTML レポートはわりと充実しているのですが、ktlint と mobsfscan の HTML レポートはだいぶ簡素なもので、とても見辛いです^^;
ワークフロー
今回は main
ブランチを対象にし、コードが push される度に当該ブランチに対するレポートをデプロイすることとします。対象とするブランチは適宜 変更してください。また、例えば Androd Lint と ktlint だけでよいという場合は、mobsfscan と Qodana に関するコード部分を削ってください。冒頭で紹介した投稿での通り、Qodana は未だ導入は見送った方がよいかもしれません。
ワークフローはこちら
name: Analysis Report
on:
push:
branches: [main]
concurrency: # deploy 中の job が既にあれば待つ
group: 'github-pages'
cancel-in-progress: false
jobs:
report:
runs-on: ubuntu-latest
permissions:
contents: read # for checkout
pages: write # for pages action
id-token: write # for pages action
environment:
name: 'github-pages'
url: ${{steps.deploy.outputs.page_url}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: 17
- run: |
mkdir -p '${{ runner.temp }}/reports/android-lint'
mkdir '${{ runner.temp }}/reports/ktlint'
mkdir '${{ runner.temp }}/reports/mobsfscan'
mkdir '${{ runner.temp }}/reports/qodana'
- run: ./gradlew app:lintDebug --continue
continue-on-error: true
- run: |
# app モジュールのもの1ファイルのみ処理
find . -regex '^.*/build/reports/lint-results.*\.html$' -type f | head -1 | while read file_path; do
cp "$file_path" '${{ runner.temp }}/reports/android-lint/index.html'
done
- run: ./gradlew ktlintCheck --continue
continue-on-error: true
- run: |
find . -regex '^.*/build/reports/ktlint-results\.html$' -type f | while read file_path; do
module="$(echo "$file_path" | awk '{gsub(/^\.\/|\/build\/.*$/,"")}1')"
mkdir -p "${{ runner.temp }}/reports/ktlint/${module}" # specify -p option to support subdirectories
cp "$file_path" "${{ runner.temp }}/reports/ktlint/${module}/index.html"
echo "<li><a href=\"./ktlint/${module}/?${{ github.run_id }}\">ktlint(${module})</a></li>" >> '${{ runner.temp }}/reports/ktlint.html'
done
- uses: MobSF/mobsfscan@0.3.6
with:
args: . --html --output '${{ runner.temp }}/reports/mobsfscan/index.html'
continue-on-error: true
- uses: JetBrains/qodana-action@v2023.3
with:
use-annotations: false
- run: cp -r '${{ runner.temp }}/qodana/results/report/'* '${{ runner.temp }}/reports/qodana/'
- run: |
cat << EOF > '${{ runner.temp }}/reports/index.html'
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body bgcolor="#FFFAFA">
<h2>reports</h2>
<p>
This page was generated in <i>$(TZ=UTC-9 date '+%Y/%m/%d %H:%M')</i> based on <a href="${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}"><i>$(sha='${{ github.sha }}';echo ${sha:0:6})</i></a> commit.<br />
Reload this page to see the latest content.
</p>
<p>
<ul>
<li><a href="./android-lint/?${{ github.run_id }}">Android Lint</a></li>
$(cat '${{ runner.temp }}/reports/ktlint.html')
<li><a href="./mobsfscan/?${{ github.run_id }}">mobsfscan</a></li>
<li><a href="./qodana/?${{ github.run_id }}">Qodana</a></li>
</ul>
</p>
</body>
</html>
EOF
- uses: actions/upload-pages-artifact@v3
with:
path: ${{ runner.temp }}/reports
- uses: actions/deploy-pages@v4
id: deploy
GitHub Pages でホスティングできるコンテンツは1つのリポジトリあたり1つなので、1つのディレクトリ配下に各静的解析ツールのレポートファイルを全て集めつつ(ktlint の場合はモジュール毎にもレポートがある)、目次ページを生成するようなワークフローになっており、その部分のコードがやや複雑となっています。もし仮に1つのレポートだけでいいという場合は、目次ページは不要なので Android Lint の結果を GitHub Pages へデプロイ のようなワークフローにした方が簡潔となると思います。
GitHub Pages をデプロイする際、必ずしも GitHub の deployment イベントを発行(上記ワークフローでいうと environment:
の箇所)する必要はありませんが、一応そうしています。もし deployment イベントを発行しない場合は actions/deploy-pages
aciton の出力をデバッグ出力してみれば URL は分かる(リポジトリ毎に恐らく固定)ので、その URL を README に貼るなりしておけばよいと思います。
目次ページからのリンクに付与している ?${{ github.run_id }}
はブラウザのキャッシュ対策です。目次ページ自体は、古そうならリロードしてねという文言の表示で済ませています。
このワークフローが動くと、リポジトリのトップページに deployment イベントのリンクが現われるので、
そのリンクの先の画面に記載されている URL からからレポートにアクセスできます。
GitHub Pages トップページ
リンク先の各レポートの例
Android Lint
ktlint
mobsfscan
Qodana
トップページの URL は恐らく固定でずっと変わらないので、README 等にリンクを記載しておくとよいと思います。
Discussion