mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
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:
committed by
GitHub
parent
47399310a0
commit
44b41d5686
+3
-2
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user