あるとき、1つのディレクトリの中に、互いに独立したプロジェクトのコードを入れたくなった(monorepoというらしい?)。
しかしこれだとCI/CDツールを動かしたときに、あるプロジェクトのテストをしたいのに、別のプロジェクトまでテストする羽目になりそうだった。なぜならCI/CDツールはリポジトリの変更があったことを検知して動くが、どのディレクトリの変更なのかは検知しないのが普通だからだ。

ディレクトリごとに変更検知・テスト実行をしてくれないと、むだな処理が増えまくるので切実に困る…。
そう思って調べたところ、Github Actionsだと求める動きをするようなので、試してみることにした。

やりたいこと

リポジトリ内に、まったく無関係な3種類のプロジェクトのコード(python, rust, scala)があり、ディレクトリに分かれている。

このとき、変更があったディレクトリだけでテストを走らせたい。
具体的にいうと、 python ディレクトリ内で変更があったときにGithubにpushするとPythonのテストだけを行い、 rustscala では何もしないでほしい。
また、 pythonrust 両方で変更があったときはPythonとRustのテストを行い、Scalaのテストはしないでほしい。

.
├── LICENSE
├── README.md
├── python
│         │
│           ...
├── rust
│         │
│           ...
└── scala
          │
            ...

できたもの

結論としては、やりたいことはすべてできた。

リポジトリ

github-actions-sample-with-paths

ymlファイル

Github Actionsの設定ファイルはディレクトリごとに、合計3種類作った。どれも似たりよったりなのでpythonのものだけ貼る。

name: Test in python directory

on:
  push:
    paths:
      - 'python/**'

defaults:
  run:
    working-directory: python

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Setup
        uses: actions/setup-python@v2
        with:
          python-version: '3.8'

      - name: Install tools
        run: |
          python -m pip install --upgrade pip
          pip install pytest

      - name: Run pytest
        run: |
          pytest

要点は以下の2つ。

paths

on:
  push:
    paths:
      - 'python/**'

on.<push|pull_request>.pathspython/** を指定している。これにより、pythonディレクトリ下で変更があったときだけ発火する。 python/* だと深い階層にはマッチせず意図と違う動きになるので注意。

working-directory

defaults:
  run:
    working-directory: python

実行するときのワーキングディレクトリを working-directory で設定している。これは jobs.<job_id>.steps[*].run でその都度指定してもいいが、面倒なので defaults.run を使う。こうするとこのワークフローの run 実行時にデフォルトでこのディレクトリをワーキングディレクトリに設定する。
なぜこれを設定するかというと ワーキングディレクトリにその言語などの設定ファイルがないとテストが動かない場合がある ため。Pythonだとリポジトリルートで pytest とやっても動くようだが(それも状況によるかも)、Rust(cargo test)やScala(sbt test)だとルートでやっても正常に動かない。これを防ぐために、リポジトリルートでなくプロジェクトルートをワーキングディレクトリにしている。

困ったこととか

working-directoryを設定できないactionがある

jobs.<job_id>.steps[*].uses を使うと、自分でrunを書かなくても、誰かが公開したactionを使える。だがactionによっては、今回のユースケースに合わず、使えないものがある。

Rustのactionにactions-rs/cargoというものがある。自分も以前はこれを使ってテストなどを行っていた。今回もこれを使おうとしたのだが、このactionでワーキングディレクトリを設定することができない。どうやら working-directoryrun のときにしか適用されず、actionを使うときは適用されないらしい。おそらくactionにワーキングディレクトリを設定する機能が実装されていなければ無理なのだろう。Rustではワーキングディレクトリを設定しないとCargo.tomlが見つからないというエラーが出てしまうため、今回はこのactionは使えない。

結局Rustではこのactionは使わず、 run でコマンドを書いた。
このケースはactionを使わなくても簡単にできるので問題なかったが、もっと複雑なactionの場合はクリティカルな問題になるかもしれない。

なお上記の問題は以下にやりとりがある。解決するPRはできているがマージされていないっぽい。(この中で --manifest-path を使えば一応解決できるということも書いてある。だがこれはワーキングディレクトリを設定するのとは効果が違う。)

いちいちGithubにpushしないとGithub Actionsの設定ファイルを検証できない?

何度もコミットしてpushしてGithub Actionsを動かすのがとてつもなく不毛だと思った。

これを解決するにはactというツールを使えばいい。ローカルでGithub Actionsを実行できる。使い方はREADMEのgif見ればわかる。 act でローカル実行、 act -n でdry-runなど。commitしなくても、現在のコードの状況で実行してくれるので楽。

今回のように .github/workflow/ に複数ファイルがある場合、すべてのワークフローが並行して走るので、ログが入り乱れて見にくくなる。これを防ぐにはワークフローファイルを指定して実行する。下のような感じ。

act -W .github/workflows/python.yml

おわりに

やりたいことがちゃんとできたので一安心。

上に書いた actions-rs/cargo の例もあるので、実際にこのような方法を採用するときは使いたいactionで検証してからの方がよさそう。あと act 便利。

参考

きっかけ

これらの記事を見てGithub Actionsのpathsを知った。

Github Actionsのpathsなどのドキュメント

Python

PythonでのGithub Actions記述方法はこれ見ればわかる。

Scala

ScalaでのGithub Actions記述方法はこれを見ればわかる。
今回はcacheを使う設定でymlを書いたが、これだとactによるローカル実行はできないっぽい(設定すればできるのかもしれないが…)。