tech.guitarrapc.cóm

Technical updates

GitHub Actionsでモノレポ上の特定パスをチェックアウトスキップしたい

モノレポでリポジトリサイズが大きくチェックアウトに時間がかかって困ることがあります。

今回はこういったときにどうできるのかを考えてみましょう。

概要

CIでは特定のパスしかいらないのに、ほかのパスもチェックアウトされるのを制御したい場合、スパースチェックアウトが使えます。

  • スパースチェックアウトを使えば、モノレポ上で、特定のパスをチェックアウトスキップしたり、特定のパスだけチェックアウトすることができる
  • actions/checkoutできるactionsるactionsチェックアウトはしたくないがやむなし
  • actions/checkoutのスパースチェックアウト対応PRはあるので、upvoteしてマージされてほしい声が多くなるとうれしい

https://github.com/actions/checkout/pull/680

GitHub Actions でスパースチェックアウトをする

GitHub Actionsでスパースチェックアウトをする例を2つ紹介します。(両方ともシャロークローンも行っています)

もともとのルート直下は次のディレクトリです。このうち、src/を対象にスパースチェックアウトしてみましょう。

$ ls -la
.editorconfig
.git
.gitatributes
.github
.gitignore
LICENSE.md
README.md
mermaid
samples
src
  • 1つ目の例Git-sparse-checkout-exclude.yamlは、特定のパスを除外してチェックアウトする例です
    • チェックアウト後、src/がチェックアウトから除外されていることがわかる
  • 2つ目の例Git-sparse-checkout-only.yamlは、特定のパスだけチェックアウトする例です
    • チェックアウト後、src/だけチェックアウトされていることがわかる

https://gist.github.com/guitarrapc/d56a096d3283cb43da5a0a0017f6eed2

過去の経験ではactions/checkoutで1m30s程度かかるリポジトリが35s程度まで高速化できました。 すでにシャロークローンしているが、それでもチェックアウトに時間がかかっており、もっと高速化が強く望まれるときはスパースチェックアウトを検討してもいいでしょう。

ただ、現在のGitHub Actionsのactions/checkoutはスパースチェックアウトに対応しておらず、自前チェックアウトが必要です。 チェックアウトのコードはメンテしにくくくなりやすいため、自前チェックアウトは避けたい気持ちもあります。

actions/checkoutにスパースチェックアウト対応が入ったら、気兼ねなく使っていけるので期待したいです。

CI におけるチェックアウトの基本

さて、CIにおいてチェックアウトをどのように行うといいのかを自分のためにまとめておきます。よく知られていることなので、知ってる方はスルーでどうぞ。

CIでのチェックアウトの基本は、「高速であればあるほど良い」であり、これは多くの方が同意するところでしょう。高速なチェックアウトをしようとして課題になりやすいのが、マネージドCIサービスでのビルドです。(GitHub Actionsなどのホストランナー環境をイメージしてください)

マネージドCIサービスは、環境を起動する度にクリーンな環境が用意されます。このため、前回のチェックアウトを再利用できず、毎回git cloneします。リポジトリサイズが大きい場合、何も考えずgit cloneをするとチェックアウト完了まで数分かかり、ビルド全体が遅くなるなるのです。一方で、Jenkinsのように前回のジョブ結果 = チェックアウト結果を再利用できるセルフホストランナー環境では、2回目のチェックアウトはGit差分で済むため高速に終わります。このため、オンプレや自社サーバー、EC2などでビルドしている場合はチェックアウト速度に悩む機会が少ないでしょう。

では、どのようにしたらマネージドCIサービスでチェックアウトを高速化できるでしょうか?よくある失敗が、前回のキャッシュを取って次の起動でリストアしてGit差分で済ませようという案です。残念ながら、キャッシュはただのダウンロード/アップロードであり、リポジトリをキャッシュに突っ込んでも高速化しないことがほとんどです。

鍵はgit cloneをいかに高速に行うかです。

git cloneを高速化する定番の方法

よく使われる方法が2つあります。

  • シャロークローン (shallow clone)
  • スパースチェックアウト (sparse checkout)

それぞれを見てみましょう。

シャロークローン

git cloneが重いので履歴をすべて持ってくるのではなく、直近履歴だけ取得する」というのがシャロークローンです。

シャロークローンはgit clone --depth 1のように--depth nを指定することで直近n件の履歴だけ持ってくることで、大量のコミット履歴があるリポジトリでもクローンが軽くなります。1

シャロークローンは、チェックアウトする履歴を直近から指定できると捉えることができます。 詳しい内容はGitHubブログがいい記事を出しているのでここを見るといいでしょう。2

https://github.blog/jp/2021-01-13-get-up-to-speed-with-partial-clone-and-shallow-clone/

Git履歴は運用や開発が進む度に積み重なります。しかしCIではビルドするためには最新コミット時点の履歴しかいらないことが多く、フルクローンはCIにおいては無駄になりやすいです。そのため、CIにおけるチェックアウトはしゃろークローンにするのが定番になっていると認識しています。

履歴指定は必ずしも--depth 1にする必要はありません。1件前のコミットログが欲しいから--depth 2にしたり、直近100件ぐらいまではほぼ速度が変わらないことから--depth 100にすることもあります。

スパースチェックアウト

git cloneが重いので全部のパスをチェックアウトするのではなく、チェックアウトアウトするパスを特定のものに絞り込む」というのがスパースチェックアウトです。

