tech.guitarrapc.cóm

Technical updates

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

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

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

tl;dr;

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

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

github.com

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/ だけチェックアウトされていることがわかります

gist.github.com

過去の経験では 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

github.blog

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

github.blog

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

私はゲーム開発が多いので、サーバーとクライアント(Unity)がモノレポで管理されているケースでほしくなる時があります。Unityは管理しているファイルごとに .meta が必ず必要になるためクライアントコードのファイル数が膨れやすい性質があります。このため、クライアントコードのファイル数が枷となって、サーバーサイドビルドのチェックアウトに影響がでることがあるのです。

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

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

Gactions/checkout はスパースチェックアウトに対応していない

GitHub Actions でリポジトリを チェックアウト = git clone するときは、actions/checkout を使うのが定番です。4

しかし、actions/checkout はスパースチェックアウトには対応していません。スパースチェックアウトをしたい場合は、自前チェックアウトが必要があります。

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

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

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 の基本がいくつか触れられています。

zenn.dev

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

stackoverflow.com


  1. 併せて --single-branch --branch=<branch> を指定することも多いでしょう。
  2. CIにおいては、そのコミットでのビルドができることが重要なので、ほとんどのケースではパーシャルクローンよりもシャロークローンが適しています。
  3. スパースチェックアウトが効果を発揮するのは、対象のパスのファイル数が数万、数十万オーダーなどチェックアウトに著しく影響ある場合です。
  4. シャロークローン、サブモジュール、PAT認証、特定のブランチチェックアウトと幅広く対応しています。特別な理由がないのに自前で git clone コマンドを書いているなら actions/checkout を使うほうがいいでしょう。