TL;DR;
GitHub Actionsのjobはmatrixを使うことで、複数の入力条件で実行できる。
また、条件として列挙されたうちの一部をexcludeで除外することができる。
しかし、excludeによってmatrixの要素数が0になると、matrixの値が空の状態で実行されてしまう。
多くの場合、これは意図しない動作になるだろう。
jobの実行は防げないので、jobの中のstepでmatrixの値をチェックして個別の処理をスキップする必要がある。
背景
複数のリソースを管理しているリポジトリで
- diffのあったリソースのみデプロイする
- ただし、特定のリソースは2つある環境(stageとproduction)のうち一方にだけデプロイする
というワークフローを書いていた。
例えば、こんな構成になっていて、
resources/
a/ # stageとproduction両方
b/ # stageとproduction両方
c/ # stageのみ
- aに変更があったときは、aをstageとproductionにリリースする
- cに変更があったときは、cをstageにリリースする
といった具合である。
これを実現するために、
- 変更のあったディレクトリを検知
- 変更したディレクトリのリストをmatrixの要素として受け取りstageのデプロイを実行
- 変更したディレクトリのリストをmatrixの要素として受け取りproductionのデプロイを実行
- ただし、excludeでcを指定
というワークフローを書いた。
そしてcに変更があった場合、期待する動きは
- 変更のあったディレクトリとしてcが検知
- 変更したディレクトリのリストとして
["c"]
をmatrixとして受け取り、stageにcをデプロイ - 変更したディレクトリのリストとして
["c"]
をmatrixとして受け取るが、cはexcludeで指定されてmatrixは空なので何も実行せず終了
だった。
しかし、実際には
- 変更のあったディレクトリとしてcが検知
- 変更したディレクトリのリストとして
["c"]
をmatrixとして受け取り、stageにcをデプロイ - 変更したディレクトリのリストとして
["c"]
をmatrixとして受け取るが、cはexcludeで指定されてmatrixは空なのだが、要素として空の値(null)が指定されて1回実行される
という挙動となった。
その結果、本来、a,b,cのいずれかを期待しているjobはエラーを起こし、failedで完了することとなった。
コードイメージ
そのコードを直接載せるのは差し支えがあるし、そもそも見づらいので、同様の問題が発生するシンプルなコードを掲載する。
name: Empty matrix deploy
on:
pull_request:
branches:
- main
jobs:
# 対象のリソースを取得する(ここでは固定でcを取得しているが、本来は動的に決まるもの)
get_target_resources:
runs-on: ubuntu-latest
outputs:
resources: ${{ steps.get_resources.outputs.resources }}
steps:
- name: get resources
id: get_resources
run: |
echo 'resources=["c"]' >> $GITHUB_OUTPUT
# stageのデプロイ(すべてのリソースをデプロイする)
deploy_stage:
runs-on: ubuntu-latest
needs: get_target_resources
strategy:
matrix:
resource: ${{ fromJson(needs.get_target_resources.outputs.resources) }}
steps:
# matrix.resourcが"c"で実行されるので、 "deploy resource: [c]" と出力される
- name: deploy resource
run: |
echo "deploy resource: [${{ matrix.resource }}]"
# productionのデプロイ(cはデプロイしないので、本来は実行されてほしくないのだが、resourceの値がnullで実行されてしまう)
deploy_production:
runs-on: ubuntu-latest
needs: get_target_resources
strategy:
matrix:
resource: ${{ fromJson(needs.get_target_resources.outputs.resources) }}
exclude:
- resource: "c"
steps:
# matrix.resourcがnullで実行されるので、 "deploy resource: []" と出力されてしまう
- name: deploy resource
run: |
echo "deploy resource: [${{ matrix.resource }}]"
とりあえずの解決方法
調べた限り、このケースにjobの実行を防ぐすべは見当たらなかった(知っている人がいたらぜひ教えてほしい)。
matrixの値にnullが入ることを利用して、以下のようにstepの中でmatrixの値をチェックし、nullでない場合のみ実行するようにすれば、意図しない(つまりmatrixの値がnullでの)実行は防ぐことができる。
deploy_production_workaround:
runs-on: ubuntu-latest
needs: get_target_resources
strategy:
matrix:
resource: ${{ fromJson(needs.get_target_resources.outputs.resources) }}
exclude:
- resource: "c"
steps:
# nullでない場合のみ実行する
- name: deploy resource
if: ${{ matrix.resource != null }}
run: |
echo "deploy resource: [${{ matrix.resource }}]"
補足: matrixとして受け取る値が空の場合
excludeによって空になるのではなく、そもそもmatrixに入る配列が空になるようなケースの対応はこちらで紹介されている。
前のjobで作った配列(になるJSON文字列)をチェックし、空と見なされるようなケースはifで除くというやり方である。
test:
needs: changes
if: ${{ needs.changes.outputs.services != '[]' && needs.changes.outputs.services != '' }}
strategy:
matrix:
service: ${{ fromJson(needs.changes.outputs.services) }}
一応補足しておくと、この方法はexcludeで空になるようなケースでは使えない。
test:
needs: changes
if: ${{ matrix.service != null }}
strategy:
matrix:
service: ${{ fromJson(needs.changes.outputs.services) }}
のような書き方をすると、このスコープではmatrixが使えないため、
Unrecognized named-value: 'matrix'
というエラーが出て実行できない。