APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

GitHub Actions workflowでループ処理を実行

ACS事業部 亀崎です。突然ですが、みなさんGitHub Actions活用していますか? そんな中でGitHub Actionsでワークフローを実行する際、ループ処理を実行したいと思うことはないでしょうか?

commandsというフォルダにcommand1.sh ~ command9.sh の9個のファイルがあり、これらの内容の処理をワークフロー内で実行したいといったケースを想定します。

今回はこのパターンを例にループ処理を実行してみたいと思います。

基本的なやり方

ファーストステップ

まずぱっと思いつくのは次のようなやり方です。

name: simple-echo-loop
on:
  workflow_dispatch:

jobs:
  job:
    name: setup target modules
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: loop
        run: |
          for file in `ls commands`; do
            echo "${file}"
          done

上記のように runでshell scriptとして for文を記載するものです。

こちらを実行すると次のようになります。(実際の実行結果

簡単ですね。

一部コマンドがエラーになる場合

command(1-9).shの中身は

#!/bin/sh

echo "command 1, success"
exit 0

といった内容になっています。ただし、command5.shだけexit 1を実行します。

#!/bin/sh

echo "command 5, error"
exit 1

さきほどのワークフローのechoの部分をこれらのファイルを実行するように書き換えて実行してみます。

name: simple-exec-loop
on:
  workflow_dispatch:

jobs:
  job:
    name: setup target modules
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: loop
        run: |
          for file in `ls commands`; do
            sh "commands/${file}"
          done
      - name: finalize
        run: |
          echo "All commands executed"
  

すると結果は次のように、command5.sh実行時点でstepが終了し、そのあとのfinalize stepは実行されません。(実際の実行結果

GitHub ActionsではデフォルトではStep中でエラーが発生した場合、その時点で処理を終了しその後のstepは実行しません。このため finalize stepは実行されないのです。

continue-on-error

ここで処理を止めたい場合なら上記の結果でよいでしょう。しかし、時にはエラーで止めたくはない場合もあります。今回の例ではエラー発生有無に関わらずcommand1.shからcommand9.shのすべてをしたいようなケースです。

GitHub ActionsにはStep中でエラーが発生しても、その後のStepの実行を継続するオプションとして continue-on-error があります。 これを使えば、エラーが発生しても継続できるのではないでしょうか。

以下の内容で試してみます。

name: simple-exec-loop-continue
on:
  workflow_dispatch:

jobs:
  job:
    name: setup target modules
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: loop
        continue-on-error: true           # この行を追加
        run: |
          for file in `ls commands`; do
            sh "commands/${file}"
          done
      - name: finalize
        run: |
          echo "All commands executed"

実行結果は次のようになります。(実際の実行結果

1つ前の例と異なりfinalize stepも実行されています。しかし肝心のloop処理の部分ではcommand5.sh実行時点で終了しています。

GitHub Actionsでは、Step中にエラーが発生すると終了します。continue-on-error オプションはエラーが発生しても以降のstepを実行することを許可するもので、step中のエラーを無視するものではありません。このため、command5.sh実行で終了しfinalize stepに移っているのです。

自作スクリプトを用意する

では、「エラー発生有無に関わらずcommand1.shからcommand9.shのすべてをしたい」を実現するにはどうすればよいでしょうか? 要は GitHub Actions workflowのstepでエラーにしなければよいのです。

そこで次のようなコマンドを自作し、それをstep中で実行するようにします。

スクリプト(loop.sh)

#!/bin/sh

for file in `ls commands`;do
  sh commands/$file
  echo exit status is $?
done

workflow yaml

name: simple-exec-shell
on:
  workflow_dispatch:

jobs:
  job:
    name: setup target modules
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: loop
        run: |
          sh loop.sh                                           # 上記スクリプトを実行する
      - name: finalize
        run: |
          echo "All commands executed"

自作スクリプト内ではエラーが発生したとしても echo $? (終了コードを表示)しており、loop.shスクリプト自体は正常終了します。

このため、以下のように当初の期待通りの動作をします。(実際の実行結果

Matrixを使う

多くの場合、shellでループ処理を実行することで、目的を達することができます。またその内容もシンプルです。 シェル実行なのでループ処理が順番に処理されるので、1つ1つの処理で時間がかかる場合は、できれば並列に実行したいケースもあるでしょう。 または、GitHub Actions Marketplaceで用意されているコマンドを活用したい場合もあります。この場合スクリプトでループすることができない場合もあります。

GitHub Actionsのstepをそのまま利用して上記ループ処理と同等のことを実行したいというケースに有効なのがMatrixを利用するパターンです。

まずはその例を示します。

name: simple-exec-strategy
on:
  workflow_dispatch:

jobs:
  setup:
    name: setup matrx
    runs-on: ubuntu-latest
    outputs:
      files: ${{ steps.create-matrix.outputs.files }}
    steps:
      - uses: actions/checkout@v4
      - name: create matrix
        id: create-matrix
        run: |
          echo "files=$(ls commands | jq -R . | jq -s -c .)" >> $GITHUB_OUTPUT

  job:
    name: setup target modules
    runs-on: ubuntu-latest
    needs: [ setup ]
    strategy:
      max-parallel: 3      
      matrix:
        file: ${{fromJson(needs.setup.outputs.files)}}
    steps:
      - uses: actions/checkout@v4
      - name: execute
        continue-on-error: true
        run: |
          sh commands/${{ matrix.file }}

  postExecute:
    name: finalize
    runs-on: ubuntu-latest
    needs: [ job ]
    steps:
      - name: finalize
        run: |
          echo "All commands executed"
  

だいぶ複雑な内容になりました。

まず setup ジョブで対象となるファイルの一覧を用意し、それをJSON Array形式に格納します。

続いて job ジョブでsetupで用意した JSON Arrayをmatrixに指定します。

docs.github.com

execute stepで それを指定して実行しています。

すべてのjob ジョブの実行を待って最後にpostExecuteジョブを実行しています。(実際の実行結果

この方式の利点はmatrixを利用しているために、 jobジョブ内の steps を複数同時並列に実行することです。また、同時実行数を制限したい場合は max-parallel で指定することもできます。 max-parallel に1を指定すればシェルのループ処理のように1つ1つ順次実行にすることもできます。

今回のような簡単な実行内容の場合matrixを利用したやり方は明らかに過剰で、しかも若干複雑になるため適しませんが、このやり方が必要になるケースが皆さんにもきっとあるのではないかと思います。

まとめ

今回は GitHub Actions workflow におけるループ処理の実行方法を実際の実行結果とともにご紹介しました。

頻繁ではないのですがループ処理が必要になるケースがあり、その時に「さあどうしたものか」と考えることもあるかと思います。私自身がまさにそうでした。

今回の内容はそうした皆様の「どうしたものか」と考える時間を少しでも削減できればと思いご紹介いたしました。 皆様にとって、少しでも参考になれば幸いです。

最後に今回利用したサンプルコードを公開しておきます。

github.com