zpaper-draft

Type to search...

to open search from anywhere

セルフホストランナーで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のrmdirENOTEMPTYで失敗する。

対策として、各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で定期的にストアを掃除する。キャンセルされたインストールの残骸がストアを腐敗させる
  • 問題は一発で直らないことがある。段階的に修正してフィードバックループを回すしかない