npm workspaces 環境で dorny/paths-filter を使って、workspaces ごとの GitHub Actions の実行を制御してみた

2023.08.07

こんにちは、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 が実行されるようになります。

.github/workflows/sub1-build.yml

on:
  pull_request:
    paths:
      - sub1/**
      - .github/workflows/sub1.yml

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Build sub1!"

.github/workflows/sub2-build.yml

on:
  pull_request:
    paths:
      - sub2/**
      - .github/workflows/sub2.yml

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Build sub2!"

しかし上記の workflow のみを実装した場合の問題点として、ルートのpackage.jsonpackage-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 から呼び出せるようにします。

.github/workflows/sub1-build.yml

on:
  pull_request:
    paths:
      - sub1/**
      - .github/workflows/sub1.yml
  workflow_call:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Build sub1!"

.github/workflows/sub2-build.yml

on:
  pull_request:
    paths:
      - sub2/**
      - .github/workflows/sub2.yml
  workflow_call:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Build sub2!"

そして、追加の workflow では pathspackage.jsonおよびpackage-lock.jsonを指定して、ルートの変更を検知します。そしてsub1-buildおよびsub2-build ジョブで 前述の2つの workflow を呼び出すのですが、この時 workflow が2重起動しないように、dorny/paths-filterを利用して変更があったファイルを抽出し、呼び出す workflow を制御するようにしています。

.github/workflows/root-build.yml

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 をトリガーさせることができそうです。

参考

以上