GitHub 組織内の全リポジトリを横断検索する方法と、gh search code の結果には漏れがある話
「組織内のどのリポジトリでこの文字列が使われているか調べたい」という場面があります。
古いシークレット名、廃止予定のライブラリ、非推奨の設定キー。
これを置き換えるにしても、まず「どこにあるのか」を知る必要があります。
そこで便利なのが、GitHub の横断コード検索です。
# GitHub CLI で組織全体を検索する
gh search code --owner YOUR_ORG "OLD_SECRET_NAME"
# ファイル種別を絞る場合
gh search code --owner YOUR_ORG --filename "*.yaml" "OLD_SECRET_NAME"ローカルに全リポジトリを clone しなくても、組織全体を一発で見渡せます。
ブラウザの検索画面でも同じことができます。
org:YOUR_ORG OLD_SECRET_NAME
私もこれで棚卸し表を作り、対象は 41 リポジトリ・128 ファイルだと結論しました。
ところが実際に作業を進めると、棚卸し表に載っていない参照が次々と見つかりました。
最終的に 5 件です。
いずれも各リポジトリでローカル grep をかけたときに発覚しました。
本記事では、なぜ横断検索が漏れるのかという構造的な理由と、それでも漏れを潰しきるための手順をまとめます。
GitHub のコード検索には仕様上の制約がある
まず押さえておきたいのは、これがバグではなくドキュメントに明記された仕様だという点です。
公式ドキュメントから、棚卸しに影響する制約を拾います。
| 制約 | 棚卸しへの影響 |
|---|---|
| デフォルトブランチのみ | feature ブランチ・タグ・未マージ PR の変更は見えない |
| 384 KB 未満のファイルのみ | 巨大な設定ファイル・生成物が対象外になる |
| プライベートは 4,000 リポジトリ上限 | 大規模組織では一部が検索対象から外れる |
| 50 万ファイル超のリポジトリは対象外 | モノレポが丸ごと抜ける |
| 1 年以上活動が無いリポジトリは対象外 | 放置された古いリポジトリが抜ける |
| 結果は 100 件(5 ページ)まで | ヒット数が多いと単純に打ち切られる |
| タイムアウト時は部分結果を返す | 全件のつもりが一部だけ、と気づけない |
公式ドキュメントには「すべてのコードがインデックスされているわけではない」(not all code is indexed)とも書かれています。
つまり横断検索は「該当するすべて」を返すことを保証していません。
incomplete_results という静かな警告
特に気をつけたいのが、タイムアウト時の挙動です。
検索がタイムアウトすると、API はそこまでに見つかった分だけを返し、レスポンスの incomplete_results を true にします。
エラーにはなりません。
CLI 越しに使っていると、このフラグは目に入らないまま結果だけが表示されます。
「128 ファイル見つかった」という数字が全件なのか途中なのか、区別できないわけです。
API を直接叩くなら、ここを確認する価値があります。
gh api "/search/code?q=OLD_SECRET_NAME+org:YOUR_ORG" \
--jq '{total: .total_count, incomplete: .incomplete_results}'実際に漏れたケース
私が踏んだ 5 件の漏れには、共通した傾向がありました。
いずれも普段あまり実行されないワークフローのファイルでした。
リリース時にだけ動くもの、手動起動専用のもの、特定条件でしか呼ばれないもの。
また、棚卸しを実施した後に追加されたファイルもありました。
これは検索の制約というより、単純に時間差の問題です。
数週間かかる作業では、棚卸し表そのものが作業中に古くなります。
この観点は見落としやすいので、強調しておきたいところです。
「最初に完璧な一覧を作れば後は消化するだけ」という前提が崩れます。
対策:横断検索は当たりをつける用途に限る
実務上の結論はシンプルです。
横断検索は「どのリポジトリを触るか」を絞る用途に留め、確定はローカル grep で行う。
手順にすると次のようになります。
- 横断検索で候補リポジトリを洗う。ここで得た一覧は「少なくともこれは対象」という目安として扱う
- 各リポジトリの作業時にローカル
grepを必ず打つ。棚卸し表と件数が合うか照合する - 最終確認は全リポジトリを一時 clone して洗い直す。組織の規模次第だが、これが唯一の確定手段
2 番を「作業手順に組み込む」ことが肝心です。
棚卸し表を信じて機械的に置換していくと、表に無いものは永遠に見つかりません。
逆に各リポジトリで一度 grep する運用にしておけば、漏れはその場で拾えます。
私の 5 件はすべてこの経路で見つかりました。
仕上げのダミー値差し替え
もう一つ有効だったのが、置換対象そのものを壊してみる方法です。
シークレット名の移行であれば、移行完了と判断した時点で元のシークレットの値をダミーに差し替えます。
まだ参照が残っていれば、そのワークフローは失敗します。
削除ではなくダミー化にするのがポイントです。
削除だと参照側が「空文字」として動いてしまい、失敗せずに誤動作する可能性があります。
ダミー値なら認証エラー等で明示的に落ちるので、残存を検出できます。
ただしこれは定期実行や次回リリースを待つことになるため、時間はかかります。
最後の関門として置くのが現実的です。
もう一つの盲点:イベントの「消費側」
ここまでは「置換対象の文字列を探す」話でした。
しかし grep の範囲を誤る、もっと厄介なパターンがあります。
置換対象の文字列を含んでいないのに、影響を受けるファイルです。
私が踏んだ実例を説明します。
あるリポジトリで、リリースを作成するワークフローのトークンを置き換えました。
ここで GITHUB_TOKEN を使うと、そのリリース作成では後続のワークフローが発火しません。
GitHub Actions の仕様で、GITHUB_TOKEN による操作は新たなワークフロー実行を引き起こさないためです。
問題は、そのリポジトリに on: release で起動する別のワークフローが存在したことです。
パッケージを公開する処理でした。
つまり置換によって、公開処理が静かに動かなくなる状態でした。
なぜ grep で見つからないのか
この npm-publish 側のワークフローは、置換対象のトークンを一切使っていませんでした。
だからトークン名で grep しても、絶対に引っかかりません。
影響を受ける側なのに、検索キーワードを含んでいないのです。
これに気づいたのはレビューでの指摘でした。
自分の grep では原理的に見つけられない類の漏れです。
対策:生成するイベントの消費側を探す
教訓は、「そのワークフローが何を生み出すか」から逆に検索することです。
リリースを作るワークフローを触るなら、on: release を探す。
PR を作るなら on: pull_request、push するなら on: push を探す。
# 置換対象のトークン名で探す(従来のやり方)
grep -rn "OLD_TOKEN_NAME" .github/workflows/
# 生成するイベントの消費側も探す(見落としがちな方)
grep -rn -A 3 "^on:" .github/workflows/ワークフロー同士がイベントで連鎖している構造を把握してから作業する、ということです。
この考え方は GitHub Actions に限りません。
イベント駆動の仕組みを触るときは常に、発行側だけでなく購読側も探す必要があります。
メッセージキュー、Webhook、Pub/Sub なども同じ構造です。
「使われていないから消してよい」の罠
関連して、もう一つ注意点があります。
棚卸しの過程で「このリポジトリはもう使っていない」と判断する場面が出てきます。
ここで気をつけたいのが、リポジトリの利用終了と成果物の利用終了は別だということです。
私のケースでは、開発が止まっているリポジトリが公開しているパッケージを、現役のプロジェクトが依存していました。
リポジトリ自体は誰も触っていないのに、公開の連鎖は維持しなければならない状態です。
「開発が止まっている」を「消してよい」と読み替えると事故になります。
成果物の消費先を確認してから判断してください。
まとめ
- GitHub のコード検索はデフォルトブランチのみ・384 KB 未満・プライベート 4,000 件上限などの制約があり、全件を保証しない
- タイムアウト時は部分結果を返して
incomplete_results: trueにするだけ。エラーにならない - 横断検索は候補を絞る用途に留め、確定は各リポジトリのローカル
grepで行う - 数週間かかる作業では棚卸し表そのものが古くなる。作業手順に
grepを組み込む - 仕上げはダミー値差し替え。削除ではなくダミー化にして、残存を明示的に失敗させる
- 置換対象の文字列を含まないのに影響を受けるファイルがある。生成するイベントの消費側を探す
- リポジトリの利用終了 ≠ 成果物の利用終了。消す前に消費先を確認する
横断検索は強力ですが、返ってきた一覧は「これで全部」ではなく「少なくともこれだけはある」というものでした。
そして本当に厄介な漏れは、検索キーワードを含んでいないファイルのほうにありました。
あわせて読みたい(GitHub Actions 運用)
- GitHub App トークンの権限不足は「エラー」ではなく「0 件」で返る(権限不足でも 0 件になるので検索漏れと紛らわしい)
- 【GitHub Actions】PAT を GitHub App トークンに置き換える手順と 3 つの落とし穴(GITHUB_TOKEN では後続ワークフローが発火しない件の詳細)
- gr2m/create-or-update-pull-request-action から gh CLI へ移行する時に踏む落とし穴 7選(gh CLI の挙動でつまずく箇所のまとめ)
