モノレポ環境で各パッケージに対して reviewdog (eslint) を効率的に実行する

はじめに

本記事ではモノレポ環境で各パッケージに対して reviewdog (eslint) を効率的に実行する方法について紹介します。

今回使用したリポジトリは以下になります。実装はテキトーに AI に書かせています。

GitHub - nabekou29/monorepo-reviewdog-sample

Contribute to nabekou29/monorepo-reviewdog-sample development by creating an account on GitHub.

github.com

モノレポ環境で reviewdog を使う例があまり見当たらず、良い方法を探すのに苦労したので、今回はその調査の結果を共有したいと思います。

やりたいこと

私が普段開発しているプロジェクトでは、pnpm のワークスペース機能などを活用して、複数の JS パッケージを 1 つのリポジトリで管理しています。

monorepo-project/
apps/
app1/
app2/
packages/
ui/
utils/

ざっくりこんな構成です。実際はもっとパッケージの数は多いです。

当然アプリケーションとそれ以外のパッケージは適用したいルールが違います。 例えばアプリの方には Next.js 用の設定が入っていたり、TypeScript を使っていないパッケージでは簡単なルールのみだったりします。

つまり以下のような形になります。

monorepo-project/
apps/
app1/
eslint.config.mjs (Next.js 用)
app2/
eslint.config.mjs (Next.js 用)
packages/
ui/
eslint.config.mjs (React x TypeScript 用)
utils/
eslint.config.mjs (TypeScript 用)

今回はこのようなモノレポの構成において GitHub Actions でそれぞれのパッケージに対して reviewdog を実行する方法を紹介します。
以下のサンプルでは、2つのパッケージのみの例を紹介しますが、パッケージが増えても同じように対応できるようになっています。

❌️ 方法1 - matrix でそれぞれのパッケージに対して実行する

WARNING

ここで紹介しているワークフローファイルでは動きません。理由は方法2で説明します。 方法2を理解すれば動くように修正もできるのですが、ダメな例として読んでください。

もともとはこの方法を採用していました。

reviewdog/action-eslint を利用して、各パッケージに対して独立した Job を作成します。

matrix で実行したいパッケージを指定し、あとは reviewdog/action-eslint のドキュメントに従って設定をするだけです。 以下のようなワークフローファイルを作成します。

name: reviewdog-1
on:
pull_request:
types: [opened, synchronize]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
jobs:
eslint:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
pull-requests: write
strategy:
matrix:
include:
- name: web
working-directory: apps/web
- name: ui
working-directory: packages/ui
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: install dependencies
run: pnpm install --frozen-lockfile
- uses: reviewdog/action-eslint@v1
with:
workdir: ${{ matrix.working-directory }}
github_token: ${{ secrets.GITHUB_TOKEN }}

評価(方法1)

  • メリット
    • ワークフローファイルがシンプル
    • 実行時間が短い
  • デメリット
    • パッケージの数によっては Billable Time が膨らむ

補足: Billable Time について

