夜間CIの回帰チェックで起こる静的解析のバイナリ誤検知(PNGQUANT)と、バンドルサイズ比較における測定日時ズレによる失敗を、キャラクターが調査する様子。2つの誤検知の罠が図解されている。

はじめに

nightlyで「マージ済みPRの回帰チェック」を回している場合、コード自体には何の問題もないのに誤検知が出続けることがあります。

以前あるプロジェクトのCIで、種類の異なる2つの誤検知に立て続けに遭遇しました。

1つは静的解析ツールが「npmパッケージとしては存在しない外部CLIバイナリ」を未解決依存として報告するケースです。

もう1つはバンドルサイズの回帰検知ロジックが、「計測タイミングのズレ」を本物の回帰と誤認するケースです。

一見まったく別の問題に見えますが、根本原因を辿ると「チェック対象と比較対象の前提がズレている」という同じ構造に行き着きました。

この記事では、それぞれの誤検知パターンと、実際に加えた修正を紹介します。

こんな人におすすめ

  • knipなどの静的解析ツールをCIに組み込んでいて、未解決依存の誤検知に悩んだことがある方
  • バンドルサイズの回帰検知をCIで自動化している方
  • 「計測時刻」や「baselineの更新」を考慮していない回帰チェックを運用している方
  • nightlyのCI通知に慣れてしまい、誤検知を見逃しがちな方

罠1: 静的解析ツールが外部CLIバイナリを未解決依存と誤検知する

コード内で execFileSync などを使って外部CLIバイナリを呼び出している場合、そのバイナリはnpmの依存関係としては解決できません。

例えば画像最適化スクリプトからpngquantのようなシステムインストール済みバイナリを呼んでいるケースです。静的解析ツール側からは「package.jsonに存在しないのに参照されている未解決の依存」に見えてしまい、falseとして検出されます。

この手の誤検知は、許可リストに明示的に追加することで解決します。

"ignoreBinaries": [
  // scripts/optimize-pwa-assets.mjs が execFileSync で呼ぶ外部 CLI。npm 依存ではなく
  // システム / CI にインストールされるバイナリのため、静的解析ツールは npm 解決できず
  // unlisted binary と誤検知する。
  "pngquant"
],

ポイントは追加すること自体より、コメントで「どのスクリプトが」「何のために」呼んでいるかを残しておくことです。

半年後に見返した人が「なぜこのバイナリが許可されているか」を、コードを読み直さずに判断できます。

罠2: バンドルサイズの回帰検知が「計測タイミングのズレ」を回帰と誤認する

もう一つの誤検知は少し込み入っています。バンドルサイズの回帰チェックは、通常「現在のartifact」と「baseline」を比較して増加していれば失敗させます。

ここで見落としがちなのが、baseline自体が後から更新される可能性があることです。

baselineが大きく縮小方向に更新されると、それより前に計測された古いartifactは相対的に「大きく」見えてしまいます。実際には回帰していないのに、古いartifact全件がfailするという事故が起きます。

この問題は、artifactとbaselineそれぞれの計測日時を比較することで切り分けられます。

const artifactMeasuredAt = Date.parse(current.measured_at ?? '')
const baselineMeasuredAt = Date.parse(baseline.measured_at ?? '')
const artifactPredatesBaseline =
  Number.isFinite(artifactMeasuredAt) &&
  Number.isFinite(baselineMeasuredAt) &&
  artifactMeasuredAt < baselineMeasuredAt

if (artifactPredatesBaseline && current.total_gzip_bytes > baseline.total_gzip_bytes) {
  // baseline 更新前の計測なので、現在のものさしとの再比較は意味を持たない。
  // null を返して呼び出し側で PR 時点の判定にフォールバックさせる。
  return null
}

「artifactの計測日時がbaselineより古い」かつ「artifactの値がbaselineより大きい」の2条件が揃った場合だけ、再検証をスキップしてPR時点のコメント判定にフォールバックさせます。

非対称な扱いがポイント

ここで意図的に非対称な扱いにしているのが重要な設計判断です。

stale(古い)artifactでも、値が縮小方向(downgrade)に見えるケースは再検証をスキップしません。これはbaseline更新によって吸収された正常なケースなので、従来どおり比較を続けても問題がないからです。

逆に「stale かつ悪化方向に見える」ケースだけが、ものさし自体がズレている偽陽性の疑いが強いため、スキップ対象にします。

すべてのstale artifactを一律スキップにしてしまうと、本物の回帰を見逃すリスクが生まれます。この条件分岐は、誤検知抑制と品質担保のバランスを取る要になっています。

テストを書く際は、計測日時を差し替えられるオプションをテストヘルパーに用意しておくと、時系列がズレたケースと正常なケースの両方を再現しやすくなります。日時に依存するロジックは、日時を注入可能な形にしておくのが後々効いてきます。

つまづきやすいポイント

  • 静的解析ツールの「未解決依存」警告を鵜呑みにせず、それが本当にnpm依存なのか外部CLIバイナリなのかを先に切り分ける
  • 回帰検知の再検証ロジックを書くときは、比較対象(baseline)自体が更新され得ることを前提に設計する
  • staleなartifactを一律スキップにすると本物の回帰を見逃すため、「悪化方向」のケースだけに絞る非対称な条件分岐にする
  • 誤検知は「起きなくなった」で終わらせず、artifactレベルで原因を実証してから直すと、後から見た人が修正の妥当性を検証しやすい

まとめ

nightlyの回帰チェックで誤検知が続く場合、多くは「チェック対象と比較対象の前提がズレている」ことが原因です。

外部CLIバイナリは許可リストとコメントで解決できますが、baselineの更新を伴う回帰検知はもう一段複雑で、計測日時の比較を挟む必要があります。

「stale かつ悪化方向」だけをスキップ対象にする非対称な設計は、誤検知を減らしつつ本物の回帰を見逃さないための現実的なバランス感覚だと感じています。

同種の誤検知が他の回帰検知スクリプトに潜んでいないか、横展開で確認しておくのも有効です。

💡 CTA 指示: ここに D1「開発壁打ち(¥12,000)」 の CTA を WordPress 側で設定してください。