GitHub ActionsのPRレビューがマージ直後にfetchで失敗した記録

PR を開いてからマージまで、わずか 6 秒でした。14.5 秒後、GitHub Actions の runner は消えた head branch を取りに行き、終了コード 128 で止まりました。
Command failed with code 128: git fetch origin --depth=20 fix/strip-private-source-links
fatal: couldn't find remote ref fix/strip-private-source-links
認証は通っています。actions/checkoutも PR の merge ref を取得できていました。今回の失敗は token ではなく、PR の寿命よりレビュー処理のほうが長かったために起きた競合です。
私は PR を作る工程とマージする工程を分けていても、実行順を守らなければ境界は機能しないと考えています。レビューの起動を確認しただけでは足りません。完了と成功を確認するまで、head branch を残す必要があります。
前回から増えた確認
前回のGitHub ActionsでOpenCodeに記事PRを自動レビューさせるでは、persist-credentials: trueと runner token を使い、checkout 後のgit fetchを通しました。今回は認証の先で起きた失敗を切り分けます。
| 前回完了時 | 今回完了後 |
|---|---|
| checkout後のfetchへ認証情報を渡します | fetch対象のhead branchが残っているか確認します |
| PRを開くとレビューが起動します | gh pr checksが終了コード0になるまでマージを待ちます |
| 失敗を認証と権限から調べます | PR作成、マージ、fetch失敗の時刻差も調べます |
PR のチェックをすでに待ってからマージしている場合は、「PR #38 の時系列」から「待つ操作を 1 つのゲートにする」まで飛ばせます。
なぜcheckout成功後にfetchが失敗するのか
pull_requestで起動した workflow では、GITHUB_REFがrefs/pull/<番号>/mergeになります。GitHub の公式ドキュメントにも、actions/checkoutが標準でこの merge branch を取得するとあります。
一方、OpenCode Action は checkout の後で PR の head branch を改めて取得しました。PR #38 のログでは、次の 2 段階が分かれています。
actions/checkout
git fetch origin +8ec9bb9...:refs/remotes/pull/38/merge
└─ 成功
anomalyco/opencode/github
git fetch origin --depth=20 fix/strip-private-source-links
└─ remote refがなく失敗
最初の fetch は PR 用の merge ref を指定しています。2 回目はfix/strip-private-source-linksという branch 名を指定しています。取得先が違うため、checkout の成功は 2 回目の成功を保証しません。
GitHub はマージ済み PR の head branch を削除できます。hibanas-net のdelete_branch_on_mergeは確認時点でfalseでした。ただし、PR #38 の head ref は API で404 Not Foundです。自動削除が原因とは断定できませんが、OpenCode が fetch した時点で remote ref が存在しなかったことは確認できます。
用語表
| 用語 | 今回の意味 |
|---|---|
| head branch | PRの変更元です。PR #38ではfix/strip-private-source-linksでした |
| base branch | 変更先です。hibanas-netではmainです |
| merge ref | GitHubがPRの検査用に作るrefs/pull/38/mergeです |
| status check | CI、レビュー、デプロイなどの検査結果です |
| race condition | 処理順や時刻によって成功と失敗が変わる競合です |
| exit code 8 | gh pr checksでチェックが未完了のときに返る値です |
| exit code 1 | 今回のgh pr checks 38では失敗したチェックがあり返りました |
PR #38の時系列を取り直す
最初に、PR の作成時刻、マージ時刻、head branch を GitHub から読みました。private repository のため、本文から PR や Actions の URL へはリンクしません。
~/.local/bin/gh pr view 38 \
--repo dicekanbe/hibanas-net \
--json number,state,createdAt,mergedAt,headRefName,headRefOid,mergeCommit \
--jq '{number,state,createdAt,mergedAt,headRefName,headRefOid,mergeCommit:.mergeCommit.oid}'
実行結果です。
{"createdAt":"2026-08-23T02:15:12Z","headRefName":"fix/strip-private-source-links","headRefOid":"42646f05219341f3bc871eea22d84b46ba485bfa","mergeCommit":"41de550bbc9218f8cca12dbb427e91100017b143","mergedAt":"2026-08-23T02:15:18Z","number":38,"state":"MERGED"}
UTC の02:15:12に作成し、02:15:18にマージしています。差は 6 秒です。OpenCode の失敗ログは02:15:32.5173588Zでした。マージから失敗した fetch までは 14.517358 秒です。
PR作成 02:15:12Z
│ 3秒
├─ Actions run作成 02:15:15Z
│ 3秒
├─ PRマージ 02:15:18Z
│ 8秒
├─ checkout完了 02:15:26Z
│ 6.5秒
└─ head branchのfetch失敗 02:15:32.5173588Z
Actions の run 自体は PR のマージ前に作られています。それでも runner が実際に OpenCode の fetch へ到達したのはマージ後でした。イベントが発火した時刻と、job が対象を読む時刻を同じものとして扱えません。
head branchの不在をAPIで確認する
repository の設定と ref を別々に確認しました。設定だけを見て原因を決めつけないためです。
~/.local/bin/gh api repos/dicekanbe/hibanas-net \
--jq '{private,delete_branch_on_merge,default_branch}'
実行結果です。
{"default_branch":"main","delete_branch_on_merge":false,"private":true}
次に、PR #38 の head branch を Git refs API で引きました。
~/.local/bin/gh api \
repos/dicekanbe/hibanas-net/git/ref/heads/fix/strip-private-source-links
実行結果です。
{"message":"Not Found","documentation_url":"https://docs.github.com/rest/git/refs#get-a-reference","status":"404"}
gh: Not Found (HTTP 404)
delete_branch_on_merge: falseは、現在の repository 設定です。過去の削除操作までは説明しません。ここで運用判断に使える事実は、「OpenCode の fetch 時点で同名の remote ref が見つからなかった」です。
2つの失敗を混ぜない
PR #38 では、checkとreviewが両方失敗しました。しかし原因は別です。
~/.local/bin/gh pr checks 38 --repo dicekanbe/hibanas-net
実行結果です。
check fail 35s
review fail 18s
Cloudflare Pages pass 0
reviewは、ここまで扱った head branch の fetch 失敗です。checkは textlint で止まりました。
src/content/posts/astro-shiki-box-drawing-line-height.md
22:6 error 原則として、全角文字と半角文字の間にスペースを入れます。
24:6 error 原則として、全角文字と半角文字の間にスペースを入れます。
2 problems (2 errors, 0 warnings, 0 infos)
この 2 行は、その後の PR #39 で修正しました。PR #32ではをPR #32 ではへ、PR #34でをPR #34 でへ変えています。branch の競合を直しても、textlint の失敗は残ります。チェック名ごとにログを分けて読む必要があります。
待つ操作を1つのゲートにする
手元で直せる最小の変更は、workflow YAML ではなくマージ手順です。PR を開いた直後にはマージせず、gh pr checksが終了コード 0 になるまで待ちます。
pr=39
~/.local/bin/gh pr checks "$pr" \
--repo dicekanbe/hibanas-net \
--watch --fail-fast
--watchはチェックが終わるまで更新を続けます。GitHub CLI のマニュアルでは、未完了時の追加終了コードを 8 としています。--fail-fastを付けると、1 件でも失敗した時点で監視を終えます。
今回、完了後の PR #39 で同じ項目を確認すると、終了コード 0 でした。
Cloudflare Pages pass 0
check pass 56s
review pass 52s
exit=0
この 0 をマージの前提にします。レビューコメントの内容も必要なため、実運用ではチェック成功だけで自動マージしません。特に OpenCode の成功は、記事公開の許可ではありません。
PR作成
│
├─ gh pr checks --watch --fail-fast
│ ├─ exit 8: まだ待ちます
│ ├─ exit 1: ログを読み、修正して再実行します
│ └─ exit 0: レビュー内容を確認します
│
├─ 人またはレビュー担当が対象差分を承認します
│
├─ PRをマージします
│
└─ head branchを削除します
branch の削除を最後に置けば、Action が branch 名で追加 fetch する時間を確保できます。GitHub の required status checks を使える repository なら、checkとreviewを必須にすると手操作の抜けも防げます。hibanas-net でその保護設定を有効にした事実は確認していないため、ここでは導入済みとは書きません。
branch削除よりSHAを優先できる場所
自作 workflow なら、可変の branch 名より commit SHA を使うほうが安定します。GitHub の公式ドキュメントは、head 側だけを検査する場合にgithub.event.pull_request.head.shaを checkout する例を示しています。
ただし、今回の.github/workflows/opencode-pr-review.ymlは、先にactions/checkout@v6を実行した後、anomalyco/opencode/github@latestを呼びます。失敗した branch fetch は第三者 Action の内部処理です。外側の checkout を SHA に変えるだけで、内部 fetch まで止まるとは確認できません。
私は、確認していない YAML を完成形として記事へ置かない方針です。先に採用するのは、チェック完了までマージと branch 削除を待つ運用です。Action 側を変える場合は、固定したバージョンで branch 削除後の再現テストを行ってから別の変更にします。
よくあるエラー
| 症状 | 切り分け | 対応 |
|---|---|---|
could not read Usernameです | 認証情報がfetchへ渡っていません | persist-credentialsとtoken設定を確認します |
couldn't find remote refです | branch名がremoteに残っているかAPIで確認します | チェック完了までマージとbranch削除を待ちます |
| checkoutは成功したのにreviewが失敗します | merge refとhead branchを混同しています | Action内の2回目のfetchまでログを読みます |
gh pr checksが終了コード8です | チェックが未完了です | マージせず、そのまま待ちます |
gh pr checksが終了コード1です | 1件以上のチェックが失敗しています | チェック名ごとにログを読みます |
| CIとreviewが同時に失敗します | 同じ原因とは限りません | PR #38のようにtextlintとfetchを分けます |
| check成功だけで記事が公開されます | レビューと公開の境界がありません | 公開対象を明示した別操作までdraft: trueを保ちます |
今回の判断
PR #38 は、PR を作成して 6 秒後にマージされました。OpenCode が head branch を fetch したのは、その 14.5 秒後です。その時点で ref は見つかりませんでした。
この記録から変えるべきものは、まず順序です。起動を成功とみなさず、gh pr checks --watch --fail-fastの終了コード 0 を待ちます。失敗したら、check 名ごとにログを読みます。レビューが終わるまで、head branch を消しません。
速くマージすることより、レビュー対象が生きている時間を保証するほうが大切です。AI エージェントを PR へ組み込むほど、この待機は単なる待ち時間ではなく、処理対象を守るためのゲートになります。
出典
| 一次情報 | 確認した内容 |
|---|---|
| GitHub Docs: Events that trigger workflows | pull_requestのGITHUB_REFとmerge branchの扱い |
| GitHub Docs: Status checks | checkのstatus、conclusion、必須チェックの意味 |
| GitHub CLI manual: gh pr checks | --watch、--fail-fast、未完了時の終了コード8 |
| GitHub Docs: Managing the automatic deletion of branches | マージ後にhead branchを自動削除できるrepository設定 |
| GitHub REST API: Git references | head branchのrefが存在するかを確認するAPI |
