🤖

[Android] 静的解析の結果を GitHub Pages へデプロイ

2024/03/21に公開

はじめに

下記の投稿では Androd Lint、ktlint、mobsfscan、Qodana といった静的解析ツールの SARIF 形式のレポートと reviewdog を連携して、コードの問題をプルリクへコメントさせる方法について書きました。

https://zenn.dev/yumemi_inc/articles/fae967dcaa89e7

これらの静的解析ツールは、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 の行を追加します。

build.gradle.kts
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 は未だ導入は見送った方がよいかもしれません。

ワークフローはこちら
analysis-report.yml
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