Files
small-scripts/specs/wait-for-release.spec.md
Paul O'Reilly 5a1b4b14bc 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>
2026-04-17 20:06:24 +12:00

4.4 KiB

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

# 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.

wait-for-release skynet/myapp "v${VERSION}.*"