GitHub Actionsの課金時間のこと。利用するランナーによって時間あたりの料金が決まっており、 最終的な金額は [Billable Time (min)] x [(ランナーの料金 ($/min)] で計算される。

GitHub Actions では Job ごとに分単位に切り上げられて Billable Time が計算されるため、Job あたりの時間が短くとも Job の数が多いと金額が膨らむ。

例えば、1 つの Job が 30秒で終わる場合でも Job が 10 個あれば Billable Time は合計 10 分となり、実際に実行にかかった時間以上の料金が発生する。

参考: GitHub Docs - GitHub Actions の課金について

(おすすめ)方法2 - 単一の Job で複数回 reviewdog を実行する

方法1のデメリットを解消するために、単一の Job で複数回 reviewdog を実行する方法です。

package.json に scripts を追加

以下のように reviewdog の対象にしたいパッケージごとにスクリプトを追加します。

各パッケージには lint:reviewdog というスクリプトを追加し、ルートの package.json でそれぞれのパッケージをまとめて実行するようにします。

  • --format=rdjson を使用するためには、事前に eslint-formatter-rdjson がインストールされている必要があります。
  • 重要なポイントとして、-name オプションに別々の値を設定する必要があります。 理由は後述。
apps/web/package.json
{ ...
"scripts": {
"lint:reviewdog": "eslint --format=rdjson | reviewdog -f=rdjson -name=eslint-web -reporter=${REVIEWDOG_REPORTER:-'github-pr-review'}"
}
...
}
// packages/ui/package.json
{ ...
"scripts": {
"lint:reviewdog": "eslint --format=rdjson | reviewdog -f=rdjson -name=eslint-ui -reporter=${REVIEWDOG_REPORTER:-'github-pr-review'}"
}
...
}
// package.json
{ ...
"scripts": {
"lint:reviewdog": "pnpm -r run lint:reviewdog" // pnpm -r --parallel run lint:reviewdog でも可
}
...
}

-name を設定する理由

reviewdog/service/github/github.goをサッと見たところ次のことがわかりました。

reviewdog は実行の度に一度 PR のコメントを削除しているようで、その目印として -name で指定した値を使用しているようでした。 これにより、何度 reviewdog が実行されても PR に重複したコメントがつかないようにしつつ、複数ツールでも正しく動作するようになっているようです。

extractMetaCommentoutdatedComments などのワードで実装を追いかけると詳しい処理が見れます。私は軽く見ただけなので間違っているかもしれません。

-name を全て同じにしてしまうと、最後に実行した reviewdog で追加されるコメント以外は削除されてしまいます。 そのため、コメントが消えないようにパッケージごとに異なる -name を設定することで、それぞれのコメントが残るようにできます。

方法1でも同じように tool_name というオプションがあるので設定することで正しく動くようにできます。

GitHub Actions で実行

基本は方法1と同じです。

name: reviewdog-2
on:
pull_request:
types: [opened, synchronize]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
jobs:
eslint:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: reviewdog/action-setup@v1 # 追加
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: install dependencies
run: pnpm install --frozen-lockfile
# reviewdog/action-eslint から変更
- name: run eslint
run: pnpm run lint:reviewdog
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}

動作確認

まず、CLI で警告がでていることを確認します。

CLI で各パッケージに対して警告がでていることを確認した結果

その上で、name を変えずに実行すると以下の画像で示すように、片方のパッケージの警告しかでません。

失敗例。片方のパッケージでしか動いていない

name を変えて実行すると、以下のように両方のパッケージの警告が表示されます。

成功例。両方のパッケージで動いている

評価(方法2)

  • メリット
    • ちゃんと動く
    • Billable Time が抑えられる
    • 実行時間もそこまで伸びない
      • 伸びてきた場合も pnpm --parallel で並列実行するなり、パッケージが大量になったらジョブを数個に分割したりで工夫できそう。
  • デメリット
    • なにかあるかな?

各パッケージの scripts を書くのが面倒な場合は、ちょっとしたシェルスクリプトを書いても良さそうです。パッケージが増えてきた場合は、このほうが楽かもしれません。

scripts/eslint-reviewdog.sh
# Usage: pnpm -rc exec "bash $(git rev-parse --show-toplevel)/scripts/eslint-reviewdog.sh"
# config がない場合はスキップ
if [ ! -f "eslint.config.mjs" ]; then
echo "Skip eslint"
exit 0
fi
# PNPM_PACKAGE_NAME は pnpm exec で実行したときに取得できる
pnpm exec eslint -f=rdjson | reviewdog -f=rdjson -name=eslint-${PNPM_PACKAGE_NAME} -reporter=${REVIEWDOG_REPORTER:-'github-pr-review'}

そのほかの改善ポイント

filter-modeadded (デフォルト) で実行する場合、すべてのファイルに対して eslint を実行する必要はないので、変更されたファイルだけ実行するようにすると実行時間を短縮できます。
git diff --name-only --diff-filter=d --relative origin/main などで変更されたファイルを取得し、eslint に渡すようにすればよいです。

結論

reviewdog で同じツールを複数回実行する場合は、name が被らないようにしましょう。

おわりに

実のところ方法2のやり方は記事を書きながら発見したもので、本来は失敗した方法として紹介予定でした。

-name を指定すればうまくいくことに気づいておらず、 なんなら、方法1が正しく動いていないことすら気づいていませんでした。
turborepo を活用して差分のないパッケージは実行をスキップするようにしていたので、大抵の場合はひとつのパッケージでしか reviewdog が実行されておらず、動いてると勘違いしていたのだと思います。

本来は eslint --format=rdjson で出力される JSON をすべてマージして、一度に reviewdog に渡す方法を紹介予定でした。 問題なく動いていたのですが、一時ファイルを作成したり、jq によるマージ処理が必要だったりと複雑になってしまっていたので、より簡単な方法を見つけられて良かったです。

この記事がモノレポでの reviewdog の実行方法に困っている人や、実行コストが気になっている人に参考になれば幸いです。かなり限定的だとは思いますが。