Rebounder Tech Blog

運用している当事者が書く、本番システムの記録。

本番のpackage-lockはVercel deployments APIのgit commit shaで取る

公開 読了時間 約5分執筆: Rebounder 開発チーム(当該システムの運用当事者)

※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。

結論

本番とローカルの作業ブランチが分かれて運用されているリポジトリでは、作業ツリーのpackage.jsonを読む監査はローカルの作業ブランチの版を見ているだけで、本番の実体を保証しない。

結論

ローカルの複数リポジトリを横断してnpmの依存脆弱性を定期監査するとき、対象の中に「本番のデプロイ元ブランチ」と「ふだんの作業ブランチ」が分かれて運用されているリポジトリが混じっていると、作業ツリーのpackage.json/package-lock.jsonを読む監査は本番の実体を保証しない。 本番側は既に脆弱性を直した版へ上げていても、ローカルの作業ブランチの更新が本番より遅れていれば「まだ脆弱版のまま」という誤報が続く。直し方は、ホスティング側のAPI(Vercel Deployments API)で本番に実際に乗っているデプロイのgit commit shaを取得し、そのshaでgit show <sha>:package-lock.jsonして取り出した「本番の実体」のlockfileに対してnpm auditをかけること。

症状

ローカルの複数リポジトリを横断して依存の脆弱性を見る日次の監査スクリプトがある。各リポジトリについて、package-lock.json(pnpmならそのロックファイル)を読んでnpm audit --json --package-lock-only --omit=devを実行し、critical/highの脆弱性が無いかを確認する、という作り。GitHub側でDependabotが見ているリポジトリはスキップリストに名前を列挙して対象から外し、Dependabotの無い手元リポだけをこの監査でカバーする設計になっている。

ところがスキップリストに入れたはずの、あるリポジトリで2026-10-28〜30にかけて「Next.jsが脆弱版のまま」という誤報が出た。実際には本番側は既に修正版(next 16.4.1)へ上げ済みだったが、監査は「16.2.7のまま」と報告し続けていた。

原因

