npm/pnpm依存の更新で本番を壊さない手順 — lockfile・semver・Renovate
「npm update」一発で本番が落ちた失敗から学んだ依存管理の手順。lockfileの意味、^と~の違い、メジャー更新の進め方、Renovate自動化、npm auditまで。
金曜の夕方、「ついでに依存も新しくしとくか」と軽い気持ちで npm update を叩きました。
翌朝、本番のビルドが落ちていました。原因は、間接的に入っていた小さなパッケージのメジャー更新。自分では一行も書き換えていないのに、package-lock.json の中身がごっそり入れ替わっていたんです。
このとき僕が痛感したのは、依存パッケージの管理は「最新版にすること」ではないということ。何を、どこまで、どの順番で上げるかを決める作業なんですね。今日はそのやり方を、僕が踏んだ地雷つきで書きます。
この記事の要点
package.jsonは「この範囲で使いたい」という希望、lockfile は「実際に入った実物」。事故は両者のズレから起きる。- バージョンの
^(キャレット)と~(チルダ)の意味を知らないと、npm installのたびに中身が静かに変わる。 - patch/minor はまとめてOK、メジャー更新だけは1つずつ。これを守るだけで原因切り分けが一気に楽になる。
- 自動更新は Renovate か Dependabot。ただしテストが薄いプロジェクトで automerge を増やすのは火に油。
npm auditの警告は焦って--forceしない。直接依存か間接依存かで対応がまるで違う。
lockfileって、結局なんなのか
最初にここをはっきりさせます。ここが曖昧だと、何度でも同じ事故を踏みます。
package.json に書く "react": "^18.2.0" は、希望の範囲です。「18系の新しめならどれでもいい」という宣言にすぎません。一方 lockfile は、今回ほんとうに入った実物の記録です。バージョンも、ダウンロード元も、改ざんされていないかのハッシュも、全部固定されています。
例えるなら、package.json はレシピの「塩 少々」、lockfile は「実際に入れた塩 3.2g」の記録。レシピだけ共有して各自が「少々」を解釈すると、人によって味が変わります。lockfile は、その味を全員ぴったり再現するための実測値です。
| ツール | lockfileの名前 | CIで使う基本コマンド | 古い依存の確認 | 脆弱性チェック |
|---|---|---|---|---|
| npm | package-lock.json | npm ci | npm outdated | npm audit --audit-level=high |
| pnpm | pnpm-lock.yaml | pnpm install --frozen-lockfile | pnpm outdated | pnpm audit --audit-level high |
| Yarn (modern) | yarn.lock | yarn install --immutable | yarn up -i | yarn npm audit --recursive --severity high |
npm公式ドキュメントのnpm ciには、はっきりこう書いてあります。package.json と lockfile が食い違っていたら、lockfile を更新せずにエラーで止まる、と。pnpm の --frozen-lockfile も Yarn の --immutable も発想は同じで、CIでは lockfile を勝手に直さない。これが鉄則です。
僕の金曜の事故は、ここを破ったことが原因でした。npm update はローカルの lockfile を書き換えます。それをそのまま push して、CIが「新しい lockfile」で初めてビルドしたら、メジャー更新の非互換が一気に表面化した、というわけです。
^ と ~ の違いで、中身は静かに変わる
package.json のバージョン表記、なんとなく ^ を付けていませんか。僕はそうでした。この記号の意味を知らないと、npm install のたびに、何も触っていないのに依存が変わります。
^1.2.3… マイナーまで上がる。1.x.xの最新を拾う(メジャーは固定)。~1.2.3… パッチだけ上がる。1.2.xの最新を拾う。1.2.3… 完全固定。何も上がらない。
^ は便利ですが、「気づかないうちにマイナー更新が入る」という性質でもあります。だからこそ lockfile が効いてくる。^ で範囲を広く取っていても、lockfile があれば全員・全環境で同じ実物に揃います。範囲は package.json で、再現は lockfile で。役割が分かれているわけです。
ちなみにバージョン番号の メジャー.マイナー.パッチ(semver)は、ざっくりこう読みます。パッチ=バグ修正、マイナー=後方互換のある機能追加、メジャー=壊れる変更あり。だから怖いのはメジャーだけ。ここを分けて考えるのが、依存管理の出発点です。
Claude Codeにはまず「棚卸し」をさせる
ここでClaude Code(AnthropicのCLI型コーディングエージェント。ターミナルからファイルを読ませたり調べさせたりできます)の出番です。ただし、いきなり npm install xxx@latest を実行させてはいけません。最初にやらせるのは更新ではなく棚卸しです。
棚卸しとは、今どのパッケージを使っていて、何が古く、何を上げると危ないかを一覧にすること。依頼文はこのくらい具体的にします。
claude -p "
このリポジトリの依存管理を調査して。まだファイルは編集しないで。
報告してほしいこと:
- パッケージマネージャーとlockfileの種類
- 古くなっている直接依存の一覧
- 実行すべき脆弱性監査コマンド
- 依存更新後に必ず通すべきscript(test/build/typecheckなど)
- 1つずつ慎重に上げるべき危険なメジャー更新
ファイルパスと正確なコマンドも添えて。
"
「編集しないで」を最初に入れるのがコツです。AIは放っておくと気を利かせて勝手に直し始めます。冒頭の僕の事故と同じで、良かれと思った自動更新がいちばん怖い。まず現状を見える化して、人間が方針を決める。順番はいつもこれです。
コピペで使える「更新後チェック」スクリプト
依存を上げたあと、毎回同じ確認を手で打つのは続きません。続かない手順は、忙しい日に必ず飛ばします。そこで、固定インストール → 脆弱性監査 → 型チェック → テスト → ビルドを一気に通すスクリプトを置きます。npm/pnpm/Yarnを自動判定するので、scripts/verify-deps.mjs として置けばWindowsでもLinuxのCIでも動きます。
#!/usr/bin/env node
// 依存更新後の検証をまとめて流す。更新はしない、確認だけ。
import { existsSync, readFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
function readPackageJson() {
return JSON.parse(readFileSync("package.json", "utf8"));
}
// packageManagerフィールド優先、なければlockfileの有無で判定する
function detectPackageManager(pkg) {
const declared = pkg.packageManager || "";
if (declared.startsWith("pnpm@")) return "pnpm";
if (declared.startsWith("yarn@")) return "yarn";
if (declared.startsWith("npm@")) return "npm";
if (existsSync("pnpm-lock.yaml")) return "pnpm";
if (existsSync("yarn.lock")) return "yarn";
if (existsSync("package-lock.json")) return "npm";
throw new Error("パッケージマネージャーもlockfileも見つかりません。");
}
function run(command, args) {
const label = [command, ...args].join(" ");
console.log(`\n$ ${label}`);
const result = spawnSync(command, args, {
stdio: "inherit",
shell: process.platform === "win32",
});
if (result.status !== 0) {
throw new Error(`コマンド失敗: ${label}`);
}
}
const pkg = readPackageJson();
const manager = detectPackageManager(pkg);
// CIではlockfileを固定してインストール(勝手に直させない)
const installCommands = {
npm: ["npm", ["ci"]],
pnpm: ["pnpm", ["install", "--frozen-lockfile"]],
yarn: ["yarn", ["install", "--immutable"]],
};
const auditCommands = {
npm: ["npm", ["audit", "--audit-level=high"]],
pnpm: ["pnpm", ["audit", "--audit-level", "high"]],
yarn: ["yarn", ["npm", "audit", "--recursive", "--severity", "high"]],
};
const scriptCommands = {
npm: (name) => ["npm", ["run", name]],
pnpm: (name) => ["pnpm", ["run", name]],
yarn: (name) => ["yarn", ["run", name]],
};
const requiredScripts = ["typecheck", "test", "build"];
run(...installCommands[manager]);
run(...auditCommands[manager]);
for (const name of requiredScripts) {
if (pkg.scripts?.[name]) {
run(...scriptCommands[manager](name));
} else {
console.log(`\n- skip ${name}: scriptが無いので飛ばす`);
}
}
console.log(`\n依存の検証OK(${manager})。`);
package.json にはこう登録します。
{
"scripts": {
"deps:verify": "node scripts/verify-deps.mjs"
}
}
味噌は、これを「更新スクリプト」ではなく「検証スクリプト」にしていることです。誰が更新しても——自分でも、Claude Codeでも、Renovateでも——最後は同じ門番を通す。これで「ローカルでは動いたのにCIで落ちる」が激減します。
メジャー更新は、1つずつ運ぶ
patch/minor はまとめて上げて、テストで拾えれば十分です。問題はメジャー更新。ここで僕が学んだのは、たった一つのルールです。メジャーは1つずつ、PRも分ける。
react、vite、eslint、typescript のメジャーを一気に上げて、ビルドが落ちたとします。さて、犯人は誰でしょう。4つ同時だと、どれが原因か切り分けるのに半日溶けます。1つずつなら、落ちたPRがそのまま犯人です。
Claude Codeに任せるときも、条件を絞ります。
claude -p "
開発依存(devDependencies)だけの更新プランを作って。
現在のパッケージマネージャーを使うこと。
lint/test/build系ツールのpatchとminorだけまとめて。
メジャー更新は含めないで。
変更後に npm run deps:verify(または相当コマンド)を実行して。
変更したパッケージと、失敗したコマンド名を報告して。
"
「devDependenciesだけ」「メジャー禁止」「失敗したコマンド名を返す」。この3点を付けるだけで、AIの暴走範囲がぐっと狭まります。逆にこれを言わないと、気を利かせて typescript のメジャーまで上げてきて、型エラーの海に沈みます(経験者は語る)。
npm auditの警告で、焦って —force しない
npm audit を打つと、赤い警告がずらっと並んで心臓に悪いです。ここで npm audit fix --force を叩きたくなる。これが二つ目の地雷でした。
--force はメジャー更新を含む修正を平気で入れてきます。認証まわり、決済、ルーティング、ビルド設定——壊れてほしくないところほど巻き添えにします。脆弱性は直っても、アプリが動かなくなったら本末転倒です。
まずは結果を機械が読める形で出して、分類から始めます。
npm audit --json
そしてClaude Codeには、修正ではなく分類を頼みます。
claude -p "
このaudit結果を分類して。まだ audit fix は実行しないで。
high と critical の各項目について報告:
- パッケージ名
- 直接依存か間接依存か
- 入っているバージョンと、修正済みバージョン(あれば)
- lockfile更新だけで直るか
- メジャー更新が必要か
そのうえで、いちばん小さく安全な対処を提案して。
"
ここで効くのが直接依存と間接依存の区別です。直接依存は、自分の package.json に名前が書いてあるパッケージ。間接依存は、そのパッケージがさらに内部で使っているパッケージです。間接依存の警告なら、lockfile更新や親のマイナー更新だけで消えることがよくあります。直接依存のメジャーが必要なときだけ、普通の機能改修と同じ重さで扱う。この見極めで対応コストが何倍も変わります。
なお、脆弱性対応の優先度判断や、悪意あるパッケージの見分け方をもっと踏み込んで知りたい人は、Claude Codeでセキュリティ監査を回す手順に分けて書いています。この記事は「更新の運用」、あちらは「監査の深掘り」と役割分担しています。
Renovateで「テストが通ったものだけ」自動マージ
毎週手で npm outdated を見るのも続きません。そこで自動更新PRを作る Renovate か Dependabot を入れます。Claude Codeは、設定の生成とPRレビューで使うのが現実的です。
Renovateは細かいルールを書けるのが強みです。config:recommended をベースに、開発ツールの低リスク更新だけ automerge する例がこちらです。
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"dependencyDashboard": true,
"lockFileMaintenance": {
"enabled": true,
"schedule": ["before 5am on monday"],
"automerge": true
},
"packageRules": [
{
"description": "低リスクな開発ツール更新は自動マージ",
"matchDepTypes": ["devDependencies"],
"matchUpdateTypes": ["patch", "minor"],
"automerge": true
},
{
"description": "本番依存はpatchのみ、CI通過後に自動マージ",
"matchDepTypes": ["dependencies"],
"matchUpdateTypes": ["patch"],
"automerge": true
},
{
"description": "メジャー更新は人間がレビュー",
"matchUpdateTypes": ["major"],
"labels": ["dependency", "major-update"],
"automerge": false
}
]
}
ここで注意。Renovate公式のautomerge解説には、automergeは必須テストが通るのを待ってから動く、と明記されています。逆に言うと、テストが薄いプロジェクトでautomergeだけ増やすのは火に油です。守ってくれる門番(テスト)がいないのに、自動でマージしてしまう。Claude Codeには設定を書かせるだけでなく、「このリポジトリのテストで自動マージしてよい範囲はどこか」までレビューさせるのが安全です。
ちなみに config:base という古い名前を見かけたら、それは旧版です。Renovate v36 で config:recommended に改名されました。新規に書くなら config:recommended を使ってください。
Dependabotなら最小構成で始められる
GitHubだけで完結させたいなら Dependabot で十分です。.github/dependabot.yml に置きます。npm でNode系の依存を、github-actions でワークフロー自体の更新を見ます。
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "06:00"
open-pull-requests-limit: 5
groups:
dev-tooling:
dependency-type: "development"
update-types:
- "minor"
- "patch"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
github-actions を入れておくと、actions/checkout や actions/setup-node が古いまま放置されるのを防げます。地味ですが、ここが古いとある日突然CIが警告を出し始めるので、入れておく価値があります。設定項目の詳細はDependabot options referenceが一次情報です。
依存を増やしすぎないという判断
最後に、いちばん効くのに見落とされがちな話を。そもそも依存を入れないという選択肢です。
npm install left-pad で文字列を左詰めできます。でもそれ、数行の関数で済むことも多い。依存を1つ入れるたびに、更新の追従、脆弱性の監視、メジャー移行のコストが、未来の自分に積み上がります。便利さの裏で、保守の負債を借りているわけです。
僕が新しいパッケージを入れる前に自問するのは、この3つです。
- これは自分で20〜30行で書けないか(書けるなら書く)。
- メンテされているか(最終更新が2年前なら赤信号)。
- これが入ると、間接依存が何個増えるか(
npm install --dry-runで確認できる)。
依存は資産でもあり負債でもあります。入れる瞬間がいちばん安く、外す瞬間がいちばん高い。だから入り口で一呼吸おくのがいちばん効きます。
なお、依存の追加・更新をCI全体の中でどう検証するかはClaude Code CI/CDセットアップに、複数パッケージをまたぐ pnpm workspace での境界設計はClaude Codeとpnpm workspaceにまとめています。
よくある質問
Q. npm install と npm ci は何が違いますか。
npm install は lockfile を書き換えることがあります。npm ci は lockfile を一切変えず、package.json と食い違っていたらエラーで止まります。ローカルの開発は npm install、CIや本番ビルドは npm ci と使い分けてください。
Q. lockfileはgitにコミットすべきですか。 アプリ(実行されるプロジェクト)なら必ずコミットします。これが全員・全環境で同じ実物を再現する根拠になります。逆に「ライブラリとして公開するパッケージ」では、利用側の依存解決に任せるためコミットしないのが一般的です。
Q. ^ と ~、どちらを使えばいいですか。
迷ったら ^(マイナーまで上がる)で問題ありません。lockfileがあれば再現性は保たれます。特定のパッケージで「マイナー更新も怖い」なら ~ に、絶対に動かしたくないなら完全固定にします。
Q. メジャー更新が大量に溜まってしまいました。
一気に上げず、依存度の低い葉のパッケージから1つずつPRにします。各PRで deps:verify を通し、落ちたら原因を1つに絞って対処。溜まっているなら、まず Renovate の dependency dashboard で全体像を出すと優先順位が見えます。
Q. npm audit の警告がゼロになりません。
ゼロを目指す必要はありません。high と critical を優先し、間接依存で修正版が出ていないものは無理に潰さない。--force で全部消すより、リスクの高いものだけ確実に対処するほうが安全です。
実際に試した結果
この手順は Node.js 22 と npm / pnpm / Yarn modern で確認しました。
いちばん効いたのは、scripts/verify-deps.mjs という一枚の門番を置いたことです。自分で更新しても、Claude Codeに任せても、Renovateが自動でPRを出しても、最後は必ず同じ検証を通る。「誰が更新したか」を気にしなくてよくなった瞬間、依存更新がただの定型作業になりました。
そして、メジャー更新を自動化から外し、patch/minorの開発依存だけをCI通過後の automerge に寄せたら、金曜夕方のあの事故は二度と起きていません。lockfileの差分を毎回ちらっと見る——たったこれだけの習慣で、「気づいたら本番が落ちていた」がなくなります。賢く全部任せるより、転んでも戻せる手順を先に作る。遠回りに見えて、これが結局いちばん速い、というのが今の実感です。
依存更新のルールや automerge の範囲を、自分のリポジトリ前提で一緒に詰めたいときは研修・相談で、CLAUDE.md やレビューpromptのテンプレートが欲しいときは教材一覧を覗いてみてください。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Codeに1ファイルだけ直させる指示文のつくり方
「もっと良くして」で40行も変えられた失敗から学んだ、触る範囲・検証・戻し方をセットにしたClaude Code用の依頼文テンプレートを紹介します。
Claude Code の権限拒否から復旧する: 止まった理由を次の安全手順に変える
Claude Code のコマンドが拒否されたとき、焦って許可を広げずに、拒否理由、代替手順、証拠コマンド、再試行条件へ分解する方法。
Claude Codeにビルド→スモークテスト→自動修正を回させる足場の作り方
最小スモークテストの選び方、失敗ログを食わせて直させるループ、回数上限と確認ゲートで暴走を止める方法を、コピペで動くコード付きで紹介します。