GitHub App トークンの権限不足は「エラー」ではなく「0 件」で返る
GitHub Actions で PAT(Personal Access Token)を GitHub App トークンに置き換える作業をしていて、こんな事象に出会いました。
ラベルで PR を絞り込むワークフローが、エラーも出さず正常終了するのに、何も処理しない。
gh pr list --label "auto-update" --json numberPAT なら該当 PR が返ってくるのに、App トークンだと [] が返る。
ワークフローは緑のまま「対象なし」として終わるので、壊れていることに気づけません。
原因は権限不足でした。
ただし不足していたのは pull-requests ではなく、issues です。
本記事では、この直感に反する権限依存の理由と、権限不足がサイレント故障になる構造、そして推測せずに必要な権限を調べる方法をまとめます。
なぜ PR のラベルに issues 権限が必要なのか
PR を扱う操作なのに issues が要る、というのは一見おかしく見えます。
しかし GitHub のデータモデルを踏まえると筋が通っています。
公式ドキュメントの表現がそのまま答えです。
「すべての Pull Request は Issue であるが、すべての Issue が Pull Request とは限らない」(Every pull request is an issue, but not every issue is a pull request.)
そのため、Issue と PR で共通して使える操作は Issues API 側に置かれています。
具体的には次の 3 つです。
- ラベル(labels)
- アサイニー(assignees)
- マイルストーン(milestones)
エンドポイントの形を見ると、より納得できます。
POST /repos/{owner}/{repo}/issues/{issue_number}/labels
DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{name}
PR にラベルを付ける操作でも、パスは /issues/ です。
つまり権限スコープは「機能の見た目」ではなく「API の実装単位」に対応しているということです。
ここを理解しておくと、権限設計での勘違いが減ります。
なぜエラーにならず 0 件になるのか
権限が足りないなら 403 Forbidden で落ちてほしいところです。
ところが実際は、正常なレスポンスとして空の結果が返ります。
この挙動は GitHub CLI の Issue として報告されています。
報告内容は、フル権限なら 42 件返るクエリが、Issues 権限のない App トークン(Pull requests は read あり)だと常に 0 件になる、というものです。
注目したいのは、この Issue が blocked と platform というラベル付きでクローズされている点です。
つまり GitHub CLI 側の不具合ではなく、GitHub プラットフォーム側の挙動として扱われています。
CLI の更新を待っても解決しない、ということです。
手前で権限を正しく渡すしかありません。
この故障が厄介な理由
権限不足がエラーで返るなら、CI が赤くなって気づけます。
しかし 0 件で返ると、次のような経路で何も起きないまま成功扱いになります。
- ラベル付き PR を探して一括マージするワークフロー → 対象 0 件なのでマージせず終了
- 特定ラベルの PR をクローズする定期処理 → 何もクローズせず終了
- ラベルで対象を絞ってリリースノートを生成 → 空のノートを生成
いずれもログには「該当 0 件」と出るだけで、権限の話は一言も出てきません。
移行直後に「たまたま対象が無かった日」だと思い込むと、発覚がさらに遅れます。
私のケースでは、レビューで指摘されて初めて気づきました。
推測せずに必要な権限を調べる
「この操作にはどの権限が必要か」を勘で決めるのは危険だと分かりました。
GitHub は確認手段を用意しています。
X-Accepted-GitHub-Permissions ヘッダを見る
最も確実なのがこれです。
REST API のレスポンスには X-Accepted-GitHub-Permissions というヘッダが含まれ、そのエンドポイントに必要な権限が書かれています。
gh api -i /repos/OWNER/REPO/issues/1/labels 2>&1 \
| grep -i "x-accepted-github-permissions"ドキュメントを探すより速く、しかも実物のエンドポイントに対する答えが得られます。
発行したトークンの権限を確認する
App トークンを発行したとき、レスポンスには permissions オブジェクトが含まれます。
意図した権限が実際に乗ったかどうかは、ここを見れば分かります。
ここで重要な制約があります。
トークン発行時に要求できる権限は、App 自体に付与された権限の範囲内だけです。
actions/create-github-app-token の permission-* 入力で issues: read を指定しても、App に Issues 権限が付いていなければ通りません。
App 側の権限追加は組織のオーナーによる承認が必要なので、そこも作業計画に織り込む必要があります。
権限の削除は即時反映されますが、追加は承認待ちになります。
もう一つの罠:CI 状態の読み取りは 3 点セット
同じ構造の落とし穴が、CI の状態を読むときにもあります。
PR の CI が通っているかを判定する処理です。
gh pr view 123 --json statusCheckRollupこれを App トークンで動かすには、3 つの権限がすべて必要です。
| 権限 | 対応する対象 |
|---|---|
| checks: read | Check Run(GitHub Actions 等が作る新方式) |
| statuses: read | Status Context(旧 Commit Status API 方式) |
| actions: read | ワークフロー実行の情報 |
理由は statusCheckRollup という名前のとおりで、これが複数方式の CI 状態を「まとめた」ビューだからです。
GitHub には CI 状態を表す仕組みが 2 系統あります。
新しい Check Run と、古い Commit Status です。
1 つの PR に両方が混在しうるため、片方の権限しか無いと結果が欠けます。
そして厄介なことに、ここでも欠けたぶんはエラーにならず、単に見えないだけです。
「CI が全部通っている」と誤判定してマージする、という事故につながります。
公式ドキュメントにも「一部のエンドポイントは複数の権限を要求する」と明記されています。
直感で 1 つに絞らないほうが安全です。
App では届かない場所もある
権限の話をもう一つ。
これは設定でどうにもならない、構造上の限界です。
GitHub App は「どの組織のどのリポジトリにインストールするか」という単位で存在します。
そのため、別の組織のプライベートリポジトリには、原理的に到達できません。
私が遭遇したのは、go.mod が別組織のプライベートモジュールに依存しているリポジトリでした。
自組織にインストールした App のトークンでは、その依存を取得できません。
権限を足せば解決する話ではなく、App のインストール範囲そのものの問題です。
PAT を撲滅する計画を立てるときは、こういうApp で代替できないケースを例外として洗い出しておく必要があります。
「全部 App に置き換えられる」前提で計画すると、最後に詰まります。
移行時のチェックリスト
ここまでを踏まえた実務手順です。
- ラベルを扱うなら
issues: read(書くなら write)を足す。pull-requestsだけでは足りません - CI 状態を読むなら
checks/statuses/actionsの 3 点セット - 迷ったら
X-Accepted-GitHub-Permissionsヘッダで確認する。推測しない - App 自体の権限を先に確認する。トークン発行時に要求できるのは App 権限の範囲内だけ。追加はオーナー承認待ち
- 「0 件」を成功と信じない。移行後の検証では、対象が 1 件以上ある状態で実行して件数を比較する
5 番が最も実践的です。
移行の動作確認を「ワークフローが緑になったか」で済ませると、この故障は必ず見逃します。
置き換え前と後で、取得件数が一致するかを見るのが確実です。
そのために、検証時はあえて対象が存在する状態を作っておくとよいでしょう。
まとめ
gh pr list --labelにはissues権限が必要。すべての PR は Issue であり、ラベルは Issues API 側の概念だから- 権限不足はエラーではなく 0 件で返る。CI は緑のまま何も処理しない
- この挙動は GitHub CLI 側ではなくプラットフォーム側の問題として扱われている
statusCheckRollupはchecks/statuses/actionsの 3 点セット。Check Run と Commit Status の 2 系統が混在するため- 必要な権限は
X-Accepted-GitHub-Permissionsヘッダで確認できる - 別組織のプライベートリポジトリには App トークンは原理的に届かない
権限スコープは機能の見た目ではなく、API の実装単位に沿って切られています。
「PR の操作だから pull-requests だけ」と考えると足をすくわれる、というのが今回の教訓でした。
あわせて読みたい(GitHub Actions 運用)
- 【GitHub Actions】PAT を GitHub App トークンに置き換える手順と 3 つの落とし穴(本記事の前提になる置き換え手順)
- GitHub 組織内の全リポジトリを横断検索する方法と、gh search code の結果には漏れがある話(同じ「エラーを出さずに空を返す」型の罠)
- gr2m/create-or-update-pull-request-action から gh CLI へ移行する時に踏む落とし穴 7選(gh CLI 移行そのものの落とし穴)
