tech.guitarrapc.cóm

Technical updates

GitHub Actions でスパースチェックアウトを使ってモノレポの一部だけをチェックアウトする

モノレポをCIでビルドするにあたり、最初にして最大の課題がチェックアウトです。先日GitHub Actionsのactions/checkoutでスパースチェックアウトできるようになったので、これを使ってモノレポのチェックアウトを高速化しました。

今後のモノレポチェックアウトの定番になるであろう、スパースチェックアウトを使ったモノレポのチェックアウトを紹介します。

tl;dr;

  • CIのチェックアウトは早いが正義、早ければ早いほどよい
  • スパースチェックアウトなら、モノレポのビルドでも特定のパスだけをチェックアウトしてビルド時間を短縮できる
  • シャロークローン(shallow-clone) とスパースチェックアウト (sparse checkout) を併用するとモノレポで最速のチェックアウトができる
  • actions/checkoutでスパースチェックアウトがサポートされたので、GitHub Actionsでスパースチェックアウトを気軽に使えるようになった
  • actions/checkoutはデフォルトでシャロークローン (depth=1) なのでスパースチェックアウトを簡単に併用できる

GitHub Actionsでのスパースチェックアウトの例を含んだリポジトリを用意しました。参考にしてください。

https://github.com/guitarrapc/githubactions-lab

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) のようにマイクロサービスごとに分かれています。

image

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

さて、このリポジトリのAndroidクライアントだけをGitHub Actionsでビルドしたいと考えたときに、どうすればよいでしょうか?

シャロークローン

スパースチェックアウトがない場合、シャロークローンを使って最新のコミットを取得してからビルドするのが定番です。 シャロークローンは履歴を最新N件に絞ることで取得するデータ量が大幅に減らし、フルクローンに比べてチェックアウトが数倍高速化します。

image

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>

image

スパースチェックアウト

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

シャロークローンを使うだけだとできなかった、必要なファイルだけをチェックアウトできました。

image

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

スパースチェックアウトはシャロークローンと併用もできるので、最新1件の履歴のうち、必要なファイルだけチェックアウトできます。まさに最速のチェックアウトです。

image

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/*"

参考