リンドくんたなべさん、GitHub連携を実装したんですけど、接続は成功してアカウント名も出るのに、非公開リポジトリが全部「見つかりません」になるんです。
たなべそれ、みんな一度は踏むやつだね。
先に結論を言うと、リポジトリ名もスコープも間違っていない。アプリを「認可」しただけで、まだ「インストール」していないんだ。
リンドくん認可とインストールって、別なんですか?同じ画面で終わった気がしてました。
症状から書きます
GitHub App を使った連携を実装すると、次の状態に必ず一度は出会います。
- OAuth の認可は成功する
- ログイン名もアバターも取得できる
- なのに、非公開リポジトリはすべて 404 を返す
401 ではなく 404 です。「権限がありません」ではなく「ありません」と言われます。だから最初は、リポジトリ名の間違いや、ブランチ名の指定ミスを疑うことになります。
原因はそこではありません。
authorization と installation は別のもの
たなべOAuth App しか使ったことがないと、この区別は存在しなかったんだよ。認可すれば、そのユーザーが見えるものはトークンでも見えた。
リンドくんGitHub App だと違うと。
たなべうん。認可は「あなたが誰か」、インストールは「どのリポジトリに触れてよいか」。別々に必要なんだ。
GitHub App には、性質の違う2つの操作があります。
ここが本体
認可(authorization) はユーザートークンを発行します。インストール(installation) はアプリがリポジトリに触れる権限を与えます。認可しただけのトークンが到達できるのは、アプリがインストールされているリポジトリだけです。
OAuth App しか使ったことがない場合、この区別は存在しませんでした。認可すれば、そのユーザーが見られるものはトークンでも見られたからです。GitHub App では違います。
- ユーザーが認可する → 「あなたが誰か」は分かる
- ユーザーがインストールする → 「どのリポジトリを読んでよいか」が決まる
インストールしていなければ、読んでよいリポジトリは0件です。そして GitHub は、アクセス権のないリソースの存在を教えません。存在を伏せるために 404 を返します。 これは仕様として正しい挙動であり、だからこそ原因に見えません。
実装側で必ず起きる帰結
この2段構えは、UIにそのまま表れます。
- 接続ボタンを押す
- 認可が完了し、アカウント名が表示される
- リポジトリ一覧が空になる
ここで「連携は済んでいるのに一覧が空」という、説明のつかない画面ができあがります。利用者から見れば、接続は成功しているのです。
「リポジトリが0件」と「未インストール」を区別する
リンドくん一覧が空なのは分かりましたけど、「リポジトリを1つも持っていない人」と「インストールしていない人」って、コード側からは同じに見えませんか?
たなべそこは別のエンドポイントで聞き分けられる。インストール数を先に数えるんだ。
0件なら、リポジトリの有無に関係なく「まだ何も読めない」状態だと断定できる。
APIレベルでは、この2つは別の質問です。
GET /user/installations
-> インストール数が 0 なら、まだ何も読めない
GET /user/installations/{id}/repositories
-> インストール済みのアカウントで、実際に読めるリポジトリ/user/repos を使ってはいけません。これはユーザーがアクセスできるリポジトリを返すので、アプリがインストールされていないリポジトリまで一覧に出ます。選んだ瞬間に解析が失敗する候補を並べることになります。
必要なのは、ユーザーのアクセス権とアプリのインストールの積集合です。それを返すのが /user/installations/{id}/repositories です。
インストールへの導線を自前で出す
インストール数が0のときは、UIがそれを名指しする必要があります。
GitHubとは接続済みですが、アプリがどのアカウントにもインストールされていません。
非公開リポジトリを解析するにはインストールが必要です。
→ リポジトリへのアクセスを許可するリンク先は https://github.com/apps/<slug>/installations/new です。<slug> はアプリの公開URLの末尾で、アプリ名を小文字化してハイフンでつないだものです。
設定の落とし穴
アプリ登録時の Where can this GitHub App be installed? を「Only on this account」にしていると、訪問者は自分のアカウントにインストールできません。第三者に使ってもらうなら Any account が必須です。
権限は「実装した機能」とだけ対応させる
もう一点、最初から決めておくと後で楽になることがあります。要求する権限を、実装済みの機能に対応するものだけに限ることです。
読み取りしかしないのなら、要求するのは次の2つだけで足ります。
| 権限 | アクセス |
|---|---|
| Contents | Read-only |
| Metadata | Read-only |
これは道義的な話ではなく、実利です。認可画面には要求した権限がそのまま並びます。個人が作ったツールに write 権限を求められて、そのまま許可する開発者はほとんどいません。
接続解除は3種類ある
リンドくん利用者から「アクセスを消してほしい」と言われたら、アプリの「接続を解除」ボタンを案内すればいいですか?
たなべそれだけだと、GitHub 側には何も起きていないんだ。消えるのはこちらが持っているセッションだけ。
本当に権限を切るなら、GitHub 側の Revoke と Uninstall を案内する必要がある。
サポート対応で必ず混乱するので、先に整理しておきます。
| 操作 | 効果 |
|---|---|
| アプリ側の「接続を解除」 | そのブラウザのセッションを消すだけ |
| GitHub の Revoke | ユーザートークンを無効化する |
| GitHub の Uninstall | アプリのリポジトリアクセスを取り除く |
「アクセスを消してほしい」と言われたときに案内すべきなのは、下の2つです。アプリ側のボタンは、アプリが情報を保持しなくなるだけで、GitHub 側には何も起きていません。
まとめ
- 認可とインストールは別。認可だけでは0件
- アクセス権のないリポジトリは 404 になる。401 ではない
/user/reposではなく/user/installations/{id}/repositoriesを使う- インストール数0を検出して、導線を出す
- 権限は実装した機能の分だけ要求する
FRKZ のリポジトリ解析も、この段取りで作られています。公開リポジトリは接続なしで解析できるので、挙動を確かめるだけなら何も接続せずに試せます。


