モノレポをCIでビルドするにあたり、最初にして最大の課題がチェックアウトです。先日GitHub Actionsのactions/checkout
でスパースチェックアウトできるようになったので、これを使ってモノレポのチェックアウトを高速化しました。
今後のモノレポチェックアウトの定番になるであろう、スパースチェックアウトを使ったモノレポのチェックアウトを紹介します。
- tl;dr;
- GitHub Actions のチェックアウト
- スパースチェックアウトとは
- GitHub Actions でスパースチェックアウトを使う
- スパースチェックアウトでどの程度改善するのか
- 他のCI でもスパースチェックアウトを使いたい
- 参考
tl;dr;
- CIのチェックアウトは早いが正義、早ければ早いほどよい
- スパースチェックアウトなら、モノレポのビルドでも特定のパスだけをチェックアウトしてビルド時間を短縮できる
- シャロークローン(shallow-clone) とスパースチェックアウト (sparse checkout) を併用するとモノレポで最速のチェックアウトができる
- actions/checkoutでスパースチェックアウトがサポートされたので、GitHub Actionsでスパースチェックアウトを気軽に使えるようになった
- actions/checkoutはデフォルトでシャロークローン (depth=1) なのでスパースチェックアウトを簡単に併用できる
GitHub Actionsでのスパースチェックアウトの例を含んだリポジトリを用意しました。参考にしてください。
GitHub Actions のチェックアウト
GitHub Actionsのチェックアウトは、 actions/checkout を使うのが定番です。特に理由がないなら、自分でチェックアウトするのではなくactions/checkout
を使うことで、シャロークローンがデフォルトで有効になったり、サブモジュールやLFSのサポートもされます。
しかし唯一actions/checkout
に足りていなかったのが、スパースチェックアウトでした。
スパースチェックアウトとは
Bring your monorepo down to size with sparse-checkout | The GitHub Blog が分かりやすい上に詳しいのでまずはこの記事をみるのをおすすめします。
簡単にいうと、スパースチェックアウトはチェックアウトするファイル(パス)を指定することで、特定のファイルだけをチェックアウトできる機能です。
Git 2.25.0からgit sparse-checkout
コマンドで使えるようになりました。
なぜ「特定のファイルだけをチェックアウトできる」ことが嬉しいのか、先のブログの図を使って簡単に説明します。
モノレポのディレクトリ構造をイメージした次の図を見てください。このモノレポでは、1つのリポジトリで写真ストレージと共有サービスのコードが管理されています。 ディレクトリは、クライアント(client)、サーバーサイドロジック(service)、フロントのウェブ(web) のようにマイクロサービスごとに分かれています。
https://github.blog/2020-01-17-bring-your-monorepo-down-to-size-with-sparse-checkout/ より引用
さて、このリポジトリのAndroidクライアントだけをGitHub Actionsでビルドしたいと考えたときに、どうすればよいでしょうか?
シャロークローン
スパースチェックアウトがない場合、シャロークローンを使って最新のコミットを取得してからビルドするのが定番です。 シャロークローンは履歴を最新N件に絞ることで取得するデータ量が大幅に減らし、フルクローンに比べてチェックアウトが数倍高速化します。
https://github.blog/jp/2021-01-13-get-up-to-speed-with-partial-clone-and-shallow-clone/ より引用
しかしシャロークローンでは、Androidビルドをしたいだけにも関わらずビルドに不要な他クライアント、サーバー、Webのコードが含まれてしまっておりチェックアウト時間が長くなります。
# シャロークローンで最新のコミットを取得しているが、不要なコードもチェックアウトしてしまっている git clone --depth=1 <repository_url>
スパースチェックアウト
client/android
のファイルだけをチェックアウトできれば、他のコードを取得しないため高速にチェックアウトできそうです。
そこでスパースチェックアウトを使えば、必要なファイルだけチェックアウトできます! 例えばclient/android
にあるファイルだけをチェックアウトするなら次のようなコマンドでできます。
# スパースチェックアウトで client/android だけチェックアウトする。 # git clone に --depth 1 をつければシャロークローンと併用もできる。 git clone --filter=blob:none --no-checkout --sparse <repository_url> cd <repository_name> git sparse-checkout set client/android git sparse-checkout init git checkout
シャロークローンを使うだけだとできなかった、必要なファイルだけをチェックアウトできました。
https://github.blog/2020-01-17-bring-your-monorepo-down-to-size-with-sparse-checkout/ より引用
スパースチェックアウトはシャロークローンと併用もできるので、最新1件の履歴のうち、必要なファイルだけチェックアウトできます。まさに最速のチェックアウトです。
GitHub Actions でスパースチェックアウトを使う
モノレポでスパースチェックアウトを使いたい理由が分かったところで、GitHub Actionsでスパースチェックアウトを使う方法を紹介します。
これまでactions/checkout
はスパースチェックアウトのサポートがありませんでしたが、2023/6/15現在actions/checkout@v3
で利用できます。もし手元のチェックアウトがactions/checkout@v2
ならv3に更新するだけでスパースチェックアウトが使えるようになります。
Add support for sparse checkouts by dscho · Pull Request #1369 · actions/checkout
スパースチェックアウトでチェックアウトするパスを指定する
スパースチェックアウトを使うには、sparse-checkout
にチェックアウトするパスを列挙します。
例えばsrc
ディレクトリだけをチェックアウトする場合は次のようになります。
name: git sparse-checkout on: pull_request: branches: ["main"] jobs: sparse-checkout: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: sparse-checkout: | src
一見スパースチェックアウトをしているだけですが、actions/checkout
はデフォルトがシャロークローン (depth=1) なので併用されてチェックアウトが最速になります。簡単、便利で最高の体験です。
除外指定とコーンモード
actions/checkout
のスパースチェックアウトはデフォルトがコーン(cone
) モードなので、 !
を使った除外指定ができません。
除外指定をするには、sparse-checkout-cone-mode: false
でコーンモードを無効にする必要があります。コーンモードを無効にした場合、sparse-checkout
にはパス形式で指定する必要があります。
例えば、 src
ディレクトリを除外して、それ以外をチェックアウトする場合は次のようになります。
コーンモードが有効な↑の例ではsrc
と指定しましたが、コーンモードを無効にしたので!src/*
や/*
とパス形式で指定しているのがポイントです。
name: git sparse-checkout (exclude) on: pull_request: branches: ["main"] jobs: sparse-checkout: runs-on: ubuntu-latest timeout-minutes: 5 steps: - uses: actions/checkout@v3 with: sparse-checkout: | !src/* /* sparse-checkout-cone-mode: false # required for ! entry to work
スパースチェックアウトでどの程度改善するのか
手元のモノレポの1つでは、シャローンクローンを使ってもチェックアウトに1m20sかかっていたのが、スパースチェックアウトを併用することで1s~15sに改善しました。
1sは.github
など最低限必要なパスだけをスパースチェックアウトした場合で、15sはビルドに必要なパスだけをスパースチェックアウトした場合でした。
一日に何度もCIが回り、複数のジョブで何度もチェックアウトしていたので、CIの回転時間が大幅に改善されました。なにより、 actions/checkout
が高速に終わるので利用をためらわなくなったのが最高です。
意識しにくいですが、Billable minutesも大幅に減っているのでコスト管理的にも嬉しいですね! 財布に優しく、高速にCIを回せる、スパースチェックアウトは大好きです。
他のCI でもスパースチェックアウトを使いたい
残念ながらCircleCI、AzureDevOps、Jenkinsのいずれにおいても、標準的に利用されているチェックアウト方法ではスパースチェックアウトはサポートされていません。
使いたい人向けに、参考までにactions/checkoutにスパースチェックアウトが来るまで使っていた方法をおいておきます。
name: git sparse-checkout on: pull_request: branches: ["main"] jobs: sparse-checkout: runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: sparse checkout run: | git clone --filter=blob:none --no-checkout --depth 1 --sparse "https://${{ env.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" . echo "git sparse-checkout set exclude directory" git sparse-checkout set --no-cone "${{ env.SPARSECHECKOUT_DIR }}" echo "git sparse-checkout without cone" # cone not allow pattern filter, therefore don't use cone. git sparse-checkout init echo "git sparse-checkout list" git sparse-checkout list echo "git checkout" git checkout "${GITHUB_SHA}" # if you have submodules in Private Repo, use PAT instead of secrets.GITHUB_TOKEN if [[ -f ./.gitmodules ]]; then echo "replace submodule url" sed -i -e "s|https://github.com|https://${{ env.GITHUB_TOKEN }}@github.com|g" ./.gitmodules echo "submodule update" git submodule update --init --recursive fi echo "git reset" git reset --hard "${GITHUB_SHA}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SPARSECHECKOUT_DIR: "src/*"