🎬

Github Action workflow_runトリガーでファイル差分に応じてjobを動かす

2022/10/23に公開

タイトルは正しいと思うんですがわかりくいですね。まずは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を作ることです。

  1. ci.ymlcd.ymlの2つのworkflowを作る。
  2. ciはテストを、cdはbuildとdeployのタスクをまとめる。
  3. ci.ymlが完了してからcd.ymlを実行したい。
  4. モノレポ配下にある各アプリディレクト以下の変更内容に応じて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つです。

  1. workflow_runのContext情報が足りない
  2. 必ずデフォルトブランチ上で実行される

workflow_runのContext情報が足りない

何が足りないかというと以下がありません。

  • 比較対象にする「前のpushのcommit hash」
    • pushイベントだとgithub.event.beforeで取れる
  • デフォルトブランチと比較すべきbranch名 (feature/xxxとか)
    • pull_requestイベントだとgithub.head_refで取れる

これはworkflow_runが「前のworkflowが終わったら呼び出す」という仕組みなので、前のworkflowがpushpull_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できるようにする仕組みです。
https://docs.github.com/ja/actions/using-workflows/storing-workflow-data-as-artifacts

これを使い、ファイル差分に必要な

  • 比較基準とすべきcommit hashまたはブランチ名
  • 現在のブランチ名
    を後段のworkflow_runで起動されるworkflowに渡します。

Artifactのアップロード

actions/upload-artifact@v3を使います。アップロードは簡単なのですが、ポイントは以下のymlでBASECURRENT_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をカレントディレクトリに展開してくれるのでここでは単純なBASECURRENT_BRANCHに対応したテキストファイルが2つ保存されます。そして、以降のstepwithで使えるようにshellで$GITHUB_ENVに内容を出力してenvとしてアクセスできるようにしています。

また、actions/checkoutrefCURRENT_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を一読すると良いです。
https://github.com/dorny/paths-filter

以上でworkflow_runを使ったworkflowでも正しくファイル差分を比較することができるようになります。

サンプルコード

今回の調査で作ったコードはこちらのレポにまとめてあります。
https://github.com/gki/paths-filter-workflow-run-workaround

余談

当初は自分のやりたいことがdorny/paths-filterのissueにあるworkflor_runで使うと変更差分が全部true(変更あり)になるというissueにまさにハマってしまい、これコード側で解決できないかと調査していました。
https://github.com/dorny/paths-filter/issues/147

しかし調査の結果として前述したworkflor_runイベントのContext情報不足がどうにもできない、という結論に達し、コード側で解決することを諦めてArtifactを使う方法にシフトしました。

シフトした後もworkflor_runの制約にハマっていた時間もそれなりにあったこともありドキュメントを何度も読みましたが、workflow_runは落とし穴的な制約が多いので将来的には別の方法が提供されるかもな、と思ったりしました。

Discussion