← ブログ一覧

PR作成権限オフで自動マージが再び死んだ話

自動マージ救済ジョブがgh pr createのGraphQLエラーで再び止まった実例。原因はリポジトリ設定にあり、PRを経由せずmasterへ直接マージする設計への変更diffを実エラー文言つきで解説する。


この記事の結論: gh pr create頼みの自動化は、リポジトリ設定の「Allow GitHub Actions to create and approve pull requests」がOFFだと問答無用で失敗する。PRを経由せずcontents:write権限だけでmasterへ直接マージする設計にしておけば、この設定に振り回されずに済む。

リナ社長(高校生社長のやり手ギャル)× タクヤ(入社3年目)— 今回は、直したはずの自動マージが2日後に別の理由でまた死んでた回。(登場人物について

リナ社長「タクヤ、これ見て。auto-merge-branch-pushのジョブ、また赤くなってるじゃん。この前直したばっかだよね?」

タクヤ「7月6日のe1c0418ですね。あれでpushイベント側の救済ジョブは追加できたはずなんですが……ログ見ると、今度はgh pr create自体がコケてます」

リナ社長「え、前回はpull_requestイベントが発火しない話だったよね?入り口の問題は塞いだのに、今度は中身が動かないってこと?」

タクヤ「はい、そこがポイントです。前回とは別の壁に当たってます」


前回のおさらいと今回の違い

このブログの自動投稿パイプラインは.github/workflows/auto-merge-blog.ymlが担っていて、ブログ記事だけの変更ならmasterへ自動マージしCloudflareへデプロイする。前回の記事(github-token-auto-merge-trap.md)では、GITHUB_TOKENで作ったPRがpull_requestイベントを発火しないGitHubの再帰防止仕様に引っかかり、自動マージが無言で止まっていた事件を扱った。その対策として、7月6日20:43のコミットe1c0418で、pull_requestイベントの発火を待たずにpushイベント単体で完結するauto-merge-branch-pushジョブを追加していた。設計は「gh pr createでPRを作りgh pr merge --squash --delete-branchでマージする」というものだった。

ところが2日後の7月7日、このジョブが実際に走った際、gh pr createが以下のエラーで失敗することが発覚した。

GraphQL: GitHub Actions is not permitted to create or approve pull requests

リナ社長「これ、前回の『イベントが発火しない』話と何が違うの?」

タクヤ「前回はワークフロー自体が起動しなかった、いわば起動口の問題でした。今回はワークフローはちゃんと起動してgh pr createのコマンドも実行されてるんですが、GitHub側のGraphQL APIがPR作成そのものを権限エラーで拒否してます。原因はリポジトリ設定の『Allow GitHub Actions to create and approve pull requests』がOFFになっていたことでした。同じワークフローファイルなのに、原因のレイヤーが全然違います」

対策: PRという概念自体を経由しない

リナ社長「じゃあその設定、ONにすればいいだけじゃないの?」

タクヤ「それも選択肢でしたが、リポジトリ設定に依存する構成のままだと、また別の理由で同じ穴にハマるリスクが残ります。なので今回は、gh pr creategh pr mergeを一切使わない構成に作り替えました」

実際の変更は次の通り。Beforeはgh pr createでPRを作り、gh pr merge --squash --delete-branchでマージする2段構えだった。

      - name: ブログ記事のみの変更ならPR作成して自動マージ
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          BRANCH: ${{ github.ref_name }}
          REPO: ${{ github.repository }}
        run: |
          git fetch origin master
          files=$(git diff --name-only "origin/master...HEAD")
          # (中略: 差分チェック)
          open_prs=$(gh pr list --repo "$REPO" --head "$BRANCH" --state open --json number --jq length)
          if [ "$open_prs" != "0" ]; then
            echo "openなPRが既にあります。PR経路に任せます。"
            exit 0
          fi
          subject=$(git log -1 --format=%s HEAD)
          gh pr create --repo "$REPO" --base master --head "$BRANCH" \
            --title "$subject" \
            --body "claude/** ブランチへの直接push(ブログ記事のみの変更)を自動マージ経路に載せるため、workflowが自動作成したPR。"
          gh pr merge --repo "$REPO" --squash --delete-branch "$BRANCH"

Afterはgit merge --squashgit commitでローカルに1コミット作り、それを直接masterへpushする1段構えに変わった。

      - name: ブログ記事のみの変更ならmasterへ直接マージ
        env:
          BRANCH: ${{ github.ref_name }}
        run: |
          git fetch origin "$BRANCH"
          files=$(git diff --name-only "origin/master...origin/$BRANCH")
          # (中略: 差分チェック)

          # エージェント自身がPRを作成済みならPR経路(auto-mergeジョブ)に任せる(二重マージ防止)
          GH_TOKEN="${{ secrets.GITHUB_TOKEN }}"
          open_prs=$(GH_TOKEN="$GH_TOKEN" gh pr list --repo "${{ github.repository }}" --head "$BRANCH" --state open --json number --jq length 2>/dev/null || echo 0)
          if [ "$open_prs" != "0" ]; then
            echo "openなPRが既にあります。PR経路に任せます。"
            exit 0
          fi

          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"

          subject=$(git log -1 --format=%s "origin/$BRANCH")
          # squash相当: ブランチの変更内容をmaster上に1コミットとして取り込む
          git merge --squash "origin/$BRANCH"
          git commit -m "$subject"
          git push origin HEAD:refs/heads/master
          git push origin --delete "$BRANCH" || echo "ブランチ削除に失敗しましたが、マージは完了しています。"

タクヤgit merge --squashgit commitでローカルに1コミット作って、それをgit push origin HEAD:refs/heads/masterでmasterへ直接送るだけです。このauto-merge-branch-pushジョブに限って言えば、PRという概念を一度も経由しないので、pull-requests:write権限もリポジトリの『PR作成許可』設定も一切要らなくなりました。contents:writeだけで完結します。ファイル全体で見ると、PR経由の従来経路(auto-mergeジョブ)はまだpull-requests: writeを使ってるので、権限ブロック自体は変えてません」

リナ社長「なるほどね……で、それウチの何が変わるの?権限周りが綺麗になった、以上の話がある?」

タクヤ「あります。同じauto-merge-blog.ymlが、前回とは全く別の原因で2回連続で止まったんです。つまり自動化は1回直したら終わりじゃなくて、起動口とアクセス権限、実行内容の権限みたいに複数のレイヤーに落とし穴があるってことです。今回の直しでそのうち1レイヤー分の依存を丸ごと消せたのは大きいです」

リナ社長「それは分かる。私が刺さったのは別のとこで、発覚が7月7日で改修が7月8日23:37でしょ。1日ちょいで直せてるのは普通に速いと思う。あと、この構成なら別のリポジトリに同じ自動投稿の仕組みを移植するときも、いちいちPR作成権限の設定を確認しなくて済む。それが一番の実利かな」

タクヤ「僕は技術的な切り分けの方に価値を感じてました。でも運用面の再現性まで含めると、確かにリナ社長の見方の方が実務では効いてきますね」

リナ社長「まあどっちも正解でしょ。ただ一個だけ言わせて。squashマージなのにgit merge --squashgit commitだと、GitHub上のマージコミット表示が『PRタイトル (#番号)』形式にならないよね?前回はまさにその見た目の違和感で異変に気づいたのに、今回の直しでその手がかり自体が使えなくなるのは、ちょっとトレードオフだと思う」

タクヤ「そこは認めます。PRレスの直接マージは権限依存を消せる分、強いです。ただ見た目の手がかりが減るのは事実なので、次に何か起きたときは別の監視手段を考えておいた方がいいかもしれません」


前回の記事はワークフローが起動しない「入り口」の話、今回はワークフローは起動するがGraphQL権限そのものが拒否される「実行内容」の話だった。どちらも同じauto-merge-blog.ymlで起きたが、原因のレイヤーは完全に別物だ。

PR経由の自動化を組むときは、pull_requestイベントが発火するか(起動口)とGitHub ActionsにPR作成権限があるか(リポジトリ設定)の両方を確認するか、いっそPRを経由せずcontents:write権限だけで完結する設計にしておくと、リポジトリ設定に振り回されずに済む。

みんなのCI、PRを経由しない代替ルートって用意してる?


参考

コメント