リンドくんリンドくん

たなべさん、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にそのまま表れます。

  1. 接続ボタンを押す
  2. 認可が完了し、アカウント名が表示される
  3. リポジトリ一覧がになる

ここで「連携は済んでいるのに一覧が空」という、説明のつかない画面ができあがります。利用者から見れば、接続は成功しているのです。

「リポジトリが0件」と「未インストール」を区別する

リンドくんリンドくん

一覧が空なのは分かりましたけど、「リポジトリを1つも持っていない人」と「インストールしていない人」って、コード側からは同じに見えませんか?

たなべたなべ

そこは別のエンドポイントで聞き分けられる。インストール数を先に数えるんだ。
0件なら、リポジトリの有無に関係なく「まだ何も読めない」状態だと断定できる。

APIレベルでは、この2つは別の質問です。

text
GET /user/installations
  -> インストール数が 0 なら、まだ何も読めない

GET /user/installations/{id}/repositories
  -> インストール済みのアカウントで、実際に読めるリポジトリ

/user/repos を使ってはいけません。これはユーザーがアクセスできるリポジトリを返すので、アプリがインストールされていないリポジトリまで一覧に出ます。選んだ瞬間に解析が失敗する候補を並べることになります。

必要なのは、ユーザーのアクセス権とアプリのインストールの積集合です。それを返すのが /user/installations/{id}/repositories です。

インストールへの導線を自前で出す

インストール数が0のときは、UIがそれを名指しする必要があります。

text
GitHubとは接続済みですが、アプリがどのアカウントにもインストールされていません。
非公開リポジトリを解析するにはインストールが必要です。
→ リポジトリへのアクセスを許可する

リンク先は https://github.com/apps/<slug>/installations/new です。<slug> はアプリの公開URLの末尾で、アプリ名を小文字化してハイフンでつないだものです。

設定の落とし穴

アプリ登録時の Where can this GitHub App be installed? を「Only on this account」にしていると、訪問者は自分のアカウントにインストールできません。第三者に使ってもらうなら Any account が必須です。

権限は「実装した機能」とだけ対応させる

もう一点、最初から決めておくと後で楽になることがあります。要求する権限を、実装済みの機能に対応するものだけに限ることです。

読み取りしかしないのなら、要求するのは次の2つだけで足ります。

権限アクセス
ContentsRead-only
MetadataRead-only

これは道義的な話ではなく、実利です。認可画面には要求した権限がそのまま並びます。個人が作ったツールに write 権限を求められて、そのまま許可する開発者はほとんどいません。

接続解除は3種類ある

リンドくんリンドくん

利用者から「アクセスを消してほしい」と言われたら、アプリの「接続を解除」ボタンを案内すればいいですか?

たなべたなべ

それだけだと、GitHub 側には何も起きていないんだ。消えるのはこちらが持っているセッションだけ。
本当に権限を切るなら、GitHub 側の Revoke と Uninstall を案内する必要がある。

サポート対応で必ず混乱するので、先に整理しておきます。

操作効果
アプリ側の「接続を解除」そのブラウザのセッションを消すだけ
GitHub の Revokeユーザートークンを無効化する
GitHub の Uninstallアプリのリポジトリアクセスを取り除く

「アクセスを消してほしい」と言われたときに案内すべきなのは、下の2つです。アプリ側のボタンは、アプリが情報を保持しなくなるだけで、GitHub 側には何も起きていません。

まとめ

  • 認可とインストールは別。認可だけでは0件
  • アクセス権のないリポジトリは 404 になる。401 ではない
  • /user/repos ではなく /user/installations/{id}/repositories を使う
  • インストール数0を検出して、導線を出す
  • 権限は実装した機能の分だけ要求する

FRKZ のリポジトリ解析も、この段取りで作られています。公開リポジトリは接続なしで解析できるので、挙動を確かめるだけなら何も接続せずに試せます。