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

参考