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:
@@ -25,6 +25,7 @@ This project uses **spec-driven development** (OpenSpec) as a testbed for agent-
|
||||
| `unreflected-logs` | Scan projects for unreflected session logs | Done |
|
||||
| `validate-skill` | Validate SKILL.md files against known Claude Code restrictions | Done |
|
||||
| `semver-ci` | Compute `MAJOR.MINOR.PATCH.BUILD` from commit-message tokens, maintain `VERSION.md`, and (on main) tag/push/create a Gitea release — see [`docs/semver-ci.md`](docs/semver-ci.md) and template at [`templates/gitea-workflow-version.yml`](templates/gitea-workflow-version.yml) | Done |
|
||||
| `wait-for-release` | Poll a Gitea repo's releases until a glob pattern matches or a timeout expires (default 600s) | Done |
|
||||
|
||||
### Engagement modes (claude-profile)
|
||||
|
||||
|
||||
171
scripts/wait-for-release
Executable file
171
scripts/wait-for-release
Executable file
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
|
||||
DEFAULT_SERVER="https://gitea.oreillyit.nz"
|
||||
DEFAULT_TIMEOUT=600
|
||||
DEFAULT_INTERVAL=5
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: wait-for-release [OPTIONS] REPO PATTERN
|
||||
|
||||
Poll a Gitea repo's releases until one matches PATTERN (shell-style glob)
|
||||
or the timeout expires.
|
||||
|
||||
Arguments:
|
||||
REPO owner/name (e.g. skynet/myapp)
|
||||
PATTERN shell-style glob matched against each release's tag_name
|
||||
(e.g. 'v1.2.3.*', 'v1.2.*', 'v1.*'). Quote it to prevent
|
||||
your shell from expanding it.
|
||||
|
||||
Options:
|
||||
-t, --token TOKEN Gitea access token (else $GITEA_TOKEN / $GITHUB_TOKEN)
|
||||
--timeout SECONDS Max total wait time (default: 600; 0 = poll once)
|
||||
--interval SECONDS Polling interval (default: 5; min: 1)
|
||||
--server URL Gitea base URL (else $GITHUB_SERVER_URL, else
|
||||
https://gitea.oreillyit.nz)
|
||||
-v, --verbose Print polling progress to stderr
|
||||
-n, --dryrun Show resolved configuration without making API calls
|
||||
-h, --help Show this help
|
||||
|
||||
Exit codes:
|
||||
0 matching release found (tag_name printed to stdout)
|
||||
1 timeout expired with no match
|
||||
2 usage error
|
||||
3 terminal API error (401, 403, 404)
|
||||
EOF
|
||||
}
|
||||
|
||||
timeout=$DEFAULT_TIMEOUT
|
||||
interval=$DEFAULT_INTERVAL
|
||||
server="${GITHUB_SERVER_URL:-$DEFAULT_SERVER}"
|
||||
token="${GITEA_TOKEN:-${GITHUB_TOKEN:-}}"
|
||||
verbose=false
|
||||
dryrun=false
|
||||
repo=""
|
||||
pattern=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-n|--dryrun) dryrun=true; shift ;;
|
||||
-v|--verbose) verbose=true; shift ;;
|
||||
-t|--token)
|
||||
[[ $# -lt 2 ]] && { echo "Error: --token requires a value" >&2; exit 2; }
|
||||
token="$2"; shift 2 ;;
|
||||
--timeout)
|
||||
[[ $# -lt 2 ]] && { echo "Error: --timeout requires a value" >&2; exit 2; }
|
||||
timeout="$2"; shift 2 ;;
|
||||
--interval)
|
||||
[[ $# -lt 2 ]] && { echo "Error: --interval requires a value" >&2; exit 2; }
|
||||
interval="$2"; shift 2 ;;
|
||||
--server)
|
||||
[[ $# -lt 2 ]] && { echo "Error: --server requires a value" >&2; exit 2; }
|
||||
server="$2"; shift 2 ;;
|
||||
--) shift; break ;;
|
||||
-*) echo "Error: Unknown option: $1" >&2; exit 2 ;;
|
||||
*)
|
||||
if [[ -z "$repo" ]]; then repo="$1"
|
||||
elif [[ -z "$pattern" ]]; then pattern="$1"
|
||||
else echo "Error: Unexpected argument: $1" >&2; exit 2
|
||||
fi
|
||||
shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$repo" ]] && { echo "Error: REPO is required" >&2; echo >&2; usage >&2; exit 2; }
|
||||
[[ -z "$pattern" ]] && { echo "Error: PATTERN is required" >&2; echo >&2; usage >&2; exit 2; }
|
||||
[[ "$repo" != */* ]] && { echo "Error: REPO must be in 'owner/name' form (got: $repo)" >&2; exit 2; }
|
||||
[[ ! "$timeout" =~ ^[0-9]+$ ]] && { echo "Error: --timeout must be a non-negative integer (got: $timeout)" >&2; exit 2; }
|
||||
[[ ! "$interval" =~ ^[0-9]+$ ]] && { echo "Error: --interval must be a positive integer (got: $interval)" >&2; exit 2; }
|
||||
(( interval < 1 )) && { echo "Error: --interval must be >= 1" >&2; exit 2; }
|
||||
|
||||
api_url="${server%/}/api/v1/repos/${repo}/releases?limit=50"
|
||||
|
||||
if $dryrun; then
|
||||
echo "[dryrun] Server: ${server}"
|
||||
echo "[dryrun] Repo: ${repo}"
|
||||
echo "[dryrun] Pattern: ${pattern}"
|
||||
echo "[dryrun] Timeout: ${timeout}s"
|
||||
echo "[dryrun] Interval: ${interval}s"
|
||||
if [[ -n "$token" ]]; then
|
||||
echo "[dryrun] Auth: yes"
|
||||
else
|
||||
echo "[dryrun] Auth: no"
|
||||
fi
|
||||
echo "[dryrun] API URL: ${api_url}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
extract_tags() {
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
jq -r '.[]?.tag_name // empty' 2>/dev/null
|
||||
else
|
||||
grep -oE '"tag_name"[[:space:]]*:[[:space:]]*"[^"]+"' \
|
||||
| sed -E 's/.*"([^"]+)"$/\1/'
|
||||
fi
|
||||
}
|
||||
|
||||
curl_cmd=(curl -sS -m 30 -w $'\n%{http_code}')
|
||||
[[ -n "$token" ]] && curl_cmd+=(-H "Authorization: token ${token}")
|
||||
|
||||
start=$(date +%s)
|
||||
deadline=$(( start + timeout ))
|
||||
poll=0
|
||||
|
||||
while true; do
|
||||
poll=$(( poll + 1 ))
|
||||
|
||||
response="$("${curl_cmd[@]}" "$api_url" 2>/dev/null)"
|
||||
curl_rc=$?
|
||||
|
||||
if (( curl_rc != 0 )); then
|
||||
$verbose && echo "wait-for-release: poll ${poll}: network/curl error (rc=${curl_rc})" >&2
|
||||
else
|
||||
http_code="${response##*$'\n'}"
|
||||
body="${response%$'\n'*}"
|
||||
|
||||
case "$http_code" in
|
||||
200)
|
||||
match=""
|
||||
while IFS= read -r tag; do
|
||||
[[ -z "$tag" ]] && continue
|
||||
# shellcheck disable=SC2053 # intentional glob match
|
||||
if [[ "$tag" == $pattern ]]; then
|
||||
match="$tag"
|
||||
break
|
||||
fi
|
||||
done < <(printf '%s' "$body" | extract_tags)
|
||||
|
||||
if [[ -n "$match" ]]; then
|
||||
$verbose && echo "wait-for-release: poll ${poll}: matched '${match}'" >&2
|
||||
echo "$match"
|
||||
exit 0
|
||||
fi
|
||||
$verbose && echo "wait-for-release: poll ${poll}: HTTP 200, no match yet" >&2
|
||||
;;
|
||||
401|403)
|
||||
echo "Error: Gitea API authentication failed (HTTP ${http_code}) for ${repo}" >&2
|
||||
exit 3 ;;
|
||||
404)
|
||||
echo "Error: repo not found: ${repo} (HTTP 404)" >&2
|
||||
exit 3 ;;
|
||||
*)
|
||||
$verbose && echo "wait-for-release: poll ${poll}: HTTP ${http_code}, retrying" >&2
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
now=$(date +%s)
|
||||
if (( now >= deadline )); then
|
||||
break
|
||||
fi
|
||||
remaining=$(( deadline - now ))
|
||||
sleep_time=$interval
|
||||
(( sleep_time > remaining )) && sleep_time=$remaining
|
||||
$verbose && echo "wait-for-release: poll ${poll}: sleeping ${sleep_time}s (${remaining}s remaining)" >&2
|
||||
sleep "$sleep_time"
|
||||
done
|
||||
|
||||
echo "Error: timeout after ${timeout}s waiting for '${pattern}' in ${repo}" >&2
|
||||
exit 1
|
||||
111
specs/wait-for-release.spec.md
Normal file
111
specs/wait-for-release.spec.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# wait-for-release
|
||||
|
||||
## Purpose
|
||||
|
||||
Poll a Gitea repository's releases endpoint until a release whose `tag_name` matches a shell-style glob pattern is published, or a timeout elapses. Designed to run in the background of a Claude Code session while a CI pipeline produces the release.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
wait-for-release [OPTIONS] REPO PATTERN
|
||||
```
|
||||
|
||||
### Arguments
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `REPO` | Gitea repo in `owner/name` form (e.g. `skynet/myapp`). |
|
||||
| `PATTERN` | Shell-style glob matched against each release's `tag_name` (e.g. `v1.2.3.*`). **Quote it** to prevent the caller's shell from expanding it. |
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-t`, `--token TOKEN` | Gitea access token. Fallback order: `$GITEA_TOKEN`, `$GITHUB_TOKEN`. Omitted → unauthenticated request. |
|
||||
| `--timeout SECONDS` | Max total wait time (default: 600). `0` means poll once then give up. |
|
||||
| `--interval SECONDS` | Polling interval (default: 5; min: 1). |
|
||||
| `--server URL` | Gitea base URL. Fallback order: `$GITHUB_SERVER_URL`, `https://gitea.oreillyit.nz`. |
|
||||
| `-v`, `--verbose` | Emit polling progress to stderr. |
|
||||
| `-n`, `--dryrun` | Print resolved configuration and exit; make no API calls. |
|
||||
| `-h`, `--help` | Show usage. |
|
||||
|
||||
## Behaviour
|
||||
|
||||
1. Validate inputs: REPO must be `owner/name`; `--timeout` is a non-negative integer; `--interval` is an integer ≥ 1.
|
||||
2. Compute `API_URL = <server>/api/v1/repos/<owner>/<name>/releases?limit=50`.
|
||||
3. Compute `DEADLINE = now + timeout`.
|
||||
4. Loop:
|
||||
1. `GET API_URL` with a 30-second per-request timeout. Add `Authorization: token <TOKEN>` when a token is available.
|
||||
2. On HTTP 200: extract `tag_name` values from the JSON body (via `jq` when installed, otherwise a simple regex). For each tag, test `[[ "$tag" == $PATTERN ]]` (bash glob). On the first match, print the tag to stdout and exit 0.
|
||||
3. On HTTP 401 or 403: print auth error to stderr, exit 3.
|
||||
4. On HTTP 404: print not-found error to stderr, exit 3.
|
||||
5. On any other HTTP code or network failure: keep retrying (log to stderr when `--verbose`).
|
||||
5. Between polls, sleep for `--interval` seconds, capped so the sleep never overshoots the deadline.
|
||||
6. When the deadline has passed without a match, print a timeout error to stderr and exit 1.
|
||||
|
||||
## Dryrun Behaviour
|
||||
|
||||
When `--dryrun` is passed, no HTTP requests are made. Output (to stdout):
|
||||
|
||||
```
|
||||
[dryrun] Server: https://gitea.oreillyit.nz
|
||||
[dryrun] Repo: skynet/myapp
|
||||
[dryrun] Pattern: v1.2.3.*
|
||||
[dryrun] Timeout: 600s
|
||||
[dryrun] Interval: 5s
|
||||
[dryrun] Auth: yes
|
||||
[dryrun] API URL: https://gitea.oreillyit.nz/api/v1/repos/skynet/myapp/releases?limit=50
|
||||
```
|
||||
|
||||
`Auth: yes` when a token was supplied or present in the environment; otherwise `Auth: no`.
|
||||
|
||||
## Exit Codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 0 | A release matching PATTERN was found; its `tag_name` is on stdout. |
|
||||
| 1 | Timeout elapsed with no match. |
|
||||
| 2 | Usage error (missing / invalid arguments). |
|
||||
| 3 | Terminal API error (401, 403, 404). |
|
||||
|
||||
## Edge Cases
|
||||
|
||||
| Case | Handling |
|
||||
|------|----------|
|
||||
| REPO missing the slash | Error, exit 2. |
|
||||
| PATTERN unquoted by caller and expanded by their shell | Caller's responsibility; the spec requires quoting. |
|
||||
| No token for a private repo | API returns 401/403 → exit 3. |
|
||||
| Invalid token | HTTP 401 → exit 3. |
|
||||
| Repo doesn't exist / hidden | HTTP 404 → exit 3. |
|
||||
| Transient 5xx or network error | Retry each `--interval` seconds until timeout. |
|
||||
| Multiple releases match PATTERN | The first match in API response order (most recent first) wins. |
|
||||
| PATTERN matches an already-published release | Returns on the first poll. |
|
||||
| `--timeout 0` | Exactly one poll, then timeout if no match. |
|
||||
| `--interval 0` | Error, exit 2. |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Wait for any build of a specific semver
|
||||
wait-for-release skynet/myapp 'v1.2.3.*'
|
||||
|
||||
# Exact build
|
||||
wait-for-release skynet/myapp 'v1.2.3.45'
|
||||
|
||||
# Any v1.x release, 20-minute window
|
||||
wait-for-release --timeout 1200 skynet/myapp 'v1.*'
|
||||
|
||||
# With explicit token for a private repo
|
||||
wait-for-release -t "$MY_TOKEN" skynet/private 'v1.2.*'
|
||||
|
||||
# Dryrun preview
|
||||
wait-for-release --dryrun skynet/myapp 'v1.*'
|
||||
```
|
||||
|
||||
### In a Claude Code session
|
||||
|
||||
Invoke via the Bash tool with `run_in_background=true`. The session is notified when the script exits. On success stdout holds the matched tag; the exit code signals the outcome.
|
||||
|
||||
```bash
|
||||
wait-for-release skynet/myapp "v${VERSION}.*"
|
||||
```
|
||||
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