|
| 1 | +--- |
| 2 | +title: "GitHub 標準の Annotation を活用してレビューを可視化する" |
| 3 | +date: 2025/06/04 00:00:00 |
| 4 | +postid: a |
| 5 | +tag: |
| 6 | + - GitHub |
| 7 | + - GitHubActions |
| 8 | +category: |
| 9 | + - Infrastructure |
| 10 | +thumbnail: /images/20250604a/thumbnail.png |
| 11 | +author: 武田大輝 |
| 12 | +lede: "コードレビューを自動で可視化するためのツールといえばreviewdogが有名です。" |
| 13 | +--- |
| 14 | +[CI/CD連載](/articles/20250603a/) 2本目の記事です。 |
| 15 | + |
| 16 | +## はじめに |
| 17 | + |
| 18 | +コードレビューを自動で可視化するためのツールといえば [reviewdog](https://github.com/reviewdog) が有名です。 |
| 19 | + |
| 20 | +最近(2025年03月)reviewdog のソースリポジトリが侵害され、reviewdog を実行しているリポジトリにおいて [シークレット情報がワークフロー上に漏洩するセキュリティインシデントが発生](https://www.wiz.io/blog/new-github-action-supply-chain-attack-reviewdog-action-setup) したことは記憶に新しいでしょう。 |
| 21 | + |
| 22 | +筆者も reviewdog にはお世話になっている開発者の一人ですが、本件を受けて「そもそも GitHub の標準機能だけで同じようなことができないか」という考えを持ち、あらためて GitHub 標準の Annotation 機能について調べてみた記事になります。 |
| 23 | + |
| 24 | +なお、reviewdog 自体の有用性を否定するものでは一切ありません。 |
| 25 | + |
| 26 | +## レビューの可視化とは |
| 27 | + |
| 28 | +まず誤解のないよう本記事における「レビューの可視化」とは具体的に何を指すのかを説明しておきます。 |
| 29 | + |
| 30 | +レビューの可視化とは、Linter や Formatter などソースコードの解析結果をもとに、結果をわかりやすく表示してくれるしくみのことを指しています。実際にソースコードのエラーや警告を検出する部分の話ではなく、その結果を解析してよい感じに開発者へフィードバックしてくれる部分の話となります。 |
| 31 | + |
| 32 | +## reviewdog とは |
| 33 | + |
| 34 | +reviewdog は Linter や Formatter などの出力結果を GitHub などのコードホスティングサービス上にコメントなどで投稿してくれるツールです。 |
| 35 | + |
| 36 | +その思想や誕生の背景については、開発者のブログを貼る形で本記事では割愛したいと思います。 |
| 37 | + |
| 38 | +<http://haya14busa.com/reviewdog/> |
| 39 | + |
| 40 | +## GitHub Annotation とは |
| 41 | + |
| 42 | +GitHub Annotation(アノテーション)とは、GitHub Actions のワークフロー実行中に出力されるエラーや警告などを、対象のファイルや行に紐付けて GitHub の UI 上に表示する標準のしくみです。公式ドキュメントでは、GitHub Actions のワークフローコマンドの中で説明されています。 |
| 43 | + |
| 44 | +<https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions> |
| 45 | + |
| 46 | +具体的には `::error`, `::warning`, `::notice` といったコマンドをファイルパスや行数とともに出力することで、アノテーションを作成できます。 |
| 47 | + |
| 48 | +```text |
| 49 | +::error file={name},line={line},endLine={endLine},title={title}::{message} |
| 50 | +``` |
| 51 | + |
| 52 | +実際にアノテーションを作成するサンプルを見てみましょう。 |
| 53 | +次のようなワークフローファイルを作成して GitHub Actions を実行してみます。`echo "::error ..."` や `echo "::warning..."` と記載している部分がコマンド部分になります。 |
| 54 | + |
| 55 | +```yaml |
| 56 | +name: Emit Annotation Directly |
| 57 | + |
| 58 | +on: |
| 59 | + pull_request: |
| 60 | + workflow_dispatch: |
| 61 | + |
| 62 | +jobs: |
| 63 | + demo: |
| 64 | + runs-on: ubuntu-latest |
| 65 | + steps: |
| 66 | + - name: Checkout |
| 67 | + uses: actions/checkout@v4 |
| 68 | + |
| 69 | + - name: Emit Error and Warning Annotations |
| 70 | + run: | |
| 71 | + echo "::error file=.github/workflows/annotation.yaml,line=16,col=5::Example Error: This is a sample error message for demonstration purposes." |
| 72 | + echo "::warning file=.github/workflows/annotation.yaml,line=17,col=5::Example Warning: This is a sample warning message for demonstration purposes." |
| 73 | +``` |
| 74 | +
|
| 75 | +このワークフローを実行して GitHub Actions の実行結果サマリをみると [次のように](https://github.com/rhumie/github-annotation-demo/actions/runs/14806970834) アノテーションが出力されます。 |
| 76 | +
|
| 77 | +<img src="/images/20250604a/annotations_in_summary.png" alt="annotations_in_summary.png" width="1200" height="653" loading="lazy"> |
| 78 | +
|
| 79 | +また、このワークフローが PR(Pull Request)をトリガとして実行されている場合は、 [次のように](https://github.com/rhumie/github-annotation-demo/pull/1/files) PR の「Files changed」タブから該当する箇所にインラインでアノテーションが表示されていることが確認できます。 |
| 80 | +
|
| 81 | +<img src="/images/20250604a/annotations_in_pull_request.png" alt="annotations_in_pull_request.png" width="1200" height="726" loading="lazy"> |
| 82 | +
|
| 83 | +このように、GitHub Annotation を活用することで該当ファイル・該当行にピンポイントでメッセージを表示できます。 |
| 84 | +サンプルではファイルや行数をベタ書きしましたが、 Linter や Formatter などの解析ツールと組み合わせることで、これらのツールの実行結果をアノテーションとしてユーザにフィードバックできます。 |
| 85 | +
|
| 86 | +## GitHub Annotation の実践的な活用方法 |
| 87 | +
|
| 88 | +GitHub Annotation の概要について理解できたところで、実践的な使い方について説明していきます。 |
| 89 | +
|
| 90 | +### Problem Matcher の利用 |
| 91 | +
|
| 92 | +[Problem Matcher](https://github.com/actions/toolkit/blob/main/docs/problem-matchers.md) とは、GitHub Actions におけるログ出力からエラーや警告の情報を自動的に抽出し、アノテーションとして表示するためのしくみです。 |
| 93 | +
|
| 94 | +先ほどの例では直接 `echo "::error ..."` と記述していましたが、これをより汎用的に使いやすくした機能だと理解してもらえれば OK です。 |
| 95 | + |
| 96 | +Problem Matcher はパターン(正規表現)を JSON 形式で定義することで、任意のツールの出力形式にマッチさせます。 |
| 97 | +公式ドキュメントにも記載されている ESLint の出力結果に対応させる例をみてみましょう。 |
| 98 | + |
| 99 | +```text |
| 100 | +test.js |
| 101 | + 1:0 error Missing "use strict" statement strict |
| 102 | + 5:10 error 'addOne' is defined but never used no-unused-vars |
| 103 | +✖ 2 problems (2 errors, 0 warnings) |
| 104 | +``` |
| 105 | + |
| 106 | +#### 設定ファイル |
| 107 | + |
| 108 | +この ESLint の出力結果に対応する Problem Matcher の設定ファイルは次のとおりです。 |
| 109 | + |
| 110 | +```json |
| 111 | +{ |
| 112 | + "problemMatcher": [ |
| 113 | + { |
| 114 | + "owner": "eslint-stylish", |
| 115 | + "pattern": [ |
| 116 | + { |
| 117 | + // Matches the 1st line in the output |
| 118 | + "regexp": "^([^\\s].*)$", |
| 119 | + "file": 1 |
| 120 | + }, |
| 121 | + { |
| 122 | + // Matches the 2nd and 3rd line in the output |
| 123 | + "regexp": "^\\s+(\\d+):(\\d+)\\s+(error|warning|info)\\s+(.*)\\s\\s+(.*)$", |
| 124 | + // File is carried through from above, so we define the rest of the groups |
| 125 | + "line": 1, |
| 126 | + "column": 2, |
| 127 | + "severity": 3, |
| 128 | + "message": 4, |
| 129 | + "code": 5, |
| 130 | + "loop": true |
| 131 | + } |
| 132 | + ] |
| 133 | + } |
| 134 | + ] |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +ESLint の出力のように、最初にファイル名が 1 行だけ表示され、その後に複数のエラーや警告が続く「マルチライン形式」の出力に対応するため、Problem Matcher では複数の `pattern` を組み合わせて定義できます。 |
| 139 | + |
| 140 | +最初のパターンでは、ファイル名の行を検出します。正規表現(`"regexp": "^([^\\s].*)$"`)により、先頭が空白で始まらない行をファイル名として抽出し、ファイル名として指定(`"file": 1`)しています。ここでの 1 は、正規表現内の括弧 () によって囲まれた 1 番目のキャプチャグループを指しています。 |
| 141 | + |
| 142 | +最初のパターンはファイル名の出力に対応する部分となります。正規表現(`"regexp": "^([^\\s].*)$"`)でマッチした部分をファイル名(`"file": 1`)として設定しています。`"file": 1` の `1` は正規表現のキャプチャグループの番号を表しています。 |
| 143 | +続く 2 つ目のパターンでは、各エラー・警告行の情報(行番号、列番号、深刻度、メッセージ、ルール名など)をそれぞれのキャプチャグループから取り出し、アノテーションとして表示するための各要素にマッピングしています。各要素は次のとおりです。 |
| 144 | + |
| 145 | +| 項目 | 説明 | 実際の値 | |
| 146 | +| -------- | ------------------------------------ | ------------------------------ | |
| 147 | +| line | 行番号 | 1 | |
| 148 | +| column | 列番号 | 0 | |
| 149 | +| severity | エラーの重大度 | error | |
| 150 | +| message | メッセージ | Missing "use strict" statement | |
| 151 | +| code | メッセージに対応するルール名や識別子 | strict | |
| 152 | + |
| 153 | +最後の `"loop": true` は、同じファイルに対して複数行のエラー・警告が連続して出力される場合に、1 回目に取得したファイル名を保持したまま、後続の行に繰り返しこのパターンを適用するための設定です。 |
| 154 | + |
| 155 | +#### 登録と削除 |
| 156 | + |
| 157 | +Problem Matcher は、GitHub Actions のワークフロー内で `::add-matcher::` および `::remove-matcher::` コマンドを使用することで動的に登録・削除できます。 |
| 158 | + |
| 159 | +たとえば、先ほどの `eslint-matcher.json` ファイルを `.github/matchers/` ディレクトリに保存した場合、次のように登録できます。 |
| 160 | + |
| 161 | +```yaml |
| 162 | +- name: Add ESLint Problem Matcher |
| 163 | + run: echo "::add-matcher::.github/matchers/eslint-matcher.json" |
| 164 | +``` |
| 165 | + |
| 166 | +このようにすると、以降のステップで ESLint の出力に応じたアノテーションが自動的に表示されます。 |
| 167 | +不要になった場合は、次のように削除します。 |
| 168 | + |
| 169 | +```yaml |
| 170 | +- name: Remove ESLint Problem Matcher |
| 171 | + run: echo "::remove-matcher::eslint-stylish" |
| 172 | +``` |
| 173 | + |
| 174 | +ここで指定する `eslint-stylish` は、Problem Matcher の設定ファイル内で定義した `owner` に対応しています。 |
| 175 | + |
| 176 | +ただし、実際のところ ESLint を GitHub Actions 上で実行する場合、通常この登録処理を明示的に記述することはありません。 |
| 177 | +なぜなら、ESLint を実行する前には一般的に [actions/setup-node](https://github.com/actions/setup-node/tree/main) を使って Node.js のセットアップを行いますが、このステップの中で [ESLint 用の Problem Matcher](https://github.com/actions/setup-node/blob/main/.github/eslint-stylish.jsons) が自動的に登録されるためです。 |
| 178 | + |
| 179 | +ほかにも `setup-python` や `setup-go` そして `setup-dotnet` などでも言語に応じて標準的な Problem Matcher が登録されるようになっています。 |
| 180 | + |
| 181 | +### GitHub Annotation にネイティブに対応しているツールの利用 |
| 182 | + |
| 183 | +Python 用の Linter である [Ruff](https://github.com/astral-sh/ruff) など GitHub Annotation に対応した出力をサポートしているツールもあります。 |
| 184 | +Ruff では `--output-format` に `github` を指定することで、次のような出力を得ることができます。 |
| 185 | + |
| 186 | +```shell |
| 187 | +ruff check test.py --output-format github |
| 188 | +::error title=Ruff (F401),file=test.py,line=1,col=8,endLine=1,endColumn=10::test.py:1:8: F401 `os` imported but unused |
| 189 | +``` |
| 190 | + |
| 191 | +## GitHub Annotation の制約 |
| 192 | + |
| 193 | +GitHub Actions はワークフロー実行時のアノテーション数を次のように制限しています。 |
| 194 | +<https://github.com/orgs/community/discussions/26680> |
| 195 | + |
| 196 | +- 1 ステップ(ワークフロー定義の `run` や `uses` の単位)あたりエラー 10 件、警告 10 件、通知 10 件 |
| 197 | +- 1 ジョブ(ワークフロー定義のステップを 1 つ以上含む `jobs` の単位)あたり 50 件 |
| 198 | +- 1 実行(ワークフローの実行)あたり 50 件 |
| 199 | + |
| 200 | +大量にエラーや警告が出力される場合は、一度にすべてを表示できないため、注意が必要です。 |
| 201 | +ただし通常 Linter などはローカルでも動作させてエラーを確認できるため、このあたりは割り切れるケースが多いのではないでしょうか。 |
| 202 | + |
| 203 | +## reviewdog との比較 |
| 204 | + |
| 205 | +ここまで GitHub 標準の Annotation 機能を紹介してきましたが、先に紹介した reviewdog と比較したときのメリット・デメリットを整理しておきます。 |
| 206 | + |
| 207 | +Annotation 機能を利用するメリットは次のとおりです。 |
| 208 | + |
| 209 | +- **サードバーティのアクションに依存しない** |
| 210 | + GitHub が公式に提供しているしくみであり、追加のツールや依存パッケージを導入する必要がありません。 |
| 211 | +- **パーミッションの付与が不要でセキュア** |
| 212 | + reviewdog を使用する場合は GitHub API を操作するため、GITHUB_TOKEN を使います。一方、Annotation 機能はログ出力ベースで動作するため、追加の認証情報を必要とせずセキュリティ的に安心です。 |
| 213 | +- **GitHub API の Rate Limit を気にしなくてよい** |
| 214 | + reviewdog は PR コメントを投稿する際に GitHub API を呼び出すため、大量のコメントや高頻度の実行で Rate Limit に引っかかる恐れがあります。Annotation はログ出力ベースで動作するため、Rate Limit の考慮は不要です。 |
| 215 | +- **標準機能であるため将来的な安定性が高い** |
| 216 | + GitHub が提供するしくのため、GitHub Actions のアップデートや仕様変更にも継続的に対応される可能性が高く、メンテナンスコストや不具合のリスクが比較的小さいと考えられます。 |
| 217 | + |
| 218 | +Annotation 機能にはない、reviewdog ならではのメリットは次のとおりです。 |
| 219 | + |
| 220 | +- **コメントの対象を差分ファイルに限定できる** |
| 221 | + reviewdog では、差分のある行に限定してコメントを残すことができるため、古いコードや無関係な部分に過剰にコメントが付与されることを防げます。これにより、修正しなければならない対象が明確になります。 |
| 222 | +- **多くのツールに標準で対応している** |
| 223 | + reviewdog は [Checkstyle 形式](https://checkstyle.sourceforge.io/) や [SARIF 形式](https://sarifweb.azurewebsites.net/) など [さまざまな形式に標準で対応](https://github.com/reviewdog/reviewdog?tab=readme-ov-file#input-format) しており、自分でフォーマットを定義しなくてもさまざまなツールに対応するできます。 |
| 224 | +- **PR コメントとして明示的に残せる** |
| 225 | + reviewdog は PR 上に直接コメントを残すことができます。これにより通知などでレビューイが気付きやすく、PR 上でディスカッションするトリガにもなります。 |
| 226 | + そのほか、GitHub の Annotation や GitHub PR Checks などの出力にも対応しており、さまざまな出力先を選べるのが特徴です。 |
| 227 | +- **GitHub 以外のプラットフォームに対応できる** |
| 228 | + GitLab や Bitbucket など、複数の SCM プラットフォームで利用可能な点も reviewdog の強みです。マルチリポジトリ・マルチサービス環境を前提とした CI にも対応できます。 |
| 229 | + |
| 230 | +## どちらを使うべきか |
| 231 | + |
| 232 | +機能的には当然 GitHub Annotation より reviewdog の方が高機能です。 |
| 233 | + |
| 234 | +当たり前のことを言ってしまえば、reviewdog の機能が必要であれば reviewdog を使うべきですし、GitHub Annotation で事足りるなら標準機能のみで実現する形が望ましいでしょう。 |
| 235 | + |
| 236 | +根本的にはフラットに比較すべきものではないような気がします。 |
| 237 | + |
| 238 | +おそらく多くの場合ポイントになるのは、差分出力ではないでしょうか。 |
| 239 | + |
| 240 | +ここについては Linter や Formatter が導入されていない既存のコードに対して後から CI を導入する場合は、大量のエラーや警告が発生し、それがノイズになることが多くあります。逆に、新規開発において始めから PR 駆動できっちりと CI を回すケースにおいては、全件出力でも問題にならないでしょう。 |
| 241 | + |
| 242 | +## おわりに |
| 243 | + |
| 244 | +案外 GitHub Annotation でもやれるんじゃないか、という記事でした。 |
0 commit comments