セルフホストランナーでpnpm/action-setupが壊れる問題を3段階で修正した話
概要
Takazudo ModularのサイトのCIがGitHub Actionsのセルフホストランナー上で壊れていた。pnpm/action-setupがクラッシュする問題で、3つのPRに分けて段階的に修正した。1つ直すと次の問題が出てきて、結局3レイヤーの修正が必要だったという話。
背景
Takazudo ModularのウェブサイトはAstroで作っていて、CIはGitHub Actionsで動いている。セルフホストランナーはWSL2上で複数インスタンス(x0x-wsl2, x0x-wsl2-2, x0x-wsl2-3, etc.)が走っている。
この複数のランナーインスタンスが同一マシンの同じ$HOMEディレクトリを共有しているというのがそもそもの問題の根源。
mainブランチへのpush時にはProduction Deploy、Security Checks、Sync main to doc、Sync main to styleguideなど複数のワークフローが同時に走る。これらが全部同じ~/setup-pnpmディレクトリに対してpnpm/action-setupを実行するので、衝突が起きる。
GitHub-hostedランナーでは毎回クリーンな環境が提供されるのでこの問題は起きない。セルフホストランナー固有の問題。
Layer 1: rm -rf ~/setup-pnpmクリーンアップ追加
最初のエラーはこれ。
Error: ENOTEMPTY: directory not empty, rmdir '/home/takazudo/setup-pnpm/node_modules/.bin/store/v10/files/12'
pnpm/action-setupはデフォルトで~/setup-pnpmにpnpmをインストールする。セルフホストランナーでは前回のランの残骸が残っていて、action内部のNode.jsのrmdirがENOTEMPTYで失敗する。
対策として、各pnpm/action-setupの直前にrm -rf ~/setup-pnpmを追加した。全10ワークフローファイル、14箇所。
- name: Clean pnpm setup cache
run: rm -rf ~/setup-pnpm
- uses: pnpm/action-setup@v4
with:
version: 10
Layer 2: || trueが必要
Layer 1の修正をデプロイしたら、今度はrm -rf自体がENOTEMPTYで失敗。
rm: cannot remove '/home/takazudo/setup-pnpm/node_modules/.bin/store/v10/files/cb': Directory not empty
##[error]Process completed with exit code 1.
GitHub Actionsのデフォルトシェルはbash -e(エラーで即終了)なので、rm -rfの失敗がジョブ全体を止めてしまう。
NFSの.nfs*ロックファイルや、他のプロセスがファイルハンドルを保持していると、rm -rfでも削除できない場合がある。rm -rfは万能ではない。
対策として|| trueを付けた。
- name: Clean pnpm setup cache
run: rm -rf ~/setup-pnpm || true
部分的なクリーンアップでも、無いよりマシということで。
Layer 3: pnpm store prune + ユニークなdest:
クリーンアップは通るようになったが、今度は別のエラー。
ERROR Worker pnpm#1 exited with code 1
ERR_PNPM_LINKING_FAILED ENOENT: no such file or directory
Error: ENOENT: process.cwd failed with error no such file or directory
原因は2つある。
pnpmストアの腐敗
セルフホストランナーの永続的なpnpmストア(~/.local/share/pnpm/store)に、前回のキャンセルされたインストールの不完全なエントリが残っている。
ここで整理すると、~/setup-pnpmはpnpmバイナリのインストール先で、~/.local/share/pnpm/storeはパッケージのコンテンツアドレッサブルストア。別物。Layer 1-2で掃除していたのはバイナリ側で、ストア側はまだ腐ったエントリを抱えていた。
対策としてpnpm installの前にpnpm store prune || trueを追加。
concurrent dest conflict
mainへのpush時に複数のワークフローが同時実行され、全部が~/setup-pnpmを共有していた。
concurrencyグループは同一ワークフローの重複を防ぐが、異なるワークフロー間の同時実行は防がない。mainへのpush時にProduction Deploy、Security、Sync doc、Sync styleguideが全部同時に走る。
対策として、ワークフローごとにユニークなdest:を設定した。
# main-deploy.yml
- name: Clean pnpm setup cache
run: rm -rf ~/setup-pnpm ~/setup-pnpm-main || true
- uses: pnpm/action-setup@v4
with:
version: 10
dest: ~/setup-pnpm-main
# security.yml — 別のディレクトリ
- name: Clean pnpm setup cache
run: rm -rf ~/setup-pnpm ~/setup-pnpm-security || true
- uses: pnpm/action-setup@v4
with:
version: 10
dest: ~/setup-pnpm-security
E2Eテストのシャードジョブ(4並列)はmatrix変数を含めて完全に分離した。
dest: ~/setup-pnpm-main-e2e-${{ matrix.shard }}
まとめ
3つのPRに分けて段階的に修正した。各PRで新しい問題が見つかり、次のPRで対処するというイテレーション。
セルフホストランナーは速くて安いが、永続的な環境ゆえの問題がある。エフェメラルなGitHub-hostedランナーでは問題にならない「前回の残骸」が、セルフホストでは毎回のCIに影響する。
整理すると以下。
- CIのクリーンアップコマンドには必ず
|| trueを付ける。bash -eの下ではrm -rfの失敗もジョブ停止になる - 並行実行されるワークフローは共有リソースを分離する。
concurrencyグループでは異なるワークフロー間の衝突は防げない pnpm store pruneで定期的にストアを掃除する。キャンセルされたインストールの残骸がストアを腐敗させる- 問題は一発で直らないことがある。段階的に修正してフィードバックループを回すしかない