GitHub Actionsで変更ファイルに応じた処理

GitHub Actionsで、変更されたファイルに応じて処理を切り替える方法について整理する。

ここでは「Pull Requestで、Gemfile.lockや.rubocop.ymlに変更があった場合はすべてのファイルに対してRuboCopを実行し、それ以外の場合は変更のあったRubyのファイルに対してRuboCopを実行する」という例を考えてみる。

name: rubocop

on:
  pull_request:

jobs:
  rubocop:
    runs-on: ubuntu-20.04
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true
      - id: changed-files
        uses: tj-actions/[email protected]
      - id: rubocop-all
        if: |
          contains(steps.changed-files.outputs.all_changed_files, '.rubocop_todo.yml') ||
          contains(steps.changed-files.outputs.all_changed_files, '.rubocop.yml') ||
          contains(steps.changed-files.outputs.all_changed_files, '.ruby-version') ||
          contains(steps.changed-files.outputs.all_changed_files, 'Gemfile.lock')
        run: bundle exec rubocop
      - if: steps.rubocop-all.conclusion == 'skipped' && steps.changed-files.outputs.all_changed_files
        run: bundle exec rubocop --force-exclusion ${{ steps.changed-files.outputs.all_changed_files }}

tj-actions/changed-filesというActionを使うと、変更されたファイルのパス一覧を空白区切りの文字列として参照できるので、これを条件分岐に使うことにする。このActionを正しく動作させるために、actions/checkout@v2に fetch-depth: 0 を指定している。

ifの項目で contains 関数や || オペレーターを利用すると、特定のファイルが変更されたときにのみ処理を行える。今回の例では、4つのファイルのいずれかが変更されたときに全てのファイルに対してRuboCopを実行、即ち引数無しで rubocop コマンドを実行している。

ifの項目で steps.*.conclusion を利用すると、if-else的な表現を行える。このためにif側のstepにidを割り当てている。前述の通り、rubocop コマンドは引数無しだと全てのファイルに対して実行してしまうので、変更されたファイルが存在しない場合は処理自体を実行しないようにも配慮している。つまりelse ifになっている。

GitHub Actionsのドキュメントでは、関数やオペレーターの説明はExpressionsのページに、conclusionの説明はContextのページに記載されている。

変更されたファイルにはRubyではないファイルも含まれるが、rubocop --force-exclusion のようにオプションを付けると、例えコマンドライン引数としてファイルパスを指定したとしても、設定を元に対象のファイルかどうかを判断してくれるようになるので、これで上手くいく。例えばそういった機能が無いコマンドを対象とする場合、拡張子で判断するならば、次の例のようにもできる。これはSlimテンプレートを対象に、slimcop コマンドを実行する例。

- if: steps.slimcop-all.conclusion == 'skipped'
  run: |
    echo ${{ steps.changed-files.outputs.all_changed_files }} |
    tr " " "\n" |
    grep "\.slim$" |
    tee >(xargs --no-run-if-empty bundle exec slimcop)

空白区切りの文字列を、tr コマンドで改行区切りに変換し、grep コマンドで特定の拡張子を持つパスだけを取り出し、tee コマンドとプロセス置換で実行対象のパス一覧を標準出力させつつ、xargs コマンドでコマンドライン引数としてパス一覧を slimcop コマンドに渡している。--no-run-if-empty オプションを利用し、入力が空の場合には実行しないようにしている。