GitHub Actionsで並列実行するワークフローをPRの必須ステータスチェックに使う方法
はじめに
GitHub Actionsのワークフローが成功した場合のみ、Pull Requestをマージできるようにしたいことがよくあります。
下記の条件を満たすワークフローを作るには少し工夫が必要だったので紹介します。
- 対象ファイルに差分があった場合のみワークフローを実行する
- ワークフロー内のジョブは並列実行する
- 対象ファイルに差分がなかった場合、もしくはすべてのジョブが成功した場合に成功とし、Pull Requestがマージできる
対象読者
- GitHub Actionsを利用している方
結論
- 対象ファイルに変更があったかどうかにはpaths-filterを使う
- ステータスチェック用のジョブをワークフローの最後に追加する
- ステータスチェック用のジョブを、Branch protection ruleのRequire status checks to pass before mergingに追加する
本文
よくある要件だと思いますが、GitHub Actionsのドキュメントを読んでも意外と解決方法がわからなかったので、ワークフロー作成時に発生した問題と解決方法を紹介していきます。
対象ファイルに変更がない場合にステータスがpendingになり続けてしまう問題
問題
GitHub Actions標準機能のpathsを使えば、対象ファイルに変更がない場合はワークフローをスキップすることが出来ます。
しかし、その場合はGitHub Actionsの仕様により、ステータスチェックは成功になりません。
GitHub Docsには下記のように記載されています。
パス フィルター、ブランチ フィルター、またはコミット メッセージによってワークフローがスキップされた場合、そのワークフローに関連付けられているチェックは "Pending" 状態のままになります。 これらのチェックを成功させる必要がある pull request は、マージが禁止されます。
ただし、ワークフロー内のジョブが条件付きのためにスキップされた場合、状態は "成功" として報告されます。 詳しくは、「条件を使用してジョブの実行を制御する」をご覧ください。
ワークフロー自体をスキップするとPending状態のままになってしまうが、ワークフロー内のジョブがスキップされた場合は成功にすることができるということです。
解決方法
GitHub Actions標準機能のpathsを使わずに、paths-filterを使えば、ワークフロー自体は実行し、対象ファイルに変更がない場合はpaths-filter以降のジョブをスキップすることが出来ます。
これで対象ファイルに変更がない場合にPending状態のままになってしまう件は解決です。
backendディレクトリに変更がなかったらスキップする場合は下記のようになります。
name: backend-test
on:
pull_request:
branches:
name: backend-test
on:
pull_request:
branches:
- develop
- main
# paths:
# - backend/** ワークフロー自体は実行したいのでpathsは取り除く
jobs:
paths-filter:
runs-on: ubuntu-latest
timeout-minutes: 1
outputs:
has-changes: ${{ steps.changes.outputs.has-changes }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: changes
with:
filters: |
has-changes:
- backend/**
- .github/workflows/backend-test.yaml
backend-test:
runs-on: ubuntu-latest
timeout-minutes: 5
needs: paths-filter
if: ${{ needs.paths-filter.outputs.has-changes == 'true' }} # paths-filterの結果によって実行するかどうか決める
defaults:
run:
working-directory: backend
steps:
- uses: actions/checkout@v4
...
ワークフロー内のジョブを並列実行する場合に、必須ステータスチェックの条件が作れない問題
問題
ワークフロー内のジョブを並列実行する場合、GitHub Actions標準機能のmatrix strategyを使います。
テストの実行時間が長い場合に対象ファイルを分割して並列実行したり、複数の実行環境したい場合に使うことが出来て便利です。
しかし、matrix strategyを使うことでジョブの名前が変更されてしまい、必須ステータスチェックが通らなくなります。
例えば必須ステータスチェックにしているジョブがbackend-testで、matrix strategyで1/2, 2/2を指定しているの場合は下記のようになります。
// before
backend-test
// after
backend-test(1/2)
backend-test(2/2)
Branch protection ruleのRequire status checks to pass before mergingに指定するジョブをbackend-testからbackend-test(1/2)とbackend-test(2/2)にすれば解決しそうですが、そうすると今度は対象ファイルに変更がなく、ジョブがスキップされた場合にステータスチェックがpendingのままになってしまいます。
解決方法
この問題を解決するためには、新たにステータスチェック用のジョブを追加し、ステータスチェック用のジョブをBranch protection ruleのRequire status checks to pass before mergingに追加します。
ジョブがスキップされた場合でも実行された場合でもこの処理を通るので、やりたかったことが実現できます。(CTOにこれから方法探そうと思うと相談したところ、CTOが個人開発で採用している方法をその場で教えてくれました)
paths-filterは毎回成功する想定ですが,backend-testが失敗した場合にもbackend test status checkを実行するためにif always()をつけています
ワークフローがキャンセルされたとき、タイムアウトしたときに成功扱いになってしまうので、cancelledも条件に加えています(2025/6/7に追記しました)
test-result:
name: backend test status check
runs-on: ubuntu-latest
needs: [paths-filter, backend-test] # 必須ステータスチェックに使用したいジョブを追加する
if: always()
steps:
- name: Some checks failed
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')}}
run: exit 1
- name: All checks ok
run: exit 0
余談
ステータスチェックで使用するジョブをコピペして他のワークフローでも使ったところ、ステータスチェックが全く機能しなくなりました。nameを変えることで解決しましたが、GitHub Actionsは問題なく動いているワークフローの処理をコピペすることがよくあると思うので気をつけましょう。
まとめ
- GitHub Actionsではpathsを使ってワークフロー自体をスキップすると、ステータスチェックはPendingのままになる
- paths-filterを使えば解決する
- GitHub Actionsではmatrix strategyを使うとジョブの名前が変わる
- ステータスチェック用のジョブを作成し、Branch protection ruleのRequire status checks to pass before mergingにはステータスチェック用のジョブを指定する
- GitHub Actionsでは同じ名前のジョブが複数のワークフローで使われていて、そのジョブ名がBranch protection ruleのRequire status checks to pass before mergingに指定されていても無効になることがある
- 同じジョブの名前を使わないようにする
最終的なワークフローは下記のようになりました。
name: backend-test
on:
pull_request:
branches:
- develop
- main
jobs:
paths-filter:
runs-on: ubuntu-latest
timeout-minutes: 1
outputs:
has-changes: ${{ steps.changes.outputs.has-changes }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: changes
with:
filters: |
has-changes:
- backend/**
- .github/workflows/backend-test.yaml
backend-test:
runs-on: ubuntu-latest
timeout-minutes: 5
needs: paths-filter
if: ${{ needs.paths-filter.outputs.has-changes == 'true' }}
defaults:
run:
working-directory: backend
strategy:
fail-fast: false
matrix:
shard: [1/2, 2/2]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
id: setup_node
with:
node-version-file: .tool-versions
- name: Install node modules
run: npm ci
- name: Run tests
run: npm test:ci --shard=${{ matrix.shard }}
test-result:
name: backend test status check
runs-on: ubuntu-latest
needs: [paths-filter, backend-test]
if: always()
steps:
- name: Some checks failed
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')}}
run: exit 1
- name: All checks ok
run: exit 0
Discussion