fix(install): resolve the latest stable tag without the GitHub API
Follow the releases/latest web redirect to get the tag instead of calling api.github.com, which rate-limits or blocks anonymous IPs (HTTP 403). The tag is validated before use, and the error suggests GORDON_VERSION.
- commit
- a549ea65a8d6d5ac250529565d0dc0dd9630c552
- author
- brice · · 2026-10-17 06:27 UTC
- parent
- 593c1c3
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email - refs
- fix/install-latest-without-api
3 files · +44 -10
M docs/installation.md +2 -2
| @@ -343,7 +343,7 @@ Without this setting, Cloudflare traffic receives `403 Forbidden: Only certifica | |||
| 343 | 343 | ||
| 344 | 344 | ```bash | |
| 345 | 345 | # Install an exact release | |
| 346 | - | curl -fsSL https://bnema.dev/gordon/install | GORDON_VERSION=v2.30.1 sh | |
| 346 | + | curl -fsSL https://bnema.dev/gordon/install | GORDON_VERSION=v3.4.0 sh | |
| 347 | 347 | ||
| 348 | 348 | # Install the latest pre-release | |
| 349 | 349 | curl -fsSL https://bnema.dev/gordon/install | GORDON_PRERELEASE=1 sh | |
| @@ -352,7 +352,7 @@ curl -fsSL https://bnema.dev/gordon/install | GORDON_PRERELEASE=1 sh | |||
| 352 | 352 | curl -fsSL https://bnema.dev/gordon/install | GORDON_CHANNEL=next sh | |
| 353 | 353 | ``` | |
| 354 | 354 | ||
| 355 | - | Stable, exact-version, and pre-release installs download release binaries and verify their published checksums. The `next` channel is an **unverified development source build**: it resolves the branch through the GitHub API, pins the resulting commit SHA, downloads that exact source snapshot, and builds it locally for the detected platform. It requires the Go version declared by that commit's `go.mod`, is not covered by release checksums, and may be unstable. Do not combine `GORDON_CHANNEL=next` with `GORDON_VERSION` or `GORDON_PRERELEASE`. | |
| 355 | + | Stable, exact-version, and pre-release installs download release binaries and verify their published checksums. The default stable install resolves the latest tag from the `github.com/bnema/gordon/releases/latest` redirect, so it does not use the rate-limited GitHub API; exact-version installs make no lookup at all. Pre-release lookups still use the GitHub API. The `next` channel is an **unverified development source build**: it resolves the branch through the GitHub API, pins the resulting commit SHA, downloads that exact source snapshot, and builds it locally for the detected platform. It requires the Go version declared by that commit's `go.mod`, is not covered by release checksums, and may be unstable. Do not combine `GORDON_CHANNEL=next` with `GORDON_VERSION` or `GORDON_PRERELEASE`. | |
| 356 | 356 | ||
| 357 | 357 | ## Verify Installation | |
| 358 | 358 | ||
| @@ -343,7 +343,7 @@ Without this setting, Cloudflare traffic receives `403 Forbidden: Only certifica | |||||
| 343 | 343 | ||||
| 344 | ```bash | 344 | ```bash | ||
| 345 | # Install an exact release | 345 | # Install an exact release | ||
| 346 | - | curl -fsSL https://bnema.dev/gordon/install | GORDON_VERSION=v2.30.1 sh | 346 | + | curl -fsSL https://bnema.dev/gordon/install | GORDON_VERSION=v3.4.0 sh |
| 347 | 347 | ||||
| 348 | # Install the latest pre-release | 348 | # Install the latest pre-release | ||
| 349 | curl -fsSL https://bnema.dev/gordon/install | GORDON_PRERELEASE=1 sh | 349 | curl -fsSL https://bnema.dev/gordon/install | GORDON_PRERELEASE=1 sh | ||
| @@ -352,7 +352,7 @@ curl -fsSL https://bnema.dev/gordon/install | GORDON_PRERELEASE=1 sh | |||||
| 352 | curl -fsSL https://bnema.dev/gordon/install | GORDON_CHANNEL=next sh | 352 | curl -fsSL https://bnema.dev/gordon/install | GORDON_CHANNEL=next sh | ||
| 353 | ``` | 353 | ``` | ||
| 354 | 354 | ||||
| 355 | - | Stable, exact-version, and pre-release installs download release binaries and verify their published checksums. The `next` channel is an **unverified development source build**: it resolves the branch through the GitHub API, pins the resulting commit SHA, downloads that exact source snapshot, and builds it locally for the detected platform. It requires the Go version declared by that commit's `go.mod`, is not covered by release checksums, and may be unstable. Do not combine `GORDON_CHANNEL=next` with `GORDON_VERSION` or `GORDON_PRERELEASE`. | 355 | + | Stable, exact-version, and pre-release installs download release binaries and verify their published checksums. The default stable install resolves the latest tag from the `github.com/bnema/gordon/releases/latest` redirect, so it does not use the rate-limited GitHub API; exact-version installs make no lookup at all. Pre-release lookups still use the GitHub API. The `next` channel is an **unverified development source build**: it resolves the branch through the GitHub API, pins the resulting commit SHA, downloads that exact source snapshot, and builds it locally for the detected platform. It requires the Go version declared by that commit's `go.mod`, is not covered by release checksums, and may be unstable. Do not combine `GORDON_CHANNEL=next` with `GORDON_VERSION` or `GORDON_PRERELEASE`. |
| 356 | 356 | ||||
| 357 | ## Verify Installation | 357 | ## Verify Installation | ||
| 358 | 358 | ||||
M install.sh +10 -8
| @@ -359,15 +359,17 @@ if [ "$VERSION" = "latest" ] && [ -n "$GORDON_PRERELEASE" ]; then | |||
| 359 | 359 | echo "Using pre-release version: ${VERSION}" | |
| 360 | 360 | elif [ "$VERSION" = "latest" ]; then | |
| 361 | 361 | echo "Using latest stable release" | |
| 362 | - | # Resolve "latest" to actual tag name since GitHub download URLs require exact tags | |
| 363 | - | RELEASE_DATA=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" 2>/dev/null || echo "") | |
| 364 | - | if [ -z "$RELEASE_DATA" ]; then | |
| 365 | - | echo "Error: Failed to fetch latest release information from GitHub API" | |
| 366 | - | exit 1 | |
| 367 | - | fi | |
| 368 | - | VERSION=$(echo "$RELEASE_DATA" | grep -m1 '"tag_name"' | sed -E 's/.*"tag_name":\s*"([^"]+)".*/\1/') | |
| 362 | + | # Resolve "latest" to an exact tag by following the web redirect | |
| 363 | + | # (releases/latest -> releases/tag/<tag>). Unlike the REST API, this is | |
| 364 | + | # not rate-limited per IP for anonymous clients. | |
| 365 | + | LATEST_URL=$(curl -fsSL -o /dev/null -w '%{url_effective}' "https://github.com/${REPO}/releases/latest" 2>/dev/null || echo "") | |
| 366 | + | VERSION=${LATEST_URL#"https://github.com/${REPO}/releases/tag/"} | |
| 367 | + | case "$VERSION" in | |
| 368 | + | "$LATEST_URL"|''|*[!0-9A-Za-z._+-]*) VERSION="" ;; | |
| 369 | + | esac | |
| 369 | 370 | if [ -z "$VERSION" ]; then | |
| 370 | - | echo "Error: Could not determine latest stable version" | |
| 371 | + | echo "Error: Could not determine latest stable version from https://github.com/${REPO}/releases/latest" | |
| 372 | + | echo "Set GORDON_VERSION (e.g. GORDON_VERSION=v3.4.0) to install a specific release." | |
| 371 | 373 | exit 1 | |
| 372 | 374 | fi | |
| 373 | 375 | echo "Resolved version: ${VERSION}" | |
| @@ -359,15 +359,17 @@ if [ "$VERSION" = "latest" ] && [ -n "$GORDON_PRERELEASE" ]; then | |||||
| 359 | echo "Using pre-release version: ${VERSION}" | 359 | echo "Using pre-release version: ${VERSION}" | ||
| 360 | elif [ "$VERSION" = "latest" ]; then | 360 | elif [ "$VERSION" = "latest" ]; then | ||
| 361 | echo "Using latest stable release" | 361 | echo "Using latest stable release" | ||
| 362 | - | # Resolve "latest" to actual tag name since GitHub download URLs require exact tags | 362 | + | # Resolve "latest" to an exact tag by following the web redirect |
| 363 | - | RELEASE_DATA=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" 2>/dev/null || echo "") | 363 | + | # (releases/latest -> releases/tag/<tag>). Unlike the REST API, this is |
| 364 | - | if [ -z "$RELEASE_DATA" ]; then | 364 | + | # not rate-limited per IP for anonymous clients. |
| 365 | - | echo "Error: Failed to fetch latest release information from GitHub API" | 365 | + | LATEST_URL=$(curl -fsSL -o /dev/null -w '%{url_effective}' "https://github.com/${REPO}/releases/latest" 2>/dev/null || echo "") |
| 366 | - | exit 1 | 366 | + | VERSION=${LATEST_URL#"https://github.com/${REPO}/releases/tag/"} |
| 367 | - | fi | 367 | + | case "$VERSION" in |
| 368 | - | VERSION=$(echo "$RELEASE_DATA" | grep -m1 '"tag_name"' | sed -E 's/.*"tag_name":\s*"([^"]+)".*/\1/') | 368 | + | "$LATEST_URL"|''|*[!0-9A-Za-z._+-]*) VERSION="" ;; |
| 369 | + | esac | |||
| 369 | if [ -z "$VERSION" ]; then | 370 | if [ -z "$VERSION" ]; then | ||
| 370 | - | echo "Error: Could not determine latest stable version" | 371 | + | echo "Error: Could not determine latest stable version from https://github.com/${REPO}/releases/latest" |
| 372 | + | echo "Set GORDON_VERSION (e.g. GORDON_VERSION=v3.4.0) to install a specific release." | |||
| 371 | exit 1 | 373 | exit 1 | ||
| 372 | fi | 374 | fi | ||
| 373 | echo "Resolved version: ${VERSION}" | 375 | echo "Resolved version: ${VERSION}" | ||
M tests/install_next_test.sh +32 -0
| @@ -36,6 +36,7 @@ printf '%s\n' "\$*" >>'$case_dir/curl.log' | |||
| 36 | 36 | case "\$*" in | |
| 37 | 37 | *api.github.com/repos/bnema/gordon/commits/next*) printf '%s\n' '{"sha":"$SHA"}' ;; | |
| 38 | 38 | *codeload.github.com/bnema/gordon/tar.gz/$SHA*) : >'$case_dir/source.tar.gz' ;; | |
| 39 | + | *github.com/bnema/gordon/releases/latest*) printf '%s' "\${MOCK_LATEST_URL-https://github.com/bnema/gordon/releases/tag/v9.9.9}" ;; | |
| 39 | 40 | *) exit 90 ;; | |
| 40 | 41 | esac | |
| 41 | 42 | EOF | |
| @@ -244,6 +245,37 @@ test_explicit_global_override() { | |||
| 244 | 245 | fi | |
| 245 | 246 | } | |
| 246 | 247 | ||
| 248 | + | test_stable_latest_without_api() { | |
| 249 | + | run_case stable-latest env sh "$ROOT/install.sh" | |
| 250 | + | # The mock refuses the checksum download; resolution is what is tested. | |
| 251 | + | if grep -q "Resolved version: v9.9.9" "$case_dir/out" && | |
| 252 | + | grep -q "github.com/bnema/gordon/releases/download/v9.9.9/checksums.txt" "$case_dir/curl.log" && | |
| 253 | + | ! grep -q "api.github.com" "$case_dir/curl.log"; then | |
| 254 | + | pass "stable latest resolves from the release redirect without the API" | |
| 255 | + | else | |
| 256 | + | cat "$case_dir/out" | |
| 257 | + | fail "stable latest resolves from the release redirect without the API" | |
| 258 | + | fi | |
| 259 | + | ||
| 260 | + | run_case stable-latest-bad env MOCK_LATEST_URL='https://github.com/bnema/gordon/releases' sh "$ROOT/install.sh" | |
| 261 | + | if [ "$status" -ne 0 ] && grep -q "Could not determine latest stable version" "$case_dir/out" && | |
| 262 | + | ! grep -q "releases/download" "$case_dir/curl.log"; then | |
| 263 | + | pass "unexpected latest redirect is rejected" | |
| 264 | + | else | |
| 265 | + | cat "$case_dir/out" | |
| 266 | + | fail "unexpected latest redirect is rejected" | |
| 267 | + | fi | |
| 268 | + | ||
| 269 | + | run_case stable-latest-unsafe env MOCK_LATEST_URL='https://github.com/bnema/gordon/releases/tag/v1.0.0;rm' sh "$ROOT/install.sh" | |
| 270 | + | if [ "$status" -ne 0 ] && grep -q "Could not determine latest stable version" "$case_dir/out"; then | |
| 271 | + | pass "unsafe latest tag is rejected" | |
| 272 | + | else | |
| 273 | + | cat "$case_dir/out" | |
| 274 | + | fail "unsafe latest tag is rejected" | |
| 275 | + | fi | |
| 276 | + | } | |
| 277 | + | ||
| 278 | + | test_stable_latest_without_api | |
| 247 | 279 | test_next_build | |
| 248 | 280 | test_conflicts | |
| 249 | 281 | test_go_version | |
| @@ -36,6 +36,7 @@ printf '%s\n' "\$*" >>'$case_dir/curl.log' | |||||
| 36 | case "\$*" in | 36 | case "\$*" in | ||
| 37 | *api.github.com/repos/bnema/gordon/commits/next*) printf '%s\n' '{"sha":"$SHA"}' ;; | 37 | *api.github.com/repos/bnema/gordon/commits/next*) printf '%s\n' '{"sha":"$SHA"}' ;; | ||
| 38 | *codeload.github.com/bnema/gordon/tar.gz/$SHA*) : >'$case_dir/source.tar.gz' ;; | 38 | *codeload.github.com/bnema/gordon/tar.gz/$SHA*) : >'$case_dir/source.tar.gz' ;; | ||
| 39 | + | *github.com/bnema/gordon/releases/latest*) printf '%s' "\${MOCK_LATEST_URL-https://github.com/bnema/gordon/releases/tag/v9.9.9}" ;; | |||
| 39 | *) exit 90 ;; | 40 | *) exit 90 ;; | ||
| 40 | esac | 41 | esac | ||
| 41 | EOF | 42 | EOF | ||
| @@ -244,6 +245,37 @@ test_explicit_global_override() { | |||||
| 244 | fi | 245 | fi | ||
| 245 | } | 246 | } | ||
| 246 | 247 | ||||
| 248 | + | test_stable_latest_without_api() { | |||
| 249 | + | run_case stable-latest env sh "$ROOT/install.sh" | |||
| 250 | + | # The mock refuses the checksum download; resolution is what is tested. | |||
| 251 | + | if grep -q "Resolved version: v9.9.9" "$case_dir/out" && | |||
| 252 | + | grep -q "github.com/bnema/gordon/releases/download/v9.9.9/checksums.txt" "$case_dir/curl.log" && | |||
| 253 | + | ! grep -q "api.github.com" "$case_dir/curl.log"; then | |||
| 254 | + | pass "stable latest resolves from the release redirect without the API" | |||
| 255 | + | else | |||
| 256 | + | cat "$case_dir/out" | |||
| 257 | + | fail "stable latest resolves from the release redirect without the API" | |||
| 258 | + | fi | |||
| 259 | + | ||||
| 260 | + | run_case stable-latest-bad env MOCK_LATEST_URL='https://github.com/bnema/gordon/releases' sh "$ROOT/install.sh" | |||
| 261 | + | if [ "$status" -ne 0 ] && grep -q "Could not determine latest stable version" "$case_dir/out" && | |||
| 262 | + | ! grep -q "releases/download" "$case_dir/curl.log"; then | |||
| 263 | + | pass "unexpected latest redirect is rejected" | |||
| 264 | + | else | |||
| 265 | + | cat "$case_dir/out" | |||
| 266 | + | fail "unexpected latest redirect is rejected" | |||
| 267 | + | fi | |||
| 268 | + | ||||
| 269 | + | run_case stable-latest-unsafe env MOCK_LATEST_URL='https://github.com/bnema/gordon/releases/tag/v1.0.0;rm' sh "$ROOT/install.sh" | |||
| 270 | + | if [ "$status" -ne 0 ] && grep -q "Could not determine latest stable version" "$case_dir/out"; then | |||
| 271 | + | pass "unsafe latest tag is rejected" | |||
| 272 | + | else | |||
| 273 | + | cat "$case_dir/out" | |||
| 274 | + | fail "unsafe latest tag is rejected" | |||
| 275 | + | fi | |||
| 276 | + | } | |||
| 277 | + | ||||
| 278 | + | test_stable_latest_without_api | |||
| 247 | test_next_build | 279 | test_next_build | ||
| 248 | test_conflicts | 280 | test_conflicts | ||
| 249 | test_go_version | 281 | test_go_version | ||