CI/CD
PR

GitHub App トークンの権限不足は「エラー」ではなく「0 件」で返る

saratogax
記事内に商品プロモーションを含む場合があります

GitHub Actions で PAT(Personal Access Token)を GitHub App トークンに置き換える作業をしていて、こんな事象に出会いました。

ラベルで PR を絞り込むワークフローが、エラーも出さず正常終了するのに、何も処理しない

gh pr list --label "auto-update" --json number

PAT なら該当 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 が blockedplatform というラベル付きでクローズされている点です。

つまり 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-tokenpermission-* 入力で issues: read を指定しても、App に Issues 権限が付いていなければ通りません。

App 側の権限追加は組織のオーナーによる承認が必要なので、そこも作業計画に織り込む必要があります。

権限の削除は即時反映されますが、追加は承認待ちになります。

もう一つの罠:CI 状態の読み取りは 3 点セット

同じ構造の落とし穴が、CI の状態を読むときにもあります。

PR の CI が通っているかを判定する処理です。

gh pr view 123 --json statusCheckRollup

これを App トークンで動かすには、3 つの権限がすべて必要です。

権限対応する対象
checks: readCheck Run(GitHub Actions 等が作る新方式)
statuses: readStatus 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 に置き換えられる」前提で計画すると、最後に詰まります。

移行時のチェックリスト

ここまでを踏まえた実務手順です。

  1. ラベルを扱うなら issues: read(書くなら write)を足すpull-requests だけでは足りません
  2. CI 状態を読むなら checks / statuses / actions の 3 点セット
  3. 迷ったら X-Accepted-GitHub-Permissions ヘッダで確認する。推測しない
  4. App 自体の権限を先に確認する。トークン発行時に要求できるのは App 権限の範囲内だけ。追加はオーナー承認待ち
  5. 「0 件」を成功と信じない。移行後の検証では、対象が 1 件以上ある状態で実行して件数を比較する

5 番が最も実践的です。

移行の動作確認を「ワークフローが緑になったか」で済ませると、この故障は必ず見逃します。

置き換え前と後で、取得件数が一致するかを見るのが確実です。

そのために、検証時はあえて対象が存在する状態を作っておくとよいでしょう。

まとめ

  • gh pr list --label には issues 権限が必要。すべての PR は Issue であり、ラベルは Issues API 側の概念だから
  • 権限不足はエラーではなく 0 件で返る。CI は緑のまま何も処理しない
  • この挙動は GitHub CLI 側ではなくプラットフォーム側の問題として扱われている
  • statusCheckRollupchecks / statuses / actions の 3 点セット。Check Run と Commit Status の 2 系統が混在するため
  • 必要な権限は X-Accepted-GitHub-Permissions ヘッダで確認できる
  • 別組織のプライベートリポジトリには App トークンは原理的に届かない

権限スコープは機能の見た目ではなく、API の実装単位に沿って切られています。

「PR の操作だから pull-requests だけ」と考えると足をすくわれる、というのが今回の教訓でした。

あわせて読みたい(GitHub Actions 運用)

ABOUT ME
saratoga
saratoga
フリーランスエンジニア
仕事にも趣味にも IT を駆使するフリーランスエンジニア。技術的な TIPS や日々の生活の中で深堀りしてみたくなったことを備忘録として残していきます。
記事URLをコピーしました