このリポジトリは、本番へのデプロイ元ブランチ(deploy/*やfeat/*から都度切って出す運用)と、日常の作業ツリーが置かれているブランチが別だった。監査スクリプトは当然「ローカルにcloneされている作業ツリーのpackage.json/package-lock.json」を読むので、見ているのは「いまローカルで作業中のブランチ」の版であって、「いま本番で動いているデプロイ」の版ではない。両者が一致していれば監査結果は正しいが、作業ブランチの更新が本番の更新より遅れていると、本番は直っていてもローカルは古いまま、という食い違いが生じる。これが10/28〜30の「next 16.4.1でも16.2.7と誤報していた」の直接の原因だった。npm audit自体は渡されたlockfileを正確に読んでいるだけで、ズレていたのは「どのlockfileを渡すか」という前提の方。

直す

このリポジトリだけ、監査対象を「作業ツリーのpackage.json」ではなく「本番に実際に乗っているデプロイのsha」に差し替える関数を追加した。

# 本番デプロイのshaのlockfileを監査する(抜粋・骨格のみ)
pj = json.loads(PROJECT_VERCEL.read_text())  # .vercel/project.json
tok = json.loads(
    (Path.home() / "Library/Application Support/com.vercel.cli/auth.json").read_text()
)["token"]
req = urllib.request.Request(
    f"https://api.vercel.com/v6/deployments?projectId={pj['projectId']}&teamId={pj['orgId']}"
    "&target=production&state=READY&limit=1",
    headers={"Authorization": f"Bearer {tok}"},
)
with urllib.request.urlopen(req, timeout=30) as r:
    d = json.load(r)["deployments"][0]
sha = d["meta"]["gitCommitSha"]

with tempfile.TemporaryDirectory() as tmp:
    for f in ("package.json", "package-lock.json"):
        (Path(tmp) / f).write_text(sh(["git", "-C", str(REPO_DIR), "show", f"{sha}:{f}"]))
    # ここで取り出したlockfileに対してnpm auditをかける
    subprocess.run(["npm", "audit", "--json", "--package-lock-only", "--omit=dev"], cwd=tmp, ...)

ホスティング側のDeployments API(VercelならGET /v6/deployments?...&target=production&state=READY&limit=1)を叩くと、いま本番に実際に乗っているデプロイのメタデータが返り、meta.gitCommitShaにそのデプロイのコミットshaが入っている。そのshaを使ってgit show <sha>:package.json / git show <sha>:package-lock.jsonを実行すれば、ローカルで今どのブランチをcheckoutしているかに関係なく「本番にデプロイされた時点のファイルの中身」をテキストとして取り出せる。これを一時ディレクトリに書き出し、そこを作業ディレクトリにしてnpm audit --json --package-lock-only --omit=devをかければ、監査対象は常に「本番の実体」になる。

変更後、このリポジトリの監査だけ「作業ツリー基準」ではなく「本番デプロイのsha基準」に切り替わった。ローカルの作業ブランチが本番よりどれだけ遅れていても、監査結果は本番で実際に動いている依存バージョンを指すようになる。

再発防止

横断監査スクリプトの元の前提は「ローカルにcloneされたリポジトリの作業ツリー=本番で動いているもの」だった。mainブランチからそのままデプロイする運用のリポジトリでは、これは成立する。問題が起きたのは、デプロイ元ブランチと日常の作業ブランチを意図的に分けている運用のリポジトリだけで、しかもこの前提のズレは監査スクリプト側からは見えない——npm auditはエラーも警告も出さず、渡された作業ツリーの版を正確に報告するだけなので、見ている対象自体が本番とズレていることには気づけない。

複数リポジトリを同じやり方でまとめて見る横断監査のような仕組みを作るときは、個々のリポジトリの運用(デプロイ元ブランチが固定か、作業ブランチと一致する前提が本当に成立するか)を暗黙に仮定しない方がいい。本番の実体を指す基準(デプロイAPIが返すgit commit shaなど)が取れるリポジトリについては、作業ツリーではなくそちらを監査対象にする方が、ブランチ運用の都合に左右されずに済む。

よくある質問

Q1なぜ全リポジトリを本番のshaで監査しないのですか?

ほとんどのリポジトリはmainブランチからそのままデプロイする運用で、ローカルの作業ツリーをmainに合わせておけば本番と一致する。デプロイAPIを呼ぶ分のコストと複雑さを払う必要があるのは、デプロイ元ブランチと日常の作業ブランチが分かれているリポジトリだけ。

Q2作業ツリーを本番のブランチにcheckoutし直せば済む話では?

横断監査は複数リポジトリを続けて機械的に回す前提で、個々のリポジトリで人がcheckoutを切り替える運用には乗らない。しかも監査の瞬間だけ本番ブランチに切り替えると、監査後に元の作業ブランチへ戻す手間と、戻し忘れた場合に別の処理が誤ったブランチを見るリスクが生まれる。checkoutを動かさずにAPI経由でファイルの中身だけ取り出す方が安全。

Q3Vercel以外のホスティングでも同じ直し方ができますか?

本番に実際に乗っているデプロイのgit commit shaを返すAPIがあれば同じ形で直せる。Vercelはmeta.gitCommitShaを返すが、無い場合は本番ホスト側に別の手段(デプロイログやタグ運用)でshaを特定する必要がある。

Q4npm audit自体がおかしかったわけではないのですか?

npm auditは渡されたpackage-lock.jsonを正確に読んで脆弱性を報告しており、動作自体に誤りは無かった。ズレていたのは「どのlockfileを渡すか」という監査側の前提で、本番と作業ツリーが一致しないリポジトリでは渡すファイルそのものが古かった。

確認した環境

  • Vercel Deployments API(`GET /v6/deployments?...&target=production&state=READY&limit=1`)・Vercel CLIのauth.jsonトークンで認証
  • npm audit --json --package-lock-only --omit=dev
  • 2026-10-28〜30に誤報・同時期に本番shaのlockfileを監査する関数を追加して修正

この記事の根拠

  • Pythonファイル 341〜370行目コミット b582334
  • Pythonファイル 373〜411行目コミット b582334

本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。