mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-16 22:35:32 +08:00
ci: verify Gitee Pull mirrors with resilient GitHub notifications
Use native Gitee Pull mirrors for code and static artifacts, and verify branch and tag SHAs alongside image builds. Trigger stale mirrors through small GitHub REST ref updates, retry transient errors with backoff, and keep polling when a notification fails.
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env bash
|
||||
# Observe Pull mirrors; optionally notify them through a small GitHub push.
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 3 || $# -gt 4 ]]; then
|
||||
echo 'Usage: wait-for-mirror.sh SOURCE MIRROR REF [branch|tag]' >&2
|
||||
exit 2
|
||||
fi
|
||||
source=$1
|
||||
mirror=$2
|
||||
ref=$3
|
||||
ref_type=${4:-branch}
|
||||
wait_seconds=${QL_MIRROR_WAIT_SECONDS:-600}
|
||||
poll_interval=${QL_MIRROR_POLL_INTERVAL:-30}
|
||||
query_timeout=${QL_MIRROR_QUERY_TIMEOUT:-20}
|
||||
include_version_branch=${QL_MIRROR_INCLUDE_VERSION_BRANCH:-false}
|
||||
notify_repo=${QL_MIRROR_NOTIFY_REPO:-}
|
||||
retry_after=${QL_MIRROR_RETRY_AFTER_SECONDS:-310}
|
||||
notify_on_start=${QL_MIRROR_NOTIFY_ON_START:-true}
|
||||
|
||||
timings=("$wait_seconds" "$poll_interval" "$query_timeout")
|
||||
if [[ -n "$notify_repo" ]]; then timings+=("$retry_after"); fi
|
||||
for value in "${timings[@]}"; do
|
||||
if [[ ! "$value" =~ ^[1-9][0-9]{0,4}$ ]] || (( value > 86400 )); then
|
||||
echo 'Mirror timing values must be integers between 1 and 86400 seconds' >&2
|
||||
exit 2
|
||||
fi
|
||||
done
|
||||
case "$ref_type" in
|
||||
branch) refs=("refs/heads/$ref") ;;
|
||||
tag)
|
||||
refs=("refs/tags/$ref")
|
||||
if [[ "$include_version_branch" == true ]]; then
|
||||
refs+=("refs/heads/$ref")
|
||||
fi
|
||||
;;
|
||||
*) echo 'Mirror ref type must be branch or tag' >&2; exit 2 ;;
|
||||
esac
|
||||
if [[ "$include_version_branch" != true && "$include_version_branch" != false ]]; then
|
||||
echo 'QL_MIRROR_INCLUDE_VERSION_BRANCH must be true or false' >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ "$notify_on_start" != true && "$notify_on_start" != false ]]; then
|
||||
echo 'QL_MIRROR_NOTIFY_ON_START must be true or false' >&2
|
||||
exit 2
|
||||
fi
|
||||
for requested_ref in "${refs[@]}"; do
|
||||
git check-ref-format "$requested_ref"
|
||||
done
|
||||
command -v timeout >/dev/null || { echo 'GNU timeout is required' >&2; exit 2; }
|
||||
if [[ -n "$notify_repo" ]]; then
|
||||
if [[ ! "$notify_repo" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then
|
||||
echo 'QL_MIRROR_NOTIFY_REPO must be a GitHub owner/repository' >&2
|
||||
exit 2
|
||||
fi
|
||||
command -v gh >/dev/null || { echo 'GitHub CLI is required for notifications' >&2; exit 2; }
|
||||
fi
|
||||
export GIT_TERMINAL_PROMPT=0
|
||||
polling=false
|
||||
|
||||
notification_command() {
|
||||
local budget=$((notification_deadline - SECONDS))
|
||||
if (( budget <= 0 )); then return 1; fi
|
||||
if (( budget > 10 )); then budget=10; fi
|
||||
timeout --signal=KILL "${budget}s" "$@"
|
||||
}
|
||||
|
||||
notify_static_mirror() {
|
||||
local notification_deadline=$((SECONDS + 30))
|
||||
if [[ "$polling" == true ]] && (( notification_deadline > wait_seconds )); then
|
||||
notification_deadline=$wait_seconds
|
||||
fi
|
||||
local trigger_ref=refs/heads/gitee-sync-trigger
|
||||
local attempt remote_refs source_sha parent tree commit delay remaining
|
||||
local commit_args
|
||||
if ! remote_refs=$(notification_command git ls-remote --refs "$source" "${refs[0]}"); then
|
||||
echo '::warning::Could not read the published source; continuing mirror verification'
|
||||
return 1
|
||||
fi
|
||||
source_sha=${remote_refs%%$'\t'*}
|
||||
if [[ ! "$source_sha" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "::error::Published source ref ${refs[0]} does not exist"
|
||||
return 2
|
||||
fi
|
||||
for attempt in 1 2 3; do
|
||||
if (( attempt > 1 )); then
|
||||
delay=$((attempt * 2 - 2))
|
||||
remaining=$((notification_deadline - SECONDS))
|
||||
if (( remaining <= delay )); then break; fi
|
||||
sleep "$delay"
|
||||
fi
|
||||
# Refresh the parent on every attempt. REST avoids the Git push endpoint's
|
||||
# transient 500s, and non-forced updates preserve concurrent notifications.
|
||||
if parent=$(notification_command gh api "repos/$notify_repo/git/ref/heads/gitee-sync-trigger" --jq '.object.sha' 2>&1); then
|
||||
if ! tree=$(notification_command gh api "repos/$notify_repo/git/commits/$parent" --jq '.tree.sha'); then
|
||||
echo "::warning::Could not read notification history (attempt $attempt)"
|
||||
continue
|
||||
fi
|
||||
elif [[ "$parent" == *'(HTTP 404)'* ]]; then
|
||||
parent=''
|
||||
# A new notification branch starts with one tiny marker, never artifacts.
|
||||
# Later commits reuse this tree and have no changed files in the webhook.
|
||||
if ! tree=$(printf '%s' '{"tree":[{"path":".gitee-sync-trigger","mode":"100644","type":"blob","content":"Gitee Pull mirror notifications.\n"}]}' |
|
||||
notification_command gh api --method POST "repos/$notify_repo/git/trees" --input - --jq '.sha'); then
|
||||
echo "::warning::Could not create notification tree (attempt $attempt)"
|
||||
continue
|
||||
fi
|
||||
else
|
||||
echo "::warning::Could not read notification ref (attempt $attempt)"
|
||||
continue
|
||||
fi
|
||||
commit_args=(-f "tree=$tree" -f "message=Trigger Gitee mirror for ${refs[0]} at $source_sha")
|
||||
if [[ -n "$parent" ]]; then commit_args+=(-f "parents[]=$parent"); fi
|
||||
if ! commit=$(notification_command gh api --method POST "repos/$notify_repo/git/commits" "${commit_args[@]}" --jq '.sha'); then
|
||||
echo "::warning::Could not create notification commit (attempt $attempt)"
|
||||
continue
|
||||
fi
|
||||
if [[ -n "$parent" ]]; then
|
||||
if notification_command gh api --method PATCH "repos/$notify_repo/git/refs/heads/gitee-sync-trigger" -f "sha=$commit" -F force=false --silent; then
|
||||
echo '::notice::Sent a small GitHub push event via REST to trigger the Gitee Pull mirror'
|
||||
return 0
|
||||
fi
|
||||
elif notification_command gh api --method POST "repos/$notify_repo/git/refs" -f "ref=$trigger_ref" -f "sha=$commit" --silent; then
|
||||
echo '::notice::Sent a small GitHub push event via REST to trigger the Gitee Pull mirror'
|
||||
return 0
|
||||
fi
|
||||
echo "::warning::Mirror notification update failed (attempt $attempt)"
|
||||
done
|
||||
echo '::warning::Could not send the mirror notification; continuing mirror verification'
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ -n "$notify_repo" && "$notify_on_start" == true ]]; then
|
||||
if notify_static_mirror; then :; elif [[ $? -eq 2 ]]; then exit 1; fi
|
||||
fi
|
||||
|
||||
SECONDS=0
|
||||
polling=true
|
||||
attempt=0
|
||||
retried=false
|
||||
last_source='(not read)'
|
||||
last_mirror='(not read)'
|
||||
while (( SECONDS < wait_seconds )); do
|
||||
attempt=$((attempt + 1))
|
||||
outputs=()
|
||||
ready=true
|
||||
# Re-read the source after the mirror so a moving source cannot pass with
|
||||
# the previous SHA. --refs compares annotated tag objects as well as branches.
|
||||
urls=("$source" "$mirror" "$source")
|
||||
labels=('source' 'mirror' 'source confirmation')
|
||||
for index in 0 1 2; do
|
||||
remaining=$((wait_seconds - SECONDS))
|
||||
if (( remaining <= 0 )); then
|
||||
ready=false
|
||||
break
|
||||
fi
|
||||
budget=$query_timeout
|
||||
if (( budget > remaining )); then budget=$remaining; fi
|
||||
if ! output=$(timeout --signal=KILL "${budget}s" git ls-remote --refs \
|
||||
"${urls[$index]}" "${refs[@]}" | LC_ALL=C sort); then
|
||||
echo "::warning::Could not read ${labels[$index]} refs on attempt $attempt; retrying"
|
||||
ready=false
|
||||
break
|
||||
fi
|
||||
if (( index == 0 )); then last_source=$output; fi
|
||||
if (( index == 1 )); then last_mirror=$output; fi
|
||||
for requested_ref in "${refs[@]}"; do
|
||||
found=false
|
||||
while IFS=$'\t' read -r sha returned_ref; do
|
||||
if [[ "$returned_ref" == "$requested_ref" && "$sha" =~ ^[0-9a-f]{40}([0-9a-f]{24})?$ ]]; then
|
||||
found=true
|
||||
fi
|
||||
done <<< "$output"
|
||||
if [[ "$found" == false ]]; then
|
||||
echo "::notice::${labels[$index]} is missing $requested_ref; waiting"
|
||||
ready=false
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ "$ready" == false ]]; then break; fi
|
||||
outputs+=("$output")
|
||||
# Only confirm a source when the initial source and mirror reads match.
|
||||
if (( index == 1 )) && [[ "${outputs[0]}" != "${outputs[1]}" ]]; then
|
||||
ready=false
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ "$ready" == true && "${outputs[0]}" == "${outputs[2]}" ]]; then
|
||||
echo "::notice::Mirror matches the latest source refs after ${SECONDS}s"
|
||||
printf '%s\n' "${outputs[2]}"
|
||||
exit 0
|
||||
fi
|
||||
echo "Waiting for mirror (attempt $attempt, ${SECONDS}/${wait_seconds}s)"
|
||||
remaining=$((wait_seconds - SECONDS))
|
||||
if (( remaining <= 0 )); then break; fi
|
||||
# Gitee requires five minutes between sync requests. Retry once after that
|
||||
# interval without restarting the polling deadline.
|
||||
if [[ -n "$notify_repo" && "$retried" == false ]] && (( SECONDS >= retry_after )); then
|
||||
echo '::notice::Retrying the GitHub notification after the Gitee sync interval'
|
||||
if notify_static_mirror; then :; elif [[ $? -eq 2 ]]; then exit 1; fi
|
||||
retried=true
|
||||
continue
|
||||
fi
|
||||
delay=$poll_interval
|
||||
if (( delay > remaining )); then delay=$remaining; fi
|
||||
if [[ -n "$notify_repo" && "$retried" == false ]] && (( SECONDS < retry_after && delay > retry_after - SECONDS )); then
|
||||
delay=$((retry_after - SECONDS))
|
||||
fi
|
||||
sleep "$delay"
|
||||
done
|
||||
|
||||
echo "::error::Mirror did not match the latest source refs within ${wait_seconds}s"
|
||||
printf 'Last source refs:\n%s\nLast mirror refs:\n%s\n' "$last_source" "$last_mirror"
|
||||
exit 1
|
||||
Reference in New Issue
Block a user