チュートリアル
pnpm exec は cd を無視する。wrangler が本番サイトを古いビルドで上書きした
· 読了まで約9分
目次
2026-08-19 の朝、このサイトの記事4本が 404 になった。消したわけでも DNS を触ったわけでもなく、その4本を含むビルドは手元の dist/ に正しく残っていた。やったのは、リポジトリの中にある小さな Worker を自分のディレクトリからデプロイすることだけだった。
cd workers/hn-proxy
pnpm exec wrangler deploy
wrangler は成功と表示した。ただしデプロイされたのは Worker ではなくサイト本体で、しかも2日前の dist/ が中身だった。18日と19日に公開した4本はそのビルドに存在しないので、そのまま 404 になった。
操作ミスそのものより、2つのツールがそれぞれ独立にディレクトリを上へ遡っていて、cd に従ったのは片方だけだったという構造のほうが厄介だ。説明を読んでも半信半疑だったので、手元で測った結果を先に置く。
同じリポジトリにデプロイ先が2つある
リポジトリのルートはサイトそのものだ。Astro のビルド結果を Cloudflare Workers の static assets として配信していて、設定はルートに置いてある。
// wrangler.jsonc (リポジトリのルート)
{
"name": "astro-p4ni",
"compatibility_date": "2026-07-27",
"assets": { "directory": "./dist", "not_found_handling": "404-page" },
"routes": [{ "pattern": "astro.p4ni.com", "custom_domain": true }]
}
その隣に workers/ があり、サイトとは無関係の小さな Worker が入っている。GA4 Data API や Search Console から数値を取ってくる自動化の部品だ。事故のときに叩いていた Worker はその後 GitHub Actions に置き換えて消したので、以下は同じ形で残っているほうを例にする。設定はこうなっている。
// workers/gsc-stats/wrangler.jsonc
{
"name": "p4ni-gsc-stats",
"main": "index.js",
"compatibility_date": "2026-08-19",
"workers_dev": true
}
名前もエントリポイントもルートも別で、ファイルの側に曖昧さはない。曖昧なのは、コマンドがどのディレクトリで動くかのほうだ。
pnpm exec は cd した先では動かない
事故の全部がこの表に入っている。使い捨てのディレクトリを作って測った。ルートに package.json を置き、その下に package.json の無い sub/ と、package.json のある sub-pkg/ を作って、それぞれの場所から作業ディレクトリを報告させる。
pnpm exec node -e 'console.log(process.cwd())'
npx --no-install node -e 'console.log(process.cwd())'
pnpm 10.22.0 / npm 11.4.2 / Node 24.4.1 での結果。
| シェルがいる場所 | pnpm exec が動く場所 | npx / npm exec が動く場所 |
|---|---|---|
ルート(package.json あり) | ルート | ルート |
sub/(package.json なし) | ルート | sub/ |
sub-pkg/(package.json あり) | sub-pkg/ | sub-pkg/ |
事故は真ん中の行だ。pnpm exec は最寄りのパッケージ、つまり package.json を持つ直近の親ディレクトリを探し、そこでコマンドを動かす。workers/gsc-stats/ に package.json は無いので、最寄りのパッケージはリポジトリのルートになる。wrangler が起動する前の時点で、プロセスは静かにルートへ引き戻されていた。
npx と npm exec はこれをやらない。最寄りの node_modules/.bin を PATH に足すだけで、作業ディレクトリはそのままだ。もう一つ実測しておくと、pnpm exec は INIT_CWD も設定しない(npx はシェルのディレクトリを入れる)。元いた場所を子プロセスへ運ぶ環境変数が無いので、wrangler が動き出す時点でその情報はどこにも残っていない。
上へ遡る探索が二重にかかる
wrangler は wrangler.jsonc や wrangler.toml を、作業ディレクトリから親へ順にたどって探す。この挙動自体は妥当で、src/ の下から wrangler deploy を叩いてもプロジェクトの設定に当たるのはこのおかげだ。
2つの規則を並べると、事故は勝手に組み上がる。
cd workers/hn-proxyでシェルが移動するpnpm execが最寄りのパッケージ、つまりリポジトリのルートへプロセスを戻す- ルートに立った wrangler が設定を上へ探し、一発目でルートの
wrangler.jsoncを見つける - その設定には「
./distを astro.p4ni.com へ」と書いてある
どちらのツールも文書化されていない動きはしていない。単体で見ればどちらも妥当だ。噛み合わせたときだけ壊れて、その噛み合わせは誰もテストしない。
設定が「無い」なら止まるが、「別のがある」と成功する
設定ファイルが見つからないときの wrangler はうるさい。見つからないと言って止まる。ところが違う設定を掴んだときは静かで、それは wrangler から見て何も異常が無いからだ。妥当な設定を見つけ、そこに書かれた assets のディレクトリを見つけ、そこに書かれた Worker へアップロードした。wrangler が実行できる検査の範囲では、これは完全な成功になる。
唯一の手がかりは、読み飛ばした成功メッセージの中にあった。デプロイ先の Worker 名が p4ni-gsc-stats ではなく astro-p4ni と出ていた。何百回も見たことのある行に混ざった一語の違いが、分析用のエンドポイントを更新するのか、サイト全体を古いビルドで作り直すのかを分けていた。
デプロイ先を間違えたことと同じくらい、中身が古かったことが効いている。dist/ はビルド結果で、git の管理外だ。前回のビルドが残しただけのものが入っている。私の手元にあったのは8月17日のもので、それ以降に公開したものは全部消えた。もし dist/ が空だったらサイトは真っ白になっていたし、逆に直前にビルドしていれば、誤爆したデプロイは同じ内容の再デプロイになってこのバグに気づかないままだった。
どう防ぐか
手が届く順に5つ。
--config を明示する。採用したのはこれで、理由はディレクトリの移動に負けない唯一の方法だからだ。
pnpm exec wrangler deploy --config workers/gsc-stats/wrangler.jsonc
パスはプロセスが最終的に立つ場所からの相対だが、pnpm exec は必ずリポジトリのルートに立つので、ルート基準のパスは安定して効く。リポジトリのどこから叩いても同じ結果になる。
--cwd を渡す。wrangler 4.119 には --cwd がある。「指定したディレクトリで起動したかのように動かす」オプションで、pnpm exec が捨てたディレクトリを入れ直せる。
pnpm exec wrangler --cwd workers/gsc-stats deploy
pnpm exec を使わない。npx wrangler deploy も ./node_modules/.bin/wrangler deploy もシェルのディレクトリを尊重するので、cd が見たとおりの意味になる。引き換えに pnpm の解決から外れるので、その場の一回なら十分だが、チームに配る手順としては弱い。
サブディレクトリに package.json を置く。表の3行目をもう一度見てほしい。sub-pkg/ に package.json があるだけで、pnpm exec は遡るのをやめてそこで動いた。workers/gsc-stats/ に2行の package.json を置けば、cd は誰もが期待する意味になる。将来ワークスペースのパッケージにするつもりがあるなら、こちらのほうが筋がいい。
pnpm -C は黙って外さず、エラーで落ちる。pnpm -C workers/gsc-stats exec ... なら綺麗に解決すると思って試したが、そこにパッケージが無いので ERR_PNPM_RECURSIVE_EXEC_NO_PACKAGE で止まる。これは良い失敗だ。推測せずに拒否している。素の pnpm exec に足りないのはまさにこの性質だった。
リポジトリに入れたガード
このリポジトリのエージェント向け指示書に、ディレクトリ構成の説明の隣へ命令形で書き足した。デプロイには必ず --config を付ける、cd workers/<name> してから pnpm exec wrangler deploy を実行するとサイト本体が古い dist/ で上書きされる、と。仕組みを解説したコメントでは自分は救えなかったと思う。必要だったのは、フラグが最初から入った状態のコマンド行が、コマンドをコピーしにいく場所に置いてあることだった。
同じ形のリポジトリなら、あと2つやっておくといい。デプロイを package.json の scripts に入れてフラグを二度と手で打たないようにすること。それから、サイト本体のデプロイは必ずビルドを先に走らせること。こちらは pnpm run deploy の中身を astro build && wrangler deploy にしてあるので復旧はコマンド1本で済んだし、仮に放置しても予約公開のための日次デプロイが数時間で直していたはずだ。
教訓は pnpm と wrangler の外にも効く。あるツールが文脈を求めて上へ遡り、別のランナーが「自分がいるつもりのディレクトリ」を書き換えているとき、その2つは組み合わせられない。しかも失敗の出方はクラッシュではない。間違った対象についての成功メッセージだ。