くりにっき

フルスタックキュアエンジニアです

GitHub Actionsでmatrixを共通化する

前置き

GitHub Actionsではworkflowのyamlファイルに下記のように jobs.<job-id>.strategy.matrix を書くことでmatrix buildを作ることができます。 *1

# .github/workflows/build.yml
jobs:
  test:
    name: test (Ruby ${{ matrix.ruby }}, Go ${{ matrix.go }})

    runs-on: ubuntu-latest

    strategy:
      fail-fast: false

      matrix:
        ruby:
          - "3.3"
          - "3.4"
        go:
          - "1.23"

しかし、このmatrixの組み合わせを別のworkflowでも使いたくなった時に、普通にやると全く同じものを別ファイルにコピペする必要が出てきます。

このエントリでは複数のworkflowで利用するmatrixを共通化する小ネタを紹介します

tl;dr;

適当なjsonファイルから読み込んだ値をmatrixの値として利用するサンプル

# .github/workflows/build.yml
jobs:
  generate-matrix:
    runs-on: ubuntu-latest

    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}

    steps:
      - uses: actions/checkout@v4

      - id: set-matrix
        run: echo "matrix=$(cat matrix.json | jq -c)" >> $GITHUB_OUTPUT
        working-directory: .github/workflows/

  test:
    name: test (Go ${{ matrix.go }}, Ruby ${{ matrix.ruby }})

    needs:
      - generate-matrix

    runs-on: ubuntu-latest

    strategy:
      fail-fast: false

      matrix: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }}

.github/workflows/matrix.json の中身は下記のような形式を想定。

{
  "go": [
    "1.23"
  ],
  "ruby": [
    "3.3",
    "3.4"
  ]
}

解説

動的にmatrixを生成する

GitHub Actionsでmatrixを動的に生成する方法は下記で紹介されているので参考になります。

docs.github.com

要は別のjobで生成したmatrix(json文字列)をoutputs経由で後続のjobに引き回すという方法です。

.github/workflows/matrix.json の値をworkflowのmatrixとして引き回す準備をしてるのが下記です。

  generate-matrix:
    runs-on: ubuntu-latest

    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}

    steps:
      - uses: actions/checkout@v4

      - id: set-matrix
        run: echo "matrix=$(cat matrix.json | jq -c)" >> $GITHUB_OUTPUT
        working-directory: .github/workflows/

jsonファイルの置き場所はどこでもいいですが、自分はCI関係のスクリプト.github/workflows/ に置くことが多いです。

ここでのポイントは jq -c してるところ。

matrixの値(json文字列)は環境変数経由で渡す必要があるんですが、json文字列が複数行だと環境変数にそのままセットすると環境変数には1行目しかセットされないし、かといって複数行文字列を頑張って環境変数としてセットするのも割と面倒なので jq -c で1行jsonになるようにしてます。

$ cat matrix.json
{
  "go": [
    "1.23"
  ],
  "ruby": [
    "3.3",
    "3.4"
  ]
}

$ cat matrix.json | jq -c
{"go":["1.23"],"ruby":["3.3","3.4"]}

元のjsonファイルを最初から1行にしていてもいいけどリポジトリにコミットする場合はdiffの見やすさから複数行の方が嬉しいんですよね、、、(好みの問題)

自分でスクリプトを書けばjsonファイル以外をmatrixのソースにすることもできるけどこれが一番シンプルだと思います。

working-directory を削って

      - id: set-matrix
        run: echo "matrix=$(cat .github/workflows/matrix.json | jq -c)" >> $GITHUB_OUTPUT

でもいいですが、そうするとworkflowファイル内で defaults.run.working-directory を書いてた場合 *2 にそこだけちょっと変える必要が出てくる(完全にコピペできない)のでコピペビリティ重視でset-matrix側で working-directory を書く方法に落ち着きました。 

JSON文字列からmatrixを生成する

generate-matrix jobのoutputsからJSON文字列を取り出してfromJSONでオブジェクトに変換してセット

  test:
    needs:
      - generate-matrix

    strategy:
      fail-fast: false

      matrix: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }}

上記はさっきの例でいうと

  test:
    strategy:
      fail-fast: false

      matrix:
        ruby:
          - "3.3"
          - "3.4"
        go:
          - "1.23"

と同じになります。

ちなみにmatrix.jsonの中の単一のkeyだけをmatrixの値として利用したい(全部のkeyをmatrixに利用したくない)時は必要なkeyだけ取り出せばいいです。

例)*3

  test:
    strategy:
      fail-fast: false

      matrix:
        ruby: ${{ fromJSON(needs.generate-matrix.outputs.matrix).ruby }}