🧐

GitHub Actionsワークフロー間のデータ共有方法について調べてみた

2023/03/31に公開

経緯

とあるプロジェクトでは、CD のワークフローが実行完了後、デプロイされたアプリケーションに対して、E2E テストのワークフローを実行させる仕組みが作られています。

しかし、設定上の問題で、E2E テストのワークフロー実行時に利用するコードベースがマスターになっていて、PR で変更された内容が反映されない問題がありました。

この問題を解決するために、E2E テストのワークフロー実行時に、CD のワークフローで利用されていたブランチの情報を取得して、E2E のワークフローでチェックアウトすると考えていました。

色々と調査と実験を繰り返した結果、結論から言うと、ワークフローの間に情報共有するには少なくとも以下の方法が存在しています。

  1. workflow-run
  2. workflow-dispatch
  3. repository-dispatch
  4. HTTP POST request
  5. reusing workflow

それぞれの方法について例を挙げなら説明したいと思います。

workflow-run

最初に実装した方法はこちらでした。

workflow-run は何をするかというと、仮に二つのワークフロー A と B があるとします。A の後に B を実行し、A の情報を B に渡したい時に、B の中でon workflow_runを通して指定できます。

name: "workflow A"

on:
  push:
    branches:
      - "feature/*"

jobs:
# ...
name: "workflow B"

on:
  workflow_run:
    workflows:
      - "workflow A"
    types:
      - completed

jobs:
# ...

つまり、ワークフロー B は、ワークフロー A が完了となった時に実行することになります。

ワークフロー A のデータは B で取得するために、2 つの方法があります。まずは github-scripts を導入します。

name: "workflow B"

on:
  workflow_run:
    workflows:
      - "workflow A"
    types:
      - completed

jobs:
  job_name:
    - uses: actions/github-script@v6
      id: get-head
      with:
        result-encoding: string
        script: |
          console.log('context: ', context)
          return context?.ref || context.payload?.workflow_run?.head_branch || 'master'
    - uses: actions/checkout@v3
      with:
        ref: ${{ steps.get-head.outputs.result }}

script 内には js のコードが書けます。context の中身は workflow A の各種のデータとなっています。詳しくはこちら を参照。

もう一つの方法というのは、github-scriptsを介さず、ワークフロー A のイベントペイロードもアクセス可能になります(参考)。実質、github-scriptsでアクセスしているコンテキスト情報もここから来ているかと。

name: "workflow B"

on:
  workflow_run:
    workflows:
      - "workflow A"
    types:
      - completed

jobs:
  job_name:
    - uses: actions/checkout@v3
      with:
        ref: ${{ github.event.workflow_run.head_branch }}

今回の取得したいブランチレフはgithub-scriptsから取得するか、ペイロードの中からhead_shaとかを使うことが可能になるため、目的は達成できます。

しかし、 一つ問題点として、このイベントでトリガーされたワークフローはマスターブランチのものでなければなりません。つまり、変更後のワークフローファイル B をマスターにマージしないと、仮に A が別の PR ブランチで実行されたとしても、B はマスターのものが使用されるので、変更は反映されません。

それ以外にも、この方法を運用するときの注意点として、

  • チェインする時に最大 3 回しかチェインできない(A->B->C->D->Eの場合だと D までしか実行されない)
  • workflowsを複数指定した場合、いずれかのワークフロー(ANDではなくOR)が条件に満たせば実行となります

workflow-dispatch

レポはこちらworkflow_dispatchは通常、手動でワークフローを実行する、いわば「手動トリガー」の時に利用されますが、指定のワークフローをトリガーすることもできます。

name: "workflow A"

on:
  push:
    branches:
      - "feature/*"

jobs:
  # ...
  - name: Trigger workflow B
    uses: benc-uk/workflow-dispatch@v1
    with:
      workflow: workflow-b.yml
      inputs: '{ "ref": "${{ github.ref }}" }'
name: "workflow B"

on:
  workflow_dispatch:
    inputs:
      ref:
        description: "branch ref"
        type: string
        required: false
        default: "master"
# ...

この方法は、別のレポジトリーのワークフローもトリガー可能ですが、認証用の PAT トークンとレポ名の指定が必要です。今回は目的ではないので割愛します。

また、この方法もファイルがデフォルトブランチに存在しないと機能しないので、ワークフローファイル自体の変更はマージしないと反映されません。

resository-dispatch

レポはこちら。公式ドキュメントはこちら

この方法は、同じレポジトリー内のワークフローだけではなく、他のレポジトリーのワークフローもトリガーすることが可能です。同じワークフロー内は割と設定がシンプルなので、試してみました。

name: "workflow A"

on:
  push:
    branches:
      - "feature/*"

jobs:
  job_name:
    runs-on: ubuntu-latest
    permissions:
      contents: write # repository-dispatchにはこちらの権限が必須
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      # ...
      - name: Deploy
        id: deployment
        run: deploy command
      - name: Dispatch E2E Test
        if: steps.deployment.outcome == 'success' # if ${{ success() }}, 成功の場合のみワークフローBを実行する
        uses: peter-evans/repository-dispatch@v2
        with:
          # デフォルトトークンはこれ、他のレポジトリーをアクセスする場合はPATが必要になる
          # token: ${{ secrets.GITHUB_TOKEN }}
          # repository: repo_name # 同じレポの場合は不要
          event-type: post-deploy
          # ここで送りたいデータを入れる
          client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}"}'
