Add wait-for-release: poll a Gitea repo's releases until a glob matches
Polls /api/v1/repos/<owner>/<repo>/releases and matches each tag_name against a shell-style glob (e.g. v1.2.3.*). Default 600s timeout, 5s interval, both overridable. Exits 0 with the matched tag on stdout, 1 on timeout, 2 on usage error, 3 on terminal API error (401/403/404). Intended to run in the background of a Claude Code session while CI produces the release. Includes spec and 42-assertion test using a PATH-shadowed mock curl. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
256
tests/test-wait-for-release.sh
Executable file
256
tests/test-wait-for-release.sh
Executable file
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
WAIT="$SCRIPT_DIR/scripts/wait-for-release"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
|
||||
assert_eq() {
|
||||
local desc="$1" expected="$2" actual="$3"
|
||||
if [[ "$expected" == "$actual" ]]; then
|
||||
echo -e "${GREEN}PASS${NC}: $desc"
|
||||
((pass++)) || true
|
||||
else
|
||||
echo -e "${RED}FAIL${NC}: $desc"
|
||||
echo " expected: $expected"
|
||||
echo " actual: $actual"
|
||||
((fail++)) || true
|
||||
fi
|
||||
}
|
||||
|
||||
assert_match() {
|
||||
local desc="$1" pattern="$2" actual="$3"
|
||||
if [[ "$actual" =~ $pattern ]]; then
|
||||
echo -e "${GREEN}PASS${NC}: $desc"
|
||||
((pass++)) || true
|
||||
else
|
||||
echo -e "${RED}FAIL${NC}: $desc"
|
||||
echo " pattern: $pattern"
|
||||
echo " actual: $actual"
|
||||
((fail++)) || true
|
||||
fi
|
||||
}
|
||||
|
||||
assert_exit() {
|
||||
local desc="$1" expected="$2" actual="$3"
|
||||
if [[ "$expected" -eq "$actual" ]]; then
|
||||
echo -e "${GREEN}PASS${NC}: $desc"
|
||||
((pass++)) || true
|
||||
else
|
||||
echo -e "${RED}FAIL${NC}: $desc"
|
||||
echo " expected exit: $expected"
|
||||
echo " actual exit: $actual"
|
||||
((fail++)) || true
|
||||
fi
|
||||
}
|
||||
|
||||
assert_between() {
|
||||
local desc="$1" lo="$2" hi="$3" val="$4"
|
||||
if (( val >= lo && val <= hi )); then
|
||||
echo -e "${GREEN}PASS${NC}: $desc (${val} in [${lo},${hi}])"
|
||||
((pass++)) || true
|
||||
else
|
||||
echo -e "${RED}FAIL${NC}: $desc (${val} not in [${lo},${hi}])"
|
||||
((fail++)) || true
|
||||
fi
|
||||
}
|
||||
|
||||
# --- test env setup ---
|
||||
|
||||
tmp_root="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp_root"' EXIT
|
||||
|
||||
MOCK_DIR="$tmp_root/bin"
|
||||
mkdir -p "$MOCK_DIR"
|
||||
|
||||
# Mock curl: emits fixture body then "\n<code>", ignoring all flags except URL.
|
||||
cat > "$MOCK_DIR/curl" <<'MOCK'
|
||||
#!/usr/bin/env bash
|
||||
url=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
http://*|https://*) url="$1"; shift ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
[[ -n "${MOCK_CURL_LOG:-}" ]] && echo "$url" >> "$MOCK_CURL_LOG"
|
||||
[[ -n "${MOCK_CURL_FAIL:-}" ]] && exit 7
|
||||
if [[ -n "${MOCK_CURL_BODY:-}" && -f "$MOCK_CURL_BODY" ]]; then
|
||||
cat "$MOCK_CURL_BODY"
|
||||
fi
|
||||
printf '\n%s' "${MOCK_CURL_CODE:-200}"
|
||||
MOCK
|
||||
chmod +x "$MOCK_DIR/curl"
|
||||
|
||||
run_with_mock() {
|
||||
PATH="$MOCK_DIR:$PATH" "$WAIT" "$@"
|
||||
}
|
||||
|
||||
# Fixtures
|
||||
FIX_MATCH="$tmp_root/fix_match.json"
|
||||
cat > "$FIX_MATCH" <<'JSON'
|
||||
[
|
||||
{"id":10,"tag_name":"v1.2.3.45","name":"v1.2.3.45"},
|
||||
{"id":9,"tag_name":"v1.2.2.40","name":"v1.2.2.40"},
|
||||
{"id":8,"tag_name":"v1.1.0.30","name":"v1.1.0.30"}
|
||||
]
|
||||
JSON
|
||||
|
||||
FIX_NO_MATCH="$tmp_root/fix_no_match.json"
|
||||
cat > "$FIX_NO_MATCH" <<'JSON'
|
||||
[
|
||||
{"id":1,"tag_name":"v0.1.0.3","name":"v0.1.0.3"}
|
||||
]
|
||||
JSON
|
||||
|
||||
FIX_EMPTY="$tmp_root/fix_empty.json"
|
||||
echo "[]" > "$FIX_EMPTY"
|
||||
|
||||
echo "=== wait-for-release tests ==="
|
||||
echo
|
||||
|
||||
# --- Help ---
|
||||
out=$("$WAIT" --help 2>&1); rc=$?
|
||||
assert_exit "help exits 0" 0 "$rc"
|
||||
assert_match "help mentions REPO" "REPO" "$out"
|
||||
assert_match "help mentions PATTERN" "PATTERN" "$out"
|
||||
assert_match "help lists exit codes" "Exit codes" "$out"
|
||||
|
||||
# --- Usage errors ---
|
||||
out=$("$WAIT" 2>&1); rc=$?
|
||||
assert_exit "no args exits 2" 2 "$rc"
|
||||
assert_match "no args error" "REPO is required" "$out"
|
||||
|
||||
out=$("$WAIT" owner/repo 2>&1); rc=$?
|
||||
assert_exit "no pattern exits 2" 2 "$rc"
|
||||
assert_match "no pattern error" "PATTERN is required" "$out"
|
||||
|
||||
out=$("$WAIT" noslash 'v*' 2>&1); rc=$?
|
||||
assert_exit "REPO without slash exits 2" 2 "$rc"
|
||||
assert_match "REPO slash error" "owner/name" "$out"
|
||||
|
||||
out=$("$WAIT" --timeout abc owner/repo 'v*' 2>&1); rc=$?
|
||||
assert_exit "non-numeric timeout exits 2" 2 "$rc"
|
||||
|
||||
out=$("$WAIT" --interval 0 owner/repo 'v*' 2>&1); rc=$?
|
||||
assert_exit "interval 0 exits 2" 2 "$rc"
|
||||
|
||||
out=$("$WAIT" --unknown-flag owner/repo 'v*' 2>&1); rc=$?
|
||||
assert_exit "unknown flag exits 2" 2 "$rc"
|
||||
|
||||
# --- Dryrun ---
|
||||
out=$("$WAIT" --dryrun myorg/myrepo 'v1.2.3.*')
|
||||
rc=$?
|
||||
assert_exit "dryrun exits 0" 0 "$rc"
|
||||
assert_match "dryrun shows server" "Server: " "$out"
|
||||
assert_match "dryrun shows repo" "Repo: myorg/myrepo" "$out"
|
||||
assert_match "dryrun shows pattern" 'Pattern: v1\.2\.3\.\*' "$out"
|
||||
assert_match "dryrun shows timeout default" "Timeout: 600s" "$out"
|
||||
assert_match "dryrun shows interval default" "Interval: 5s" "$out"
|
||||
assert_match "dryrun shows API URL" "api/v1/repos/myorg/myrepo/releases" "$out"
|
||||
|
||||
# Auth: no (no token anywhere)
|
||||
out=$(env -u GITEA_TOKEN -u GITHUB_TOKEN "$WAIT" --dryrun myorg/myrepo 'v*')
|
||||
assert_match "no token -> Auth: no" "Auth: no" "$out"
|
||||
|
||||
# Auth: yes via --token
|
||||
out=$(env -u GITEA_TOKEN -u GITHUB_TOKEN "$WAIT" --dryrun -t tok123 myorg/myrepo 'v*')
|
||||
assert_match "--token -> Auth: yes" "Auth: yes" "$out"
|
||||
|
||||
# Auth: yes via GITEA_TOKEN env
|
||||
out=$(env -u GITHUB_TOKEN GITEA_TOKEN=xyz "$WAIT" --dryrun myorg/myrepo 'v*')
|
||||
assert_match "GITEA_TOKEN env -> Auth: yes" "Auth: yes" "$out"
|
||||
|
||||
# --server override appears in URL
|
||||
out=$("$WAIT" --dryrun --server https://example.test myorg/myrepo 'v*')
|
||||
assert_match "custom --server in URL" "https://example\.test/api/v1/repos/myorg/myrepo/releases" "$out"
|
||||
|
||||
# --- Real match via mock curl ---
|
||||
export MOCK_CURL_BODY="$FIX_MATCH"
|
||||
export MOCK_CURL_CODE=200
|
||||
|
||||
out=$(run_with_mock --timeout 5 --interval 1 test/repo 'v1.2.3.*')
|
||||
rc=$?
|
||||
assert_exit "immediate match exits 0" 0 "$rc"
|
||||
assert_eq "immediate match prints tag" "v1.2.3.45" "$out"
|
||||
|
||||
out=$(run_with_mock --timeout 5 --interval 1 test/repo 'v1.*')
|
||||
assert_eq "wildcard v1.* matches newest" "v1.2.3.45" "$out"
|
||||
|
||||
out=$(run_with_mock --timeout 5 --interval 1 test/repo 'v1.2.2.40')
|
||||
assert_eq "exact tag match" "v1.2.2.40" "$out"
|
||||
|
||||
out=$(run_with_mock --timeout 5 --interval 1 test/repo 'v1.1.?.30')
|
||||
assert_eq "? wildcard matches" "v1.1.0.30" "$out"
|
||||
|
||||
# --- No match -> timeout (short) ---
|
||||
export MOCK_CURL_BODY="$FIX_NO_MATCH"
|
||||
start_t=$(date +%s)
|
||||
out=$(run_with_mock --timeout 2 --interval 1 test/repo 'v9.*' 2>&1); rc=$?
|
||||
end_t=$(date +%s)
|
||||
elapsed=$(( end_t - start_t ))
|
||||
assert_exit "no match exits 1" 1 "$rc"
|
||||
assert_match "no match: timeout error" "timeout after 2s" "$out"
|
||||
assert_between "timeout elapsed within tolerance" 2 4 "$elapsed"
|
||||
|
||||
# --- Empty releases list -> timeout ---
|
||||
export MOCK_CURL_BODY="$FIX_EMPTY"
|
||||
out=$(run_with_mock --timeout 1 --interval 1 test/repo 'v1.*' 2>&1); rc=$?
|
||||
assert_exit "empty list exits 1 (timeout)" 1 "$rc"
|
||||
|
||||
# --- 401 -> exit 3 ---
|
||||
export MOCK_CURL_BODY="$FIX_MATCH"
|
||||
export MOCK_CURL_CODE=401
|
||||
out=$(run_with_mock --timeout 5 --interval 1 test/repo 'v*' 2>&1); rc=$?
|
||||
assert_exit "401 exits 3" 3 "$rc"
|
||||
assert_match "401 error message" "authentication failed" "$out"
|
||||
|
||||
# --- 403 -> exit 3 ---
|
||||
export MOCK_CURL_CODE=403
|
||||
out=$(run_with_mock --timeout 5 --interval 1 test/repo 'v*' 2>&1); rc=$?
|
||||
assert_exit "403 exits 3" 3 "$rc"
|
||||
|
||||
# --- 404 -> exit 3 ---
|
||||
export MOCK_CURL_CODE=404
|
||||
out=$(run_with_mock --timeout 5 --interval 1 test/repo 'v*' 2>&1); rc=$?
|
||||
assert_exit "404 exits 3" 3 "$rc"
|
||||
assert_match "404 error message" "not found" "$out"
|
||||
|
||||
# --- Transient 500 -> retry until timeout ---
|
||||
export MOCK_CURL_CODE=500
|
||||
out=$(run_with_mock --timeout 2 --interval 1 test/repo 'v*' 2>&1); rc=$?
|
||||
assert_exit "persistent 500 exits 1 (timeout)" 1 "$rc"
|
||||
|
||||
# --- Network error (curl fails) -> retry until timeout ---
|
||||
export MOCK_CURL_FAIL=1
|
||||
unset MOCK_CURL_CODE
|
||||
out=$(run_with_mock --timeout 2 --interval 1 test/repo 'v*' 2>&1); rc=$?
|
||||
assert_exit "network error exits 1 (timeout)" 1 "$rc"
|
||||
unset MOCK_CURL_FAIL
|
||||
|
||||
# --- Verbose emits polling info to stderr ---
|
||||
export MOCK_CURL_BODY="$FIX_NO_MATCH"
|
||||
export MOCK_CURL_CODE=200
|
||||
stderr_out=$(run_with_mock --verbose --timeout 1 --interval 1 test/repo 'v99.*' 2>&1 >/dev/null || true)
|
||||
assert_match "verbose mentions poll" "poll " "$stderr_out"
|
||||
|
||||
# --- Polling still succeeds when --token is provided ---
|
||||
export MOCK_CURL_BODY="$FIX_MATCH"
|
||||
export MOCK_CURL_CODE=200
|
||||
out=$(run_with_mock --token secret123 --timeout 5 --interval 1 test/repo 'v1.2.3.*')
|
||||
assert_eq "polling with token still matches" "v1.2.3.45" "$out"
|
||||
|
||||
# --- Summary ---
|
||||
echo
|
||||
total=$(( pass + fail ))
|
||||
echo -e "Results: ${GREEN}${pass}${NC}/${total} passed"
|
||||
if (( fail > 0 )); then
|
||||
echo -e "${RED}${fail} test(s) failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user