Files
qinglong/scripts/wait-for-mirror.sh
T

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