🦋

Flutterの静的解析の結果をPRにコメントするGitHub Actionsのカスタムアクションを公開した

2023/12/06に公開

https://github.com/marketplace/actions/flutter-analyze-commenter

GitHub Actions上のCIでPRに対してFlutterの静的解析を行った際に検出された問題をPull Requestにレビューコメントとして書き込むアクションを作成しました。

以下のようにPull Requestの画面にコメントが表示されます。

内部的にはflutter analyzeの出力内容を受け取り、それをparseしてGitHubのREST APIを使ってPRに書き込む処理を行っています。

利用するにはflutter analyzeを実行するワークフローのstepに上記のアクション追加するだけで利用できます,
uses: yorifuji/flutter-analyze-commenter@v1が今回作成したアクションを利用しているstepです(詳細はREADMEを参照)。

jobs:
  flutter-analyze:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write # required to add comment on PR
    steps:
      # checkout repository and setup flutter environment
      ...

      # run flutter analyze with --write option
      - run: flutter analyze --write=flutter_analyze.log

      # use flutter-analyze-commenter
      - uses: yorifuji/flutter-analyze-commenter@v1
        if: ${{ !cancelled() }}  # required to run this step even if flutter analyze fails
        with:
          analyze_log: flutter_analyze.log

開発のきっかけやカスタムアクションの作り方、実装について紹介します。

開発のきっかけ

普段、Flutter開発ではGitHub Actionsを使ってCI/CDを組むことが多く、CIではPull Requestに対して静的解析やテストなどを行います。静的解析はflutter analyzeコマンドを使い、コードに問題があれば以下のような出力(report)が得られます、なお出力プロジェクトのlintの設定によります。


問題のあるコードの例(あくまでも例です)

flutter analyzeの出力例

% flutter analyze
Analyzing flutter_application_9...

   info • Use 'const' for final variables initialized to a constant value • lib/main.dart:4:3 • prefer_const_declarations
warning • The value of the local variable 'y1' isn't used • lib/main.dart:4:16 • unused_local_variable
  error • A value of type 'int' can't be assigned to a variable of type 'String' • lib/main.dart:4:21 • invalid_assignment

3 issues found. (ran in 0.9s)

警告にはinfo, warning, errorの3つのレベルがあります。

VSCodeにDart ExtensionをインストールしていればDart Analysis Serverによって解析が行われて問題(Problems)のタブに表示されます。参考までにこちらはdart analyzeが使われていてflutter analyzeと出力内容が少し異なっています。

GitHub Actions上で静的解析した実行した場合、その結果はCIのログに流れますがreportの内容を確認するためにCIのログを漁るのは不便なのでPRの画面で確認できるとベストです。

warningとerrorに対してはProblem Matchersを、infoに対してはDangerdanger-flutter_lintを組み合わせることでPRの画面上で結果を確認することができます(各ツールの細かな説明は省略します)。

https://itome.team/blog/2022/06/dart-analyzer-problem-matcher/

https://github.com/marketplace/actions/danger-action

https://github.com/mateuszszklarek/danger-flutter_lint

それぞれのツールの機能は目的を満たしているのですが、実際に使用すると

  • Problem Matchersの内容はPRのFiles Changedタブに表示される
  • Dangerの内容はConversationタブに表示される

という微妙な違いが気になりました。

また、Dangerは高機能で多用途である反面、技術スタックがRubyのため依存ライブラリのインストールも必要であるなど、静的解析の結果を表示するだけの用途では少しオーバースペックな印象でした。念の為補足すると上記のツールはどれも比較的枯れていて、安定性が求められるCIで利用するツールとしては良い選択だと思います。

静的解析の内容をPR上で扱うのであればそれほど難しい実装は必要ないから自分で作れば良いと思い、試しに上記2つのツールがカバーする機能を1つのアクションで実現するカスタムアクションを自作することにしました。

カスタムアクションの作成

ちなみに、ここで言っているカスタムアクションとはワークフローのことではなく、ワークフローの中でuses:で参照するアクションのことです。例を挙げるとgit checkoutを行うactions/checkout@v4も公開されているカスタムアクションの一つです。

- uses: actions/checkout@v4

ドキュメントにカスタムアクションを作成する方法が記載されています。
https://docs.github.com/ja/actions/creating-actions/about-custom-actions

アクションには

  • Docker コンテナアクション
  • JavaScript アクション
  • Composite アクション(複合アクション)

の3種類があり、DockerはLinux(ubuntu)でのみ利用できますが、残り2つはmacOSとWindowsランナーでも利用可能です。今回作成するアクションはmacOSやWindowsでも利用したいのでJavaScript or Compositeアクションから選択します。

JavaScriptアクションはNode.jsを使ってJavaScriptを実行するアクションです。npmなどを使ったエコシステムが利用できるので本格的な開発に向いているようですが、筆者はnpmを使った開発に慣れていないので今回はもう一つのComposite アクションを使うことにしました。

Compositeアクションは通常のワークフローのようにstepを組み合わせて処理を実現するアクションです。また、ワークフローから共通の再利用したい処理を別のファイルに切り出す目的でも使用できます(そちらの使い方の方が一般的かと思います)。

