ロックファイルを一行直したら、キャッシュが古い依存関係を呼び戻した
目標
パイプラインのキャッシュのキーを手で作りながら、ヒット・ミス・プレフィックスによるフォールバックを作り出し、キャッシュとアーティファクトの違いを削除の実験で確認し、キーにランタイムのバージョンとブランチを入れなかったときに何が汚染されるのかを再現したうえで、ヒット率のレポートと信頼ゲートまで作ります。
なぜ重要なのか
キャッシュは、パイプラインで最も簡単に入れられ、最も長く間違ったまま残る仕組みです。キーが狭すぎると毎回ミスしてキャッシュがないのと同じになり、広すぎると、変更された依存関係・別のランタイム・他人のブランチが作った内容をそのまま呼び戻します。後者の失敗のほうがはるかに悪く、ビルドが成功してしまうので、誰も報告しません。「キャッシュを消せば直る」という言葉が冗談ではない理由はここにあります。その言葉は、キャッシュが隠れた入力になったという意味です。そのため、キャッシュの設計は3つを決める作業です。何が変わったらキーが変わるべきか、完全に一致するものがないとき何まで借りてよいか、そして誰がキャッシュに書き込めるか。この3つを決めておけば、ツールがGitHub ActionsでもGitLab CIでもBuildKitでも、同じ設定を書き写すだけで済みます。
ステップ
/root/cache-labの下に、ラボの土台を作ってください。(1)/root/cache-lab/env/runtime.txtにpython3.12を1行。(2)/root/cache-lab/deps.lockに<이름> <판>(プレースホルダーは名前とバージョンです)の6行を、この順序で書きます:left-pad 1.3.0、fast-json 2.1.4、tiny-http 0.9.2、color-util 4.0.1、date-fmt 3.2.2、crypto-lite 1.0.7。(3)/root/cache-lab/seed-repo.sh <작업디렉터리>(プレースホルダーは作業ディレクトリです)は、ロックファイルを読んで、パッケージごとに<작업디렉터리>/pkgrepo/<이름>/<판>/f01.txtからf12.txtまで12個を作り、各ファイルには<이름> <판> <번호두자리>(プレースホルダーは、名前、バージョン、2桁の番号です)の1行だけを書きます。(4)/root/cache-lab/install.sh <작업디렉터리>は、ロックファイルを読んで、パッケージごとにsleep 0.05でダウンロードのコストを擬似的に再現したあと、pkgrepo/<이름>/<판>をvendor/<이름>へコピーし、最後にvendor/_built-with.txtにenv/runtime.txtの内容を書いて、installed=<개수> ms=<밀리초>(プレースホルダーは個数とミリ秒です)の1行を出力します(vendorは毎回新しく作ります)。(5) 2つのスクリプトを順に実行したあと、/root/cache-lab/reports/cold.jsonにfiles・bytes・msの3つの数値を書いてください。filesはfind /root/cache-lab/vendor -type f | wc -l、bytesはそれらのファイルのサイズの合計、msはinstall.shが知らせた値です。- キャッシュの保存・復元の骨格を作ってください。
/root/cache-lab/cache-save.sh <작업디렉터리> <열쇠>(プレースホルダーは作業ディレクトリとキーです)は、vendorディレクトリを<작업디렉터리>/cache/<열쇠>.tar.gzにアーカイブし(tar czf ... -C <작업디렉터리> vendor)、<작업디렉터리>/cache/index.txtに<시각><탭><열쇠>(プレースホルダーは、時刻、キーで、間はタブです)の1行を追記したあと、saved <열쇠>を出力します。/root/cache-lab/cache-restore.sh <작업디렉터리> <열쇠>は、そのtarがあればvendorを削除して展開したあと、hit <열쇠>と終了コード0で、なければvendorに触れずにmiss <열쇠>と終了コード1で終了します。続いて、キーmanual-v1で保存し、vendorをまるごと削除し、同じキーで復元してください。復元の前後のフィンガープリントを、/root/cache-lab/reports/roundtrip.txtにbefore=<지문>・after=<지문>・same=yes(プレースホルダーはフィンガープリントです)の3行で書きます。フィンガープリントはcd /root/cache-lab/vendor && find . -type f -name '*.txt' | LC_ALL=C sort | xargs sha256sum | sha256sum | cut -c1-16で計算します。 /root/cache-lab/cache-key.sh <작업디렉터리>(プレースホルダーは作業ディレクトリです)を作ってください。出力は1行で、deps-<잠금해시12자>(プレースホルダーはロックファイルのハッシュ12桁です)の形です。ロックファイルのハッシュはsha256sum < <작업디렉터리>/deps.lock | cut -c1-12です。キーには/が入ってはいけません(ファイル名になります)。必ずロックファイルのハッシュ12桁で終わる必要があります。あとのステップで、前のほうに項目をさらに付け足すからです。続いて/root/cache-lab/cache-run.sh <작업디렉터리>を作ってください。キーを作って復元を試み、ヒットしたらそのまま終了し、ミスしたらinstall.shを実行したあと、そのキーで保存します。そして<작업디렉터리>/reports/runs.jsonlに{"key":"<열쇠>","result":"hit|miss","ms":<밀리초>}(プレースホルダーは、キーとミリ秒です)の1行を追記し、画面には<결과> <열쇠> <밀리초>ms(プレースホルダーは、結果、キー、ミリ秒です)を出力します。vendorとキャッシュを削除した状態でcache-run.shを2回実行し、1回目がミスして2回目がヒットすることを確認したあと、/root/cache-lab/reports/keys.txtに4行を書いてください:run1=<첫 결과>、run2=<두 번째 결과>、lock-sha12=<지금 잠금 해시 12자>、そしてfast-jsonを2.1.4から2.2.0に変えたと仮定したときのロックファイルのハッシュ12桁をbumped-lock-sha12=<값>として書きます(プレースホルダーは、1回目の結果、2回目の結果、現在のロックファイルのハッシュ12桁、値です。ロックファイル自体は変えず、計算だけを行います)。cache-restore.shに、3つ目の引数<접두>(プレースホルダーはプレフィックスです)を追加してください。完全に一致するキーがなく、プレフィックスが与えられていたら、cache/index.txtに記録されたキーのうち、そのプレフィックスで始まり、tarが実際にある、最も後に保存されたものを展開し、partial <그열쇠>(プレースホルダーはそのキーです)と終了コード2で終了します。プレフィックスもないか、合うものがなければ、以前と同じくmissと1です。cache-run.shは、プレフィックスを"${KEY%-*}-"で計算して渡し、結果がpartialなら、必ずinstall.shをもう一度実行したあと、完全なキーで保存します(runs.jsonlのresultにpartialが加わります)。ここで危険を再現します。cp -a /root/cache-lab /root/cache-lab-bumpでコピーを作り、コピーのdeps.lockでfast-json 2.1.4をfast-json 2.2.0に変えたあと、seed-repo.shでコピーのリポジトリを埋めてください。コピーでcache-run.shを実行する直前に、プレフィックスによるフォールバックだけを起こして(完全なキーはないので)、そのときのvendor/fast-json/f01.txtに書かれたバージョンを確認し、続いてcache-run.shを最後まで実行して、バージョンが正しく直るかどうかを確認します。結果を/root/cache-lab/reports/stale.txtに4行で書いてください:exact=miss、fallback=<접두 대체로 가져온 열쇠>、before-install=fast-json <그때 판>、after-install=fast-json <끝난 뒤 판>(プレースホルダーは、フォールバックで取得したキー、そのときのバージョン、終了後のバージョンです)。元の/root/cache-labのロックファイルには触れません。/root/cache-lab/build.sh <작업디렉터리>(プレースホルダーは作業ディレクトリです)を作ってください。vendorの下のすべての*.txtのsha256sumを、パス順に集めて<작업디렉터리>/dist/bundle.txtに書き、そのファイルのsha256の先頭16文字を<작업디렉터리>/dist/bundle.idに書きます(( cd vendor && find . -type f -name '*.txt' | LC_ALL=C sort | xargs sha256sum )をそのまま使えば済みます)。時刻や乱数を混ぜないでください。/root/cache-labでcache-run.shとbuild.shを実行してアーティファクトを作ったあと、cp -a /root/cache-lab /root/cache-lab-nocacheでコピーを作り、コピーのcacheディレクトリとvendor・distをまるごと削除してください。コピーで、もう一度cache-run.shとbuild.shを実行します。/root/cache-lab/reports/cache-drop.txtに4行を書いてください:with-cache=<원본의 bundle.id>、without-cache=<사본의 bundle.id>、same=yes、artifact-in-cache=no(プレースホルダーは、元のbundle.idとコピーのbundle.idです)。最後の行は、/root/cache-lab/cacheのすべてのtarをtar tzfで開いて、dist/で始まるエントリが1つもないことを確認した結果です。- まず、事故を再現します。
cp -a /root/cache-lab /root/cache-lab-py313でコピーを作り、コピーのenv/runtime.txtをpython3.13に変えたあと、現在の(ランタイムを知らない)キーでcache-run.shを実行してください。結果はヒットで、vendor/_built-with.txtにはpython3.12が入っているはずです。続いて、/root/cache-lab/cache-key.shを直して、キーの前にOS・アーキテクチャ・ランタイムのバージョンを入れてください:<uname -s 소문자>-<uname -m>-<런타임을 영숫자 외에는 - 로 바꾼 값>-deps-<잠금해시12>(プレースホルダーは、uname -sの小文字、uname -m、英数字以外を-に置き換えたランタイム、ロックファイルのハッシュ12桁です)。(python3.12はpython3-12になります。末尾がロックファイルのハッシュ12桁であるというルールは、そのままです。)直したスクリプトをコピーにもコピーし、コピーでcache-run.shをもう一度実行して、今度はミスして3.13で新しくインストールされることを確認してください。/root/cache-lab/reports/toolver.txtに6行を書いてください:blind-key=、blind-result=、blind-built-with=、versioned-key=、versioned-result=、versioned-built-with=。前の3つはランタイムを知らないキーで実行したとき、あとの3つは直したキーで実行したときの値です(キー・結果・vendorのバージョンの記録)。 /root/cache-labをgitリポジトリにしてください(git init -b main、ユーザー名とメールの設定、vendor/・cache/・dist/・reports/を.gitignoreに入れてコミット)。/root/cache-lab/env/default-branch.txtにmainを1行書きます。/root/cache-lab/cache-policy.sh <작업디렉터리> <브랜치>(プレースホルダーは作業ディレクトリとブランチです)を作ってください。ブランチがdefault-branch.txtの値と同じならsaveと終了コード0、違えばnosaveと2、既定のブランチを読み取れなければunknownで始まる行と1です(ブランチ名をスクリプトに埋め込まないでください)。cache-key.shは、キーの先頭にブランチを加えます:<브랜치를 영숫자 외에는 - 로 바꾼 값>-<uname -s 소문자>-<uname -m>-<런타임>-deps-<잠금해시12>(プレースホルダーは、英数字以外を-に置き換えたブランチ、uname -sの小文字、uname -m、ランタイム、ロックファイルのハッシュ12桁です)。2つ目の引数でブランチを与えると、そのブランチのキーを計算します。cache-run.shは、読み取りを広く(自分のブランチのキー → 自分のブランチのプレフィックス → 既定のブランチのキー → 既定のブランチのプレフィックス)、書き込みを狭く(cache-policy.shがsaveのときだけ)します。runs.jsonlの行に"saved":"yes|no"を加えます。確認はコピーで行います。cp -a /root/cache-lab /root/cache-lab-feature、その中でgit checkout -b feature/spike、deps.lockの末尾にmd5-lite 0.4.0を加えて、seed-repo.shでリポジトリを埋めたあと、cache-run.shを実行してください。/root/cache-lab/reports/branch.txtに5行を書いてください:branch=feature/spike、key=<사본의 열쇠>、result=<그 실행의 결과>、policy=nosave、feature-tarballs=<사본의 cache 에 생긴 feature- 로 시작하는 tar 개수>(プレースホルダーは、コピーのキー、その実行の結果、コピーのcacheにできたfeature-で始まるtarの個数です)。/root/cache-lab/cache-report.sh <작업디렉터리>(プレースホルダーは作業ディレクトリです)を作ってください。<작업디렉터리>/reports/runs.jsonlとreports/cold.jsonを読んで、<작업디렉터리>/reports/summary.jsonを書き、画面にも出力します。フィールドは6つです:runs(行数)、hits・partial・misses(resultごとの行数)、hit_rate(hits × 100 ÷ runsを切り捨てた整数)、saved_ms(resultがhitの行ごとに、cold.jsonのmsからその行のmsを引いた値を足したもの。負の数は0にします)。行が1つもなければ、hit_rateは0です。そして/root/cache-lab/cache-trust.sh <작업디렉터리>を作ってください。deps.lock・vendor・env/runtime.txtのどれか1つでもなければERRORで始まる行と終了コード1、vendor/_built-with.txtがないか、env/runtime.txtと違っているか、ロックファイルのどのパッケージのバージョンがvendor/<이름>/f01.txt(プレースホルダーは名前です)のバージョンと違っていればREBUILDで始まる行と2、すべて合っていればTRUSTと0です。最後に、/root/cache-labで2つのスクリプトを実行して、/root/cache-lab/reports/summary.jsonを残してください。
参考
- このラボのPodでは、コンテナを起動できません(seccompがユーザーの名前空間の作成を防いでいます)。
podman run・podman build・buildah・unshare -Uは使わないでください。使えるのは、GNU tar・gzip・sha256sum・git・python3・jq・openssl・coreutilsです。yq・make・go・bcはありません。 - インターネットがないので、本物のパッケージのインストールはできません。そのため、依存関係のインストールは、Podの中に作ったローカルの「パッケージリポジトリ」ディレクトリからコピーし、
sleepでダウンロードのコストを擬似的に再現します。擬似的ではありますが、測る値(ファイル数・バイト数・ミリ秒)と、学ぶルールは、本物と同じです。 - すべてのスクリプトは、最初の引数として作業ディレクトリを受け取るように作ってください。ステップ4・5・6・7が、
cp -aでコピーを作って、同じスクリプトを別の状態で実行します。 - よくあるミスは、復元スクリプトに
set -eを設定して、「ミス」でスクリプトが先に終了してしまうことです。ミスはエラーではなく、正常な結果です。 - よくあるミスは、プレフィックスによるフォールバックで部分復元しておきながら、インストールを省略してしまうことです。その瞬間、vendorには、ロックファイルが要求しないバージョンが入っています。
- よくあるミスは、キャッシュにアーティファクト(dist)も一緒に入れてしまうことです。キャッシュは期限切れになり、空になるのが正常なので、その中にデプロイしたファイルがあると、期限切れがそのまま事故になります。
- GitLab · Caching in CI/CD・GitHub Actions · Caching dependencies・Docker · Build cache
キャッシュなしで一度そろえるのに、どれだけかかるのか
/root/cache-labの下に、ラボの土台を作ってください。(1) /root/cache-lab/env/runtime.txtにpython3.12を1行。(2) /root/cache-lab/deps.lockに<이름> <판>(プレースホルダーは名前とバージョンです)の6行を、この順序で書きます: left-pad 1.3.0、fast-json 2.1.4、tiny-http 0.9.2、color-util 4.0.1、date-fmt 3.2.2、crypto-lite 1.0.7。(3) /root/cache-lab/seed-repo.sh <작업디렉터리>(プレースホルダーは作業ディレクトリです)は、ロックファイルを読んで、パッケージごとに<작업디렉터리>/pkgrepo/<이름>/<판>/f01.txtからf12.txtまで12個を作り、各ファイルには<이름> <판> <번호두자리>(プレースホルダーは、名前、バージョン、2桁の番号です)の1行だけを書きます。(4) /root/cache-lab/install.sh <작업디렉터리>は、ロックファイルを読んで、パッケージごとにsleep 0.05でダウンロードのコストを擬似的に再現したあと、pkgrepo/<이름>/<판>をvendor/<이름>へコピーし、最後にvendor/_built-with.txtにenv/runtime.txtの内容を書いて、installed=<개수> ms=<밀리초>(プレースホルダーは個数とミリ秒です)の1行を出力します(vendorは毎回新しく作ります)。(5) 2つのスクリプトを順に実行したあと、/root/cache-lab/reports/cold.jsonにfiles・bytes・msの3つの数値を書いてください。filesはfind /root/cache-lab/vendor -type f | wc -l、bytesはそれらのファイルのサイズの合計、msはinstall.shが知らせた値です。
インターネットがないので、本物のダウンロードはできません。リポジトリも私たちが作ったディレクトリで、コストはsleepで擬似的に再現するだけです。それでも、測る値(ファイル数・バイト数・ミリ秒)は本物です。2つのスクリプトとも、最初の引数として作業ディレクトリを受け取るように作ってください。あとのステップで、このラボのディレクトリのコピーを作って、同じスクリプトを実行します。seq -w 1 12が、01から12までを2桁で数えてくれます。ファイルサイズの合計は、find ... -printf '%s\n'とawkで足せます(このPodにはbcがありません)。
保存して、削除して、復元してみる
キャッシュの保存・復元の骨格を作ってください。/root/cache-lab/cache-save.sh <작업디렉터리> <열쇠>(プレースホルダーは作業ディレクトリとキーです)は、vendorディレクトリを<작업디렉터리>/cache/<열쇠>.tar.gzにアーカイブし(tar czf ... -C <작업디렉터리> vendor)、<작업디렉터리>/cache/index.txtに<시각><탭><열쇠>(プレースホルダーは、時刻、キーで、間はタブです)の1行を追記したあと、saved <열쇠>を出力します。/root/cache-lab/cache-restore.sh <작업디렉터리> <열쇠>は、そのtarがあればvendorを削除して展開したあと、hit <열쇠>と終了コード0で、なければvendorに触れずにmiss <열쇠>と終了コード1で終了します。続いて、キーmanual-v1で保存し、vendorをまるごと削除し、同じキーで復元してください。復元の前後のフィンガープリントを、/root/cache-lab/reports/roundtrip.txtにbefore=<지문>・after=<지문>・same=yes(プレースホルダーはフィンガープリントです)の3行で書きます。フィンガープリントはcd /root/cache-lab/vendor && find . -type f -name '*.txt' | LC_ALL=C sort | xargs sha256sum | sha256sum | cut -c1-16で計算します。
キャッシュは、tar1つの塊として扱うほうが適切です。数千のファイルを1つずつ移すより速く、権限や空のディレクトリも一緒に保存されます。復元がミスしたときに、vendorを削除してはいけません。ミスはエラーではなく正常な結果で、そのときにすることはインストールであって、破壊ではありません。終了コードで、ヒットとミスを区別できるようにしてください。あとのステップで、この2つのコードの上に3つ目の場合を載せます。set -eを設定すると、ミスでスクリプトが先に終了します。
キーをロックファイルのハッシュで作る
/root/cache-lab/cache-key.sh <작업디렉터리>(プレースホルダーは作業ディレクトリです)を作ってください。出力は1行で、deps-<잠금해시12자>(プレースホルダーはロックファイルのハッシュ12桁です)の形です。ロックファイルのハッシュはsha256sum < <작업디렉터리>/deps.lock | cut -c1-12です。キーには/が入ってはいけません(ファイル名になります)。必ずロックファイルのハッシュ12桁で終わる必要があります。あとのステップで、前のほうに項目をさらに付け足すからです。続いて/root/cache-lab/cache-run.sh <작업디렉터리>を作ってください。キーを作って復元を試み、ヒットしたらそのまま終了し、ミスしたらinstall.shを実行したあと、そのキーで保存します。そして<작업디렉터리>/reports/runs.jsonlに{"key":"<열쇠>","result":"hit|miss","ms":<밀리초>}(プレースホルダーは、キーとミリ秒です)の1行を追記し、画面には<결과> <열쇠> <밀리초>ms(プレースホルダーは、結果、キー、ミリ秒です)を出力します。vendorとキャッシュを削除した状態でcache-run.shを2回実行し、1回目がミスして2回目がヒットすることを確認したあと、/root/cache-lab/reports/keys.txtに4行を書いてください: run1=<첫 결과>、run2=<두 번째 결과>、lock-sha12=<지금 잠금 해시 12자>、そしてfast-jsonを2.1.4から2.2.0に変えたと仮定したときのロックファイルのハッシュ12桁をbumped-lock-sha12=<값>として書きます(プレースホルダーは、1回目の結果、2回目の結果、現在のロックファイルのハッシュ12桁、値です。ロックファイル自体は変えず、計算だけを行います)。
キーは、「このキャッシュが何を含んでいるのか」を1つの文字列に要約したものです。ロックファイルはバージョンまで固定された一覧なので、その要約として適しています。逆に、package.jsonのように範囲(^1.2)が書かれたファイルをハッシュすると、内容が同じでも、実際にインストールされるものが変わってしまいます。変わらないファイルをハッシュに入れるとキーがむだに頻繁に変わり、変わるファイルを抜くと古いキャッシュがヒットし続けます。2つ目の値は、sed 's/fast-json 2.1.4/fast-json 2.2.0/' deps.lock | sha256sumのように、ファイルを直さずに計算できます。
完全に一致するキーがなければ、最も近いものを持ってくる
cache-restore.shに、3つ目の引数<접두>(プレースホルダーはプレフィックスです)を追加してください。完全に一致するキーがなく、プレフィックスが与えられていたら、cache/index.txtに記録されたキーのうち、そのプレフィックスで始まり、tarが実際にある、最も後に保存されたものを展開し、partial <그열쇠>(プレースホルダーはそのキーです)と終了コード2で終了します。プレフィックスもないか、合うものがなければ、以前と同じくmissと1です。cache-run.shは、プレフィックスを"${KEY%-*}-"で計算して渡し、結果がpartialなら、必ずinstall.shをもう一度実行したあと、完全なキーで保存します(runs.jsonlのresultにpartialが加わります)。ここで危険を再現します。cp -a /root/cache-lab /root/cache-lab-bumpでコピーを作り、コピーのdeps.lockでfast-json 2.1.4をfast-json 2.2.0に変えたあと、seed-repo.shでコピーのリポジトリを埋めてください。コピーでcache-run.shを実行する直前に、プレフィックスによるフォールバックだけを起こして(完全なキーはないので)、そのときのvendor/fast-json/f01.txtに書かれたバージョンを確認し、続いてcache-run.shを最後まで実行して、バージョンが正しく直るかどうかを確認します。結果を/root/cache-lab/reports/stale.txtに4行で書いてください: exact=miss、fallback=<접두 대체로 가져온 열쇠>、before-install=fast-json <그때 판>、after-install=fast-json <끝난 뒤 판>(プレースホルダーは、フォールバックで取得したキー、そのときのバージョン、終了後のバージョンです)。元の/root/cache-labのロックファイルには触れません。
GitHub Actionsのrestore-keys、GitLabのfallback_keysがしていることが、これです。キーが<접두>-<해시>(プレースホルダーはプレフィックスとハッシュです)の形なら、プレフィックスだけを残して、最も近いキャッシュを探せます。そのため、キーがハッシュで終わる必要があったのです。部分復元はただの得に見えますが、その瞬間、vendorにはロックファイルが要求していないバージョンが入っています。ここでインストールを省略すると、そのバージョンがそのままビルドに混ざり、キャッシュが消える日に結果が変わります。コピーで作業する理由は、元のロックファイルを守るためです。スクリプトが最初の引数として作業ディレクトリを受け取るようにしておいたことが、ここで役に立ちます。
キャッシュをまるごと削除しても、結果は同じでなければならない
/root/cache-lab/build.sh <작업디렉터리>(プレースホルダーは作業ディレクトリです)を作ってください。vendorの下のすべての*.txtのsha256sumを、パス順に集めて<작업디렉터리>/dist/bundle.txtに書き、そのファイルのsha256の先頭16文字を<작업디렉터리>/dist/bundle.idに書きます(( cd vendor && find . -type f -name '*.txt' | LC_ALL=C sort | xargs sha256sum )をそのまま使えば済みます)。時刻や乱数を混ぜないでください。/root/cache-labでcache-run.shとbuild.shを実行してアーティファクトを作ったあと、cp -a /root/cache-lab /root/cache-lab-nocacheでコピーを作り、コピーのcacheディレクトリとvendor・distをまるごと削除してください。コピーで、もう一度cache-run.shとbuild.shを実行します。/root/cache-lab/reports/cache-drop.txtに4行を書いてください: with-cache=<원본의 bundle.id>、without-cache=<사본의 bundle.id>、same=yes、artifact-in-cache=no(プレースホルダーは、元のbundle.idとコピーのbundle.idです)。最後の行は、/root/cache-lab/cacheのすべてのtarをtar tzfで開いて、dist/で始まるエントリが1つもないことを確認した結果です。
キャッシュとアーティファクトは、削除したときに起きることで分かれます。キャッシュは、なくても作り直せる必要があり、結果が変わるなら、それはキャッシュではなく隠れた入力です。アーティファクトは、なくなると作り直せないか(ビルドマシンが消えた)、作り直してはいけないもの(すでにデプロイしたそのファイル)です。そのため、キャッシュにアーティファクトを入れてはいけません。キャッシュは期限切れになり、空になるのが正常なのに、その中にデプロイしたファイルがあると、期限切れがそのまま事故になります。ビルドが決定的でなければ、この比較は成り立ちません。タイムスタンプが1行混ざると、2つの結果は常に異なります。
ランタイムのバージョンを上げたのに、古いバージョンでインストールされたキャッシュがついてきた
まず、事故を再現します。cp -a /root/cache-lab /root/cache-lab-py313でコピーを作り、コピーのenv/runtime.txtをpython3.13に変えたあと、現在の(ランタイムを知らない)キーでcache-run.shを実行してください。結果はヒットで、vendor/_built-with.txtにはpython3.12が入っているはずです。続いて、/root/cache-lab/cache-key.shを直して、キーの前にOS・アーキテクチャ・ランタイムのバージョンを入れてください: <uname -s 소문자>-<uname -m>-<런타임을 영숫자 외에는 - 로 바꾼 값>-deps-<잠금해시12>(プレースホルダーは、uname -sの小文字、uname -m、英数字以外を-に置き換えたランタイム、ロックファイルのハッシュ12桁です)。(python3.12はpython3-12になります。末尾がロックファイルのハッシュ12桁であるというルールは、そのままです。)直したスクリプトをコピーにもコピーし、コピーでcache-run.shをもう一度実行して、今度はミスして3.13で新しくインストールされることを確認してください。/root/cache-lab/reports/toolver.txtに6行を書いてください: blind-key=、blind-result=、blind-built-with=、versioned-key=、versioned-result=、versioned-built-with=。前の3つはランタイムを知らないキーで実行したとき、あとの3つは直したキーで実行したときの値です(キー・結果・vendorのバージョンの記録)。
キャッシュキーは、「このキャッシュを作り出したすべての入力」を要約している必要があります。依存関係の一覧だけを入れると、同じ一覧でも、別のランタイム・別のディストリビューションで作られたバイナリがそのままついてきます。そしてそれは、インストールが成功してしまうので、誰も気づきません。そのため、どのCIのドキュメントでも、例のキーは${{ runner.os }}-node-...のように始まります。人がキーを読んで、「このキャッシュがどこで作られたのか」がわかる必要があります。英数字以外の文字を置き換えるには、tr -c 'A-Za-z0-9' '-'が便利です。余って残る末尾のハイフンは、sed 's/-*$//'で整えます。
短命に終わったブランチが、みんなのキャッシュを汚染する
/root/cache-labをgitリポジトリにしてください(git init -b main、ユーザー名とメールの設定、vendor/・cache/・dist/・reports/を.gitignoreに入れてコミット)。/root/cache-lab/env/default-branch.txtにmainを1行書きます。/root/cache-lab/cache-policy.sh <작업디렉터리> <브랜치>(プレースホルダーは作業ディレクトリとブランチです)を作ってください。ブランチがdefault-branch.txtの値と同じならsaveと終了コード0、違えばnosaveと2、既定のブランチを読み取れなければunknownで始まる行と1です(ブランチ名をスクリプトに埋め込まないでください)。cache-key.shは、キーの先頭にブランチを加えます: <브랜치를 영숫자 외에는 - 로 바꾼 값>-<uname -s 소문자>-<uname -m>-<런타임>-deps-<잠금해시12>(プレースホルダーは、英数字以外を-に置き換えたブランチ、uname -sの小文字、uname -m、ランタイム、ロックファイルのハッシュ12桁です)。2つ目の引数でブランチを与えると、そのブランチのキーを計算します。cache-run.shは、読み取りを広く(自分のブランチのキー → 自分のブランチのプレフィックス → 既定のブランチのキー → 既定のブランチのプレフィックス)、書き込みを狭く(cache-policy.shがsaveのときだけ)します。runs.jsonlの行に"saved":"yes|no"を加えます。確認はコピーで行います。cp -a /root/cache-lab /root/cache-lab-feature、その中でgit checkout -b feature/spike、deps.lockの末尾にmd5-lite 0.4.0を加えて、seed-repo.shでリポジトリを埋めたあと、cache-run.shを実行してください。/root/cache-lab/reports/branch.txtに5行を書いてください: branch=feature/spike、key=<사본의 열쇠>、result=<그 실행의 결과>、policy=nosave、feature-tarballs=<사본의 cache 에 생긴 feature- 로 시작하는 tar 개수>(プレースホルダーは、コピーのキー、その実行の結果、コピーのcacheにできたfeature-で始まるtarの個数です)。
どのブランチでもキャッシュを保存できるようにしておくと、実験して削除したブランチが残した依存関係が、既定のブランチのビルドに染み込みます。元に戻す方法もありません。そのブランチは、すでにないからです。そのため、実務のルールは「読み取りは誰でも、書き込みは既定のブランチだけ」です。ブランチ名には/がよく入りますが、キーはファイル名になります。置き換えないと、cache/feature/spike-...tar.gzのようにディレクトリがもう1つできたり、保存そのものが失敗したりします。git -C <경로> rev-parse --abbrev-ref HEADが、現在のブランチを教えてくれます(プレースホルダーはパスです)。gitリポジトリではない場合もあるので、失敗したときの備えを入れてください。
このキャッシュを信頼してよいかを、ゲートにする
/root/cache-lab/cache-report.sh <작업디렉터리>(プレースホルダーは作業ディレクトリです)を作ってください。<작업디렉터리>/reports/runs.jsonlとreports/cold.jsonを読んで、<작업디렉터리>/reports/summary.jsonを書き、画面にも出力します。フィールドは6つです: runs(行数)、hits・partial・misses(resultごとの行数)、hit_rate(hits × 100 ÷ runsを切り捨てた整数)、saved_ms(resultがhitの行ごとに、cold.jsonのmsからその行のmsを引いた値を足したもの。負の数は0にします)。行が1つもなければ、hit_rateは0です。そして/root/cache-lab/cache-trust.sh <작업디렉터리>を作ってください。deps.lock・vendor・env/runtime.txtのどれか1つでもなければERRORで始まる行と終了コード1、vendor/_built-with.txtがないか、env/runtime.txtと違っているか、ロックファイルのどのパッケージのバージョンがvendor/<이름>/f01.txt(プレースホルダーは名前です)のバージョンと違っていればREBUILDで始まる行と2、すべて合っていればTRUSTと0です。最後に、/root/cache-labで2つのスクリプトを実行して、/root/cache-lab/reports/summary.jsonを残してください。
ヒット率だけでは、何も決められません。100%ヒットするキャッシュが、100%間違った内容を呼び戻すこともあります。それが前のステップで見たことです。そのため、レポートの隣に「信頼してよいか」を別に置きます。ゲートの判定の根拠は、すべて今ディスクにある状態でなければなりません。前回の実行が何と書き残したかではなく、今vendorに何が入っているかを見ます。jq -sは、複数行のJSONを1つの配列として読んでくれます。割り算の結果を整数にするには、| floorを使います。空の配列にaddを使うとnullが出るので、+ [0]で防ぎます。