GitHub Actionsは同じトリガーが設定された別のワークフローファイル.github/workflows/{a,b}.yamlなどを並列で処理します。しかし、1つワークフローで並列処理したいこともありますよね。たとえば、Nodejsのバージョン18,19,20でアプリケーションをビルドしなくてはいけないときなど、直列で処理してしまうとワークフローが終了するまでに3回分のビルド時間を待っている必要があります。
GitHub Actionsにはマトリックスと呼ばれる複数の組み合わせを実行するための機能が備わっていて、これを利用することで並列処理を実現します。
マトリックス
マトリックスってそもそもなんだ?っていうと、数学のマトリクス?行列演算などと同じようなものですかね。
jobs:
example_matrix:
strategy:
matrix:
version: [10, 12, 14]
os: [ubuntu-latest, windows-latest]
このように書いておくとversionとosの全ての組み合わせを実行するジョブが生成されます。
| os \ version | 10 | 12 | 14 |
|---|---|---|---|
| ubuntu-latest | v10, ubuntu-latest | v12, ubuntu-latest | v14, ubuntu-latest |
| windows-latest | v10, windows-latest | v12, windows-latest | v14, windows-latest |
もちろん、matrix以下に1つの配列を設定することも可能です。
jobs:
example_matrix:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
このように書いておけば、ubuntu-latestとwindows-latestで実行する2つのジョブが生成されます。
もう少し具体的な例を出してみましょう。例えば、いくつかのディレクトリ以下で何らかのテストを実行するとします。
パッと思いつくところで簡単に実装をするなら以下です。
jobs:
test:
steps:
- run: |
for x in dirs/{a,b,c}; do
(cd "$x" && make test)
done
forでdirs以下のa, b, cディレクトリを回し、cdしながらテストを実行しています。処理は1つずつ直列に実行されるので、1つのジョブの実行時間はテスト3つ分かかります。
そこでマトリックスの出番です。マトリックスを使用することで、この3つのテストを並列に処理します。
jobs:
test:
strategy:
matrix:
dir: [a, b, c]
steps:
- run: make test
working-directory: dirs/${{ matrix.dir }}
${{ matrix.dir }}は、それぞれa, b, cの文字列に展開され、その数分だけジョブが生成されます。
動的に分割
matrixの値を動的に決めたいこともあると思います。例えばモノレポで、変更のあったディレクトリ以下でのみテストを並列で実行したい、などです。
これはmatrixを生成するジョブと、それを読み込むジョブにわけることで実現できます。
jobs:
updated:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.updated.outputs.matrix }}
steps:
- uses: actions/checkout@4
- id: updated
run: |
a=$(git diff --name-only origin/${GITHUB_BASE_REF} HEAD -- . | sed -E 's#^(.*)/[^/]*$#"\1",#' | sort -u | tr -d '\n' | sed -E 's/,$//; s/^(.*)$/[\1]/' | jq -c '{"target-dir": .}')
echo -E "matrix=$a"
echo -E "matrix=$a" >> "$GITHUB_OUTPUT"
matrix:
needs: updated
runs-on: ubuntu-latest
strategy:
matrix:
target-dir: ${{ fromJSON(needs.updated.outputs.matrix) }}
steps:
- uses: actions/checkout@v4
- run: pwd
working-directory: dirs/${{ matrix.target-dir }}
updatedジョブのrunの中身が少し複雑になっているので解説します。
git diff --name-only origin/${GITHUB_BASE_REF} HEAD -- .変更のあったファイル一覧を取得しています。${{ github_base_ref }}はワークフローを実行したときのブランチ名が入ってくるので
git diff --name-only origin/<branch-name> HEADが実行されたのと同じです。origin/<branch-name>からHEADまでに更新のあったファイルを取得しています。sed -E 's#^(.*)/[^/]*$#"\1",#'更新のあったファイル一覧からファイル名の部分を削除し、ダブルクォーテーションで囲いつつ、カンマ区切りで出力しています。
a/file1 b/file1 b/file2 c/file1を
"a", "b", "b", "c",このフォーマットに整形しています。
sort -uディレクトリ名だけになった出力から重複しているディレクトリを削除します。
git diffは更新のあったファイルが一覧で取得されるため、1つのディレクトリ内で複数のファイルを変更した場合、同じディレクトリが複数行出力されてしまいます。tr -d '\n'改行区切りの値を
"<var1>","<var2>",...,のフォーマットで出力します。sed -E 's/,$//'; s/^(.*)$/[\1]/最終行の値も
"<var_end>",なので改行を削除しただけでは行の最後にカンマが残ってしまいますのでそれを削除し、[]で囲って出力します。jq -c '{"target-dir": .}'JSON配列を受け取って
target-dirオブジェクトに紐付けます。{"target-dir":["dirs/b","dirs/c"]}このような値を期待します。xargs printf 'matrix=%s\n'全体の文字列の前に
matrix=を付与しています。matrix={"target-dir":["dirs/b","dirs/c"]}このような値を期待しています。
ここまでで整形された値はoutputsの${{ steps.updated.outputs.matrix }}で外部のジョブから読み込みできるようにアウトプットしています。
matrixジョブではneedsを使って依存関係を表し、matrixへ生成したJSON文字列を代入しています。
関係のないドキュメントだったのですが、動的にマトリックスを生成している部分があったので参考として載せておきます。
デモ
まずは単純なマトリックスです。
Actionsの実行を見てみると、run_in_orderの方は1つのジョブとし実行されているのに対し、matrixの方は3のジョブが並んでいます。ステータスバッチは1つ目だけクリアしている状態で、2、3はまだ実行中ですので、並列で実行されているのがわかります。

matrixで3つのジョブが終了していますが、run_in_orderの方はまだ実行中です。実行時間も見てみてください。

実行ログをみるとrun_in_orderは1つずつ処理しているのに対し、matrixは1つの処理だけを実行しています。


お次は動的に実行します。
updatedというジョブが実行され、更新されたファイルからマトリックスを生成します。

生成されたマトリックスでジョブが分割され、並列実行されました。

まとめ
異なる複数の環境用にビルドしたいときなどにマトリックスは有用ですが、複数の処理があり、それが並列で実行されても問題ない場合にはマトリックスを使用することで処理時間の短縮を行うことができます。