ぽよメモ

レガシーシステム考古学専攻

GitHub Actionsのキャッシュをより細かく制御するactions/cache/restoreとactions/cache/save

はじめに

これはGitHub Actions Advent Calendar 2022 22日目の記事です。諸事情によりフライング投稿です。

GitHub Actionsのキャッシュにおいて、そのリストアと保存を別々に制御する機能が actions/cache@v3.2.0-beta.1 で実装されたので使ってみました。トピックブランチではキャッシュを保存しない、ビルドが失敗した際にもキャッシュを保存する、などこれまでは出来なかった細かい制御が可能になっています。

背景

GitHub Actionsにおいて、ダウンロード済みの依存関係などをキャッシュすることでワークフローの実行を高速化することは、一般的によく知られたテクニックです。
一方でGitHub Actionsのキャッシュはいくつかの制限があることが知られています。

  • 1リポジトリあたり合計10GBまで*1
  • デフォルトブランチおよびカレントブランチのキャッシュしかリストアできない*2
    • Pull Requestではベースブランチのキャッシュも利用可能
  • 同じキーに対するキャッシュを上書きできない*3
  • 暗黙的に定義された事後処理ステップにおいてキャッシュの保存が行われるため、無関係な箇所でジョブが失敗した場合にでもキャッシュの保存がスキップされてしまう

これにより、特にキャッシュサイズが大きい場合トピックブランチで複数回キャッシュの保存が行われるとデフォルトブランチのキャッシュが消えてしまったり、依存関係のフェッチとテストを別ジョブに分けて確実に依存関係がキャッシュされるようなワークアラウンドが必要なケースがありました。

actions/cache/restoreとactions/cache/save

以前からより詳細なキャッシュの制御がしたいという要望はあり、2019年頃から以下のようなissueがありましたが、あまり進展は見られていませんでした。

github.com

しかし、2022年12月になって急にDiscussionにてrestoreとsaveに対応するactionが実装されることが発表されました。

github.com

実際に v3.2.0-beta.1 からrestoreとsaveが実装されています。

github.com

それぞれについて書くことはそんなにありません。単にキャッシュのリストア・保存が別アクションに分かれただけです。

■ 追記(2022/12/26)

actions/cache/saveとactions/cache/restoreはv3.2.0でGAになりました。
以降のサンプルコードの@v3.2.0-beta.1@v3 で読み替えても動作します。

github.com

追記終わり

ユースケースの紹介

実際に背景で説明したいくつかの課題をこれで解決することが出来るので一例を紹介します。

トピックブランチではキャッシュを保存しない

2GBのランダムなダミーデータをキャッシュに保存してみることにします。ただし、トピックブランチではキャッシュの保存をスキップします。

name: Save cache only on main

on: [push]

jobs:
  run:
    runs-on: ubuntu-20.04
    steps:
      - uses: actions/checkout@v3
      - uses: actions/cache/restore@v3.2.0-beta.1
        with:
          path: |
            ./large-object
          key: ${{ runner.os }}-${{ runner.arch }}-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-${{ runner.arch }}-
      - name: Generate random file if needed
        run: |
          if [ ! -f ./large-object ]; then
            base64 /dev/urandom | head -c 2048M > large-object
          fi
      - uses: actions/cache/save@v3.2.0-beta.1
        if: github.ref == 'refs/heads/main'
        with:
          path: |
            ./large-object
          key: ${{ runner.os }}-${{ runner.arch }}-${{ github.sha }}

トピックブランチではsaveがスキップされる

mainにマージすると保存される

キャッシュがevictされる問題に対応出来るだけでなく、そのトピックブランチでしか有効でないキャッシュの保存にかかる時間をスキップできることも大きいです。サイズの大きなキャッシュのアップロードを無効化するだけで場合によっては数十秒〜数分の短縮に繋がることもあります。

ただし、これはトピックブランチで長い期間開発する場合にはキャッシュがないことによりむしろ実行時間が延びる可能性があります。

常にキャッシュを保存する

例えばテストが失敗するケースでも、キャッシュを保存したいというようなユースケースです。特にflakyなテストが存在する場合には有用かもしれません。

name: Save cache always

on: [push]

jobs:
  run:
    runs-on: ubuntu-20.04
    steps:
      - uses: actions/checkout@v3
      - name: Step to fail
        run: |
          echo hello > ./file-to-cache
          false
      - uses: actions/cache/save@v3.2.0-beta.1
        if: always()
        with:
          path: |
            ./file-to-cache
          key: ${{ runner.os }}-${{ runner.arch }}-${{ github.sha }}

途中のステップで失敗してもキャッシュが保存されている

途中のステップで失敗しているので、ジョブ全体のステータスとしては失敗になります。

restoreとsaveで異なるkeyを使う

これがどれくらい需要のあるユースケースなのかはわかりませんが、これまでは地味にできなかったことです。
hashFiles などを使ってハッシュを計算する際、これまでは最初のリストア時に計算されたkeyが保存時にもそのまま利用されていました。つまりそのビルド中に hashFiles による計算結果が変わる場合に対応出来ていませんでした。例として以前の挙動を確認してみます。

途中のステップで hashFiles の対象としているファイルを作成しています。最初の評価時点ではファイルが存在しないため、 hashFiles('**/hello.txt') は空文字列になります。

name: Old behavior

on: [push]

jobs:
  run:
    runs-on: ubuntu-20.04
    steps:
      - uses: actions/checkout@v3
      - uses: actions/cache@v3
        with:
          path: |
            ./hello.txt
          key: ${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/hello.txt') }}
          restore-keys: |
            ${{ runner.os }}-${{ runner.arch }}-
      - name: Generate hashFiles targets
        run: |
          echo "hello" > hello.txt

actions/cacheでは最初にkeyを評価する時の値で保存する

actions/cache/saveを使って同じ事をすると、保存時に hashFiles の結果が再度評価されていることがわかります。

name: hashFiles get different result

on: [push]

jobs:
  run:
    runs-on: ubuntu-20.04
    steps:
      - uses: actions/checkout@v3
      - uses: actions/cache/restore@v3.2.0-beta.1
        with:
          path: |
            ./hello.txt
          key: ${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/hello.txt') }}
          restore-keys: |
            ${{ runner.os }}-${{ runner.arch }}-
      - name: Generate hashFiles targets
        run: |
          echo "hello" > hello.txt
      - uses: actions/cache/save@v3.2.0-beta.1
        with:
          path: |
            ./hello.txt
          key: ${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/hello.txt') }}

save時に評価されている

hashFiles を使う場合に限らず、restoreとsaveで異なるkeyを指定することが可能になっているので刺さる人には刺さるかも知れません。

まとめ

ライトなユースケースでは従来通り actions/cache をそのまま利用するのがわかりやすく、記述も容易であるため完全に置き換わることはない印象です。
一方、より詳細なキャッシュの制御を求める人にとっては待望の新機能になりそうです。キャッシュサイズの大きさが気になっている人は、とりあえずトピックブランチでのキャッシュ保存を辞めてみると良いかも知れません。