2 ポイント 投稿者 GN⁺ 2026-01-15 | 1件のコメント | WhatsAppで共有
  • GitHub APIを使っている際、PRコメントのリンク生成機能でIDの不一致によりリンクが機能しない問題が発生
  • 調査の結果、GitHubは GraphQLのnode IDREST APIのdatabase ID という 2つのID体系 を並行して使用していることが判明
  • node IDを base64デコード したところ、下位32ビットにdatabase IDが含まれていることが確認され、単純なビットマスク演算で変換可能
  • 追加分析により、GitHubが MessagePackベースの新しいIDフォーマット文字列ベースのレガシーフォーマット を併用していることが明らかに
  • この構造は GitHub内部のオブジェクト識別体系の二重性 を示しており、開発者はAPI統合時に注意が必要

GitHubの二重ID体系の発見

  • Greptileの AIコードレビュー ツール の機能開発中、GitHubのPRコメントリンクが機能しない問題が発生
    • 保存したコメントIDをURLにつなげたが、クリックしてもGitHubページへ移動しなかった
  • GitHubのドキュメントを確認した結果、GraphQL APIのnode IDREST APIのdatabase ID が異なる体系として存在
    • node IDの例: PRRC_kwDOL4aMSs6Tkzl8
    • database IDの例: 2475899260
  • node IDはGitHub全体でオブジェクトをグローバルに識別するための base64エンコード文字列、database IDは 整数型のURL識別子 として使われる

node IDとdatabase IDの関係の分析

  • 複数のPRコメントのnode IDとdatabase IDを比較した結果、両方の値が一定の間隔で増加 していることを確認
  • node IDのbase64部分をデコードすると 96ビット整数 が生成され、この値の下位32ビットがdatabase IDと一致
    • 例: PRRC_kwDOL4aMSs6Tkzl8 → 下位32ビット = 2475899260
  • 単純な ビットマスク演算 でdatabase IDを抽出可能
    • decoded & ((1 << 32) - 1) の形の演算で変換を実行

GitHubのレガシーIDフォーマット

  • 古いリポジトリ (torvalds/linux) のnode IDをデコードすると、異なる形式の文字列 が現れた
    • 例: MDEwOlJlcG9zaXRvcnkyMzI1Mjk4010:Repository2325298
  • このフォーマットは [オブジェクトタイプ番号]:[オブジェクト名][Database ID] という構造で、明示的な文字列ベースの識別子 である
  • ツリーオブジェクトの場合は 04:Tree2325298:7201bfb9... の形となり、リポジトリIDとSHA値 を一緒に含む
  • GitHubは レガシーフォーマットと新しいフォーマットを並行使用 しており、オブジェクトの種類や作成時点によってフォーマットが異なる

新しいnode IDフォーマットの構造

  • GitHubの GraphQL移行ガイド ではnode IDを不透明な文字列として扱うよう明記しているが、内部構造は存在する
  • base64デコード後に MessagePack でアンパックすると、配列形式のデータが現れる
    • 例: [0, 47954445, 2475899260]
  • 配列の構成
    • 1番目の要素 (0): バージョン識別子と推定
    • 2番目の要素 (47954445): リポジトリのdatabase ID
    • 3番目の要素 (2475899260): オブジェクトのdatabase ID
  • オブジェクトの種類によって配列の長さが異なり、コミットはSHAを含みリポジトリは2要素のみを含む

実用的な活用と結論

  • 新しいnode IDからdatabase IDを抽出するPythonコード例
    import base64, msgpack
    def node_id_to_database_id(node_id):
        prefix, encoded = node_id.split('_')
        packed = base64.b64decode(encoded)
        array = msgpack.unpackb(packed)
        return array[-1]
    
  • この方法で PRコメントのdatabase IDを直接抽出 し、URLリンクの問題を解決できる
  • GitHubは現在 MessagePackベースの新しいID体系と文字列ベースのレガシー体系 を同時に維持している
  • この構造は GitHub内部の移行過程と互換性維持の取り組み を示しており、APIを使う開発者はIDフォーマットの違いに注意する必要がある

