- GitHub APIを使っている際、PRコメントのリンク生成機能でIDの不一致によりリンクが機能しない問題が発生
- 調査の結果、GitHubは GraphQLのnode ID と REST 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 ID と REST 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をデコードすると、異なる形式の文字列 が現れた
- 例:
MDEwOlJlcG9zaXRvcnkyMzI1Mjk4 → 010: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要素のみを含む
実用的な活用と結論
1件のコメント
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仕様
permalink、urlのようなフィールドやUniformResourceLocatableインターフェースがあり、URL を自前で組み立てる必要はないだからこそ API は permalink を提供している。ID やリンクのパターンはいつでも変わり得る
この手法は pagination トークンでもよく使われる
010:Repository2325298のような ID には明確な構造がある010は型 enum、Repositoryは名前、2325298は DB ID だつまり 長さプレフィックス(length prefix) 形式になっている。Repository は 10 文字、Tree は 4 文字だ
Opus 4.5 はこの GitHub ID デコードのトリックを知っており、自動でデコードコードを書いてくれる
投稿者が発見した内容は技術的には正しいが、文書化されておらずサポートもされていない
GitHub は過去にもノードIDの内部構造をひそかに変更したことがある
MessagePack 配列にフィールドを追加したり、エンコードを変えたり、暗号化したり、UUID ベースに変えたりすれば、
この内部構造に依存したシステムは即座に壊れる
私が明示的に保存している GitHub 識別子は 不変URLキー(issue/pr 番号やコミットハッシュ) くらいだ
コメントIDは JSON blob の中にそのまま入れている
何でも正規化しようとする必要はない。JSON は十分に速い
コメント単位で横断クエリをしない限り、性能問題として表面化することはほとんどない
リポジトリが名前を変えたり別の組織へ移されたりすると URL も変わり得る
昔の v3 API には ID がなかったので、誰かがユーザー名やリポジトリ名を変えると、誰なのか追跡するのが難しかった
そのため私は チーム単位の所有権管理システム を自作した
Terraform provider の出来がいまひとつで、オフボーディング時に「唯一の管理者だった人が抜けた」といった問題が頻発していたからだ
すべてのリポジトリはチームが所有し、アクセス権もチーム単位でのみ付与する
この チームベースのアクセス制御 は GitHub に限らず他のシステムでも有用だ
Hyrum’s Law の典型例だ — 人々が文書化されていない挙動に依存し始めると、結局は壊れる
データベース設計では、通常は外部には 不透明な自然キー を提供し、内部では連番の整数IDを使う
ただし複合IDを使えば、こうした問題は緩和できる。
たとえばリポジトリIDの中にオブジェクトIDが含まれていれば、ID を増やしても同じリポジトリ内のオブジェクトしか探索できない
そこに エントロピーやタイムスタンプ を混ぜれば、悪用はほぼ不可能になる
そのため、意味を持たない 代理キー(surrogate key) を公開するほうが安全だ
たとえば YouTube は内部でインデックス番号を使っていても、外部には意味のないコード形式の ID を提供している
GitHub チームがここ数年 Rails に sharded / multi-database 対応 を大幅に拡張してきた理由が、これで理解できる