Github Action workflow_runトリガーでファイル差分に応じてjobを動かす
タイトルは正しいと思うんですがわかりくいですね。まずはworkflow_run
について簡単な説明を。
workflow_run
って何
Github Acitonではworkflow(=yml)を連携させる方法が2つあります。1つはreusable workflowで、もう1つはworkflow_run
イベントです。前者はメインのworkflowから別のworkflowを呼び出すものであくまでメインworkflowの一部のタスクを別ymlファイルに切り出す、というものです。
一方、workflow_run
イベントは1つ目のworkflowが終わったら2つ目のworkflowを開始する、というものなので、たとえ2つ目のworkflowが失敗しても1つ目のworkflowの結果には影響しません。
Reusable Workflow
workflow_runイベント
で、今回は後者のworkflow_run
イベントにおけるTipsです。
何をやりたいか
今回やりたいことは以下を満たすcd.ymlを作ることです。
-
ci.yml
とcd.yml
の2つのworkflowを作る。 - ciはテストを、cdはbuildとdeployのタスクをまとめる。
-
ci.yml
が完了してからcd.yml
を実行したい。 - モノレポ配下にある各アプリディレクト以下の変更内容に応じてci, cdのjobの実行・スキップを制御する。
なぜやりたいのか
モノレポでこういった仕組みをつくならアプリごとにci、cdを作る(be-ci, be-cd, fe-ci, fe-cdなど)というやり方もあります。ただ、一応試したのですがslack通知が何度もきてしまうのが煩わしいと感じ、通知を1個にしたいがために全アプリ分を1つのci、1つのcdにまとめてみたかったというきっかけです。
(もしかしたら、slack通知部分をうまく作れば綺麗にできたかもしれません。ご存じの方は教えてください...)
何がつらいのか
上記のやりたいこと3はまさにworkflow_run
がぴったりなのですが、実はworkflow_run
イベントは色々と制約があります。詳しくはオフィシャルを確認して頂くとして今回のつらみの主な原因は2つです。
-
workflow_run
のContext情報が足りない - 必ずデフォルトブランチ上で実行される
workflow_run
のContext情報が足りない
何が足りないかというと以下がありません。
- 比較対象にする「前のpushのcommit hash」
-
push
イベントだとgithub.event.before
で取れる
-
- デフォルトブランチと比較すべきbranch名 (
feature/xxx
とか)-
pull_request
イベントだとgithub.head_ref
で取れる
-
これはworkflow_run
が「前のworkflowが終わったら呼び出す」という仕組みなので、前のworkflowがpush
やpull_request
イベントではないもの、例えばissues opened
など、もありえるので仕方ないところではありますが、今回のやりたいことである「モノレポ配下にある各アプリディレクト以下の変更内容に応じて」という点を実現するためには何と何を比較するか、を定めないといけません。
必ずデフォルトブランチ上で起動される
これも結構ハマったポイントでした。オフィシャルのドキュメントではここでそれが表現されています。
デフォルトブランチで実行されるという制約
ちなみに、reusable workflowのイベントである、workflow_callだとこうなっています。
workflow_callだと呼び出し元のworkflowと同じブランチを使う
つまり、featureブランチやstagingブランチなど、デフォルトブランチ以外で発生したイベントでworkflowが起動し、そのworkflowの後にworkflow_run
をトリガーにworkflowが起動されると、イベント発生のブランチではなく、デフォルトブランチ上で起動されるということです。(個人的にはこの動きを理解していないと結構ハマるポイントだと思います)
そして、前述のとおりContext情報にはトリガーの原因となったイベントのbranch情報は含まれません。
対応方法
対策としては単純に必要な情報を渡すしかありません。workflow同士で情報をやりとりする方法としてArtifactが使えます。Artifactは指定したファイルをworkflowからGithubが用意する一時保存場所に保管し、downloadできるようにする仕組みです。
これを使い、ファイル差分に必要な
- 比較基準とすべきcommit hashまたはブランチ名
- 現在のブランチ名
を後段のworkflow_runで起動されるworkflowに渡します。
Artifactのアップロード
actions/upload-artifact@v3
を使います。アップロードは簡単なのですが、ポイントは以下のymlでBASE
とCURRENT_BRANCH
に設定している情報です。
jobs:
test:
runs-on: ubuntu-latest
steps:
(...omit...)
- name: Save base ref info
run: |
if [[ '${{ github.event_name }}' == 'push' ]]; then
BASE=${{ github.event.before }}
CURRENT_BRANCH=${{ github.ref }}
elif [[ '${{ github.event_name }}' == 'pull_request' ]]; then
BASE=${{ github.base_ref }}
CURRENT_BRANCH=${{ github.head_ref }}
fi
echo $BASE > base.txt
echo $CURRENT_BRANCH > current-branch.txt
- name: Upload base ref info
uses: actions/upload-artifact@v3
with:
name: original-refs
path: |
base.txt
current-branch.txt
retention-days: 1
ここで、ファイル差分の処理が常に「BASEからCURERNT_BRANCH(のHEAD)の間で発生した差分」を求めれば良くなるように、push
イベントとpull_request
イベントで処理を分けています。
pushイベント | pull_requestイベント | |
---|---|---|
BASE |
前のpushのcommit hash | マージターゲットのブランチ名 |
CURRENT_BRANCH |
現在のブランチ名 | 現在のブランチ名 |
Artifactのダウンロードとチェックアウト
Artifacatを別workflowで使う場合、公式のactionではなくdawidd6/action-download-artifact
を使います。
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Download a single artifact
uses: dawidd6/action-download-artifact@v2
with:
workflow: first-action.yml
name: original-refs
workflow_conclusion: success
- name: set REF_BASE to env
run: |
echo "BASE=$(cat base.txt)" >> $GITHUB_ENV
echo "CURRENT_BRANCH=$(cat current-branch.txt)" >> $GITHUB_ENV
- uses: actions/checkout@v3
with:
ref: ${{ env.CURRENT_BRANCH }}
# 以降は次のセクションで解説
dawidd6/action-download-artifact
はダウンロードしたartifactをカレントディレクトリに展開してくれるのでここでは単純なBASE
とCURRENT_BRANCH
に対応したテキストファイルが2つ保存されます。そして、以降のstep
のwith
で使えるようにshellで$GITHUB_ENV
に内容を出力してenv
としてアクセスできるようにしています。
また、actions/checkout
でref
にCURRENT_BRANCH
を指定してcheckoutすることで前段のworkflowと同じリソースをチェックアウトするようにしています。
ファイル差分を検出する
ここまでくればあとはファイル差分を検出するだけです。ファイル差分検出にはdorny/paths-filter
を使います。
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Download a single artifact
uses: dawidd6/action-download-artifact@v2
with:
workflow: first-action.yml
name: original-refs
workflow_conclusion: success
- name: set REF_BASE to env
run: |
echo "BASE=$(cat base.txt)" >> $GITHUB_ENV
echo "CURRENT_BRANCH=$(cat current-branch.txt)" >> $GITHUB_ENV
- uses: actions/checkout@v3
with:
ref: ${{ env.CURRENT_BRANCH }}
- uses: dorny/paths-filter@v2
id: filter
with:
base: ${{ env.BASE }}
ref: ${{ env.CURRENT_BRANCH }}
filters: |
src:
- src/**
lib:
- lib/**
# 以降、 結果をifで参照してstepを制御する
dorny/paths-filter
を使うと比較結果を以下のようにstepのifで参照することができます。
if: steps.filter.outputs.src == 'true'
ワイルドカードや、ファイル追加だけを条件にするなど様々な指定ができるので利用する場合はREADMEを一読すると良いです。
以上でworkflow_runを使ったworkflowでも正しくファイル差分を比較することができるようになります。
サンプルコード
今回の調査で作ったコードはこちらのレポにまとめてあります。
余談
当初は自分のやりたいことがdorny/paths-filter
のissueにあるworkflor_runで使うと変更差分が全部true(変更あり)になるというissueにまさにハマってしまい、これコード側で解決できないかと調査していました。
しかし調査の結果として前述したworkflor_runイベントのContext情報不足がどうにもできない、という結論に達し、コード側で解決することを諦めてArtifactを使う方法にシフトしました。
シフトした後もworkflor_run
の制約にハマっていた時間もそれなりにあったこともありドキュメントを何度も読みましたが、workflow_run
は落とし穴的な制約が多いので将来的には別の方法が提供されるかもな、と思ったりしました。
Discussion