mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-02 06:58:52 +08:00
227 lines
8.8 KiB
Bash
227 lines
8.8 KiB
Bash
#!/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
|
|
# Ubuntu 26.04's default timeout is from uutils. We require GNU's process-group
|
|
# termination so git transport children cannot keep the query pipes open.
|
|
timeout_command=''
|
|
for candidate in timeout gnutimeout gtimeout; do
|
|
if version=$("$candidate" --version 2>/dev/null) && [[ "$version" == *'(GNU coreutils)'* ]]; then
|
|
timeout_command=$candidate
|
|
break
|
|
fi
|
|
done
|
|
if [[ -z "$timeout_command" ]]; then
|
|
echo 'GNU timeout is required (timeout, gnutimeout or gtimeout)' >&2
|
|
exit 2
|
|
fi
|
|
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_command" --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_command" --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
|