pnpm workspace を利用したモノレポで「この PR の影響を受けるパッケージ」をフィルタする
3行まとめ
-
pnpm の
--filter
オプションにはパッケージ名だけでなく git の commit や branch も渡すことができる -
pnpm ls --filter
をうまく使えば「main branch からの diff の影響を受けるパッケージ」の一覧を取り出せる - ドキュメントや help をちゃんと見ると、意外と知らないことが書いてある
モチベーション
LayerX のバクラク事業部では Webapp(Web Frontend アプリケーション)のモノレポ化を進めており、1つのリポジトリに複数の Next.js アプリケーションが存在します。
そこで悩ましいのが CI でのテストなどの実行です。
- そのまま全件実行すると時間が長くなっていく
- e.g. アプリAのコードしか変更してないのに、アプリBのテストも実行されてしまうと時間もお金も無駄にかかる
- 何もしないとプロダクト・事業が拡大すればするほど悪化していく
- とはいえ変更ファイルだけを見た実行制御だと漏れが発生する
- e.g. パッケージCが変更されたとき、パッケージCだけではなくそれに依存するアプリAとアプリDのテストも実行すべき
ユニットテストだけならまだしも、時間がかかる Storybook のビルドやブラウザを利用したテストなどが入ってくると流石に無視できなくなってきます。
--filter
オプションを使う
pnpm の 当初はこの問題のために Turborepo などのモノレポ向けタスクランナー側での解決を模索していました。これらは主に賢いキャッシュの仕組みを使って必要なタスクのみを実行する仕組みを持ちます。
しかし、そのような外部ツールを使わずとも、pnpm の機能だけで・シンプルな仕組みで「単純に変更内容をもとに(依存関係的に)影響を受けそうなパケージに絞ってタスクを実行する」ということは実現可能です。それが pnpm の --filter
オプションです。
pnpm では --filter
オプションを指定することで、 workspace 内の条件にマッチするパッケージに絞ってコマンドを実行することが可能です
# 以下の例はドキュメントからの引用
pnpm --filter "@babel/core" test
pnpm --filter "@babel/*" test
pnpm --filter "*core" test
この --filter
は主にパッケージ名やディレクトリを指定することが多いでしょう。
しかし、実は git の commit や branch も指定することができます。
# https://pnpm.io/filtering#--filter-since
pnpm --filter "...[origin/master]" test
この機能をうまく利用し、たとえば GitHub Actions であれば以下のように base_ref
などを渡すことで、変更の影響を受けたパッケージに絞ってテストが実行されます。
on:
pull_request:
branches:
- "**"
push:
branches:
- main
jobs:
test:
# ...
steps:
# ...
# pull request では base branch からの差分の影響を受けるパッケージのみテストする
- name: Test
run: |
pnpm --filter "${TURBO_FILTER}" test
env:
TURBO_FILTER: "${{ github.event_name == 'pull_request' && format('...[origin/{0}]', github.base_ref) || '*' }}"
自分は --filter
が git の commit や branch を取れるのを知らなかったのですが、公式ドキュメントに書いてるし、よく見たら pnpm run --help
も言及があります。
--filter "[<since>]" Includes all packages changed since the specified commit/branch. E.g.: "[master]", "[HEAD~2]". It may be used together with "...". So, for instance, "...[HEAD~1]" selects all packages changed in the last commit and their dependents
pnpm ls --filter
で変更影響があったパッケージを抜き出し、job の起動制御に利用する
応用: --filter
は pnpm run
以外のコマンドでも有効です。たとえば pnpm ls --filter '...[origin/main]' --depth 1
のようにすれば、main ブランチからの diff の影響を受けるパッケージの一覧を抜き出すことができます。
# 変更影響を受けるパッケージ名の一覧を JSON 配列にする
pnpm ls --filter="...[origin/main]" --depth -1 --json \
| jq -r "map(.path | ltrimstr(\"$PWD/\")) | @json"
これを GitHub Actions の output などに詰め込んで fromJSON
や contains
と組み合わせることで柔軟な実行制御が可能になります。
以下の例では、比較的重くなりがちである Storybook のビルド・デプロイ・テストを必要なときだけ実行するようなワークフローを記述しています。
jobs:
changes:
runs-on: ubuntu-latest
outputs:
packages-json: ${{ steps.changed.outputs.packages-json }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: "0" # 変更影響を見るために履歴が必要
# ...
- run: |
packagesJson=$(pnpm ls --filter="${TURBO_FILTER}" --depth -1 --json | jq -r "map(.path | ltrimstr(\"$PWD/\")) | @json")
echo "packages-json=${packagesJson}"
echo "packages-json=${packagesJson}" >> "$GITHUB_OUTPUT"
env:
TURBO_FILTER: ${{ github.event_name == 'push' && github.ref_name == 'main' && '*' || format('...[origin/{0}]', github.base_ref) }}
id: changed
storybook:
strategy:
matrix:
package:
- path: apps/iikanji-no-webapp
- path: packages/iikanjini-benri-na-ui-components
fail-fast: false
name: Storybook - ${{ matrix.package.path }}
needs: [changes]
uses: ./.github/workflows/_storybook.yml
with:
sha: ${{ github.event.pull_request.head.sha || github.sha }}
ref_name: ${{ github.head_ref || github.ref_name }}
package_path: ${{ matrix.package.path }}
skip: ${{ !contains(fromJSON(needs.changes.outputs.packages-json), matrix.package.path) }}
Discussion