# pnpm 12のバージョン固定でpnpm-lock.yamlが2つのドキュメントに分かれる — packageManager・devEngines・GitHubが読めないロックファイル

> pnpm 12はpackageManagerが設定されていると、1つのpnpm-lock.yamlに---で区切った2つのYAMLドキュメントを書き込みます。pmOnFailでGitHubの依存関係グラフを守る方法と、Corepackを外す具体的な手順をまとめます。

- Source: https://oharu121.com/ja/blog/pnpm-12-version-pin-corepack-devengines-lockfile-dependency-graph/
- Published: 2026-10-10T09:07:11+09:00
- Tags: pnpm, GitHub, Corepack

---
## はじめに

このブログは pnpm 11.3.0 で動いていました。pnpm 12 への移行は `package.json` の `packageManager` フィールドを 1 行変えるだけに見えました。インストールは 26ms で終わって `Already up to date` と表示されたのに、`pnpm-lock.yaml` は 158 行増えていました。

増えた行は pnpm 自身を固定する新しい YAML ドキュメントで、pnpm 12 はこれをファイルの先頭に置き、元の依存関係の記述を 2 つ目のドキュメントへ押し出します。**GitHub の依存関係グラフは最初のドキュメントしか読みません**。そのままなら GitHub には pnpm しか見えず、このリポジトリの Dependabot アラートは修正済みとして閉じていたはずです。

対処は **`pmOnFail: ignore`** という設定 1 つで、ロックファイルを 1 つのドキュメントに保てます。代わりに止まるのは、pnpm がピンのバージョンへ自分で切り替える動作です。これはもともと Corepack が担っていた仕事でした。本記事では、このピンが Corepack から pnpm、そして `devEngines` へと移ってきた流れを追い、それでも Corepack を外せる理由と、最終的にピンを `devEngines` に移した経緯を示します。

## ロックファイルに 2 つの YAML ドキュメントが入った

**pnpm 12 のピンがあると、`pnpm-lock.yaml` は pnpm 自身を記録する環境ドキュメントから始まり、依存関係の記述は 2 つ目のドキュメントに移ります**。このブログのロックファイルに増えた 158 行はまさにそれでした。

```yaml title="pnpm-lock.yaml"
---
lockfileVersion: '9.0'

importers:

  .:
    configDependencies: {}
    packageManagerDependencies:
      pnpm:
        specifier: 12.10.1
        version: 12.10.1

packages:

  '@pnpm/exe.android-arm64@12.10.1':
    resolution: {integrity: sha512-…}
    cpu: [arm64]
    os: [android]
  # … プラットフォーム別バイナリごとに 1 エントリが続き、そのあと 2 つ目のドキュメントが始まる
```

