From 0ed37d8c710b9dc804cfacad62f007884232d5fb Mon Sep 17 00:00:00 2001 From: nicodes Date: Sat, 1 Aug 2026 02:35:19 -0600 Subject: [PATCH 1/2] Authenticate the latest-release lookup Resolving "latest" calls the GitHub API unauthenticated, and that is capped at 60 requests an hour PER IP ADDRESS. CI runners share addresses, GitHub-hosted macOS runners heavily so, and the result is a 403 several times an hour: curl: (56) The requested URL returned error: 403 failed to resolve latest release for aviorstudio/gdam That failed installs intermittently for every repository using gdam-actions, and it blocked a release three times in a row this morning before the cause was clear -- which is the second half of this change. The token is optional and the unauthenticated path is untouched: someone installing by hand from their own address is nowhere near the limit. GDAM_GITHUB_TOKEN is checked first so a caller can point this at a different token, then GITHUB_TOKEN and GH_TOKEN, which CI and the gh CLI already set. It reaches curl through a config on STDIN rather than as -H on the command line, because arguments are visible in the process list to every other user on the machine and this script runs on shared boxes as well as in CI. The error message now names the rate limit when no token was used. It previously said only that the release could not be resolved, which reads as "there is no release" and sends you to the wrong repository -- the limit was the actual cause every time it fired. --retry covers the transient half: 429 and 5xx are retried, and a 403 from the rate limiter is not, so an exhausted quota still fails fast rather than sleeping through three attempts. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/install_cli.sh | 53 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/scripts/install_cli.sh b/scripts/install_cli.sh index 7c70b4c..183318b 100755 --- a/scripts/install_cli.sh +++ b/scripts/install_cli.sh @@ -36,9 +36,41 @@ detect_arch() { esac } +# A token for the GitHub API, if the caller has one. Empty is fine and is the +# normal case for a person installing by hand. +# +# GDAM_GITHUB_TOKEN first so a caller can point this at a different token than +# whatever GITHUB_TOKEN happens to hold; then the two names CI and the gh CLI +# already set, so most callers need do nothing. +api_token() { + printf '%s' "${GDAM_GITHUB_TOKEN:-${GITHUB_TOKEN:-${GH_TOKEN:-}}}" +} + +# The tag of the newest release, or nothing. +# +# AUTHENTICATED when a token is available, because the unauthenticated GitHub +# API allows 60 requests an hour PER IP -- and CI runners share addresses. +# GitHub-hosted macOS runners share them heavily enough that this call returns +# 403 several times an hour, which made "latest" installs fail intermittently +# for every repository using the action. A token raises the limit to 5000/hour +# against the account rather than the address. +# +# The token reaches curl through a config on STDIN rather than as -H on the +# command line: arguments are visible in the process list to every other user +# on the machine, and this script runs on shared boxes as well as in CI. +# +# --retry covers the transient half of the same problem: 429 and 5xx are +# retried, and a 403 from the rate limiter is not, so a genuinely exhausted +# quota still fails fast rather than sleeping through three attempts. latest_tag() { - curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" \ - | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' + _url="https://api.github.com/repos/$REPO/releases/latest" + _token="$(api_token)" + if [ -n "$_token" ]; then + printf 'header = "Authorization: Bearer %s"\n' "$_token" \ + | curl -fsSL --retry 3 --retry-delay 2 -K - "$_url" + else + curl -fsSL --retry 3 --retry-delay 2 "$_url" + fi } pick_install_dir() { @@ -99,9 +131,24 @@ OS="$(detect_os)" ARCH="$(detect_arch)" if [ "$VERSION" = "latest" ]; then - TAG="$(latest_tag)" + # `|| true` because the failure is reported below with a cause attached. A + # bare pipeline would swallow curl's status anyway -- sed exits 0 on empty + # input -- so this makes that explicit rather than accidental. + API_RESPONSE="$(latest_tag || true)" + TAG="$(printf '%s' "$API_RESPONSE" | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p')" if [ -z "$TAG" ]; then printf 'failed to resolve latest release for %s\n' "$REPO" >&2 + # Name the likely cause. Without this the message says only that the + # release could not be resolved, which reads as "there is no release" -- + # and sends you looking at the wrong repository. The rate limit was the + # actual cause every time this fired in CI. + if [ -z "$(api_token)" ]; then + printf '\n' >&2 + printf 'The GitHub API allows 60 unauthenticated requests an hour per IP address,\n' >&2 + printf 'and CI runners share addresses. If this is CI, set GITHUB_TOKEN (or\n' >&2 + printf 'GH_TOKEN) in the environment to raise that to 5000/hour, or install a\n' >&2 + printf 'pinned VERSION instead of "latest".\n' >&2 + fi exit 1 fi else From d28c04c01751d1fe1624f085d291ab795dde1d37 Mon Sep 17 00:00:00 2001 From: nicodes Date: Sat, 1 Aug 2026 02:41:58 -0600 Subject: [PATCH 2/2] Correct the rate-limit figure 5000/hour is a personal access token. A workflow's GITHUB_TOKEN gets 1000/hour PER REPOSITORY, which is the number that matters here because CI is where this fires. Both are a large improvement on 60/hour shared across every repository on a runner address, and the denominator is the real change -- but the code and the error message should not state a figure nobody will observe. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/install_cli.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/install_cli.sh b/scripts/install_cli.sh index 183318b..70df8fb 100755 --- a/scripts/install_cli.sh +++ b/scripts/install_cli.sh @@ -52,8 +52,11 @@ api_token() { # API allows 60 requests an hour PER IP -- and CI runners share addresses. # GitHub-hosted macOS runners share them heavily enough that this call returns # 403 several times an hour, which made "latest" installs fail intermittently -# for every repository using the action. A token raises the limit to 5000/hour -# against the account rather than the address. +# for every repository using the action. A token scopes the limit to the token +# rather than to the address: 1000/hour per repository for a workflow's +# GITHUB_TOKEN, 5000/hour for a personal access token. The denominator is what +# matters more than the number -- one repository's runs stop competing with +# every other repository sharing that runner. # # The token reaches curl through a config on STDIN rather than as -H on the # command line: arguments are visible in the process list to every other user @@ -146,8 +149,9 @@ if [ "$VERSION" = "latest" ]; then printf '\n' >&2 printf 'The GitHub API allows 60 unauthenticated requests an hour per IP address,\n' >&2 printf 'and CI runners share addresses. If this is CI, set GITHUB_TOKEN (or\n' >&2 - printf 'GH_TOKEN) in the environment to raise that to 5000/hour, or install a\n' >&2 - printf 'pinned VERSION instead of "latest".\n' >&2 + printf 'GH_TOKEN) in the environment to scope the limit to the token instead\n' >&2 + printf '(1000/hour per repository), or install a pinned VERSION rather than\n' >&2 + printf '"latest".\n' >&2 fi exit 1 fi