name: "workflow B"

on:
  repository_dispatch:
    types: [post-deploy] # ここで上記のevent-typeを指定する

jobs:
  job_name:
    steps:
      - name: Checkout for workflow A triggered case
        if: github.event.action == 'post-deploy' # 注意:github.event_typeは、repository_dispatchとなる
        uses: actions/checkout@v3
        with:
          ref: ${{ github.event.client_payload.ref }}

github.event.actiongithub.event_type の違いについてはこちら に参考。

この方法で要注意するのは、

  • ワークフロー A で(データを共有する側)、必ずpermissionscontents: writeを設定する必要があります(トークンのデフォルト権限かもしくはファイル内で指定するか)。
    • でなければこちら のエラーと遭遇します。
  • permissionsの指定は、ワークフローレベルとジョブレベルで指定することが可能で、ジョブレベルの方が上書きするので注意が必要です。
name: "workflow A"

on:
  push:
    branches:
      - "feature/*"
permissions:
  contents: write # ワークフローレベル

jobs:
  job_name:
    runs-on: ubuntu-latest
    permissions:
      contents: read # ジョブレベルの方が上書きするので、repository_dispatchが失敗する

また、この方法の一つの欠点として、仮にワークフロー B に複数のトリガー条件がある(on:repository_dispatch以外のものがある)とすると、actions/checkoutとのステップを 2 回以上書かないといけない、とのところです。

name: "workflow B"

on:
  workflow_dispatch:
  repository_dispatch:
    types: [post-deploy] # ここで上記のevent-typeを指定する

jobs:
  job_name:
    steps:
      - name: Checkout for workflow A triggered case
        if: github.event.action == 'post-deploy'
        uses: actions/checkout@v3
        with:
          ref: ${{ github.event.client_payload.ref }}
      - name: Checkout for manually triggered case
        if: github.event_type == 'workflow_dispatch'
        uses: actions/checkout@v3

なお、この方法もデフォルトブランチでなければならないので、ワークフローファイル自体の変更は反映されません。

HTTP POST request

この方法は実質上記の workflow_dispatchおよびrepository_dispatchと同じですが、直接 post リクエストを送る形になります。

一つ目のworkflow_dispatchです。

name: "workflow A"

on:
  push:
    branches:
      - "feature/*"

jobs:
  job_name:
    - name: Trigger E2E
      run: |
        curl \
        -X POST \
        -H 'Authorization: token ${{ secrets.GITHUB_TOKEN }}' \
        -H 'Accept: application/vnd.github.v3+json' \
        https://api.github.com/repos/<org_name>/<repo_name>/actions/workflows/<workflow_file_name>.yml/dispatches \
        -d '{"ref":"${{ github.ref }}", "inputs": {"origin": "workflow A"}}'
name: "workflow B"

on:
  workflow_dispatch:
    inputs:
      origin:
        description: "triggered manually or by workflow A"
        type: string
        required: false
        default: "manually"
# ...

二つ目のrepository_dispatchです。

公式では、ポストリクエストの URL が若干違いますが、特定のワークフローファイルではなく、dispatchesエンドポイントへリクエストを投げているので、event_typeの指定が必要になります。ここのイベントタイプもworworkflow_dispatchではなく、response_dispatchとなるので、間違いないように注意してください。

curl -L \
  -X POST \
  -H "Accept: application/vnd.github+json" \
  -H "Authorization: Bearer <YOUR-TOKEN>"\
  -H "X-GitHub-Api-Version: 2022-11-28" \
  https://api.github.com/repos/<org_name>/<repo_name>/dispatches \ # ここは違う
  -d '{"event_type":"on-demand-test","client_payload":{"unit":false,"integration":true}}'

name: "workflow B"
on:
  repository_dispatch:
    types: [on-demand-test]

ちなみに、このあたりの方法はワークフローの間だけではなく、例えば js のコードからトリガーすることも可能になります。詳細のドキュメントはこちら に参照。

ただ、いずれにしても、マスターブランチのワークフローファイルがトリガーされるので、仮にワークフローファイル自身に変更がある場合は反映されません。

reusing workflow

最後に紹介するのは今回の問題を一番綺麗に解決してくれた方法です。公式ドキュメントはこちら です。

おさらいとして、今回の問題というのは

CD のワークフローが実行完了後、デプロイされたアプリケーションに対して、E2E テストのワークフローを実行させる仕組みが作られていますが、E2E テストのワークフロー実行時に利用するコードベースがマスターになっていて、PR で変更された内容が反映されない。

この問題には実は 2 つのサブ問題があります

  • CD ワークフロに続くテストのワークフローがマスターのものになっている
  • 修正後のワークフローファイルをマスターへマージしないと変更が反映されない

