依存関係の管理

サイトが npm 依存関係をどのようにインストール、検証、更新するか

npm 依存関係はコミット済みの package-lock.json によって固定され、インストール時にはレビュー済みのライフサイクルスクリプトのみが実行されます。 これらの制御の背景にある脅威モデルと根拠については、Supply-chain security を参照してください。

インストール時の動作

CI、devcontainer、Netlify は、ロック固定かつスクリプト無効でインストールし、その後、レビュー済みのフック1つだけを明示的に再有効化します。 それは固定された Hugo バイナリを取得する hugo-extended のリビルドです。 リビルドは scripts/rebuild-hugo-extended.mjs を通じて実行され、制限付きバックオフでフェッチをリトライし、HUGO_* インストーラーオーバーライドが設定されている間は実行を拒否します。 インストールではオプショナルな依存関係を保持します。npm はプラットフォーム固有のバイナリ(たとえば sass-embedded 内の Dart Sass コンパイラー)を os/cpu で選択されるオプショナルな依存関係として配布するため、省略するとビルドが壊れます。 環境ごとの詳細は以下のとおりです。

  • CI: npm run ci:min を実行します。 サイトをビルドするジョブは続けて npm run ci:prepare を実行します。
  • Devcontainer: npm run install:safe を実行します。 同じ動作を保証します。
  • Netlify: npm run install:safe を実行します。 Netlify のビルドコマンドによって、不活性な自動インストールの後、クリーンなワーキングツリーのチェックの間に実行されます。
    • ロックのドリフトやその他の Git で検出可能な変更があればビルドが失敗します。
    • インストールが変更していないパスでの失敗については、後述の古い Netlify ビルドキャッシュを参照してください。
  • ローカル: npm run install:safe、または標準の npm install を実行します。 標準の npm install は、package.json と一致する間はロックに従い、ライフサイクルスクリプトを無効化するかわりに許可リストでゲートします。 詳細はローカルセットアップを参照してください。

ネストされた Docsy テーマのセットアップも同じ動作に従います。 prepare ステップが Docsy 自身のロック固定かつスクリプト無効のテーマ依存関係インストールを呼び出します。

古い Netlify ビルドキャッシュ

Netlify はデプロイコンテキストごとにビルドキャッシュを保持します。

  • プロダクション用に1つ
  • Deploy Preview 用にヘッドブランチ名ごとに1つ。 そのブランチ名の最初のビルド時にプロダクションキャッシュからシードされます。 キャッシュの系統は明示的なクリアでのみ消滅します。 ブランチの削除は Netlify からは見えないため、同じ名前で再作成されたブランチ(リサイクルされたボットブランチ名を含む)は古いキャッシュに再接続されます。

各キャッシュにはリポジトリのクローンが含まれており、git サブモジュールを削除するコミットをチェックアウトすると、サブモジュールのワーキングツリーがそのまま残ります。 そのため、削除されたサブモジュールがキャッシュに乗って後のビルドに未追跡の残留物として混入し、クリーンなワーキングツリーのチェックに失敗する可能性があります。 デプロイログには ?? 接頭辞のステータス行としてそのパスが表示されます。

パスを .gitignore に追加するのではなく、該当するビルドキャッシュをクリアしてください。

  • プロダクション:
    • Deploys > Trigger deploy からキャッシュをクリアしてサイトをデプロイします。
  • Deploy Preview: 既にビルドされた各ブランチは独自のキャッシュコピーを保持しており、プロダクションのクリア後も影響を受けません。
    • PR の最新デプロイページから Retry > Clear cache and retry with latest branch commit でクリアします。 ブランチ全体を一括でクリアする方法はありません。

依存関係の更新

通常のバージョンアップは Renovate PR として届き、リリースクールダウンでゲートされます。 既知の脆弱性修正はアラート駆動で届きます(セキュリティ更新)。 残りのケースは手動です。 いずれの場合も、再生成したロックファイルを package.json の変更と一緒にコミットしてください。

マニフェストの変更

package.json を手動で編集した場合でも、npm run update:packages で範囲内のすべてのバージョンをアップした場合でも(提供されるバージョンにはリリースクールダウンが適用されます)、変更されたマニフェストに合わせてロックファイルを同期します。

npm install --package-lock-only --ignore-scripts

npm update(後述)とは異なり、このコマンドはマニフェストの変更に必要な部分のみを書き換え、他のエントリは固定されたままにします。 ロックファイルのマージコンフリクトも同じ方法で解決します。 main のバージョンを採用し、上記のコマンドを再実行してください。

スクリプトを持つパッケージ

allowScripts エントリを持つ(または必要とする)パッケージを追加または更新する場合、変更を行うコントリビューターは以下を行います。

  1. 新しいバージョンのライフサイクルスクリプトをレビューする。
  2. 結果を、依存関係の変更と一緒にコミットし、PR レビューで検証する。 必要なスクリプトは正確なバージョン承認として、不要なスクリプトは名前レベルの拒否(false、以降のバージョンアップ時に更新不要)として記録する。
  3. 新しい承認の場合、.github/renovate.json5 の Renovate 自動マージ除外リストにもそのパッケージを追加する。 承認済みパッケージのすべてのバージョンアップには上記の手順が必要なため、その更新 PR はコントリビューターを待つ必要がある。

