fix(scripts): avoid false PR head changes during GitHub outages (#125517)

gh exit status was treated as proof that stdout held a PR object. The Octopool cache shim reports upstream 5xx responses as exit 0 with empty stdout, so reads now use a bounded-retry validation helper. Convert 9 of 28 gh pr view sites where an empty payload could produce a wrong decision.
This commit is contained in:
Peter Steinberger
2026-08-17 19:26:51 -07:00
committed by GitHub
parent 47399310a0
commit 44b41d5686
9 changed files with 192 additions and 23 deletions
+3 -2
View File
@@ -240,8 +240,9 @@ require_cmds() {
require_main_target_pr() {
local pr="$1"
local base
base=$(gh pr view "$pr" --json baseRefName --jq .baseRefName)
local base base_json
base_json=$(read_pr_view_json "$pr" "baseRefName") || exit 1
base=$(pr_view_string_field "$base_json" "baseRefName" "$pr" "Retry the scripts/pr command.") || exit 1
if [ "$base" != "main" ]; then
echo "scripts/pr prepare and merge commands only support PRs targeting main; PR #$pr targets $base." >&2
echo "Use the reviewed release-branch landing flow for non-main PRs." >&2
+54
View File
@@ -145,6 +145,60 @@ bootstrap_deps_if_needed() {
fi
}
read_pr_view_json() {
local pr="$1"
local fields="$2"
local max_attempts=3
local temp_dir
temp_dir=$(mktemp -d "${TMPDIR:-/tmp}/openclaw-pr-view.XXXXXX") || {
echo "Unable to create temporary storage for GitHub PR metadata." >&2
return 1
}
local stdout_file="$temp_dir/stdout"
local stderr_file="$temp_dir/stderr"
local attempt exit_code reason
for attempt in $(seq 1 "$max_attempts"); do
exit_code=0
if gh pr view "$pr" --json "$fields" >"$stdout_file" 2>"$stderr_file"; then
if [ -s "$stdout_file" ] && jq -se 'length == 1 and (.[0] | type == "object")' "$stdout_file" >/dev/null 2>&1; then
cat "$stdout_file"
rm -rf "$temp_dir"
return 0
fi
if [ ! -s "$stdout_file" ]; then
reason="gh pr view returned empty stdout"
else
reason="gh pr view did not return one JSON object"
fi
else
exit_code=$?
reason="gh pr view exited with status $exit_code"
fi
[ "$attempt" -eq "$max_attempts" ] || sleep "$attempt"
done
echo "GitHub API failure while reading PR #$pr: $reason after $max_attempts attempts." >&2
[ ! -s "$stderr_file" ] || cat "$stderr_file" >&2
rm -rf "$temp_dir"
return 1
}
pr_view_string_field() {
local json="$1" field="$2" pr="$3" remedy="${4:-Retry the command.}" label value
case "$field" in
headRefOid) label="a head SHA" ;;
baseRefName) label="a base branch" ;;
headRefName) label="a head branch" ;;
*) label="a non-empty .$field string" ;;
esac
if ! value=$(printf '%s\n' "$json" | jq -er --arg field "$field" '.[$field] | if type == "string" and length > 0 then . else error("missing string field") end' 2>/dev/null); then
echo "GitHub PR metadata for #$pr did not include $label. $remedy" >&2
return 1
fi
printf '%s\n' "$value"
}
wait_for_pr_head_sha() {
local pr="$1"
local expected_sha="$2"
+2 -2
View File
@@ -4,8 +4,8 @@ run_hosted_prepare_gates() {
local changelog_only="$3"
local recent_sha=""
local remote_record remote_head remote_head_ref remote_is_cross_repository
remote_record=$(gh pr view "$pr" --json headRefName,headRefOid,isCrossRepository)
remote_head=$(printf '%s\n' "$remote_record" | jq -r .headRefOid)
remote_record=$(read_pr_view_json "$pr" "headRefName,headRefOid,isCrossRepository") || return 1
remote_head=$(pr_view_string_field "$remote_record" "headRefOid" "$pr" "Re-run prepare-init.") || return 1
remote_head_ref=$(printf '%s\n' "$remote_record" | jq -r .headRefName)
remote_is_cross_repository=$(printf '%s\n' "$remote_record" | jq -r .isCrossRepository)
if [ "$remote_head" != "$current_head" ]; then
+5 -5
View File
@@ -242,7 +242,7 @@ merge_run() {
source .local/prep.env
local pr_meta_json
pr_meta_json=$(gh pr view "$pr" --json state,isDraft)
pr_meta_json=$(read_pr_view_json "$pr" "state,isDraft") || exit 1
local is_draft
is_draft=$(printf '%s\n' "$pr_meta_json" | jq -r .isDraft)
if [ "$is_draft" = "true" ]; then
@@ -312,7 +312,7 @@ merge_run() {
# artifact, exact-head, required-check, and drift check in merge_verify.
if [ "$auto_merge_requested" = "true" ]; then
local auto_meta
auto_meta=$(gh pr view "$pr" --json state,headRefOid,mergeable,mergeStateStatus,autoMergeRequest)
auto_meta=$(read_pr_view_json "$pr" "state,headRefOid,mergeable,mergeStateStatus,autoMergeRequest") || exit 1
local auto_head_sha
auto_head_sha=$(printf '%s\n' "$auto_meta" | jq -r .headRefOid)
if [ "$auto_head_sha" != "$PREP_HEAD_SHA" ]; then
@@ -338,7 +338,7 @@ merge_run() {
print_relevant_log_excerpt .local/merge-output.log
exit 1
fi
auto_meta=$(gh pr view "$pr" --json state,headRefOid,mergeable,mergeStateStatus,autoMergeRequest)
auto_meta=$(read_pr_view_json "$pr" "state,headRefOid,mergeable,mergeStateStatus,autoMergeRequest") || exit 1
auto_head_sha=$(printf '%s\n' "$auto_meta" | jq -r .headRefOid)
mergeable=$(printf '%s\n' "$auto_meta" | jq -r '.mergeable // "UNKNOWN"')
merge_state_status=$(printf '%s\n' "$auto_meta" | jq -r '.mergeStateStatus // "UNKNOWN"')
@@ -365,7 +365,7 @@ merge_run() {
--match-head-commit "$PREP_HEAD_SHA" \
>.local/merge-output.log 2>&1
then
auto_meta=$(gh pr view "$pr" --json state,headRefOid,mergeable,mergeStateStatus,autoMergeRequest)
auto_meta=$(read_pr_view_json "$pr" "state,headRefOid,mergeable,mergeStateStatus,autoMergeRequest") || exit 1
auto_head_sha=$(printf '%s\n' "$auto_meta" | jq -r .headRefOid)
state=$(printf '%s\n' "$auto_meta" | jq -r .state)
existing_auto_method=$(printf '%s\n' "$auto_meta" | jq -r '.autoMergeRequest.mergeMethod // ""')
@@ -385,7 +385,7 @@ merge_run() {
exit 1
fi
else
auto_meta=$(gh pr view "$pr" --json state,headRefOid,autoMergeRequest)
auto_meta=$(read_pr_view_json "$pr" "state,headRefOid,autoMergeRequest") || exit 1
auto_head_sha=$(printf '%s\n' "$auto_meta" | jq -r .headRefOid)
existing_auto_method=$(printf '%s\n' "$auto_meta" | jq -r '.autoMergeRequest.mergeMethod // ""')
if [ "$auto_head_sha" = "$PREP_HEAD_SHA" ] && [ -n "$existing_auto_method" ]; then
+3 -2
View File
@@ -179,8 +179,9 @@ verify_pr_head_branch_matches_expected() {
local pr="$1"
local expected_head="$2"
local current_head
current_head=$(gh pr view "$pr" --json headRefName --jq .headRefName)
local current_head current_head_json
current_head_json=$(read_pr_view_json "$pr" "headRefName") || exit 1
current_head=$(pr_view_string_field "$current_head_json" "headRefName" "$pr" "Re-run prepare-init.") || exit 1
if [ "$current_head" != "$expected_head" ]; then
echo "PR head branch changed from $expected_head to $current_head. Re-run prepare-init."
exit 1
+9 -5
View File
@@ -268,10 +268,13 @@ enter_worktree() {
pr_meta_json() {
local pr="$1"
local metadata files expected_file_count actual_file_count head_before head_after
metadata=$(gh pr view "$pr" --json number,title,state,isDraft,author,baseRefName,headRefName,headRefOid,headRepository,headRepositoryOwner,url,body,labels,assignees,changedFiles,additions,deletions,statusCheckRollup,files)
head_before=$(printf '%s\n' "$metadata" | jq -r .headRefOid)
expected_file_count=$(printf '%s\n' "$metadata" | jq -r .changedFiles)
local metadata files expected_file_count actual_file_count head_before head_after head_after_json
metadata=$(read_pr_view_json "$pr" "number,title,state,isDraft,author,baseRefName,headRefName,headRefOid,headRepository,headRepositoryOwner,url,body,labels,assignees,changedFiles,additions,deletions,statusCheckRollup,files") || return 1
head_before=$(pr_view_string_field "$metadata" "headRefOid" "$pr" "Retry review initialization.") || return 1
if ! expected_file_count=$(printf '%s\n' "$metadata" | jq -er '.changedFiles | if type == "number" and . >= 0 and . == floor then . else error("invalid changed file count") end' 2>/dev/null); then
echo "Invalid PR metadata for #$pr: changedFiles must be a non-negative integer." >&2
return 1
fi
# `gh pr view --json files` is cacheable but stops at 100 entries. Use it
# when complete; only large or incomplete responses spend uncached REST quota.
@@ -327,7 +330,8 @@ pr_meta_json() {
fi
fi
head_after=$(gh pr view "$pr" --json headRefOid | jq -r .headRefOid)
head_after_json=$(read_pr_view_json "$pr" "headRefOid") || return 1
head_after=$(pr_view_string_field "$head_after_json" "headRefOid" "$pr" "Retry review initialization.") || return 1
if [ "$head_after" != "$head_before" ]; then
echo "PR head changed while collecting file metadata for #$pr (started at $head_before, ended at $head_after). Retry review initialization." >&2
return 1