GitHub Actions の PR 作成を gh CLI に移行した話
これまで、GitHub Actions の workflow で PR を自動作成する処理に、gr2m/create-or-update-pull-request-action を使っていました。
シンプルな記法で PR を作れる便利なアクションですよね。
ですが、ここにきて2つの問題に直面しました。
- 最終更新が 2024年で止まっている(2026年4月現在)
- Node.js 24 に対応していない
2026 年現在、GitHub Actions の runner は Node.js 24 を標準とする方向に進んでいます。
メンテナンスが止まっている以上、いずれ動作しなくなるのは時間の問題と言えます。
このまま放置するわけにもいかないので、代替手段を探すことにしました。
peter-evans/create-pull-request を検討する
移行の第一候補
代替として真っ先に候補に挙がったのが peter-evans/create-pull-request です。
GitHub Actions の PR 作成アクションとしては最も広く使われており、リポジトリも活発に更新されています。
実際に導入してみると、基本的な PR 作成はスムーズに動きました。
- uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.MY_TOKEN }}
base: main
title: "Update config"
commit-message: "Update config"
branch: "feature/update-config"
delete-branch: true
移行時に気づいた落とし穴
ですが、すべてが順調だったわけではありません。
同じブランチに対して複数回 workflow を実行し、変更を積み重ねるユースケースで問題が発生しました。
公式ドキュメントにはこんな記述があります。
The default behaviour of the action is to create a pull request that will be continually updated with new changes until it is merged or closed.
一見すると変更が積み重なるように読めますよね。
実は、実際の挙動は少し違いました。
base ブランチとの diff が基準になる
peter-evans/create-pull-request は毎回 base ブランチ(main, master)との差分をもとにブランチを構築します。
具体的には、以下のような流れになります。
- 1回目: main をチェックアウト → ファイルAを変更 → ブランチに push
- 2回目: main をチェックアウト → ファイルBを変更 → ブランチに force push
ここで問題になるのは2回目の実行です。
peter-evans は毎回 main からチェックアウトした状態で動くため、1回目に変更したファイルAの差分は workspace に存在しません。
そのままファイルBだけの変更で force push されてしまい、1回目の変更は失われます。
ドキュメントにある「continually updated」は、依存関係の自動更新のように同じファイルに繰り返し最新の状態を反映するケースを想定しているようです。
異なるファイルへの変更を回を重ねて積み上げていくような使い方には、残念ながら向いていません。
gh CLI で自由度を高める
サードパーティに頼らない選択
最終的に選んだのは、gh CLI を使って PR 作成ロジックを自前で組む方法です。
gh は GitHub 公式ツールであり、runner に標準搭載されています。
サードパーティのアクションと違って、突然メンテナンスが止まるリスクはほぼありません。
基本的な構成
具体的には、以下のような構成になります。
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.MY_TOKEN }}
- name: Checkout or create branch
run: |
BRANCH="feature/my-update"
git fetch origin $BRANCH 2>/dev/null && git checkout $BRANCH || git checkout -b $BRANCH
- name: Make changes
run: |
# ここでファイルを変更する処理
echo "updated" > config.txt
- name: Commit and push
id: commit-and-push
run: |
BRANCH="feature/my-update"
git config user.name "my-bot"
git config user.email "my-bot@example.com"
git add -A
if git diff --staged --quiet; then
echo "No changes to commit"
echo "has-changes=false" >> $GITHUB_OUTPUT
else
git commit -m "Update config"
git push origin $BRANCH
echo "has-changes=true" >> $GITHUB_OUTPUT
fi
- name: Create or update Pull Request
if: ${{ steps.commit-and-push.outputs.has-changes == 'true' }}
run: |
BRANCH="feature/my-update"
if [ -n "$(gh pr list --head $BRANCH --state open --json number,headRepositoryOwner --jq '[.[] | select(.headRepositoryOwner.login == "${{ github.repository_owner }}")] | .[0].number')" ]; then
echo "PR already exists for $BRANCH"
else
gh pr create --base main --head $BRANCH \
--title "Update config" \
--body "Automated update"
fi
env:
GH_TOKEN: ${{ secrets.MY_TOKEN }}ポイントは、既存ブランチがあればそこに checkout してからファイルを変更するという点です。
これにより、前回の変更が残ったまま新しい変更を積み重ねることができます。
実装時に踏んだいくつかの落とし穴
自前で組む分、いくつかのエッジケースに対応する必要がありました。
実際にハマったポイントを共有します。
ブランチの checkout はファイル変更の前に行う
ファイルを変更してからブランチを checkout すると、既存ブランチのファイルと競合して git checkout が失敗します。
必ずファイル変更の前にブランチを切り替えてください。
順序を間違えると「would be overwritten」エラーに悩まされることになります。
ブランチが存在しない初回に対応する
git fetch でリモートブランチの存在を確認し、なければ git checkout -b で新規作成します。
ここで注意したいのは、|| true で握りつぶしてしまうケースです。
ブランチが作成されないまま後続の git push が src refspec does not match any で失敗します。
変更がない場合は PR 作成をスキップする
git diff --staged --quiet で変更の有無を判定し、GITHUB_OUTPUT にフラグを出力します。
後続の PR 作成ステップを if 条件で制御しないと、push されていないブランチに対して gh pr create がエラーになります。
地味ですが、これを忘れると no-op な実行でジョブが失敗してしまいます。
checkout に PAT を設定する
actions/checkout のデフォルトトークン(GITHUB_TOKEN)は、リポジトリ設定によっては read-only です。
git push するためには、write 権限を持つ Personal Access Token を token に渡す必要があります。
他の workflow では問題なかったのに、この workflow だけ push が失敗する、という場面に遭遇したらこれを疑ってみてください。
PR の存在チェックはオープン状態に限定する
gh pr view はクローズ済みの PR も検出します。
過去に同じブランチ名で作成・クローズされた PR があると、「既に存在する」と判定されて新規 PR が作成されません。
gh pr list --head $BRANCH --state open を使って、オープンな PR のみを対象にしてください。
まとめ
| 方法 | メリット | デメリット |
|---|---|---|
| gr2m | 記述がシンプル | メンテ停止、Node.js 24 非対応 |
| peter-evans | 活発にメンテナンス | force push で変更が積み重ならない |
| gh CLI | 挙動を完全に制御可能 | エッジケースの考慮が必要 |
サードパーティのアクションは手軽ですが、挙動が合わなくなったときに制御しづらいという面があります。
一方で gh CLI は、一度ロジックを組めば GitHub 公式ツールとして長く使い続けられます。
workflow ごとにロジックを書く手間はありますが、「変更を積み重ねたい」「force push を避けたい」といった要件がある場合は、自前で制御する価値は十分にあると感じています。
同じ問題に悩んでいる方の参考になれば幸いです。