条件は [pnpm のロックファイルのドキュメント](https://pnpm.io/lockfile)に書かれています。環境ドキュメントが書かれるのは、プロジェクトが `devEngines.packageManager` をバージョンを問わず宣言したとき、または従来の `packageManager` フィールドで pnpm 12 以降を固定したときです。

このブログの `pnpm@11.3.0` のピンでは**ドキュメントが 1 つ、`pnpm@12.10.1` では 2 つ**になったのはこのためです。

*Figure — TwoDocumentLockfile: GitHub のパーサーは最初のドキュメントで止まります。そこにあるのは pnpm とそのプラットフォーム別バイナリだけで、プロジェクトの依存は 1 つもありません。*

ドキュメントを 1 つだけ読み込む YAML パーサーは、環境ドキュメントしか受け取りません。**GitHub の依存関係グラフもその 1 つです**。

[dependabot/dependabot-core#15904](https://github.com/dependabot/dependabot-core/issues/15904) では、あるリポジトリの SBOM（ソフトウェア部品表）エクスポートが、2 ドキュメント構成のロックファイルがデフォルトブランチに入ってから 1 日で 652 パッケージから 14 に減りました。残った 14 は pnpm、その `@pnpm/exe.*` バイナリ、ワークフローのアクション、そしてリポジトリ自身でした。最初の報告では、依存を何も更新していないのにアラートが閉じています。

> **ちなみに**
>
> **この問題はどちらの側でもまだ直っていません**
>
> dependabot-core 自体のパーサーの修正は 2026-09-02 にマージされましたが、issue はまだ開いています。2026-09-08 と 2026-09-23 の報告でも、修正後の GitHub の依存関係グラフは最初のドキュメントしか読んでいません。pnpm は [pnpm/pnpm#13805](https://github.com/pnpm/pnpm/issues/13805) で、ツール側がすべての YAML ドキュメントを読むべきだと回答しており、形式を変える予定はありません。

ピンを `devEngines.packageManager`（バージョン範囲も指定できる、`package.json` の新しいピンのフィールド）に移しても**避けられません**。依存を 1 つだけ持つ作業用プロジェクトを 3 つ用意すると、結果は次のとおりでした。

| ピン | `pnpm-lock.yaml` のドキュメント区切り |
| --- | --- |
| `"packageManager": "pnpm@12.8.2"` | 2 |
| `"packageManager": "pnpm@12.8.2"` と `pmOnFail: ignore` | 0 |
| `devEngines.packageManager` を `12.8.2` に完全一致で指定 | 2 |

## このブログのアップグレード：package.json の 1 行

**すでに pnpm 11 を使っているリポジトリなら、アップグレードはピンの変更と設定 1 つで済みます**。このブログに必要な設定の移動は pnpm 11 の時点で済んでいて、`allowBuilds` と `overrides` は今回の変更前から `pnpm-workspace.yaml` にありました。

```json title="package.json" del={1} ins={2}
  "packageManager": "pnpm@11.3.0",
  "packageManager": "pnpm@12.10.1",
```

```yaml title="pnpm-workspace.yaml" ins={1}
pmOnFail: ignore
```

[このリポジトリのアドバイザリパッチャー](/ja/blog/dependabot-transitive-pnpm-advisories-github-action-cloud-agent/)は `pnpm audit --json` を解析するので、pnpm が Rust で書き直されても、このコマンドの出力形式が変わらないことが条件でした。試したところ、問題はありませんでした。

アップグレード後、GitHub の SBOM エクスポートには **621 パッケージが並び、`@pnpm/exe` のバイナリは 1 つもありませんでした**。`pmOnFail: ignore` が効いているということです。GitHub が最初のドキュメントしか読まないのは変わりませんが、ロックファイルには依存のドキュメントしか残っていないので、依存はすべて読まれます。環境ドキュメントが残っていれば、一覧は pnpm と `@pnpm/exe` のバイナリだけになっていたはずです。

## pnpm 12 で変わること

pnpm 12 は pnpm を Rust で書き直したもので、pnpm 11 のコマンド、設定、ロックファイル形式をそのまま引き継いでいます。[リリース記事](https://pnpm.io/blog/releases/12.0)には "Upgrading should not feel like a migration" とあります。

公式の[ベンチマーク](https://pnpm.io/benchmarks)では、小さなパッケージを大量に含むテストプロジェクトで、クリーンインストールが pnpm 11 の 7.36 秒から 4.29 秒に短縮されています。キャッシュ、ロックファイル、`node_modules` がすべてそろった状態での再インストールは 396ms から 15ms です。また、pnpm は 12.4.0 から Python パッケージも管理できます（`pnpm-workspace.yaml` で `python.enabled: true` を設定）。ただし[公式ドキュメント](https://pnpm.io/python)では実験的機能とされています。

公開は 2026-08-26 で、このブログが移行した時点の最新は 12.10.1 でした。主な破壊的変更は次のとおりです。

- `pnpm install --frozen-lockfile false` は受け付けられなくなりました。`--no-frozen-lockfile` と書きます。
- `pnpm-workspace.yaml` にサポートされていないキーがあると、`ERR_PNPM_UNRECOGNIZED_WORKSPACE_SETTINGS` で失敗します。
- Git ホストから取得する依存（`github:owner/repo` や `git+ssh` の書き方）は、常にホストの HTTPS アドレス経由で解決されるようになり、ロックファイルに SSH アドレスが記録されなくなりました。

バイナリの形も変わりました。pnpm 12 は、インストール後は **Node.js を必要としないネイティブ実行ファイル**です。ただし npm 経由でインストールする場合は、今も Node.js 22.13 以降が必要です。

大きな移行だったのは pnpm 11 のほうです。pnpm 11 は設定を `package.json` と `.npmrc` から移し、`onlyBuiltDependencies` を `allowBuilds` に置き換えました。pnpm 10 から直接上がるリポジトリはこれらを一度に踏むので、記事の最後に落とし穴としてまとめています。

## pnpm-lock.yaml を 1 つのドキュメントに保つ

**`pmOnFail: ignore` にすると、pnpm はロックファイルに自分自身を記録しなくなります**。[`pmOnFail`](https://pnpm.io/settings/cli#pmonfail) は pnpm 11 で入った設定で、実行中の pnpm がピンのバージョンと違うときの動作を決めます。値は `download`（デフォルト）、`error`、`warn`、`ignore` の 4 つです。`ignore` では pnpm がピンに従って動かなくなるので、記録するものもなくなります。

**ドキュメントを 1 つに保てる値は `ignore` だけです**。名前にある「fail」は、実行中の pnpm とピンのバージョンが一致しないことを指します。ただし、バージョンが一致していても、`pmOnFail` が `ignore` でなければ環境ドキュメントは書き込まれます。スタンドアロンの pnpm 12.10.1 を、`pnpm@12.8.2` に固定した作業用プロジェクトで実行した結果は次のとおりです。一致するときの列は、`pnpm@12.10.1` に固定して実行した結果です。

| `pmOnFail` | 実行中の pnpm がピンと違うとき | バージョンが違うときの `---` 区切りの数 | 一致するときの `---` 区切りの数 |
| --- | --- | --- | --- |
| `download`（デフォルト） | 12.8.2 に切り替わる | 2 | 2 |
| `error` | `ERR_PNPM_BAD_PM_VERSION` で失敗する | —（インストール失敗） | 2 |
| `warn` | `[WARN] This project is configured to use 12.8.2 of pnpm. Your current pnpm is v12.10.1` を表示して 12.10.1 で実行する | 2 | 2 |
| `ignore` | 何も表示せずに 12.10.1 で実行する | 0 | 0 |

ロックファイルにすでに環境ドキュメントがある状態でこの設定を足しても、**環境ドキュメントは消えません**。`pnpm install` は `Already up to date` と表示し、ロックファイルには 2 つのドキュメントが残ったままでした。ピンを変える前のロックファイルを取り戻してから、インストールし直すことで解消しました。

```bash
git checkout main -- pnpm-lock.yaml && pnpm install
# Already up to date
# Done in 34ms using pnpm v12.10.1
```

```bash
grep -c '^---$' pnpm-lock.yaml
# 0
```

代償として、pnpm はバージョンを自動で切り替えられなくなります。`pnpm@12.8.2` に固定したプロジェクトでスタンドアロンの pnpm 12.10.1 を実行すると、`pmOnFail: ignore` を設定する前はピンのバージョンを、設定した後は自分自身のバージョン 12.10.1 を返します。

```bash title="before pmOnFail: ignore"
pnpm --version
# 12.8.2
```

```bash title="after pmOnFail: ignore"
pnpm --version
# 12.10.1
```

**`ignore` では、`PATH` 上にある pnpm がそのまま実行されます**。ピンが効くのは、pnpm 以外の何かがピンを読む場所だけです。たとえば CI では `pnpm/action-setup` がピンのバージョンをインストールします。

## ピンを読むのは誰か：Corepack、pnpm、そして devEngines

Corepack は Node.js 16.9 から 24 に同梱されていた小さなツールで、「プロジェクトごとにどのバージョンのパッケージマネージャーを使うか」という問題を解決するためのものです。プロジェクトが宣言したバージョンを読み、対応する Yarn や pnpm を自動でダウンロードして実行するので、事前にインストールしておく必要がありません。

`packageManager` はもともと Corepack のフィールドで、後から pnpm も自分で読むようになりました。`devEngines.packageManager` は、pnpm が現在推奨している、ツールをまたいで使えるフィールドです。どのツールがどのフィールドに従って動くのかがわかって初めて、`pmOnFail: ignore` の代償が見えてきます。

*Figure — PinTimeline: ピンは Corepack のフィールドとして始まりました。切り替えは pnpm が引き継ぎ、`devEngines` がツールをまたいで使える新しい置き場所になっています。*

Corepack は 2021 年 9 月に Node.js 16.9.0 に同梱され、`packageManager` フィールドも一緒に登場しました。値は `pnpm@x.y.z` の完全一致で、後ろにハッシュを付けることもできます。Corepack の `pnpm` シムがこのフィールドを読み、一致するリリースを取得します。

pnpm 9.7.0 は 2024 年 8 月に `manage-package-manager-versions` を追加し、Corepack なしでも同じ切り替えができるようにしました。pnpm 10.0.0 は 2025 年 1 月にこれをデフォルトで有効にしました。

`devEngines` は、OpenJS Foundation のパッケージメタデータ相互運用に関する議論から生まれました。npm は 2024 年 10 月に 10.9.0 で実装し、pnpm 11.0.0 は 2026 年 4 月に `devEngines.packageManager` を追加しました。バージョン範囲と、フィールドごとの `onFail` ポリシーに対応しています。

pnpm のドキュメントは現在 `packageManager` を**従来のフィールド**と呼んでいます。pnpm 11.23.0 以降の `pnpm init` は両方を書き出します。`devEngines.packageManager` と、Corepack 向けに残した完全一致の `packageManager` です。

*Figure — PinReaders: ピンの読み方はツールごとに少しずつ違います。`onFail` か `pmOnFail` を `ignore` にしない限り、どのフィールドで固定しても環境ドキュメントが書き込まれ、GitHub の依存関係グラフには依存が見えなくなります。*

このマシンで確かめた結果です。

- 素の pnpm 11.3.0 バイナリは、`pmOnFail: ignore` で止めるまで `packageManager` のバージョンに自分で切り替わりました。
- Corepack 0.34.6 は、完全一致の `devEngines.packageManager` しか持たない `package.json` から pnpm 12.8.2 を実行しました。
- 同じ Corepack は**範囲指定を拒否しました**。`Invalid package manager specification in package.json (pnpm@^12.8.0); expected a semver version` です。

残りはソースコードから確認しました。`pnpm/action-setup` v6.1.0 は、`version` 入力、`devEngines.packageManager`、`packageManager` の順に見ます。Dependabot は実行する pnpm を決めるために両方のフィールドを読み、`packageManager` を優先し、どちらも更新しません（[#4830](https://github.com/dependabot/dependabot-core/issues/4830)、[#13722](https://github.com/dependabot/dependabot-core/issues/13722)）。

### ランタイム、パッケージマネージャー、依存パッケージ：3 つの層と 3 つのピン

**ランタイム、パッケージマネージャー、依存パッケージは 3 つの層で、それぞれ別のものが固定しています**。ロックファイルを読むのは pnpm で、pnpm は Node.js の上で動きます。ロックファイルが解析される時点では、この 2 つはもう決まっています。

*Figure — VersionLayers: ロックファイルは自分を解析するプログラムを選べません。バージョンのピンが package.json にあるのはそのためです。*

このブログがローカルマシンで固定していないのは、ランタイムの層です。`"engines": { "node": ">=24" }` は確認するだけで、`>=24` は **Node.js 26 も通します**。ローカルの `pnpm build` も、Node.js 26 で動いていることを警告していました。

```text
00:30:29 [WARN] [@astrojs/vercel]
	The local Node.js version (26) is not supported by Vercel Serverless Functions.
	Your project will use Node.js 24 as the runtime instead.
	Consider switching your local version to 24.
```

CI では `actions/setup-node` で Node.js を固定しています。ローカルマシンで同じバージョンにそろえるには、mise、asdf、nvm のようにバージョンファイルを読むツールが必要です。そもそも Corepack は、Node.js のバージョンを管理するものではありません。

pnpm 12 の環境ドキュメントは、ロックファイルが 1 つ上の層に手を伸ばして pnpm 自身を固定したものです。GitHub の依存関係グラフは、まだそこまで追いついていません。

## Corepack を外す

**pnpm を使うなら、Corepack はもう不要です**。

1. pnpm は 10.0.0 からデフォルトでピンのバージョンに自分で切り替えます。
2. Corepack は `devEngines` が想定しているバージョン範囲を受け付けません。
3. Node.js は、2025 年 3 月の [TSC の投票](https://github.com/nodejs/TSC/pull/1697)を経て、2025-10-15 リリースの 25.0.0 で Corepack の同梱をやめました。

Corepack 自体のメンテナンスは続いています。npm で公開されていて、2026-08-28 時点で 0.36.0 です。メンテナーは、少なくとも Node.js 24 のサポート終了までは保守すると表明しています。Node.js 25 以降では、別途インストールする必要があります。

このマシンでは、`pnpm` コマンドは**実は最初から Corepack のシムでした**。シムとは同じ `pnpm` という名前の小さなプログラムで、実行するとプロジェクトのピンを読み、対応するバージョンの pnpm をダウンロードしてから、そのバージョンにコマンドを渡します。次の `ls` の結果から、`pnpm` が Corepack 内のファイルを指していることがわかります。

```bash
ls -la $(which pnpm)
# lrwxr-xr-x  1 root  wheel  41 Mar  3  2026 /usr/local/bin/pnpm -> ../lib/node_modules/corepack/dist/pnpm.js
```

ブログのディレクトリでは `pnpm --version` が 12.10.1 を、ホームディレクトリでは 11.3.0 を返していたのは、このシムのせいです。macOS では、次の 3 ステップで置き換えました。

1. ピンのバージョンで pnpm そのものをインストールします。スタンドアロンのスクリプトは `PNPM_VERSION` を読み、pnpm 12 のネイティブバイナリ（40 MB の arm64 実行ファイル）を `~/Library/pnpm` に置きます。

   ```bash
   curl -fsSL https://get.pnpm.io/install.sh | PNPM_VERSION=12.10.1 sh -
   ```

   スクリプトは最後に、自分では置き換えられないシムを名指しする警告を出して終わります。

   ```text
   Warning: "pnpm" on PATH is /usr/local/bin/pnpm (a Corepack shim), which comes before ~/Library/pnpm/bin.
   Your shell keeps running that pnpm, not the one pnpm installed to ~/Library/pnpm/bin.
   To finish switching, run "corepack disable pnpm" or move ~/Library/pnpm/bin ahead of /usr/local/bin in PATH.
   ```

2. シムを削除します。**`/usr/local` にグローバルインストールした Corepack は、シムを root の所有にします**。そのため `corepack disable pnpm` には `sudo` が必要です。

   ```bash out={2}
   corepack disable pnpm
   Internal Error: EACCES: permission denied, unlink '/usr/local/bin/pnpm'
   ```

   `sudo corepack disable pnpm` で `pnpm` と `pnpx` が消え、Corepack の `yarn` のシムはそのまま残りました。

3. シェルがどの pnpm を見つけるか確認します。

   ```bash out={2-3}
   which pnpm && pnpm --version
   ~/Library/pnpm/bin/pnpm
   12.10.1
   ```

> **気を付けて**
>
> **Corepack なしでは `ignore` は何も強制しない**
>
> pnpm はインストールされているバージョンで動き、ピンと違っていても何も表示しません。ピンのバージョンをインストールするのは、CI の `pnpm/action-setup` だけになります。

ステップ 3 で表示された 12.10.1 がこのリポジトリのピンと一致しているのは、インストール時に `PNPM_VERSION=12.10.1` を指定したからにすぎません。`pmOnFail: ignore` の下では pnpm は自分で切り替えないので、あとでピンを新しいバージョンに変えても、マシンの pnpm は何の表示もなく 12.10.1 のままです。

**`pnpm self-update <version>` を使えば、この 2 つを一致させたまま保てます**。プロジェクト内で実行すると `packageManager` を書き換え、インストール済みの pnpm も同じバージョンに更新します。`pmOnFail: ignore` を設定して `pnpm@12.10.1` に固定したプロジェクトで 12.10.0 に変えたときの出力は次のとおりで、ロックファイルは 1 つのドキュメントのままでした。

```bash
pnpm self-update 12.10.0
# The current project has been updated to use pnpm v12.10.0
# Successfully updated pnpm to v12.10.0
```

## ピンを `devEngines` に移す

**Corepack を外したので、Corepack のフィールドである `packageManager` を残す理由はなくなりました**。pnpm 12 が推奨しているのは `devEngines.packageManager` なので、このブログは最終的にピンをそちらへ移しました。

```json title="package.json" del={1} ins={2-8}
  "packageManager": "pnpm@12.10.1",
  "devEngines": {
    "packageManager": {
      "name": "pnpm",
      "version": "12.10.1",
      "onFail": "ignore"
    }
  },
```

`devEngines.packageManager` には `pmOnFail` と同じ働きをする独自の `onFail` があるので、`pnpm-workspace.yaml` の `pmOnFail: ignore` も同時に外しました。依存を 1 つだけ持つ作業用プロジェクトでの結果は次のとおりです。

| ピン | `pnpm-lock.yaml` の `---` 区切りの数 |
| --- | --- |
| `devEngines` で `12.10.1` を完全一致で指定 | 2 |
| さらに `pnpm-workspace.yaml` に `pmOnFail: ignore` | 0 |
| 代わりに `devEngines` 内の `"onFail": "ignore"` | 0 |

`pnpm self-update <version>` も `devEngines.packageManager.version` を同じように書き換え、ロックファイルは 1 つのドキュメントのままです。このブログのロックファイルは、切り替えの前後でまったく変わりませんでした。

## pnpm 10 から上がるときの落とし穴

ここからは 2 つ目のリポジトリでの話です。Docker ビルドと GitHub Actions を使うモノレポで、同じ週に pnpm 10 から pnpm 12 へ直接移行しました。pnpm 11 の変更がすべて一度に降りかかりました。

### 落とし穴 1：package.json と .npmrc の設定が黙って無視される

pnpm 11 以降は `package.json` の `"pnpm"` フィールドを読まず、`.npmrc` からは認証とレジストリの設定しか読みません。`package.json` に `overrides` ブロックがあっても、出るのは警告だけです。

```text
[WARN] The "pnpm" field in package.json is no longer read by pnpm. The following keys were ignored: "pnpm.overrides". See https://pnpm.io/settings for the new home of each setting.
```

`.npmrc` の `shamefully-hoist=true` には**何のメッセージも出ませんでした**。pnpm 12.10.1 で試しにインストールしたところ、`node_modules/.modules.yaml` には `"publicHoistPattern": []` が記録されていました。ホイスティングに頼っていたパッケージは、実行時に暗黙の import を失います。どちらも camelCase にして `pnpm-workspace.yaml` に移します。

```yaml title="pnpm-workspace.yaml"
overrides:
  '@faker-js/faker': ^8.4.1

shamefullyHoist: true
```

これらの設定が読まれていなくてもインストールは成功と表示されるので、失敗を待つのではなく警告を探してください。

### 落とし穴 2：pnpm 10 がスキップしていたビルドスクリプトでインストールが失敗する

**`allowBuilds` が `onlyBuiltDependencies` に代わり、`strictDepBuilds` のデフォルトが true になりました**。一覧にないビルドスクリプトを持つ依存があるとインストールが失敗します。pnpm 10 では、何も言わずにスクリプトを飛ばしていました。

```text
Error: ERR_PNPM_IGNORED_BUILDS

  × installing dependencies
  ╰─▶ Ignored build scripts: @firebase/util@1.14.0, classic-level@1.4.1, core-
      js@3.48.0, protobufjs@7.5.4, typeorm-fixtures-cli@3.1.0
  help: Run "pnpm approve-builds" to pick which dependencies should be allowed
        to run scripts.
```

それぞれを `false` として並べると、以前の許可リストにある依存のスクリプトだけを実行していた pnpm 10 の動作をそのまま保てます。

```yaml title="pnpm-workspace.yaml"
allowBuilds:
  grpc-tools: true
  sharp: true
  '@firebase/util': false
  classic-level: false
  core-js: false
  protobufjs: false
  typeorm-fixtures-cli: false
```

### 落とし穴 3：新しく足したピンで `--frozen-lockfile` が壊れる

pnpm 12 のプロジェクトに `packageManager` を足すと、ロックファイルに含めるべき内容が変わります。それぞれロックファイルを持つ 2 つのサブプロジェクトにピンだけを足したところ、CI が実行する frozen インストールが拒否されました。

```text
  × resolve package manager dependencies
  ╰─▶ Cannot update packageManagerDependencies with "frozen-lockfile" because
      the lockfile is not up to date
```

CI の `--frozen-lockfile` インストールを通す方法は 2 つあります。

1. **各プロジェクトでフラグなしの `pnpm install` を 1 回実行する**。pnpm が環境ドキュメントをロックファイルに書き込むので、更新されたロックファイルをコミットします。
2. **`pmOnFail: ignore`、または `devEngines.packageManager` の `"onFail": "ignore"` を設定する**。pnpm が環境ドキュメントを書き込まなくなるので、このエラーはそもそも起きません。このブログは最終的に `onFail` のほうを選びました。

その代わり、このチェックもなくなります。ロックファイルは pnpm のバージョンを記録しなくなるので、**後からピンを上げても `pnpm-lock.yaml` は変わらず**、`--frozen-lockfile` には比べる対象がありません。`pnpm self-update` は以前、`ignore` でも環境ドキュメントを書き戻していました（[pnpm/pnpm#12228](https://github.com/pnpm/pnpm/issues/12228)、修正済み）。ピンを変えたら、そのたびに区切りの数を数え直してください。

```bash
grep -c '^---$' pnpm-lock.yaml
# 0
```

### 落とし穴 4：pnpm/action-setup@v4 は pnpm 12 をインストールできない

**`pnpm/action-setup` は v6.0.0 で pnpm 11 に、v6.1.0 で pnpm 12 に対応しました**。`@v6` に上げて `version:` 入力を消せば、アクションは `package.json` からピンを読みます。両方を残して食い違わせると、`Multiple versions of pnpm specified` で失敗します。

Docker イメージでは逆の扱いが必要で、`pnpm@12` ではなく **`npm install -g pnpm@12.8.2` のように完全一致で指定します**。このコマンドは npm 経由で動き、npm は pnpm の `minimumReleaseAge` を読みません。メジャーバージョンだけの指定では、数分前に公開されたリリースが入ってしまいます。

### 落とし穴 5：Vercel のビルドイメージは pnpm 10 までしかない

[Vercel のパッケージマネージャーのドキュメント](https://vercel.com/docs/package-managers)は pnpm 6 から 10 までを挙げていて、バージョンは `lockfileVersion` から選びます。`packageManager` を読むのは `ENABLE_EXPERIMENTAL_COREPACK=1` で Corepack を有効にしたときだけです。pnpm 11 と 12 への対応を求める [vercel/vercel#17434](https://github.com/vercel/vercel/issues/17434) はまだ開いています。

このブログがこの問題を避けられているのは、`vercel build` を GitHub Actions の中で `pnpm/action-setup` の後に実行しているからです。CLI は `PATH` 上にすでにある pnpm でインストールします。pnpm 12 で初めて本番デプロイしたときのログには、ロックファイルから pnpm@10.x と判定した行と、実際には pnpm v12.10.1 でインストールした行が両方とも出ていました。

```text
Detected `pnpm-lock.yaml` version 9 generated by pnpm@10.x with package.json#packageManager pnpm@12.10.1
Installing dependencies...
? Verifying lockfile against supply-chain policies (616 entries)...
✓ Lockfile passes supply-chain policies (616 entries in 2.3s)
Lockfile is up to date, resolution step is skipped
Done in 2.6s using pnpm v12.10.1
```

Vercel 自身の Git 連携でビルドするプロジェクトでは、**イメージに入っている pnpm が使われます**。

## まとめ

- pnpm 12 は pnpm 11 の設定とロックファイル形式を引き継いでいるので、すでに pnpm 11 のリポジトリは `package.json` のピンを変え、`pnpm-workspace.yaml` に設定を 1 つ足すだけでアップグレードできます。
- **pnpm 12 のピンは `pnpm-lock.yaml` の依存関係の記述より前に環境ドキュメントを書き込み、GitHub の依存関係グラフはその最初のドキュメントしか読みません**。`devEngines.packageManager` でも同じことが起きます。
- `pmOnFail: ignore` でロックファイルは 1 つのドキュメントに保たれます。`download`、`error`、`warn` はバージョンが一致していても 2 つ書き込みます。後から足しても既存の環境ドキュメントは消えないので、以前のロックファイルを戻してインストールし直します。
- この設定は pnpm の自己切り替えを止めます。それはもともと Corepack の仕事でした。`packageManager` は Corepack のフィールドとして始まり、`devEngines.packageManager` は pnpm が現在推奨する、ツールをまたいで使えるフィールドです。
- pnpm を使うなら Corepack は不要で、Node.js 25 以降には同梱されていません。Corepack がなく `pmOnFail: ignore` の場合、ピンが強制されるのは CI だけです。インストール済みの pnpm とピンは `pnpm self-update <version>` で一致させておけます。
- このブログは最終的に `devEngines.packageManager` でピンを固定し、`pmOnFail` の代わりにその `onFail: "ignore"` を使っています。
- pnpm 10 から上がる場合は、`package.json` と `.npmrc` の設定を `pnpm-workspace.yaml` に移し、ビルドスクリプトをすべて `allowBuilds` に並べ、ピンを足したらロックファイルを作り直し（または `pmOnFail: ignore` を使い）、`pnpm/action-setup` を v6.1.0 に上げます。

## 参考リンク

- [pnpm 12.0 release post](https://pnpm.io/blog/releases/12.0)
- [pnpm 11.0 release post](https://pnpm.io/blog/releases/11.0)
- [pnpm 公式ベンチマーク](https://pnpm.io/benchmarks)
- [pnpm の Python 依存サポート（実験的機能）](https://pnpm.io/python)
- [pnpm v10 からの移行と v11 の設定 codemod](https://pnpm.io/migration)
- [pnpm のロックファイルのドキュメント（環境ドキュメントが書かれる条件を含む）](https://pnpm.io/lockfile)
- [pnpm の設定: pmOnFail](https://pnpm.io/settings/cli#pmonfail)
- [pnpm の package.json ドキュメント: devEngines.packageManager と従来の packageManager フィールド](https://pnpm.io/package_json)
- [dependabot/dependabot-core#15904: pnpm v12 env lockfile document is parsed as an empty dependency graph](https://github.com/dependabot/dependabot-core/issues/15904)
- [pnpm/pnpm#13805: the env lockfile document makes pnpm-lock.yaml unreadable to single-document consumers](https://github.com/pnpm/pnpm/issues/13805)
- [Corepack の配布停止を決めた Node.js TSC の投票](https://github.com/nodejs/TSC/pull/1697)
- [Corepack README](https://github.com/nodejs/corepack#readme)
- [pnpm/action-setup のリリース](https://github.com/pnpm/action-setup/releases)
- [Vercel のパッケージマネージャーのドキュメント](https://vercel.com/docs/package-managers)
