GitHub Actionsワークフロー間のデータ共有方法について調べてみた
経緯
とあるプロジェクトでは、CD のワークフローが実行完了後、デプロイされたアプリケーションに対して、E2E テストのワークフローを実行させる仕組みが作られています。
しかし、設定上の問題で、E2E テストのワークフロー実行時に利用するコードベースがマスターになっていて、PR で変更された内容が反映されない問題がありました。
この問題を解決するために、E2E テストのワークフロー実行時に、CD のワークフローで利用されていたブランチの情報を取得して、E2E のワークフローでチェックアウトすると考えていました。
色々と調査と実験を繰り返した結果、結論から言うと、ワークフローの間に情報共有するには少なくとも以下の方法が存在しています。
- workflow-run
- workflow-dispatch
- repository-dispatch
- HTTP POST request
- 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.action
と github.event_type
の違いについてはこちら に参考。
この方法で要注意するのは、
- ワークフロー A で(データを共有する側)、必ず
permissions
にcontents: 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_run
かreusing workflow
が公式的に進められています。いずれも回数・階層制限があるので注意は必要。 - GitHub Actions のウェブページから手動トリガーを作りたい時は基本
workflow_dispatch
一択です。 - HTTP POST リクエストの方法は実質
workflow_dispatch
とrepository_dispatch
と同じですが、プログラム(アプリコード)からトリガーする場合はこの方法になります。
といった感じです。
ではでは、今回はこれで。
オプティマインドは、「世界を少しでも良くしたい」「大きな社会課題に挑みたい」という熱い気持ちを胸に秘めたメンバーが集まり、自社のミッションである「新しい世界を技術で創る」べく、日々技術に磨きをかけています。
また、自社のビジョンである「世界のラストワンマイルを最適化する」ために何ができるのか、ひとりひとりがしっかりと考えながら、真摯に向き合っています。
わたしたちのミッション・ビジョンに共感頂ける方、一緒に伴走してくださる方、ちょっとでもオプティマインドが気になる方、お気軽にドアノックしてください。
カジュアル面談でお会いできることを楽しみにしております。
ぜひ一緒に、大きな社会課題に挑みながら、新しい世界を創っていきましょう!
Discussion