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

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_REFrefs/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 branchPRの変更元です。PR #38ではfix/strip-private-source-linksでした
base branch変更先です。hibanas-netではmainです
merge refGitHubがPRの検査用に作るrefs/pull/38/mergeです
status checkCI、レビュー、デプロイなどの検査結果です
race condition処理順や時刻によって成功と失敗が変わる競合です
exit code 8gh 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 では、checkreviewが両方失敗しました。しかし原因は別です。

~/.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 なら、checkreviewを必須にすると手操作の抜けも防げます。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 workflowspull_requestGITHUB_REFとmerge branchの扱い
GitHub Docs: Status checkscheckの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 referenceshead branchのrefが存在するかを確認するAPI
この記事をシェア