---
title: "gh search code のインデックスは遅延する"
description: "Renovateプリセットの旧ファイル削除前に `gh search code` で参照残存を確認したら、マージ済みの新参照がまだヒットしてくれませんでした。コード検索インデックスの遅延に気づいた経緯と、Contents API でファイル本文を直接取得して実体検証する手順、検索ベース API への向き合い方を扱います。"
category: "DevOps"
tags: ["GitHub","gh CLI","DevOps","Renovate"]
publishedAt: "2026-06-13"
lastmod: "2026-07-22"
---

[gh CLI](https://cli.github.com/) の [`gh search code`](https://cli.github.com/manual/gh_search_code) は GitHub の[コード検索](https://docs.github.com/en/search-github/github-code-search/about-github-code-search) API をラップしたコマンドで、Org 内の参照を一括で洗い出すときに便利です。ただし検索結果は直近のコミットが反映されない場合があり、削除や移行の最終判断に使うと事故につながります。Contents API でファイル本文を直接取得すれば、検索インデックスの状態に依存せず実体を確認できます。

```bash
# 検索ではなく、ファイル本文を直接取得して grep する
gh api repos/LabeeHive/example-repo/contents/.github/renovate.json5 \
  --jq '.content' | base64 -d | grep -F 'LabeeHive/.github:default.json5'
```

この記事では、Renovate プリセットの移行作業で `gh search code` のインデックス遅延に気づいた経緯、Contents API で本文を取得して参照残存を確認する手順、そしてこの考え方をほかの検索 API にも当てはめる視点を扱います。

## Renovate プリセット移行で最後のクリーンアップに詰まった

ラビー合同会社の GitHub Organization では、Renovate の共通プリセットを `LabeeHive/.github` リポジトリに置いていました。

::card[https://labee.jp/posts/renovate-cloudflare-workers-grouping]

設定の管理を専用リポジトリに分離したくなり、`LabeeHive/renovate-config` を作って共通プリセットを移しました。各リポジトリの `renovate.json5` を新しい参照先に書き換える移行 PR を Org 横断に投入してすべてマージし、最後に旧ファイル本体を消そうとした、というのが今回の状況です。

::card[https://labee.jp/posts/github-api-bulk-pr-migration]

最終ステップは「旧 `LabeeHive/.github/default.json5` を削除する PR を作ってマージする」だけのつもりでした。削除前に Org 内へ旧参照が残っていないことを確認できれば、安全に消せます。

## gh search code で確認しようとして違和感に気づいた

最初に取った確認手段が `gh search code` です。GitHub のコード検索を CLI から叩けるので、Org 全体を1コマンドで横断できます。

```bash
gh search code "LabeeHive/.github:default.json5" \
  --owner LabeeHive --limit 100
```

ここで違和感がありました。すべてのリポジトリの `renovate.json5` を `renovate-config` 側へ書き換える PR をマージし切ったはずなのに、検索結果には旧参照を含むファイルが残って表示されます。差分自体は GitHub の UI でも確認できますし、各リポジトリの `main` ブランチを開けば新しい参照になっています。

検索 API の結果と、リポジトリの最新コミットが一致していません。これは「ファイルがまだ書き換わっていない」のではなく、「コード検索のインデックスが最新コミットを反映していない」状態でした。

## コード検索はインデックスを介する API

GitHub の[コード検索ドキュメント](https://docs.github.com/en/search-github/github-code-search/about-github-code-search)は、コードに対する全文検索の構文と利用条件を説明していますが、インデックス更新の遅延に関する明示的な SLA はありません。それでも、コード検索が内部的にインデックスを構築する仕組みであることは、過去のリリース履歴とコミュニティの議論から追えます。

GitHub は 2020 年の [GitHub Changelog](https://github.blog/changelog/2020-12-17-changes-to-code-search-indexing/) で、コード検索のインデックス対象を「直近 1 年以内に活動のあったリポジトリだけ」に絞ると発表しています。インデックス対象を絞り込めるという仕様は、コード検索が実体のコミットを直接読まず、別途構築されたインデックスを参照する設計だから成立する話です。

実運用での反映までの体感時間は、[GitHub Community Discussion #13516](https://github.com/orgs/community/discussions/13516) で利用者が「push 後にコード検索で新しいコミットがヒットしない」と報告し、GitHub スタッフが「インデックスの更新には時間がかかる」と返している流れから読み取れます。今回のように複数リポジトリへ立て続けに PR をマージした直後だと、旧内容を返してくる場面に遭遇します。

![移行PRをマージした時点でリポジトリの実体は新参照に切り替わり、Contents APIは直後から新しい内容を返す。一方コード検索はインデックスを介するため、インデックスが追いつくまで旧参照を返し続ける。この2つがずれている区間だけ、同じ問いに対する答えが食い違う](/images/posts/gh-search-code-indexing-delay/index-lag.svg)

この性質を知らずに `gh search code` の結果だけを見て「もう参照は残っていない」と判断すると、まだ旧プリセットを extends しているリポジトリがある状態で本体を削除してしまい、次の Renovate 実行で **全リポジトリの設定読み込みが失敗します**。Renovate のジョブが落ちて初めて気づく、という最悪の流れになりかねません。

検索 API は探索や概観の用途には向きますが、「削除しても問題ないか」を判定する最終確認には向きません。今回はこの境界を踏み外しかけたところで違和感に救われました。

## Contents API でファイル本文を実体取得する

検索インデックスに頼らない方法として採用したのが、[GitHub Contents API](https://docs.github.com/en/rest/repos/contents) で `renovate.json5` の本文を直接取得して `grep` する手法です。

```bash
gh api repos/LabeeHive/example-repo/contents/.github/renovate.json5 \
  --jq '.content' | base64 -d
```

Contents API はリポジトリの最新コミットを参照するため、検索インデックスのタイムラグに左右されません。レスポンスの `content` フィールドは base64 エンコードされているので、`base64 -d` でデコードします。これでファイルの本文をそのまま標準出力に流せます。

実運用では、Org の非アーカイブリポジトリを `gh repo list` で列挙してループを回しました。

```bash
gh repo list LabeeHive --no-archived --limit 100 \
  --json name --jq '.[].name' | while read -r repo; do
    body=$(gh api "repos/LabeeHive/${repo}/contents/.github/renovate.json5" \
      --jq '.content' 2>/dev/null | base64 -d 2>/dev/null)
    if echo "$body" | grep -qF 'LabeeHive/.github:default.json5'; then
      echo "HIT: ${repo}"
    fi
  done
```

`renovate.json5` が存在しないリポジトリでは Contents API が **404 を返す**ため、`2>/dev/null` で抑えています。実際に流したところ、残存ヒットはゼロでした。`gh search code` 側でまだ旧参照を返していたリポジトリも、本文を取得すれば新しい参照に置き換わっていることを直接確認できました。

ここまで来て、ようやく旧 `default.json5` の削除 PR を作成・マージしました。

## 本文ベースで「動作に影響するか」を切り分ける

実体検証の結果、コード本文に旧参照は残っていませんでしたが、別のレイヤーで旧文字列が残っているリポジトリはありました。たとえば公開ドキュメントリポジトリ [`LabeeHive/standards`](https://github.com/LabeeHive/standards) の `renovate.md` や `setup.md` は、設定例として旧参照を引用しており、Renovate 本体が読み込むわけではありません。`renovate.json5` のように Renovate が直接読むファイルだけを書き換えれば、ドキュメント側の表記は当時の事実として残しても運用には影響しません。

「どこに何の目的で文字列が登場しているか」を Contents API で本文ごと確認すると、`renovate.json5` のような動作に影響する場所と、ドキュメントのような影響しない場所をはっきり切り分けられます。検索結果の件数だけ見て「まだ残っているからダメ」と判断すると、影響のない参照に振り回されることになります。

## 検索 API を鵜呑みにしない考え方をほかの API にも広げる

今回は GitHub のコード検索でしたが、検索ベースの API は基本的にインデックスを介しています。Slack の検索、Notion の検索、Confluence の検索なども、書き込み直後の内容が即座に反映されるとは限りません。検索結果は「だいたい正しい一覧」を素早く得るのには向きますが、「ここに何かが存在する・しない」の最終判定には不向きです。

実体検証に切り替えるかどうかの判断軸は、操作が不可逆かどうかです。探索や概観（どこを直すべきかの当たりをつける）には検索 API で十分です。一方、削除や本番デプロイ、外部公開のような不可逆な操作の直前は、本文や実体を直接取得して確認します。

GitHub であれば Contents API、Slack なら `conversations.history` のような時系列 API、ファイルシステムなら `find` と `grep` の組み合わせなど、対象ごとに「インデックスを介さずに実体を読む」手段があります。検索 API の便利さに乗りつつ、最終判定だけは実体に降りるという二段構えにしておけば、インデックス遅延のような落とし穴を踏まずに済みます。

## Contents API ベースの確認を運用に取り込むときの注意

Contents API ベースの確認は強力ですが、Rate Limit はそれなりに食います。`gh api` は認証済みリクエストとして扱われるため、認証なしより上限は緩いものの、数百リポジトリを一気に回すと制限に当たります。実行前に `gh api rate_limit` で残量を確認しておき、必要なら `sleep` を挟むか、対象を `--no-archived` や言語フィルターで絞り込みます。

確認スクリプトは捨てコードにせず、移行プロジェクト用のリポジトリに置いておくと再利用が効きます。同じ Org で別のプリセット移行や共通設定の差し替えが起きたとき、対象パスと検索文字列を変えるだけでそのまま使えます。ラビー合同会社では、Renovate プリセットの移行・削除に限らず、共有 GitHub Actions のバージョン固定を剥がす作業など、同じ枠組みを何度か再利用しています。

`gh search code` を最初に叩くこと自体は否定しません。Org を横断して「だいたいここに残っていそう」を素早くつかむのには有効です。そこから一歩踏み込んで Contents API で本文を見るところまでをセットにしておけば、検索インデックスの遅延に振り回されずに済みます。