GitHub Actions - 複数 jobs, 複数 stepsの処理結果をまとめて判定する

# 内容

[オブジェクトフィルタ](https://docs.github.com/ja/enterprise-cloud@latest/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#object-filters)、[contains](https://docs.github.com/ja/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#contains)を利用して、  
複数Jobs、複数Stepsの処理終了後に処理結果をまとめて判定する方法を記載する

# stepsの処理結果をまとめて判定する

## `steps.<step_id>.outcome`を使う

```yaml
jobs:
  test_A:
    name: test A
    runs-on: ubuntu-latest
    outputs:
      test_result: ${{ steps.test_result.outputs.result }}
    steps:
      - uses: actions/checkout@v4

      - name: test 1
        id: test_1
        shell: bash
        # continue-on-error: true  # stepが失敗した場合も後続のstepを実行したい場合に指定する
        run: |
          ...

      - name: test 2
        id: test_2
        shell: bash
        # continue-on-error: true  # stepが失敗した場合も後続のstepを実行したい場合に指定する
        run: |
          ...

      - name: test result
        id: test_result
        if: ${{ always() }}  # 前段のstepが失敗しても実行する
        uses: actions/github-script@v7
        env:
          TEST_FAILED: ${{ contains(steps.*.outcome , 'failure') }}  # idが振られたstepの全結果に失敗が含まれている場合にtrueを返す
        with:
          result-encoding: string
          script: | # 結果を文字列に変換
            const result = ${{ env.TEST_FAILED }} ? 'Failed' : 'Successful';
            return result;
```

:::note info
stepのidは必須プロパティではないが、`steps.<step_id>.outcome`は、step idが定義されているstepしか参照できない
:::

`${{ contains(steps.*.outcome , 'failure') }}`で前段の複数stepから成るテスト結果をまとめている  
[steps コンテキスト](https://docs.github.com/ja/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#steps-context)の`outcome`をオブジェクトフィルタでリスト化した上で、  
`contains`でリストに`failure`が含まれているかをtrue/falseで取得している  
`steps.<step_id>.outcome`には`success`、`failure`、`cancelled`、`skipped`のいずれかが入る  

`continue-on-error: true`を指定し、stepが失敗した場合、`steps.<step_id>.outcome`には、step結果が"失敗"から"成功"に変更される前の"失敗"が入っている  
逆に変更された後の"成功"を取得したい場合は、`steps.<step_id>.conclusion`を使用する

### `continue-on-error`

stepで失敗した場合も後続のstepを実行したい場合は、`continue-on-error: true`を使用する  
ただし、該当stepが失敗した場合も該当stepに加え、Jobが成功扱いとなる

```yaml: ./.github/workflows/reuse_test_a.yml
    steps:
      - uses: actions/checkout@v4

      - name: test 1
        id: test_1
        shell: bash
        continue-on-error: true  # stepが失敗した場合もstepとJobは成功扱いになる
        run: |
          ...
```

```yaml
jobs:
  test_A:
    name: test A
    uses: ./.github/workflows/reuse_test_a.yml
    secrets: inherit

  test_B:
    name: test B
    needs: [test_A]  # test_Aが失敗した場合もtest_Bは実行される
    uses: ./.github/workflows/reuse_test_b.yml
    secrets: inherit
```

`test_B`は`test_A`に依存しているが、`test_A`が失敗した場合も成功扱いとなるので`test_B`が実行されてしまう

## `success()`, `failure()`を使う

```yaml
jobs:
  test_A:
    name: test A
    runs-on: ubuntu-latest
    outputs:
      test_result: ${{ steps.xxx.outputs.result }}  # 単純にstepのoutputsで返却は無理
    steps:
      - uses: actions/checkout@v4

      - name: test 1
        id: test_1
        shell: bash
        run: |
          ...

      - name: test 2
        id: test_2
        shell: bash
        run: |
          ...

      - name: test result success
        id: test_result_success
        if: ${{ success() }}  # 前段のstepが全て成功の場合に実行
        uses: actions/github-script@v7
        with:
          result-encoding: string
          script: |
            return 'Successful';

      - name: test result failure
        id: test_result_failure
        if: ${{ failure() }}  # 前段のstepに失敗が有った場合に実行
        uses: actions/github-script@v7
        with:
          result-encoding: string
          script: |
            return 'Failed';
```

[ステータスチェック関数](https://docs.github.com/ja/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#status-check-functions)を使っても`steps.<step_id>.outcome`を使うパターンと似たようなことは可能だが、  
結果の判定stepが2つになるため、単純にJobのoutputsとして返却することはできない

# Jobsの処理結果をまとめて判定する

## `needs.<job_id>.result`を使う

```yaml
jobs:
  test_A:
    name: test A
    uses: ./.github/workflows/reuse_test_a.yml
    secrets: inherit

  test_B:
    name: test B
    uses: ./.github/workflows/reuse_test_b.yml
    secrets: inherit

  test_result:
    name: test result
    needs: [test_A, test_B]
    if: ${{ always() }}
    uses: ./.github/workflows/reuse_test_result.yml
    secrets: inherit
    with:
      failed: ${{ contains(needs.*.result, 'failure') }}  # needsで指定したJobの全結果に失敗が含まれている場合にtrueを渡す
```

`${{ contains(needs.*.result, 'failure') }}`で依存Job（needsで指定したJob）の結果をまとめている  
[needs コンテキスト](https://docs.github.com/ja/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#needs-context)の`result`をオブジェクトフィルタでリスト化した上で、  
`contains`でリストに`failure`が含まれているかをtrue/falseで取得している  
`needs.<job_id>.result`には`success`、`failure`、`cancelled`、`skipped`のいずれかが入る  

### `needs.<job_id>.outputs.<output name>`

以下のように`needs.<job_id>.outputs.<output name>`をオブジェクトフィルタでリスト化することはできない  
全jobに該当のoutputが含まれていることが保障されないためだと思われる

```yaml
jobs:
  test_A:
    name: test A
    uses: ./.github/workflows/reuse_test_a.yml
    secrets: inherit

  test_B:
    name: test B
    uses: ./.github/workflows/reuse_test_b.yml
    secrets: inherit

  test_result:
    name: test result
    needs: [test_A, test_B]
    if: ${{ always() }}
    uses: ./.github/workflows/reuse_test_result.yml
    secrets: inherit
    with:
      result_list: ${{ needs.*.outputs.test_result }}  # ※ これは不可
```

`needs`オブジェクト

```json
{
  "test_A": {
    "result": "success",
    "outputs": {
      "test_result": "Successful"
    }
  },
  "test_B": {
    "result": "failure",
    "outputs": {
        "test_result": "Failed"
    }
  }
}
```

`needs.<job_id>.outputs.<output name>`をまとめて参照する場合、  
`needs`オブジェクトを`toJSON`でJSONに変換すると参照することができる  

```yaml
  test_result:
    name: test result
    needs: [test_A, test_B]
    if: ${{ always() }}
    runs-on: ubuntu-latest
    env:
      NEEDS: ${{ toJSON(needs) }}  # JSON化
    steps:
      - name: test result
        uses: actions/github-script@v7
        with:
          result-encoding: string
          script: |
            const needs = ${{ env.NEEDS }};
            console.log(needs);
            const test_result = Object.keys(needs).map(jobId => needs[jobId].outputs.test_result);
            console.log(test_result);
```

console.log出力結果

![2024-12-31-14-42-47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695430/1d9a9ce3-0870-f235-a33f-565030da6451.png)

以下のように`toJSON`せずに参照することはできない

```yaml
  test_result:
    name: test result
    needs: [test_A, test_B]
    if: ${{ always() }}
    runs-on: ubuntu-latest
    steps:
      - name: test result
        uses: actions/github-script@v7
        with:
          result-encoding: string
          script: |
            const needs = ${{ needs }};  // これは不可
            console.log(needs);
            const test_result = Object.keys(needs).map(jobId => needs[jobId].outputs.test_result);
            console.log(test_result);
```

console.log出力結果

![2024-12-31-14-58-26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695430/08fc250e-edd4-a3a2-47f8-370e17bba8a8.png)

reusable-workflowsのinputsに`toJSON(needs)`を渡して参照することは可能

```yaml
  test_result:
    name: test result
    needs: [test_A, test_B]
    if: ${{ always() }}
    uses: ./.github/workflows/reuse_needs_output.yml
    secrets: inherit
    with:
      needs: ${{ toJSON(needs) }}
```

```yaml:./.github/workflows/reuse_needs_output.yml
name: needs output

on:
  workflow_call:
    inputs:
      needs:
        required: true
        type: string
jobs:
  test_result:
    name: test result
    runs-on: ubuntu-latest
    steps:
      - name: test result
        uses: actions/github-script@v7
        with:
          result-encoding: string
          script: |
            const needs = ${{ inputs.needs }};
            console.log(needs);
            const test_result = Object.keys(needs).map(jobId => needs[jobId].outputs.test_result);
            console.log(test_result);
```