MIXI DEVELOPERS NOTE
🛠️

GitHub Actions の matrix で並列にファイルを生成しつつ、生成した複数のファイルを次の job に渡す方法

2023/08/15に公開

3行まとめ

  • GitHub Actions でディレクトリごとにファイルを並列で生成しつつ、生成したファイルをコミットしたい。
  • 並列処理は matrix でやる。その場合、次の job にファイルが引き継がれないため、artifact の upload / download を利用してファイルを渡すことにした。
  • ただし、artifact の upload / download ではファイルのパス情報は失われてしまうため、artifact 名に base64 化したパス情報を埋め込んだらうまくいった。

経緯

同じようなことをピンポイントでやりたい人がいるかどうかわからないですが、GitHub Actions でディレクトリごとにファイルを並列で生成しつつ、生成したファイルをコミットしたい時がありました。

例えば、以下のような複数のディレクトリがあるリポジトリで、

$ ls
dir1  dir2  dir3  ...

GitHub Actions でそれぞれのディレクトリでファイルを並列に生成して、生成したファイルをリポジトリにコミットしたい。

ここで、GitHub Actions で並列に処理を実行するには、 matrix が使えます。
https://docs.github.com/ja/actions/using-jobs/using-a-matrix-for-your-jobs

ところが matrix の処理は、それぞれ別の runner で実行されるため、生成したファイルをそのまま次の step に引き継ぐことができません。
また、 matrix に限り、GITHUB_OUTPUT を使用した値の受け渡しも対応していません。
https://github.com/orgs/community/discussions/17245

そのため、 matrix を使用してファイルをやり取りするには、 artifact を利用するぐらいしか方法がありませんでした。

artifact にファイルをアップロードするには、一般的には actions/upload-artifact を使ってファイルをアップロードします。
そのため、生成したファイルをそのまま artifact にアップロードしようと考えました。

ところがこれを使ってみると、アップロードしたファイルのパスの情報が失われてしまうことに気がつきました。
どういうことかというと、例えば以下の step でファイルをアップロードし、

- name: upload files
  uses: actions/upload-artifact@v3
  with:
    name: upload-file
    path: dir1/generated-file.txt

以下の step でファイルをダウンロードしてくると、

- name: download artifacts
  uses: actions/download-artifact@v3

こうなってしまいます。

$ tree
.
└── upload-file
    └── generated-file.txt

2 directories, 1 file

artifact 名でディレクトリが作られて、その中に指定したファイルが単体で入っています。
指定したファイルのディレクトリ構成が維持されておらず、どこのディレクトリで生成したファイルかわからないという状況です。

どうしたか

artifact を介して受け渡しができる情報は、artifact name とファイルそのものぐらいしかありません。
そのため、今回はパス情報を artifact name に入れて受け渡しをすることにしました。

ここで、最初は artifact name にパスをそのまま入れてみたのですが、artifact name はどうやら / が使えないようです。

Artifact name is not valid: ************ Contains the following character: Forward slash /

Invalid characters include: Double quote ", Colon :, Less than <, Greater than >, Vertical bar |, Asterisk *, Question mark ?, Carriage return \r, Line feed \n, Backslash , Forward slash /

These characters are not allowed in the artifact name due to limitations with certain file systems such as NTFS. To maintain file system agnostic behavior, these characters are intentionally not allowed to prevent potential problems with downloads on different file systems.

そのため、パス情報を url safe な base64 に変換した上で、 artifact name を指定してアップロードすることにしました。
具体的には以下のような感じです。

...
jobs:
  generate-files:
    name: generate files
    runs-on: ubuntu-latest
    strategy:
      matrix:
        target:
	  - "dir1"
	  - "dir2"
	  - ...
    steps:
      - name: generate file
        working-directory: ${{ matrix.target }}
	id: generate-file
        run: |
          set -euxo pipefail
	  echo "test $RANDOM" > generated-file.txt

          # 生成したファイルのパス情報を basenc で url safe な base64 化する
          artifact="$( echo '${{matrix.target}}/generated-file.txt' | basenc --base64url --wrap 0 )"
          echo "artifact=${artifact}" >> $GITHUB_OUTPUT

      - name: upload file
        uses: actions/upload-artifact@v3
        with:
          name: ${{ steps.generate-file.outputs.artifact }}
          path: ${{ matrix.target }}/generated-file.txt

  commit:
    runs-on: ubuntu-latest
    needs: generate-files
    steps:
      - name: checkout
        uses: actions/checkout@v3

      - name: download artifacts
        uses: actions/download-artifact@v3

      - name: move generated files
        run: |
          set -euxo pipefail

          artifacts=($(ls))
          for i in "${!artifacts[@]}"; do
            artifact="${artifacts[$i]}"

            # artifact 名を decode して生成したファイルのパス情報を得る
            file_path=$(echo "$artifact" | basenc --base64url --decode)
            mv "${artifact}/generated-file.txt" "$file_path"
	    rm -r "${artifact}"
          done
...

これで無事に次の job に生成したファイルを引き継ぐことができました。
めでたしめでたし🎉

追記

artifact に上げたファイルをそのままにしていたところ、社内で 2 番目にストレージを食うリポジトリに成長しました😇
upload-atficat の action を使う時は、retention period を指定できるので、今回の用途の場合は最低保持期間の 1 日で設定すると良いと思います。
https://github.com/actions/upload-artifact
こんな感じで↓

...
      - name: upload file
        uses: actions/upload-artifact@v3
        with:
          name: ${{ steps.generate-file.outputs.artifact }}
          path: ${{ matrix.target }}/generated-file.txt
          retention-days: 1
...

ちなみにリポジトリの設定で、全ての artifact に対しての retention period を指定することもできます。(デフォルト 90 日)
https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository#configuring-the-retention-period-for-github-actions-artifacts-and-logs-in-your-repository

MIXI DEVELOPERS NOTE
MIXI DEVELOPERS NOTE

Discussion