#!/usr/bin/env bash set -euo pipefail command_name="${1:?command is required}" shift artifact_dir="" artifact_kind="" target_sha="" workflow_sha="" image_refs=() shared_package_sha256="${OPENCLAW_SHARED_IMAGE_PACKAGE_SHA256:-}" shared_archive_sha256="${OPENCLAW_SHARED_IMAGE_ARCHIVE_SHA256:-}" shared_run_id="${OPENCLAW_SHARED_IMAGE_RUN_ID:-}" shared_run_attempt="${OPENCLAW_SHARED_IMAGE_RUN_ATTEMPT:-}" archive_name="shared-images.tar.zst" manifest_path="" archive_path="" fail() { echo "$*" >&2 exit 1 } require_sha() { local label="$1" local value="$2" if [[ ! "$value" =~ ^[a-f0-9]{40}$ ]]; then fail "$label must be a lowercase full commit SHA." fi } require_positive_decimal() { local label="$1" local value="$2" if [[ ! "$value" =~ ^[1-9][0-9]*$ ]]; then fail "$label must be a positive decimal integer." fi } configure_image_artifact_inputs() { if [[ "$#" -lt 5 ]]; then fail "usage: $0 ..." fi artifact_dir="$1" artifact_kind="$2" target_sha="$3" workflow_sha="$4" image_refs=("${@:5}") manifest_path="${artifact_dir}/shared-image-artifact.json" archive_path="${artifact_dir}/${archive_name}" } is_transient_gh_api_get_error() { local error_text="$1" local not_found_policy="$2" if [[ "$error_text" =~ (^|[^0-9])(401|403|422)([^0-9]|$) || "$error_text" =~ [Bb]ad[[:space:]]+[Cc]redentials || "$error_text" =~ [Cc]redential || "$error_text" =~ [Aa]uthentication ]]; then return 1 fi if [[ "$error_text" =~ (^|[^0-9])404([^0-9]|$) ]]; then [[ "$not_found_policy" == "retry-fresh-artifact" ]] return fi [[ "$error_text" == *"i/o timeout"* || "$error_text" =~ [Cc]ontext[[:space:]]+deadline[[:space:]]+exceeded || "$error_text" =~ [Cc]onnection[[:space:]]+(refused|reset) || "$error_text" =~ [Nn]etwork[[:space:]]+is[[:space:]]+unreachable || "$error_text" =~ [Nn]o[[:space:]]+such[[:space:]]+host || "$error_text" =~ [Tt]emporary[[:space:]]+failure || "$error_text" =~ TLS[[:space:]]+handshake[[:space:]]+timeout || "$error_text" =~ [Uu]nexpected[[:space:]]+EOF || "$error_text" =~ (^|[^0-9])429([^0-9]|$) || "$error_text" =~ [Rr]ate[[:space:]-]*limit || "$error_text" =~ ([Hh][Tt][Tt][Pp]|[Ss]tatus([[:space:]_-]*code)?)[^0-9]*5[0-9]{2} ]] } gh_api_get_with_retry() { local label="$1" local endpoint="$2" local not_found_policy="$3" local attempt error_file response_file retry_delay retry_dir case "$not_found_policy" in fail-fast) ;; retry-fresh-artifact) # Artifact metadata can briefly lag upload completion. Keep 404 retries confined # to that fresh-object read so producer tuple and authentication failures stay immediate. if [[ ! "$endpoint" =~ ^repos/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/actions/artifacts/[1-9][0-9]*$ ]]; then fail "$label cannot retry 404 responses for this GitHub API endpoint." fi ;; *) fail "$label has an invalid GitHub API 404 retry policy." ;; esac retry_dir="$(mktemp -d)" response_file="${retry_dir}/response" error_file="${retry_dir}/error" for attempt in 1 2 3; do : > "$response_file" : > "$error_file" if gh api --method GET "$endpoint" > "$response_file" 2> "$error_file"; then cat "$response_file" rm -rf -- "$retry_dir" return 0 fi if [[ "$attempt" -lt 3 ]] && is_transient_gh_api_get_error "$(cat "$error_file")" "$not_found_policy"; then retry_delay=$((attempt * 2)) printf \ 'warning: %s GitHub API GET failed transiently on attempt %d/3; retrying in %ss.\n' \ "$label" "$attempt" "$retry_delay" >&2 cat "$error_file" >&2 sleep "$retry_delay" continue fi cat "$error_file" >&2 rm -rf -- "$retry_dir" fail "$label GitHub API GET failed after $attempt attempt(s)." done } verify_uploaded_artifact() { if [[ "$#" -ne 6 ]]; then fail "usage: $0 verify-upload