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

112 lines
4.4 KiB
Markdown

# 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}.*"
```