2 番目の問題は最初に github-scripts とか、repository-dispatch とかを使った時に気づきました。つまり、一回マスターへマージしないと動作確認ができない、しかし仮に動作しなかったらリバートが発生してしまうとのデメリットがあります。

今までの全ての方法は、1 番目の問題=ワークフローの間にデータを共有することを解決してくれています。2 番目の問題を解決するには、最終的に「再利用可能なワークフロー」とのアプローチになりました。

name: "workflow A"

on:
  push:
    branches:
      - "feature/*"

jobs:
  cd:
  # ...
  e2e: # ここからジョブとして追加
    needs: [cd]
    if: ${{ success() }}
    uses: ./.github/workflows/workflow-b.yaml
    secrets: inherit
    with:
      ref: ${{ github.ref }}

上記のwithで定義したものは B のinputsとして扱われます。

name: "workflow B"
on:
  workflow_call:
    inputs:
      ref:
        description: "branch ref"
        required: true
        default: "master"
        type: string

この形式で実行すると、事実上ワークフロー間の問題を、ジョブ間の問題に変換してくれています。そのため、inputs経由だけでなく、jobsの間のoutputs経由でのデータ共有も可能になります。

on: push

jobs:
  job1:
    runs-on: ubuntu-latest
    steps:
      - name: Step 1
        run: echo "Hello, world!"
      - name: Step 2
        run: echo "::set-output name=my-output::some data"

  job2:
    runs-on: ubuntu-latest
    needs: job1
    steps:
      - name: Step 3
        run: echo "Previous output: ${{ needs.job1.outputs.my-output }}"

さらに、ワークフロー B が A の一部(ジョブとして)となって、A のコンテキストのデータが共有され、ブランチレフの情報を渡すこと自体が不要になっています。そのおかげで、最終的にこの方法が一番変更が少なく、かつ 2 つの問題も全部綺麗に解決してくれました。

name: "workflow A"

on:
  push:
    branches:
      - "feature/*"

jobs:
  cd:
  # ...
  e2e:
    needs: [cd]
    if: ${{ success() }}
    uses: ./.github/workflows/workflow-b.yaml
    secrets: inherit
name: "workflow B"
on:
  workflow_call:

jobs:
  # ...

最後に、この方法にはいくつか注意点があります。

  • <workflow_file>.yml@mainの形でブランチは指定できますが、これは同じレポジトリーのファイルには使えません。今回のように呼び出し側と同じブランチを使うので問題にならないのですが、そうでない場合は注意が必要。
  • 同じレポジトリーのファイルであっても、secretes: inheritを指定しないと、呼び出されたファイルの実行時にsecretesへのアクセスがなくなります。
  • needsの指定をしないと、依存関係がなくなるので、並列に実行されるようになります。今回のように前後依存関係がある場合は必要です。
  • 他のレポジトリーのワークフローをトリガーする場合はコードのみで足りず、アクセス権限周りの設定が必要となります。詳細はこちらに参照。

まとめ

ワークフロー間のデータ共有との問題意識から色々と実現方法を探ってみました。それぞれ応用するケースが多少違っていて、適応可能なレポ範囲とデフォルトブランチの制約の有無で次のようにまとめました。

approach same repo other repo default branch workflow chaining manual trigger program trigger
workflow-run Y N Y Y N N
workflow-dispatch Y Y Y Y Y Y(POST request)
repository-dispatch Y Y Y Y N Y(POST request)
reusing workflow Y Y N Y N N
  • 他のレポジトリーのワークフローをトリガーしたい場合は、workflow_run以外で可能ですが、いずれもトークンと権限周りの設定が必要です。
  • 今回のようにワークフロー自体を変更した場合は、デフォルトブランチにマージしないと反映されない、という制限を避ける場合、最後の一択しかなくなります。
  • ワークフローをチェイニングしていくのは基本どれも可能ですが、workflow_runreusing workflowが公式的に進められています。いずれも回数・階層制限があるので注意は必要。
  • GitHub Actions のウェブページから手動トリガーを作りたい時は基本workflow_dispatch一択です。
  • HTTP POST リクエストの方法は実質 workflow_dispatchrepository_dispatch と同じですが、プログラム(アプリコード)からトリガーする場合はこの方法になります。

といった感じです。

ではでは、今回はこれで。


オプティマインドは、「世界を少しでも良くしたい」「大きな社会課題に挑みたい」という熱い気持ちを胸に秘めたメンバーが集まり、自社のミッションである「新しい世界を技術で創る」べく、日々技術に磨きをかけています。
また、自社のビジョンである「世界のラストワンマイルを最適化する」ために何ができるのか、ひとりひとりがしっかりと考えながら、真摯に向き合っています。
わたしたちのミッション・ビジョンに共感頂ける方、一緒に伴走してくださる方、ちょっとでもオプティマインドが気になる方、お気軽にドアノックしてください。
カジュアル面談でお会いできることを楽しみにしております。
ぜひ一緒に、大きな社会課題に挑みながら、新しい世界を創っていきましょう!

https://recruit.optimind.tech/
https://www.wantedly.com/companies/optimind

Discussion