From 1b0e432fe77b6718335e395e6c4e33fd61bf430a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 00:31:28 +0000 Subject: [PATCH 1/6] SRE-946: Report why a mise download failed in `.config/mise/install.sh` The install script gave no usable diagnostic when a Vercel build failed in it: `-f` discarded GitHub's response body, so a 4xx surfaced as a bare `curl: (22)` with no status or URL, and the `curl | grep | sha256sum -c -` pipeline reported a non-checksum body as a checksum parse failure. Route both downloads through a `fetch` helper that drops `-f`, records `%{http_code}` and the response headers, and on failure prints the URL, curl's exit code with a label, the HTTP status, any rate-limit headers and the first 2000 bytes of the body. Add retries and timeouts so a transient drop is retried rather than failing the build, and send an `Authorization` header when `GITHUB_TOKEN`/`MISE_GITHUB_TOKEN` is set, since curl does not read those variables itself. The checksum entry is now grepped into a file before verification, so a truncated or error-page response is reported as such. Verification itself is unchanged. --- .config/mise/install.sh | 78 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/.config/mise/install.sh b/.config/mise/install.sh index d12a4f62876..f1992e558ec 100755 --- a/.config/mise/install.sh +++ b/.config/mise/install.sh @@ -18,9 +18,83 @@ case "$(uname -m)" in esac tarball="mise-v${MISE_VERSION}-linux-${arch}.tar.gz" +base_url="https://github.com/jdx/mise/releases/download/v${MISE_VERSION}" + +# A token lifts GitHub's anonymous rate limit; unauthenticated installs still work. +# curl never reads these variables itself, so the header has to be passed explicitly. +# Plain `-L` drops `Authorization` when a redirect crosses to another host (only +# `--location-trusted` would keep it), so the token is not handed to the asset CDN. +curl_auth=() +github_token="${MISE_GITHUB_TOKEN:-${GITHUB_TOKEN:-}}" +if [[ -n ${github_token} ]]; then + curl_auth=(--header "Authorization: Bearer ${github_token}") +fi + +# Human-readable names for the curl exit codes seen on Vercel builds. +curl_exit_label() { + case "$1" in + 0) echo "no transport error" ;; + 6) echo "could not resolve host" ;; + 7) echo "could not connect to host" ;; + 22) echo "HTTP error returned" ;; + 28) echo "operation timed out" ;; + 35) echo "TLS connect error" ;; + 56) echo "failure receiving network data" ;; + *) echo "see EXIT CODES in 'man curl'" ;; + esac +} + +# Downloads $1 to $2. `--fail` is deliberately omitted so that the response body +# survives to be printed: a bare `curl: (22)` explains nothing about why a build broke. +fetch() { + local url=$1 output=$2 + local headers="${output}.headers" + local status=000 curl_status=0 + + status=$( + curl -sS -L \ + --retry 5 --retry-all-errors --retry-delay 2 \ + --connect-timeout 10 --max-time 300 \ + ${curl_auth[@]+"${curl_auth[@]}"} \ + --dump-header "${headers}" \ + --write-out '%{http_code}' \ + --output "${output}" \ + "${url}" + ) || curl_status=$? + + if [[ ${curl_status} -ne 0 || ${status} -lt 200 || ${status} -ge 300 ]]; then + { + echo "error: failed to download ${url}" + echo " curl exit code: ${curl_status} ($(curl_exit_label "${curl_status}"))" + echo " http status: ${status}" + grep -iE '^(retry-after|x-ratelimit-[a-z]+):' "${headers}" 2> /dev/null \ + | tr -d '\r' | sed 's/^/ /' || true + echo " response body (first 2000 bytes):" + head -c 2000 "${output}" 2> /dev/null | tr -d '\r' | tr -c '[:print:]\n\t' '.' | sed 's/^/ /' + echo + } >&2 + return 1 + fi +} cd "$(mktemp -d)" -curl -fsSL -O "https://github.com/jdx/mise/releases/download/v${MISE_VERSION}/${tarball}" -curl -fsSL "https://github.com/jdx/mise/releases/download/v${MISE_VERSION}/SHASUMS256.txt" | grep " \./${tarball}$" | sha256sum -c - + +fetch "${base_url}/${tarball}" "${tarball}" +fetch "${base_url}/SHASUMS256.txt" SHASUMS256.txt + +# Extract this tarball's entry before verifying, so an error page or a truncated +# response is reported as such rather than as a checksum parse failure. mise emits +# the names with a `./` prefix today; its own installer does not rely on that. +if ! grep -E "^[0-9a-f]{64} (\./)?${tarball}$" SHASUMS256.txt > "${tarball}.sha256"; then + { + echo "error: no checksum entry for ${tarball} in ${base_url}/SHASUMS256.txt" + echo " received (first 2000 bytes):" + head -c 2000 SHASUMS256.txt | tr -d '\r' | tr -c '[:print:]\n\t' '.' | sed 's/^/ /' + echo + } >&2 + exit 1 +fi + +sha256sum -c "${tarball}.sha256" tar --no-same-owner --strip-components=2 -C /usr/local/bin -xzf "${tarball}" mise/bin/mise rm "${tarball}" From 287f0258238b9d75c28e51ae583be364a531d64d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 00:56:53 +0000 Subject: [PATCH 2/6] SRE-946: Bound the retry wait and print rate-limit headers once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `--retry-delay 2` does not bound anything on the failure this change exists for. curl honours a server `Retry-After` in preference to `--retry-delay`, and GitHub answers a rate limit with `Retry-After: 60`, so the five retries stalled each download for five minutes — silently, because `-s` suppresses curl's "Will retry" notices and `-S` only re-enables hard errors. `--max-time 300` caps a single attempt, not the series. Measured against a stub returning 429 with `Retry-After: 60`: six attempts at t=0,60,120,180,240,300s, 300.3s wall per `fetch`. Add `--retry-max-time 60` so the series gives up after a minute (measured: 60.0s, two attempts). `--dump-header` appends across retries and redirect hops, so a real 429 printed the same five-line `x-ratelimit-*` block once per attempt — six repetitions of the detail the report is meant to make readable. Replace the `grep` with an awk pass that resets on each status line and prints only the headers that followed the last one. --- .config/mise/install.sh | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/.config/mise/install.sh b/.config/mise/install.sh index f1992e558ec..710ba4c01cd 100755 --- a/.config/mise/install.sh +++ b/.config/mise/install.sh @@ -46,6 +46,13 @@ curl_exit_label() { # Downloads $1 to $2. `--fail` is deliberately omitted so that the response body # survives to be printed: a bare `curl: (22)` explains nothing about why a build broke. +# +# `--retry-max-time` is what actually bounds the wait. curl honours a server +# `Retry-After` in preference to `--retry-delay`, and GitHub answers a rate limit +# with `Retry-After: 60` — the very case these retries exist for — so without a +# ceiling the five retries stall the build for ~5 minutes per download, silently, +# because `-s` hides curl's "Will retry" notices. `--max-time` does not help: it +# caps a single attempt, not the series. fetch() { local url=$1 output=$2 local headers="${output}.headers" @@ -53,7 +60,7 @@ fetch() { status=$( curl -sS -L \ - --retry 5 --retry-all-errors --retry-delay 2 \ + --retry 5 --retry-all-errors --retry-delay 2 --retry-max-time 60 \ --connect-timeout 10 --max-time 300 \ ${curl_auth[@]+"${curl_auth[@]}"} \ --dump-header "${headers}" \ @@ -67,8 +74,14 @@ fetch() { echo "error: failed to download ${url}" echo " curl exit code: ${curl_status} ($(curl_exit_label "${curl_status}"))" echo " http status: ${status}" - grep -iE '^(retry-after|x-ratelimit-[a-z]+):' "${headers}" 2> /dev/null \ - | tr -d '\r' | sed 's/^/ /' || true + # `--dump-header` appends, so the file holds every retry and every redirect + # hop. Reset on each status line and keep only what followed the last one, + # otherwise a rate-limited download repeats the same block once per attempt. + awk ' + /^[Hh][Tt][Tt][Pp]\// { count = 0; next } + tolower($0) ~ /^(retry-after|x-ratelimit-[a-z-]+):/ { header[++count] = $0 } + END { for (i = 1; i <= count; i++) print header[i] } + ' "${headers}" 2> /dev/null | tr -d '\r' | sed 's/^/ /' || true echo " response body (first 2000 bytes):" head -c 2000 "${output}" 2> /dev/null | tr -d '\r' | tr -c '[:print:]\n\t' '.' | sed 's/^/ /' echo From bbff329bfc13a720925b797b0fab6859cb1b5489 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 00:57:43 +0000 Subject: [PATCH 3/6] SRE-946: Drop `curl_exit_label` in favour of curl's own message `-S` already makes curl print its own diagnostic for a transport failure, so the label table restated it: a connection failure emitted both `curl: (7) Failed to connect to ... Couldn't connect to server` and `curl exit code: 7 (could not connect to host)`. On the HTTP-error path curl exits 0 and the label was the vacuous "no transport error". Keep the bare numeric exit code, which curl does not always make obvious, and drop the table. --- .config/mise/install.sh | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/.config/mise/install.sh b/.config/mise/install.sh index 710ba4c01cd..c2b62781747 100755 --- a/.config/mise/install.sh +++ b/.config/mise/install.sh @@ -30,20 +30,6 @@ if [[ -n ${github_token} ]]; then curl_auth=(--header "Authorization: Bearer ${github_token}") fi -# Human-readable names for the curl exit codes seen on Vercel builds. -curl_exit_label() { - case "$1" in - 0) echo "no transport error" ;; - 6) echo "could not resolve host" ;; - 7) echo "could not connect to host" ;; - 22) echo "HTTP error returned" ;; - 28) echo "operation timed out" ;; - 35) echo "TLS connect error" ;; - 56) echo "failure receiving network data" ;; - *) echo "see EXIT CODES in 'man curl'" ;; - esac -} - # Downloads $1 to $2. `--fail` is deliberately omitted so that the response body # survives to be printed: a bare `curl: (22)` explains nothing about why a build broke. # @@ -72,7 +58,9 @@ fetch() { if [[ ${curl_status} -ne 0 || ${status} -lt 200 || ${status} -ge 300 ]]; then { echo "error: failed to download ${url}" - echo " curl exit code: ${curl_status} ($(curl_exit_label "${curl_status}"))" + # `-S` already prints curl's own message for a transport failure, so only + # the bare number is added here. + echo " curl exit code: ${curl_status}" echo " http status: ${status}" # `--dump-header` appends, so the file holds every retry and every redirect # hop. Reset on each status line and keep only what followed the last one, From 80697d20431e75c6ac34cb8709e7c0a96ac0e178 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 01:03:06 +0000 Subject: [PATCH 4/6] SRE-946: Retry `mise install` so the rustup download is covered MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `nick-fields/retry` wrapper on `Install Rust` never protected the download it was added for. `idiomatic_version_file_enable_tools = ["rust"]` makes `mise install` read `rust-toolchain.toml`, and mise's `core:rust` plugin installs the toolchain by shelling out to `rustup` instead of fetching anything itself, so the toolchain and its components are downloaded during the earlier, unwrapped `jdx/mise-action` step. Attempt 1 of `Package (@rust/hash-codec)` on this branch died exactly there: a 63s TCP connect timeout to `static.rust-lang.org` fetching `rustfmt-nightly-x86_64-unknown-linux-gnu`, after which `Install Rust` was skipped and never got to retry. mise's own `http_retries` cannot cover this — it applies to mise's HTTP client, and rustup does its own downloading in a subprocess. Setting `install: false` and running `mise install` under the wrapper does not work either: the action saves its tool cache only inside the branch that runs the install, so that would leave the 543 MB cache permanently unwritten on a cache miss. Instead let the action install and cache as before, mark it non-fatal, and retry `mise install` in a following step. The retry is skipped on the happy path, and its condition tests `!= 'success'` so an unresolved `outcome` retries rather than swallowing the failure. --- .github/actions/install-tools/action.yml | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.github/actions/install-tools/action.yml b/.github/actions/install-tools/action.yml index 9eb84f79382..f2afc82d8c5 100644 --- a/.github/actions/install-tools/action.yml +++ b/.github/actions/install-tools/action.yml @@ -22,8 +22,19 @@ runs: using: composite steps: + # `idiomatic_version_file_enable_tools = ["rust"]` makes `mise install` read + # `rust-toolchain.toml`, and mise's `core:rust` plugin installs the toolchain by shelling + # out to `rustup` rather than downloading anything itself. So mise's own `http_retries` + # does not cover those downloads, and a `static.rust-lang.org` connect timeout failed this + # step outright — before the retry-wrapped `Install Rust` step below ever ran, which is why + # that wrapper never protected the download it was added for. Treat a failed install as + # non-fatal and retry it in the next step; the action still sets mise up and saves its cache + # here, so moving `mise install` out entirely (`install: false`) is not an option — the + # action only saves the cache on the path that runs the install. - name: Run `mise install` with `ci` environment + id: mise-install uses: jdx/mise-action@146a28175021df8ca24f8ee1828cc2a60f980bd5 # v3.5.1 + continue-on-error: true with: # renovate: datasource=github-releases depName=jdx/mise version: 2026.7.14 @@ -32,6 +43,20 @@ runs: MISE_VERBOSE: 1 GITHUB_TOKEN: ${{ inputs.token }} + # Skipped on the happy path. The condition is `!= 'success'` rather than `== 'failure'` so + # that an unresolved `outcome` retries instead of silently swallowing the failure above. + - name: Retry `mise install` if it failed + if: ${{ steps.mise-install.outcome != 'success' }} + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2 + with: + timeout_minutes: 20 + max_attempts: 5 + retry_wait_seconds: 60 + command: mise install --env ci --jobs 1 --locked + env: + MISE_VERBOSE: 1 + GITHUB_TOKEN: ${{ inputs.token }} + - name: Install package manager via corepack uses: $/.github/actions/install-corepack From 9a4e98baf257584aed6c97428d6cbbb47eeaa4ca Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 01:16:52 +0000 Subject: [PATCH 5/6] SRE-946: Fail fast when `mise` is missing instead of retrying MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `continue-on-error` on `jdx/mise-action` absorbs every failure mode, not only the rustup connect timeout it was added for. When the action fails during its own setup, `mise` is never on `PATH` and the retry step spends five attempts and ~4 minutes of `retry_wait_seconds` on `mise: command not found` — less legible than the failure it replaced. Guard the retry with an explicit check on the same condition. --- .github/actions/install-tools/action.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/actions/install-tools/action.yml b/.github/actions/install-tools/action.yml index f2afc82d8c5..ed4c88f6ecf 100644 --- a/.github/actions/install-tools/action.yml +++ b/.github/actions/install-tools/action.yml @@ -43,6 +43,16 @@ runs: MISE_VERBOSE: 1 GITHUB_TOKEN: ${{ inputs.token }} + # Without `mise` on `PATH` there is no install to retry, only five minutes of `not found`. + - name: Fail fast if `mise` itself is missing + if: ${{ steps.mise-install.outcome != 'success' }} + shell: bash + run: | + command -v mise > /dev/null || { + echo "::error::mise is not on PATH: jdx/mise-action failed during its own setup, so there is no install to retry" + exit 1 + } + # Skipped on the happy path. The condition is `!= 'success'` rather than `== 'failure'` so # that an unresolved `outcome` retries instead of silently swallowing the failure above. - name: Retry `mise install` if it failed From 2e363a513e8e44e0b0fe81a958a77aeff559a65d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 01:17:17 +0000 Subject: [PATCH 6/6] SRE-946: Trim the comments to one line each The reasoning behind `--retry-max-time`, the `--dump-header` awk pass, the cross-host redirect and the `continue-on-error` retry is all in the PR body; restating it in the files put them well above the comment density of the surrounding repo. --- .config/mise/install.sh | 23 ++++------------------- .github/actions/install-tools/action.yml | 14 +++----------- 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/.config/mise/install.sh b/.config/mise/install.sh index c2b62781747..c53c483804a 100755 --- a/.config/mise/install.sh +++ b/.config/mise/install.sh @@ -20,10 +20,7 @@ esac tarball="mise-v${MISE_VERSION}-linux-${arch}.tar.gz" base_url="https://github.com/jdx/mise/releases/download/v${MISE_VERSION}" -# A token lifts GitHub's anonymous rate limit; unauthenticated installs still work. -# curl never reads these variables itself, so the header has to be passed explicitly. -# Plain `-L` drops `Authorization` when a redirect crosses to another host (only -# `--location-trusted` would keep it), so the token is not handed to the asset CDN. +# curl needs the header passed explicitly, and plain `-L` drops it on a cross-host redirect. curl_auth=() github_token="${MISE_GITHUB_TOKEN:-${GITHUB_TOKEN:-}}" if [[ -n ${github_token} ]]; then @@ -32,13 +29,7 @@ fi # Downloads $1 to $2. `--fail` is deliberately omitted so that the response body # survives to be printed: a bare `curl: (22)` explains nothing about why a build broke. -# -# `--retry-max-time` is what actually bounds the wait. curl honours a server -# `Retry-After` in preference to `--retry-delay`, and GitHub answers a rate limit -# with `Retry-After: 60` — the very case these retries exist for — so without a -# ceiling the five retries stall the build for ~5 minutes per download, silently, -# because `-s` hides curl's "Will retry" notices. `--max-time` does not help: it -# caps a single attempt, not the series. +# `--retry-max-time` bounds the series: curl prefers a server `Retry-After`, and GitHub sends 60. fetch() { local url=$1 output=$2 local headers="${output}.headers" @@ -58,13 +49,9 @@ fetch() { if [[ ${curl_status} -ne 0 || ${status} -lt 200 || ${status} -ge 300 ]]; then { echo "error: failed to download ${url}" - # `-S` already prints curl's own message for a transport failure, so only - # the bare number is added here. echo " curl exit code: ${curl_status}" echo " http status: ${status}" - # `--dump-header` appends, so the file holds every retry and every redirect - # hop. Reset on each status line and keep only what followed the last one, - # otherwise a rate-limited download repeats the same block once per attempt. + # `--dump-header` appends, so keep only the headers following the last status line. awk ' /^[Hh][Tt][Tt][Pp]\// { count = 0; next } tolower($0) ~ /^(retry-after|x-ratelimit-[a-z-]+):/ { header[++count] = $0 } @@ -83,9 +70,7 @@ cd "$(mktemp -d)" fetch "${base_url}/${tarball}" "${tarball}" fetch "${base_url}/SHASUMS256.txt" SHASUMS256.txt -# Extract this tarball's entry before verifying, so an error page or a truncated -# response is reported as such rather than as a checksum parse failure. mise emits -# the names with a `./` prefix today; its own installer does not rely on that. +# Extract the entry first, so a bad response is reported as such and not as a parse failure. if ! grep -E "^[0-9a-f]{64} (\./)?${tarball}$" SHASUMS256.txt > "${tarball}.sha256"; then { echo "error: no checksum entry for ${tarball} in ${base_url}/SHASUMS256.txt" diff --git a/.github/actions/install-tools/action.yml b/.github/actions/install-tools/action.yml index ed4c88f6ecf..b327e84d5cd 100644 --- a/.github/actions/install-tools/action.yml +++ b/.github/actions/install-tools/action.yml @@ -22,15 +22,8 @@ runs: using: composite steps: - # `idiomatic_version_file_enable_tools = ["rust"]` makes `mise install` read - # `rust-toolchain.toml`, and mise's `core:rust` plugin installs the toolchain by shelling - # out to `rustup` rather than downloading anything itself. So mise's own `http_retries` - # does not cover those downloads, and a `static.rust-lang.org` connect timeout failed this - # step outright — before the retry-wrapped `Install Rust` step below ever ran, which is why - # that wrapper never protected the download it was added for. Treat a failed install as - # non-fatal and retry it in the next step; the action still sets mise up and saves its cache - # here, so moving `mise install` out entirely (`install: false`) is not an option — the - # action only saves the cache on the path that runs the install. + # `mise install` builds the Rust toolchain by shelling out to `rustup`, so mise's own + # `http_retries` does not cover that download. Non-fatal here, retried below. - name: Run `mise install` with `ci` environment id: mise-install uses: jdx/mise-action@146a28175021df8ca24f8ee1828cc2a60f980bd5 # v3.5.1 @@ -53,8 +46,7 @@ runs: exit 1 } - # Skipped on the happy path. The condition is `!= 'success'` rather than `== 'failure'` so - # that an unresolved `outcome` retries instead of silently swallowing the failure above. + # Skipped on the happy path; `!= 'success'` so an unresolved `outcome` retries too. - name: Retry `mise install` if it failed if: ${{ steps.mise-install.outcome != 'success' }} uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2