推移的依存関係のリフレッシュ

ロックファイル全体を再解決するスケジュールはありません(解決は意図的)。 推移的依存関係をリフレッシュするには、リポジトリルートで以下をオンデマンドで実行します(ロックファイルは scripts/generate-community-data ワークスペースもカバーしています)。

npm update --package-lock-only --ignore-scripts

リリースクールダウンが適用されますが、注意すべきエッジケースがあります。 満たせるバージョンがすべてクールダウンより新しい依存関係(正確なピン指定がよくあるケースです)は、いずれかのバージョンが経過期間を超えるまで解決全体が失敗します(ETARGET)。 レビュー済みで問題ないと判断した新しいリリースの場合は、その名前だけを除外します。 クールダウンはツリーの残りの部分には引き続き適用されます。

npm_config_min_release_age_exclude=PACKAGE_NAME \
  npm update --package-lock-only --ignore-scripts

PACKAGE_NAME を問題ないと判断したパッケージ名に置き換えてください。 除外は呼び出しごとに指定してください。 .npmrc に恒久的なエントリを追加すると、その名前に対するクールダウンが永続的に免除されます。 また、リフレッシュ後のロックファイルでメジャーバージョンの変更を確認してください。 npm update はマニフェストに宣言された範囲に従うため、親パッケージが範囲を広げると新しい推移的メジャーバージョンが入る可能性があります。

予期しないロックファイルの変更

依存関係を変更していないのにロックファイルが変更された場合(postinstall チェックがインストール時にこれを検出すると警告します)、ドリフトを示しています。 ロックファイルを復元し、リライトをコミットするのではなく調査してください。

セキュリティ更新

既知の脆弱性修正は、週次の更新 PR を待たずにアラート駆動で届きます。

  • GitHub の Dependabot security updates: リポジトリ側の設定(dependabot.yml は不要)で、直接的および推移的依存関係にパッチを適用できます。 npm の場合、ロックファイルだけでなく親のマニフェストエントリの書き換えが必要になることがあります。
  • Renovate の脆弱性アラート PR: 直接的依存関係に対して即座にオープンされます。

重複は意図的なものであり、まれに PR が重複することは許容されています。 スケジュールされたロックの再解決は設計上無効であるため、これらのアラート駆動のパスが推移的修正の唯一の自動化されたルートです。 そのためリポジトリ側の設定は有効のままにしています。 2つのパスはリリースクールダウンに対して異なる挙動を示します。

  • Dependabot security updates は、すべてのリリース経過期間ゲート(.npmrc を含む)を意図的にオーバーライドします。 クールダウンより新しい修正バージョンがランドする可能性があり、その検証はレビューするメンテナーの責任です。
  • Renovate の PR は、ロックファイルを再生成する際に .npmrc のゲートの対象となるため、クールダウンより新しい修正は失敗したアーティファクト更新として届きます。 早期に採用するには、メンテナーがスコープ付き除外を実行する必要があります。

サプライチェーン制御

サプライチェーン監査

サプライチェーン監査テスト scripts/supply-chain-audit.test.mjs は、test:local-tools の実行ごとにコミット済みファイルのみから以下の制御を検証します。 これにより、制御の退行はインシデントを待たずにテスト失敗として検出されます。 監査自体の検証原則については、その設計ページを参照してください。

PR で監査が失敗した場合、アサーションメッセージに期待される条件が記載されています。 よくあるケースは以下のとおりです。

  • allowScripts エントリを持つ依存関係をバージョンアップした場合: スクリプトを持つパッケージに従ってください。 失敗メッセージにエントリが移行すべきバージョンが示されます。
  • インストールパスのスクリプト、.npmrc、または netlify.toml を変更した場合: その失敗こそが目的です。 監査はインストール面を固定し、変更のたびに意図的なレビューが行われるようにしています。 変更に合わせて対応するアサーションを更新し、PR にその理由を記載してください。

グリーンにするためだけにアサーションを緩和しないでください。 各アサーションはこのページの制御を強制しているため、まず変更によってどの制御が緩和されるかを把握してください。

監査のスコープ外:

  • GitHub ワークフローファイル
  • Renovate 設定(.github/renovate.json5): コードと同様にレビューされるが、監査による固定の対象外
  • Docsy テーマ自身の依存関係インストール(上流で監査済み)
  • インストール境界を越えたビルド側の npm スクリプト

リリースクールダウン

