Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails以外の開発一般

GitHub Actions: dependabotが作成するキャッシュにストレージを食われすぎないようにする

TL;DR

dependabotのPRによって実行されるジョブでキャッシュを作らせたくなければ、キャッシュするstepでこれを書く。

if: ${{ !startsWith(github.head_ref, 'dependabot') }}

説明

起こりうる問題

GitHub Actionsでテストなどを回すときに、ライブラリを都度インストールせずに済むよう、pathを指定してキャッシュすることがあると思います。
以下は Gemfile.lock が同一である場合にキャッシュを利用する ci.yml の例です。

on:
  pull_request:
    branches: [develop]

jobs:
  rspec:
    # ...
    steps:
      - name: Cache bundle gems
        uses: actions/cache@v3
        with:
          path: vendor/bundle
          key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}

これは通常のPRでは問題ないのですが、dependabotを使っているときに面倒になります。
dependabotはライブラリ更新のPRを出します。ということは、この例においては Gemfile.lock も更新するので、キャッシュを新しく取り直します。
このキャッシュ作成がPRのたびに発生するため、キャッシュ容量次第ではGitHub Actionsのストレージが大幅に食われます。

仮に、以下のようなタイムラインでPRが作成されていくとします。無料ストレージ容量は1GBで、それを超えたときの課金は未設定、ライブラリキャッシュの容量は300MBとしましょう。

  • 2023/03/13 12:00 開発者がPR#1001を作成
  • 2023/03/13 15:00 dependabotがPR#1002、PR#1003、PR#1004を同時に作成
  • 2023/03/13 18:00 以上のPRがマージされないまま、開発者がPR#1005を作成

このとき、PR#1005によって実行されるジョブでは、ライブラリキャッシュを利用できません。
累積のキャッシュ容量が無料ストレージ容量を超え、かつ自動課金によるスケールが未設定の場合、古いキャッシュから削除されていきます。

If you exceed the limit, GitHub will save the new cache but will begin evicting caches until the total size is less than the repository limit.
https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policyより

以上の例の場合、PR#1004が作成された時点でキャッシュ容量が1.2GBに到達するので、一番古いPR#1001のキャッシュが消え、PR#1005では利用できません。

以下を見ると分かる通り、GitHub Actionsの使用について提供される無料ストレージ容量は(GitHub Enterprise Cloudを除いて)〜2GBですので、意外と起こりうる話かと思います。(2022/03/16現在)

dependabotとキャッシュの関係

また、そもそもdependabotに、 Gemfile.lock を変更トリガとした環境でインストールするgemのキャッシュを作らせる必要性が薄いと思います。
例えば、dependabotが「ライブラリAの更新PR」「ライブラリBの更新PR」「ライブラリCの更新PR」を同時に出してきたとして、それぞれに対応するキャッシュが再利用されることはほとんどないはずです。
PR作成時およびdevelopブランチへのマージ時にCIが回るようになっている場合、developブランチへのマージ時にはキャッシュを利用できます。それでも1回しか使われず、他のライブラリ更新がマージされれば不要になります。
開発者が本当にキャッシュしてほしいのは、基本的には「ライブラリAとBとCが更新された状態のライブラリ一覧」のはずです。

解決策

ということで、dependabot経由のPRにキャッシュを作らせるのを止めます。
dependabotが作成するPRのブランチにはdependabotという共通のプレフィックスがあるため、これを利用します。
jobs.<job_id>.steps[*].if を使ってstepを飛ばしましょう。

on:
  pull_request:
    branches: [develop]

jobs:
  rspec:
    # ...
    steps:
      - name: Cache bundle gems
        if: ${{ !startsWith(github.head_ref, 'dependabot') }} # この1行を足す
        uses: actions/cache@v3
        with:
          path: vendor/bundle
          key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}

startsWith( searchString, searchValue ) は、 searchStringsearchValue から始まるかを確認します。
github.head_ref がPR元のブランチ名なので、これが dependabot から始まるかを確認しているわけです。

ruby/setup-ruby を使っている場合は、 bundler-cache: ${{ !startsWith(github.head_ref, 'dependabot') }} に設定すればいいと思います。

やること自体は簡単なんですが、ググってみてもdependabot経由のPRにキャッシュさせないという話をあまり見かけなかったので記事にしました。
GitHubでは gh-actions-cache を使う方法を紹介していますが、今回の例に限ってはdependabot経由のPRにキャッシュを作らせないほうが簡単な気がします。

世間ではどうやって対処してるんでしょうか。よかったら教えて下さい。

参考

関連記事

Gitのpush時にCIのビルドを回避する方法


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。