1件のコメント

 
GN⁺ 2026-01-15
Hacker Newsのコメント
  • 最新の GitHub グローバルノードID'X-Github-Next-Global-ID' ヘッダーで強制的に使用できる
    ID はオブジェクトの 型プレフィックス と base64 でエンコードされた msgpack ペイロードで構成される
    たとえば私のユーザーID "U_kgDOAAhEkg"[0, 541842] にデコードされ、これは REST API の databaseId と一致する
    ただし、このような内部実装に依存せず、GraphQL API の databaseId フィールドを直接参照するのが望ましい
    関連ドキュメント: GraphQL グローバルノードID移行ガイド, 私の GitHub ユーザー情報, CyberChef デコード例, GitHub ETag 実装

  • このようにデコードするのは 脆い と思う
    GraphQL のグローバルノードIDは本来 不透明(opaque) であるべきだ
    GitHub の多くの型(PullRequest など)は databaseId フィールドを提供しているので、それを使うべき
    ほとんどの GraphQL API は型名と DB ID を base64 エンコードしているが、この規則が常に維持される保証はない
    参考: PullRequest オブジェクトのドキュメント, GraphQL グローバルID仕様

    • GitHub の GraphQL 型には permalinkurl のようなフィールドや UniformResourceLocatable インターフェースがあり、URL を自前で組み立てる必要はない
    • このような内部構造は 時間がたてば壊れる可能性 が高い
      だからこそ API は permalink を提供している。ID やリンクのパターンはいつでも変わり得る
    • 識別子にメタデータを入れたいなら、ユーザーが内部構造に依存しないよう 暗号化 するのがよい
      この手法は pagination トークンでもよく使われる
  • 010:Repository2325298 のような ID には明確な構造がある
    010 は型 enum、Repository は名前、2325298 は DB ID だ
    つまり 長さプレフィックス(length prefix) 形式になっている。Repository は 10 文字、Tree は 4 文字だ

    • BitTorrent プロトコルを思い出す
    • ほとんど URN のように見える
  • Opus 4.5 はこの GitHub ID デコードのトリックを知っており、自動でデコードコードを書いてくれる

  • 投稿者が発見した内容は技術的には正しいが、文書化されておらずサポートもされていない
    GitHub は過去にもノードIDの内部構造をひそかに変更したことがある
    MessagePack 配列にフィールドを追加したり、エンコードを変えたり、暗号化したり、UUID ベースに変えたりすれば、
    この内部構造に依存したシステムは即座に壊れる

  • 私が明示的に保存している GitHub 識別子は 不変URLキー(issue/pr 番号やコミットハッシュ) くらいだ
    コメントIDは JSON blob の中にそのまま入れている
    何でも正規化しようとする必要はない。JSON は十分に速い
    コメント単位で横断クエリをしない限り、性能問題として表面化することはほとんどない

    • ただし issue/pr の URL は 不変ではない
      リポジトリが名前を変えたり別の組織へ移されたりすると URL も変わり得る
  • 昔の v3 API には ID がなかったので、誰かがユーザー名やリポジトリ名を変えると、誰なのか追跡するのが難しかった
    そのため私は チーム単位の所有権管理システム を自作した
    Terraform provider の出来がいまひとつで、オフボーディング時に「唯一の管理者だった人が抜けた」といった問題が頻発していたからだ
    すべてのリポジトリはチームが所有し、アクセス権もチーム単位でのみ付与する

    • 「ユーザーにアクセス権を与える」のではなく、「チームに権限を与え、ユーザーはそのチームの一員である」と考えるほうがはるかに効率的だ
      この チームベースのアクセス制御 は GitHub に限らず他のシステムでも有用だ
  • Hyrum’s Law の典型例だ — 人々が文書化されていない挙動に依存し始めると、結局は壊れる

  • データベース設計では、通常は外部には 不透明な自然キー を提供し、内部では連番の整数IDを使う

    • 理由は主に 2 つある
      1. 外部にオブジェクト数を露出しないため
      2. ID を単純に増やすだけで全オブジェクトを走査できないようにするため
        ただし複合IDを使えば、こうした問題は緩和できる。
        たとえばリポジトリIDの中にオブジェクトIDが含まれていれば、ID を増やしても同じリポジトリ内のオブジェクトしか探索できない
        そこに エントロピーやタイムスタンプ を混ぜれば、悪用はほぼ不可能になる
    • しかし自然キーは 変更され得る
      そのため、意味を持たない 代理キー(surrogate key) を公開するほうが安全だ
      たとえば YouTube は内部でインデックス番号を使っていても、外部には意味のないコード形式の ID を提供している
  • GitHub チームがここ数年 Rails に sharded / multi-database 対応 を大幅に拡張してきた理由が、これで理解できる