🔍

Github Actionsで複数jobの結果を確認する

2022/10/15に公開

モノレポで開発していたらGithub Actionsのworkflow.ymlの数が多くなってしまいました。それだけなら良いのですが、それぞれでslack通知をしている(または、これからやりたい)とチャンネルが通知で埋め尽くされて煩わしくなりそうだったので、jobとして1つのworkflowにまとめつつ、複数のjob結果を処理する方法を考えました。

結果確認用jobを作る

Github acitonsは複数のjobを1つのymlに設定できますが、今回の目的のためには最後に実行されるjobが以下の条件を満たすようにする必要があります。

  1. 前のjobの結果に関わらず必ず実行される
  2. 前のjobの結果を受け取れる
  3. 前のjobの独自のoutputも受け取れる

前のjobの結果に関わらず必ず実行させる

まず、「前のjob」を定義するにはneedsを使ってjob名を指定します。ただし、それだけだと前のjobが1つでも失敗するとそのjobはskipされてしまいます。「前のjobの結果に関わらず常に実行させる」を指定するにはifalways()を設定します。

check-result:
    needs: [job1, job2, job3, job4]
    if: ${{ always() }}
    runs-on: ubuntu-latest
    steps:
      - name: step
        run: (省略)

前のjobの結果を受け取る

GithubにはContextsという機能があり、さまざまな情報を実行時に取得できます。
https://docs.github.com/ja/actions/learn-github-actions/contexts

このなかで、前のjobの情報がneedsというContextに設定されます。formatはシンプルでjob名(job_id)、result, outputの3つです。これらがneedsに指定した全てのjobについて設定されます。

{
  "job1": {
    "result": "success",
    "outputs": {}
  },
  "job2": {
    "result": "success",
    "outputs": {}
  },
  "job3": {
    "result": "failure",
    "outputs": {}
  },
  "job4": {
    "result": "skipped",
    "outputs": {}
  }
}

あとはこれをjqや何らかの方法で処理して通知に使える値に変換するれば良いだけです。例えば「job毎にjob名 + スペース + resultにして\nで改行」というフォーマットにしたい場合は

echo '${{ toJSON(needs) }}' | jq 'to_entries | map(.key + " " + .value.result) | join("\n")'

をrunに指定すると、以下のような文字列が取得できます。

前のjobの独自のoutputも受け取る

needs Contextにあるoutputにはそれぞれのjobからの独自の値を設定できます。ただし、jobのoutputにset-outputコマンドの結果を使うとneeds Contextに設定されません。set-outputが非推奨になったのも関係しているんでしょうか?)
推奨されているGITHUB_ENVを使う必要があります。
https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/

Actionsのサマリーページにもこういったwarningが出るようになりました。

GITHUB_ENVでjob outputsに設定する

上記のchangelogの先にある推奨方法にあるとおり、

echo "{env名}={値}" >> $GITHUB_ENV

を使います。さらにjob outputsでenv.を使ってoutputsを設定します。

job2:
  needs: setup
  runs-on: ubuntu-latest
  outputs:
    output_via_command: ${{ steps.step.outputs.command_result }} # 別jobに渡されない
    output_via_env: ${{ env.ENV_RESULT }} # 別jobに渡される
  steps:
    - name: step
      run: |
        echo "do something2"
        echo "::set-output name=command_result::1234"
        echo "ENV_RESULT=5678" >> $GITHUB_ENV

上記ymlではわざと非推奨のset-outputを使ってjob outputoutput_via_commandを設定していますが、この方法だと別jonのneeds Contextに値が渡されません。実際にGithub actionで実行した際のtoJSON(needs)の値は以下です。

{
  "job1": {
    "result": "success",
    "outputs": {}
  },
  "job2": {
    "result": "success",
    "outputs": {
      "output_via_env": "5678"
    }
  },
  "job3": {
    "result": "failure",
    "outputs": {}
  },
  "job4": {
    "result": "skipped",
    "outputs": {}
  }
}

GITHUB_ENV経由で渡したoutputは無事にneeds Contextに設定されています。ここまでくればあとはresultと同様にjqやスクリプトで必要な処理に組み込むことができます。

ソースコード

以上で

  1. 前のjobの結果に関わらず必ず実行される
  2. 前のjobの結果を受け取れる
  3. 前のjobの独自のoutputも受け取れる
    という条件を満たす最後のjobを作成することができるようになりました。

最後に、ここまでで説明したymlを以下に記載します。また、このymlをGithub actionで実行した結果もリンクを載せておきます。job2には前述の通り意図的に非推奨のset-outputコマンドを使っている箇所があるのでご注意ください。

get-previous-job-status.yml
on:
  push:

jobs:
  setup:
    runs-on: ubuntu-latest
    steps:
      - name: step
        run: |
          echo "setup something"
          echo "${{ toJSON(github) }}"
  job1:
    needs: setup
    runs-on: ubuntu-latest
    steps:
      - name: step
        run: |
          echo "do something1"
  job2:
    needs: setup
    runs-on: ubuntu-latest
    outputs:
      output_via_command: ${{ steps.step.outputs.command_result }}
      output_via_env: ${{ env.ENV_RESULT }}
    steps:
      - name: step
        run: |
          echo "do something2"
          echo "::set-output name=command_result::1234"
          echo "ENV_RESULT=5678" >> $GITHUB_ENV
  job3:
    needs: setup
    runs-on: ubuntu-latest
    steps:
      - name: step
        run: |
          echo "do something3"
          exit 1
  job4:
    needs: setup
    if: false
    runs-on: ubuntu-latest
    steps:
      - name: step
        run: |
          echo "skip always"
  check-result:
    needs: [job1, job2, job3, job4]
    if: ${{ always() }}
    runs-on: ubuntu-latest
    steps:
      - name: step
        run: |
          echo "check something"
          echo '${{ toJSON(needs) }}'
          echo '${{ toJSON(needs) }}' | jq 'to_entries | map(.key + " " + .value.result) | join("\n")'

https://github.com/gki/try-get-previous-job-status/actions/runs/3255871267

https://github.com/gki/try-get-previous-job-status

Discussion