スパースチェックアウトはgit clone --no-checkout --sparseのようにクローン時点ではチェックアウトしません。チェックアウト前にgit sparse-checkout set "パス"でチェックアウト対象パスを指定してから、チェックアウトをします。特定のパスだけチェックアウトができるので、大量にファイルがあるリポジトリでも、クローンやチェックアウトが軽くなります。

スパースチェックアウトは、チェックアウト対象のパスを管理できると言い換えてもいいでしょう。 詳しい内容はGitHubブログがいい記事を出しているのでここを見るといいでしょう。3

https://github.blog/2020-01-17-bring-your-monorepo-down-to-size-with-sparse-checkout/

スパースチェックアウトを使って効果が出やすい例としては、モノレポでそれぞれの言語やサーバー/クライアントごとにパスを切ってビルドしている場合や、テストには特定のパスだけチェックアウトが必要というケースが考えられます。(参考にあるZennはそれ)

私はゲーム開発が多いので、サーバーとクライアント(Unity)がモノレポで管理されているケースでほしくなる時があります。Unityは「管理しているファイルごとに .metaが必ず必要になるため」クライアントコードのファイル数が膨れやすい性質を持ちます。このため、クライアントコードのファイル数が枷となって、サーバーサイドビルドのチェックアウトに影響が出ます。このようなときにスパースチェックアウトが効果を発揮します。サーバービルド時にクライアントコードをチェックアウト対象にしないことで、サーバービルドの高速化が期待できます。

GitHub Actions でスパースチェックアウトをしたい

スパースチェックアウトが便利なのはわかりましたが、GitHub Actionsではどうやればスパースチェックアウトができるのでしょうか?

スパースチェックアウトを書いてみよう

自前でスパースチェックアウトするときは、いくつか参考を見るといいでしょう。(記事の末尾にリンクをおいておきます。) 今回は、冒頭にあげたコードを使って流れを説明します。

git clone --filter=blob:none --no-checkout --depth 1 --sparse "https://github.com/なんとか.git" .

スパースチェックアウトを行う場合、git clone時点ではcheckoutをしません。このため、--no-checkout --sparseがセットで登場します。ちなみに--filter=blob:noneはパーシャルクローン(の1つであるブロブレスクローン)で、--depth 1はシャロークローンで、一緒に使うことが多いです。

git sparse-checkout set --no-cone "${{ env.SPARSECHECKOUT_DIR }}" "/*"

スパースチェックアウトのパス指定を行います。 パス指定は .gitignoreと同様のスタイルで記載できるのですが、固定パス指定ではなくパスパターンで指定する場合、--no-coneが必要です。 coneにするとパスパターン (/* みたいなやつ) が使えないの注意です。

.gitignoreで否定パスを書いたことがある人は、順番に違和感を覚えるでしょう。.gitignoreでは、否定を後に書いて打消しますが、スパースチェックアウトでは先に書きます、書くまで知らなかったです。

NOTE: ちなみに .git/info/sparse-checkout に直接書き込もうと、echo -e "/*\n!node_modules" >> .git/info/sparse-checkoutのようにするのは私は避けています。git sparse-checkout setだとパス指定に問題があるときにエラーなどで教えてくれるけど、直接書き込むとgit sparse-checkout listするまで問題があるか気づけないためです。

git sparse-checkout init

スパースチェックアウトを有効にします。先にgit sparse-checkout setしておいてから有効にするほうが、警告とか見やすいのでおすすめです。 パスパターンで指定するなら、git sparse-checkout init --coneしないこと。

git sparse-checkout list

デバッグ用です。スパースチェックアウトのパス指定があってるか表示させているだけです。.Git/info/sparse-checkoutにパスパターンで直書きしているのに、git sparse-checkout init --coneにしてると、「問題があるよ」と警告が出てくれます、便利。

git checkout "${GITHUB_SHA}"

チェックアウトします。ここで初めてファイルが取得されます。スパースチェックアウトがちゃんと指定されていれば、指定したパスだけチェックアウトしたり、指定したパスを除外してチェックアウトできているのが確認できるでしょう。

サブモジュールは割愛します。プライベートリポジトリなサブモジュールをチェックアウトする時はPATが必要なので気を付けて。

自前チェックアウトの最後はgit reset --hard SHAします。これは定番ですね。後続のステップでgit diffが出ないように気を払っておきましょう。

まとめ

GitHub Actionsのactions/checkoutはシャロークローンされており十分高速なため、99%のケースではこの記事は不要のはずです。 さらなる高速化としてスパースチェックアウトを使えば、特定のパスだけチェックアウトしたりできます。

特定のフォルダサイズでチェックアウトが重いと判明している場合は、スパースチェックアウトが効果を発揮することが期待できます。

ただ、自前チェックアウトは書いた瞬間から重めの負債だと思っているので、慎重になりますね。

参考

これは特定のパスをチェックアウトする例ですが、やってることは大体一緒です。(この記事はパスパターン指定でのexcludeなのでconeはダメなのが違う) GitHub Actionsの事情というより、Git sparse-checkoutの基本がいくつか触れられています。

https://zenn.dev/mizchi/articles/gha-run-test-only-changed

特定のフォルダだけ除外するときのコツが書かれています。coneには特に注意です。

https://stackoverflow.com/questions/33933702/Git-checkout-except-one-folder


  1. 併せて--single-branch --branch=<branch>を指定することも多いでしょう。
  2. CIにおいては、最新コミットでビルドできることが重要なので、ほとんどのケースではパーシャルクローンよりもシャロークローンが適しています。
  3. スパースチェックアウトが効果を発揮するのは、対象のパスのファイル数が数万、数十万オーダーなどチェックアウトに著しく影響ある場合です。