detached a549ea6

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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
343343
344344 ```bash
345345 # 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
347347
348348 # Install the latest pre-release
349349 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
352352 curl -fsSL https://bnema.dev/gordon/install | GORDON_CHANNEL=next sh
353353 ```
354354
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`.
356356
357357 ## Verify Installation
358358
@@ -343,7 +343,7 @@ Without this setting, Cloudflare traffic receives `403 Forbidden: Only certifica
343 343
344 ```bash344 ```bash
345 # Install an exact release345 # Install an exact release
346-curl -fsSL https://bnema.dev/gordon/install | GORDON_VERSION=v2.30.1 sh346+curl -fsSL https://bnema.dev/gordon/install | GORDON_VERSION=v3.4.0 sh
347 347
348 # Install the latest pre-release348 # Install the latest pre-release
349 curl -fsSL https://bnema.dev/gordon/install | GORDON_PRERELEASE=1 sh349 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 sh352 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 Installation357 ## Verify Installation
358 358
M install.sh +10 -8
@@ -359,15 +359,17 @@ if [ "$VERSION" = "latest" ] && [ -n "$GORDON_PRERELEASE" ]; then
359359 echo "Using pre-release version: ${VERSION}"
360360 elif [ "$VERSION" = "latest" ]; then
361361 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
369370 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."
371373 exit 1
372374 fi
373375 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" ]; then360 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 tags362+ # 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" ]; then364+ # 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 1366+ VERSION=${LATEST_URL#"https://github.com/${REPO}/releases/tag/"}
367- fi367+ 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" ]; then370 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 1373 exit 1
372 fi374 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'
3636 case "\$*" in
3737 *api.github.com/repos/bnema/gordon/commits/next*) printf '%s\n' '{"sha":"$SHA"}' ;;
3838 *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}" ;;
3940 *) exit 90 ;;
4041 esac
4142 EOF
@@ -244,6 +245,37 @@ test_explicit_global_override() {
244245 fi
245246 }
246247
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
247279 test_next_build
248280 test_conflicts
249281 test_go_version
@@ -36,6 +36,7 @@ printf '%s\n' "\$*" >>'$case_dir/curl.log'
36 case "\$*" in36 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 esac41 esac
41 EOF42 EOF
@@ -244,6 +245,37 @@ test_explicit_global_override() {
244 fi245 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_build279 test_next_build
248 test_conflicts280 test_conflicts
249 test_go_version281 test_go_version

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close