手元では npm install が通るのに、CI で npm ci を実行すると、パッケージを1つも入れないまま次のエラーで止まることがあります。
npm error code EUSAGE
npm error
npm error `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing.
npm error
npm error Missing: [email protected] from lock file
npm error Missing: [email protected] from lock file
終了コードは 1、エラーコードは EUSAGE です。npm のバージョンによっては各行の先頭が npm error ではなく npm ERR! になります(npm 9 系まで)。中身は同じで、Missing: ... from lock file のほか、版がずれている依存には Invalid: lock file's ... の行が出ます。
原因は、package.json が要求する依存と、package-lock.json に記録された依存がずれていることです。npm ci はこのずれを見つけると、ロックを書き換えずにその場で止まります。直し方は、CI に npm install を足すことではなく、整合させた package-lock.json をコミットすることです。
急いで直すなら、手元でロックを更新し直してコミットします。
npm install # package.json に合わせて package-lock.json を作り直す
git add package-lock.json
git commit -m "chore: sync package-lock.json"
なぜ CI の npm ci で出るのか:npm install はロックを黙って直し、npm ci は直さない
npm install と npm ci は、package.json と package-lock.json がずれていたときの振る舞いが正反対です。
| コマンド | ロックがずれているとき |
|---|---|
npm install | ロックを書き換えて依存を入れる(ずれは黙って解消される) |
npm ci | ロックを書き換えず、ずれを検出して停止する(EUSAGE) |
package.json の依存を変えたあと npm install を回すと、手元の package-lock.json はその場で更新されます。このとき更新後のロックをコミットし忘れるか、別ブランチで更新されたロックが手元に来ていないと、リポジトリ上では package.json と package-lock.json がずれたままになります。
手元では、次に npm install を打つたびにロックが黙って整合するので、ずれが表面化しません。一方 CI は再現性のために npm ci を使います。npm ci はロックを直さない約束のコマンドなので、コミットされたロックのずれをそのまま踏んで止まります。これが「手元は通るのに CI だけ落ちる」の中身です。
止まった環境で最初に確認するのは、package-lock.json がコミットされていて、かつ package.json と整合しているかです。
なぜ npm ci はロックを直さないのか
npm ci(clean install)は、コミット済みの package-lock.json(または npm-shrinkwrap.json)に固定された版だけを入れるコマンドです。package.json は依存の解決には使わず、ロックと整合しているかの照合にだけ使います。依存を解決し直してロックを更新するのは npm install の仕事で、npm ci はそれをしません。CI で npm ci を使うのは、ロックに固定された版がそのまま入る(=手元・CI・本番で同じ依存になる)ことを保証するためです。
その保証を守るために、npm ci は package.json と package-lock.json のずれを直さず、エラーにします。ずれを黙って直してしまうと、「ロックに書いてある版とは違う版が CI で入る」ことになり、npm ci を使う意味が消えるからです。EUSAGE(使い方の誤り)というエラーコードは、「このコマンドは同期済みのロックを前提にしている」という設計上の要求を指しています。
Missing: [email protected] from lock file は、package.json が要求する [email protected] がロックに載っていないこと、Missing: [email protected] from lock file はその依存の依存も載っていないことを示します。版だけがずれている場合は Invalid: lock file's <名前>@<版> does not satisfy <名前>@<要求版> の形になります(本ケースで実際に出したのは Missing: の行です)。どちらも「ロックが package.json に追いついていない」ことの内訳です。
直し方:整合した package-lock.json をコミットする
npm ci を通す正しい対処は、ロックを package.json に合わせて更新し、その更新をコミットすることです。
npm install # package.json を読み、package-lock.json を整合させる
npm install(引数なし)は、package.json の依存を満たすように package-lock.json を作り直します。差分が出たら、それをコミットします。
git add package-lock.json
git commit -m "chore: sync package-lock.json with package.json"
このコミットが CI に届けば、CI の npm ci は同期済みのロックを読んで終了コード 0 で入ります。直すのはロックであって、CI のコマンドではありません。npm ci はそのままにします。
package.json を編集したのにロックを更新し忘れる事故を防ぐなら、依存を変える操作を npm install <パッケージ> 経由で行う習慣にします(このコマンドは package.json と package-lock.json を同時に更新します)。package.json を直接エディタで書き換えたときは、そのあと必ず npm install を1回回してロックを追従させます。
CI に npm install を足すのは、なぜ逆効果か
このエラーで最初に試したくなるのは、CI の npm ci を npm install に置き換えることです。エラーは消えますが、npm ci を使っていた理由そのものを捨てることになります。
npm install はロックをその場で書き換えて依存を入れます。つまり CI でこれを使うと、ビルドのたびに CI がロックを解決し直し、コミットされたロックとは違う版が入りうる状態になります。ロックで版を固定して再現性を得るのが npm ci の役目だったので、npm install に替えると、手元・CI・本番で入る依存がずれる余地を戻すことになります。しかも CI が書き換えたロックはたいてい捨てられるので、ずれの原因(コミットされていない整合ロック)は残ったままです。
さらに --package-lock=false(ロックを書き換えない)や --no-save(package.json に保存しない)を付けても、コミット済みロックのずれ自体は残るので、根本原因は解消しません。整合したロックをコミットするのが、npm ci の設計に沿った対処です。
切り分け(うまくいかないとき)
- エラーの接頭辞が
npm ERR!と表示される:npm 9 系までの書式で、npm 10 以降はnpm errorに変わりました。どちらも中身は同じEUSAGEの「in sync」エラーです。検索するときは接頭辞を外した本文で探すと版によらず一致します。 package-lock.jsonそのものが無いと言われる:メッセージがcan only install with an existing package-lock.jsonなら、ずれではなくロックの不在です。対処は別で、The npm ci command can only install with an existing package-lock.json の直し方 を参照してください。npm installしてもロックの差分が出ない/すぐ戻る:git diff -- package-lock.jsonで差分の有無を確認し、CI と手元のnpm -v・プロジェクトの.npmrcを突き合わせます(生成されるロックの形は npm の版や設定で変わります)。CI が実際に読んでいるコミットに、整合済みのロックが入っているかも確認します。- モノレポ(workspaces)で出る:ルートの1つの
package-lock.jsonが全ワークスペースを束ねます。どれか1つのワークスペースのpackage.jsonを更新してロックを追従させ忘れると、同じEUSAGEになります。ルートでnpm installを回してロック全体を整合させます。 - 依存の更新 PR(Renovate / Dependabot 等)を取り込んだあとに出る:
package.jsonとpackage-lock.jsonが別コミットに分かれて片方だけマージされた、あるいはリベースでロックの更新が落ちた、という経路があります。マージ後の状態でnpm installを回し、ロックの差分をコミットし直します。 npm ciは通ったのに、今度は別のインストールエラーになる:ロックの同期とは別の依存衝突です。peer dependency の衝突なら ERESOLVE unable to resolve dependency tree を参照してください。
検証環境
node:22.23.1-alpine(npm 10.9.8)、ネットワーク有り- 再現:
package.jsonが[email protected]を要求する一方、package-lock.jsonには[email protected]だけが載っている状態でnpm ciを実行すると、can only install packages when your package.json and package-lock.json ... are in sync(Missing: [email protected] from lock file)を出して終了コード 1 - 修正:
package.jsonとpackage-lock.jsonを整合させる([email protected]とその依存をロックに載せる)と、同じnpm ciが終了コード 0・シグネチャ消滅
再現から修正までは errfix の検証ハーネスが機械的に確認しています。reproduce と fix の差は package-lock.json の依存スナップショットだけで、package.json は同一です。これは「手元の npm install は通る(ロックを書き換えるから)が、コミットされたロックがずれていると CI の npm ci は止まる」という環境境界を、ロックの整合だけを変えて再現したものです。