Compositeアクションではシェルスクリプトやuses:などを使って処理の実態を記述する必要があります。シェルスクリプトでも実装はできなくないですがログの解析やGitHub APIの利用など複雑な処理を実装するには面倒なのでactions/github-scriptを使うことにしました。

https://github.com/actions/github-script

actions/github-scriptを利用するとワークフロー(ジョブ)でJavaScriptを使った処理を記述することができます。CIのコンテキスト(context)へのアクセスやoctokit/rest.jsを使ったGitHub APIの利用もサポートされています。できることについてはリンク先をご確認ください。

実際のコードでは以下のように使用しています、ログの解析やAPIの利用など、処理の実態は別ファイル(index.js)に切り出しています。

action.yml
runs:
  using: composite
  steps:
    - uses: actions/github-script@v7
      if: ${{ github.event_name == 'pull_request' }}
      env:
        ANALYZE_LOG: ${{ inputs.analyze_log }}
        VERBOSE: ${{ inputs.verbose }}
      with:
        retries: 3
        script: |
          const analyzeLog = process.env.ANALYZE_LOG;
          const verboseLogging = process.env.VERBOSE === 'true';

          const workingDir = process.env.GITHUB_WORKSPACE;
          const actionPath = process.env.GITHUB_ACTION_PATH;

          const path = require('path');
          const script = require(path.join(actionPath, 'index.js'));
          await script({ core, github, context, workingDir, analyzeLog, verboseLogging })

(action.ymlのコード全体はこちらです)

Composite アクションを実装のする大まかな流れは以上です。Composite アクションの利点・決定を整理すると以下のようになります。

利点

  • 普段利用しているワークフローと似たような記述でカスタムアクションが記述できる
  • 同じくuses:が利用できるので既存のアクションが利用できる

欠点

  • 複雑・大規模なアクションの実装には向いていない、ケースに応じて以下の2つがおすすめ
    • 実行環境・ツール・ライブラリの依存関係を管理する場合はDockerコンテナアクション
    • npmや周辺ツールを利用した開発を行うときはJavaScriptアクション

実装

処理の実態はindex.jsに実装しました、コードはこちらです

flutter analyzeのログの解析

flutter analyze --write=filenameで出力されたログファイルから警告を取得します。--writeオプションを使うと以下のようなフォーマットで出力されます。 エラー種別、メッセージ、ファイル名、行番号が取得できるのでこの情報を使ってPull Requestへのコメントが作成できます。

[error] A value of type 'int' can't be assigned to a variable of type 'String' (/Users/yorifuji/git/flutter_analyze_commenter_ci/lib/main.dart:4:20)
[warning] The value of the local variable 'x' isn't used (/Users/yorifuji/git/flutter_analyze_commenter_ci/lib/main.dart:4:16)
[info] Use 'const' for final variables initialized to a constant value (/Users/yorifuji/git/flutter_analyze_commenter_ci/lib/main.dart:4:3)

PRに静的解析の結果を書き込む

警告の内容をPRに対してコメントとして書き込みを行いますが、flutter analyzeはリポジトリのファイル全体に対して静的解析を行うためPRのdiffには含まれない行に対する問題も検出されるので、それらを区別する必要がありました。

GitHubのREST APIを使ってPRのdiffを取得して、diffを解析するとPRで追加・削除されたファイル・行番号が取得できるので、先ほどの解析内容と付き合わせを行うとPRに含まれる警告と既知のコードに存在していた問題に分けることができます。PRに含まれる警告はファイルと行番号を指定したコメントとして、既知のコードの警告はPRのdiffに含まれないためファイルや行番号を指定しない通常のコメントの形式で書き込みを行っています。

また、PRに対して追加のコミットが発生すると静的解析の内容が変わります。例えば問題のあった行に対して修正が行われると当該行のコメントは不要になります。そのようなケースではコメントを削除する仕様にしています。REST APIを使ってPRのコメントを全件取得して最新コミットの静的解析の結果と付き合わせを行い、不要になったコメントをAPI経由で削除する処理を行っています。

コメントの状態管理についてはDangerの挙動を参考にしつつ試行錯誤しています、今後も調整する可能性がありそうです。

リリース/GitHub Marketplaceへの登録

カスタムアクションの実装が終わったらリリース作業を行います。リポジトリのトップレベルにaction.ymlが存在するとGitHubのReleasesにアクションに関する項目が追加されます。

この画面でyml記述内容に問題がないこと、READMEの有無がチェックされます。問題がなければMarketplaceにpublishすることができます。

なお、Marketplaceに公開するにはpublicリポジトリである必要がありますがカスタムアクションはprivateリポジトリでも作成は可能です。その場合privateリポジトリへの参照できるリポジトリからアクションが利用できます。例えば同じアカウントのpublic or privateリポジトリから利用することなどが可能です。試していませんが同じOrganizationなどでも参照できると思います。

以上のような手順でカスタムアクションを作成して公開することができます。

最後に

カスタムアクションが作成できるとGitHub Actionsでできることの幅が広がります。流行のAIなどを組み込んでみると面白そうかもしれません。

今回作成したアクションはまだ利用実績も少ないため不具合等があるかもしれません。ご利用いただく際にはサイドプロジェクトなどで試していただくと良いと思います。

Discussion