バージョン解決では、設定された最小経過期間より新しいリリースは無視されます。

  • 適用: .npmrcmin-release-agescripts/generate-community-data サブプロジェクトは独立したロックホームではなく npm ワークスペースであるため、ルートの .npmrc とロックファイルがその解決も管理します。
  • スコープ:
    • 影響を受けるのはバージョン解決操作のみです。 ロック固定インストール(npm ci)はバージョンを解決しません。
    • npm はプロジェクト設定をユーザー設定より優先するため、ユーザーの .npmrc でより厳しいクールダウンを設定していても、ここのプロジェクト値に緩和されます。 特定の呼び出しで自分の設定を維持するには、npm_config_min_release_age 環境変数を設定してください。 この変数は両方の設定より優先されます。
  • Renovate: 開く更新 PR に独自のクールダウンを適用します。 .github/renovate.json5minimumReleaseAge で設定されます。 人間のレビューなしでマージされる更新にはより長い期間が設定されます。 プリセット提供の3日間の npm クールダウン(security:minimumReleaseAgeNpm)は、これらの期間をオーバーライドできないよう(その経過期間の免除を含め)除外されています。 注意: そのプリセットの上流での名前変更は、暗黙的にクールダウンを再適用させます。 Renovate が日付を判定できない更新タイプ(pinreplacementrollback など)はクールダウンの対象外です。 それらの PR は通常どおりオープンされ、恒久的に保留の安定性ステータス(必須チェックではない)が表示される場合があり、通常のレビューがゲートとなります。

ライフサイクルスクリプト許可リスト

インストール時にパッケージのライフサイクルスクリプトが実行されるのは、その正確な名前とバージョンが allowScripts 許可リストに記載されている場合のみです。

  • 適用: package.jsonallowScripts マップ。 .npmrcstrict-allow-scripts によりフェイルクローズドとなります。
  • 拒否:
    • false に設定されたエントリは、レビュー済みの拒否を記録します。 パッケージはインストールされますが、そのスクリプトはスキップされます。
    • 拒否は何も許可しないため、バージョンをまたいで名前単位でパッケージをカバーします。
  • --ignore-scripts との相互作用:
    • 許可リストはフィルタリングのみを行います。 ignore-scripts が無効にしたスクリプトを再有効化することは決してないため、スクリプト無効のインストールでは許可リストの有無にかかわらず何も実行されません。
    • レビュー済みの例外は、呼び出し箇所で明示的に --ignore-scripts=false を指定する必要があります。

npm バージョンフロア

アクティブな npm が engines フロアより古い場合、インストールは失敗します。 engines フロアとは、上記の制御をサポートする最も古いバージョンです。

  • 適用:
    • package.jsonengines がフロアを設定します。
    • .npmrcengine-strict によりフェイルクローズドとなります。
  • フロアポリシー:
    • npm が制御の適用ギャップを修正するたびにフロアは引き上げられます。
    • コミット済みの .nvmrc は、バンドルされている npm がフロアを満たす Node.js リリースを固定しているため、CI、Netlify、nvm 管理のローカルセットアップは構造上これを満たします。 Renovate がこの固定値を最新に保ちます。 (lts/* のようなフローティングな .nvmrc ではこれを保証できません。CI ランナーは古い可能性のあるキャッシュから解決します。)
  • Netlify:
    • Netlify の Node バンドルのデフォルト npm はフロアより古い場合があります。 netlify.tomlNPM_VERSION でフロアを満たすバージョンを固定しています。
    • 少なくともフロアが引き上げられた際にはこの固定値も更新してください。

不活性な Netlify 自動インストール

ビルド開始時の Netlify の自動インストールは、netlify.tomlNPM_FLAGS によって無効化されます。

  • --dry-run: npm はインストールが何を変更するかを解決してログに記録しますが、何も書き込みません。
  • --ignore-scripts: ライフサイクルスクリプトは、ドライランの副作用としてではなく、明示的な指示により無効化されたままになります。

スコープ: NPM_FLAGS は Netlify のビルド設定であり npm の設定ではありません。 自動インストールにのみ適用され、ビルドコマンドの npm 実行には適用されません。

多層防御: 実際のインストールnpm ci であり、node_modules を丸ごと置換します。 そのため、自動インストールやビルドキャッシュの node_modules 内の残留物は、クリーンなワーキングツリーのチェックからは見えない(Git で検出可能な変更のみを見る)にもかかわらず、ビルドには持ち越されません。

素の npx の禁止

リポジトリの設定(パッケージスクリプト、CI、ヘルパースクリプト、コントリビュータードキュメント)は、npx BIN としてバイナリを呼び出しません。 node_modules が古いか存在しない場合、npx は公開レジストリにフォールバックし、その名前を持つパッケージが何であれ実行します。 インストールプロンプトは防御になりません。 非インタラクティブなコンテキストではスキップされ、その他の場合は反射的に「はい」を誘います。 ローカライズされたコントリビュータードキュメントのコピーは、ドリフトトラッキングを通じてこのルールに追従します。

  • 代替手段:
    • パッケージスクリプトは、依存関係が提供するバイナリを直接呼び出します。 npm が node_modules/.binPATH に追加するため、バイナリが存在しなければレジストリへの通信なしに大きなエラーで失敗します。
    • その PATH エントリがないコンテキスト(ドキュメント、スタンドアロンスクリプト)では、npm exec --no -- BIN を使用します。 これはインストールを行いません。
  • 適用: レビューの規律による。 自動化されたチェックはありません。