
npm workspaces 環境で dorny/paths-filter を使って、workspaces ごとの GitHub Actions の実行を制御してみた
この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは、CX事業本部 Delivery部の若槻です。
今回は npm workspaces 環境で dorny/paths-filter を使って、workspaces ごとの GitHub Actions の実行を制御してみました。
npm workspaces 環境で workspaces ごとのアクション実行を良い感じに制御したい
npm workspaces を利用すると、複数の npm プロジェクト(workspace)から成るモノリポ環境を容易に管理することができるようになります。
sub1およびsub2 workspace を持つ npm workspaces 環境を構築します。
npm init -y npm init -w ./sub1 -y npm init -w ./sub2 -y
するとnode_modulesおよびpackage-lock.jsonはルートに作成され、各 workspaces から参照されるようになります。
$ tree -L 2
.
├── README.md
├── node_modules
│   ├── sub1 -> ../sub1
│   └── sub2 -> ../sub2
├── package-lock.json
├── package.json
├── sub1
│   └── package.json
└── sub2
    └── package.json
この環境に対して GitHub Actions workflow を実装するなら次のようになります。これにより各 workspaces に対して、sub1およびsub2の変更があった場合にのみ workflow が実行されるようになります。
on:
  pull_request:
    paths:
      - sub1/**
      - .github/workflows/sub1.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Build sub1!"
on:
  pull_request:
    paths:
      - sub2/**
      - .github/workflows/sub2.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Build sub2!"
しかし上記の workflow のみを実装した場合の問題点として、ルートのpackage.jsonやpackage-lock.jsonなどに変更があった場合には、各 workspaces に対して workflow が実行されません。ESLint や Prettier など一部の devDependency はルートから各 workspaces に対して利用するため、ルートのpackage.jsonに記述する場合があります。
理想としては、次のようにルートのファイルが更新された場合は、各 workspaces に対して workflow が実行されるようにしたいです。また同時に workspace のファイルに更新があった場合に、同じ workspace に対する重複実行を回避するようにしたいです。
| 更新ファイル | 実行するワークフロー | 
|---|---|
| package.json,package-lock.json | sub1-build.yml,sub2-build.yml | 
| sub1/** | sub1-build.yml | 
| sub2/** | sub2-build.yml | 
dorny/paths-filter GitHub Actions を使う
そこで、更新が行われた workspace ごとに良い感じに workflow を実行するための方法としてdorny/paths-filterを利用します。
まず workspace ごとの workflow の on イベントにworkflow_callを追加して、別の workflow から呼び出せるようにします。
on:
  pull_request:
    paths:
      - sub1/**
      - .github/workflows/sub1.yml
  workflow_call:
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Build sub1!"
on:
  pull_request:
    paths:
      - sub2/**
      - .github/workflows/sub2.yml
  workflow_call:
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Build sub2!"
そして、追加の workflow では pathsでpackage.jsonおよびpackage-lock.jsonを指定して、ルートの変更を検知します。そしてsub1-buildおよびsub2-build ジョブで 前述の2つの workflow を呼び出すのですが、この時 workflow が2重起動しないように、dorny/paths-filterを利用して変更があったファイルを抽出し、呼び出す workflow を制御するようにしています。
on:
  pull_request:
    paths:
      - package.json
      - package-lock.json
      - .github/workflows/root-build.yml
jobs:
  changes:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: read
      contents: read
    outputs:
      sub1: ${{ steps.filter.outputs.sub1 }}
      sub2: ${{ steps.filter.outputs.sub2 }}
    steps:
      - uses: actions/checkout@v3
      - uses: dorny/paths-filter@v2
        id: filter
        with:
          filters: |
            sub1:
              - sub1/**
              - .github/workflows/sub1.yml
            sub2:
              - sub2/**
              - .github/workflows/sub2.yml
  sub1-build:
    needs: changes
    if: ${{ needs.changes.outputs.sub1 == 'false' }}
    uses: ./.github/workflows/sub1-build.yml
  sub2-build:
    needs: changes
    if: ${{ needs.changes.outputs.sub2 == 'false' }}
    uses: ./.github/workflows/sub2-build.yml
この dorny/paths-filter のフィルターの対象となるのは Pull Request 中で変更されたファイルです。push されたコミットではありません。よって permission として pull-requests: read を指定する必要があります。
動作確認
ルートのファイルのみ更新された場合
まず、ルートのpackage.jsonおよびpackage-lock.jsonのみ更新し、変更を Pull Request に push します。
$ npm i eslint@latest
added 96 packages, and audited 104 packages in 4s
24 packages are looking for funding
  run `npm fund` for details
found 0 vulnerabilities
$ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   package-lock.json
        modified:   package.json
no changes added to commit (use "git add" and/or "git commit -a")
Pull Request への push による CI が実行されました。

ルートの workflow が実行され、そこからsub1-buildおよびsub2-build workflow が呼び出されて実行されています。
- .github/workflows/root-build.yml / changes (pull_request)
- .github/workflows/root-build.yml / sub1-build / build (pull_request)
- .github/workflows/root-build.yml / sub2-build / build (pull_request)
sub1 ディレクトリ配下のファイルが更新された場合
続いて、sub1ディレクトリ配下のpackage.jsonと、ルートのpackage-lock.jsonを更新し、変更を Pull Request に push します。
$ git commit -m "npm i eslint@latest"
[feature-hoge-1 0402536] npm i eslint@latest
 2 files changed, 1003 insertions(+), 1 deletion(-)
$ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   sub1/package-lock.json
        modified:   sub1/package.json
no changes added to commit (use "git add" and/or "git commit -a")
Pull Request への push による CI が実行されました。

今度は先程とは異なり、ルートの workflow からsub2-build workflow のみが呼び出されて実行されています。一方 sub1-build workflow は直接呼び出されています。
- .github/workflows/root-build.yml / changes (pull_request)
- .github/workflows/sub1-build.yml / build (pull_request)
- .github/workflows/root-build.yml / sub2-build / build (pull_request)
そしてルートの workflow からsub1-build workflow の呼び出しはスキップされています。
- .github/workflows/root-build.yml / sub1-build (pull_request)
おわりに
npm workspaces 環境で dorny/paths-filter を使って、workspaces ごとの GitHub Actions の実行を制御してみました。
dorny/paths-filter GitHub Actions を上手く使うことにより、ルートのファイルが更新された場合は各 workspaces に対して workflow が実行され、また workspace のファイルに更新があった場合に同じ workspace に対する重複実行を回避するすることができました。これで npm workspaces 環境でも過不足なく GitHub Actions workflow をトリガーさせることができそうです。
参考
以上












