Skip to content

Commit dfe0364

Browse files
mdesmetclauderalphstodomingo
authored
fix(install): resilient latest-version fetch (both installers) (#946)
* fix(install): don't hard-fail when the GitHub releases API blips Reported on #930: a transient 504 from api.github.com/.../releases/latest (or the 60/hr/IP unauthenticated rate limit) aborted the whole install with "Failed to fetch version information" — even though the download itself uses releases/latest/download/<file>, which GitHub resolves server-side with no API call. The API response only feeds the version-string display and the already-installed short-circuit. Both installers now, in the latest path: - retry the API call up to 3x with linear backoff (bash uses curl --fail so a 504 retries instead of parsing an error body); - on continued failure, print a muted notice and proceed to install latest anyway (version string shown as "latest"); - only short-circuit as "already installed" on a real version match — never treat empty==empty (unresolved version + unreadable binary) as installed. Pinned-version installs (-Version / --version) are unchanged: a genuine 404 still hard-fails. Tests: version-fetch-resilience.test.ts pins the retry + graceful-degrade behavior in both installers. bash -n clean; install.ps1 parses clean and the Pester suite (6/6) still passes on PowerShell 7.6.2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(install): address latest-version-fetch review - install: append `|| true` to the retry's curl|sed assignment. Under `set -euo pipefail` a failing `curl --fail` propagated through the pipeline and aborted the script at attempt 1, before the loop could retry or degrade (sahrizvi; reproduced: exit 22 without the fix, all 3 attempts + degrade with it). Also add `--max-time 10` to bound a dead-air socket. - install.ps1: reset $specificVersion to $null (not "") on the degrade path, so the already-installed short-circuit can't false-match "" -eq "" when the version probe of a missing/corrupt binary also yields "" (dev-punia, sahrizvi). - install.ps1: add -TimeoutSec 10 to Invoke-RestMethod (defaults to 100s on PS 5.1, unbounded on PS 7+) to bound retries on dead air (sahrizvi). - tests: TS guards for `|| true`, --max-time/-TimeoutSec, and the $null reset. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019M7GkS3bYZaFhEbBhVTecG * fix(install.ps1): ASCII-only so it parses on Windows PowerShell 5.1 install.ps1 had no BOM and used a few non-ASCII characters (em dash, ellipsis, right arrow) in comments and messages. Windows PowerShell 5.1 - the default shell on Windows 10 and preinstalled on Windows 11 - reads a BOM-less file as the system ANSI codepage, not UTF-8, so those multi-byte characters corrupt the token stream and the whole script fails to parse (verified on real PS 5.1). This is a pre-existing issue (the characters predate this PR) that CI doesn't catch because the Pester job runs under pwsh (PowerShell 7, UTF-8 by default). Replacing the three characters with ASCII equivalents (-, ..., ->) makes the installer parse and run on PS 5.1 while keeping pwsh behavior identical. Also removes the now-obsolete "integrity verification deferred" NOTE comment: the sibling PR #942 implements that verification and removes the same block, so deleting it here too keeps the two PRs mergeable in either order with no conflict. Same transliteration is applied verbatim in #942. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019M7GkS3bYZaFhEbBhVTecG * test: update #930 release-validation for resilient version fetch The #952 release-validation suite asserted the latest path hard-fails with "Failed to fetch version information" (>=2) and that exit 1 appears >=3 times. This PR makes the latest path retry then degrade gracefully instead of aborting, so update those assertions: the latest path no longer hard-fails (the unsupported -arch and pinned-404 paths still exit 1, hence >=2). (This test never ran on this PR until it was retargeted from the merged feat/windows-powershell-installer branch to main.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019M7GkS3bYZaFhEbBhVTecG --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: ralphstodomingo <ralphstodomingo@users.noreply.github.com>
1 parent 31e73d0 commit dfe0364

4 files changed

Lines changed: 129 additions & 21 deletions

File tree

‎install‎

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,26 @@ else
205205

206206
if [ -z "$requested_version" ]; then
207207
url="https://github.com/AltimateAI/altimate-code/releases/latest/download/$filename"
208-
specific_version=$(curl -s https://api.github.com/repos/AltimateAI/altimate-code/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
209-
210-
if [[ $? -ne 0 || -z "$specific_version" ]]; then
211-
echo -e "${RED}Failed to fetch version information${NC}"
212-
exit 1
208+
# The download above resolves "latest" server-side, so this API call only
209+
# feeds the version display and the already-installed short-circuit. A
210+
# transient api.github.com blip or the unauthenticated rate limit
211+
# (60/hr/IP) must NOT abort the install — retry a few times with --fail
212+
# (so a 504 retries instead of parsing an error body), then proceed
213+
# without the version string.
214+
#
215+
# --max-time 10 bounds a dead-air socket (curl's default has no transfer
216+
# cap), and the trailing `|| true` is load-bearing: under `set -euo
217+
# pipefail`, a failing `curl --fail` propagates through the pipeline and
218+
# the assignment, so `set -e` would abort the script before the loop can
219+
# retry or degrade. `|| true` lets the failure resolve to an empty string.
220+
specific_version=""
221+
for attempt in 1 2 3; do
222+
specific_version=$(curl -fsSL --max-time 10 https://api.github.com/repos/AltimateAI/altimate-code/releases/latest 2>/dev/null | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p' || true)
223+
[ -n "$specific_version" ] && break
224+
[ "$attempt" -lt 3 ] && sleep "$attempt"
225+
done
226+
if [ -z "$specific_version" ]; then
227+
echo -e "${MUTED}Could not resolve the latest version from GitHub (API unavailable) — installing the latest release anyway.${NC}"
213228
fi
214229
else
215230
# Strip leading 'v' if present
@@ -255,11 +270,14 @@ check_version() {
255270
if [ -n "$probe" ]; then
256271
installed_version=$("$probe" --version 2>/dev/null || echo "")
257272

258-
if [[ "$installed_version" != "$specific_version" ]]; then
259-
print_message info "${MUTED}Installed version: ${NC}$installed_version."
260-
else
273+
# Only short-circuit on a real version match. When the latest version
274+
# couldn't be resolved (API unavailable → specific_version empty), never
275+
# treat an empty==empty as "already installed" — fall through and reinstall.
276+
if [ -n "$specific_version" ] && [[ "$installed_version" == "$specific_version" ]]; then
261277
print_message info "${MUTED}Version ${NC}$specific_version${MUTED} already installed${NC}"
262278
exit 0
279+
elif [ -n "$installed_version" ]; then
280+
print_message info "${MUTED}Installed version: ${NC}$installed_version."
263281
fi
264282
fi
265283
}
@@ -410,7 +428,7 @@ verify_checksum() {
410428
}
411429

412430
download_and_install() {
413-
print_message info "\n${MUTED}Installing ${NC}altimate ${MUTED}version: ${NC}$specific_version"
431+
print_message info "\n${MUTED}Installing ${NC}altimate ${MUTED}version: ${NC}${specific_version:-latest}"
414432
local tmp_dir="${TMPDIR:-/tmp}/altimate_install_$$"
415433
mkdir -p "$tmp_dir"
416434

‎install.ps1‎

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -152,16 +152,32 @@ function Test-Avx2 {
152152
# ---------------------------------------------------------------------------
153153
if ([string]::IsNullOrWhiteSpace($Version)) {
154154
$useLatest = $true
155-
try {
156-
$rel = Invoke-RestMethod -Uri "https://api.github.com/repos/AltimateAI/altimate-code/releases/latest" -Headers @{ "User-Agent" = "altimate-install" }
157-
$specificVersion = ($rel.tag_name -replace '^v', '')
158-
} catch {
159-
Write-Err "Failed to fetch version information"
160-
exit 1
155+
# The download below resolves "latest" server-side (releases/latest/download),
156+
# so this API call only feeds the version-string display and the
157+
# already-installed short-circuit. A transient api.github.com blip or the
158+
# unauthenticated rate limit (60/hr/IP) must NOT abort the install - retry a
159+
# few times, then proceed without the version string.
160+
$specificVersion = ""
161+
for ($attempt = 1; $attempt -le 3; $attempt++) {
162+
try {
163+
# -TimeoutSec 10 bounds a stuck socket: Invoke-RestMethod defaults to 100s
164+
# on PS 5.1 and is effectively unbounded on PS 7+, so without it three
165+
# back-to-back retries on dead air could freeze for minutes.
166+
$rel = Invoke-RestMethod -Uri "https://api.github.com/repos/AltimateAI/altimate-code/releases/latest" -Headers @{ "User-Agent" = "altimate-install" } -TimeoutSec 10
167+
$specificVersion = ($rel.tag_name -replace '^v', '')
168+
if (-not [string]::IsNullOrWhiteSpace($specificVersion)) { break }
169+
} catch {}
170+
if ($attempt -lt 3) { Start-Sleep -Seconds $attempt }
161171
}
162172
if ([string]::IsNullOrWhiteSpace($specificVersion)) {
163-
Write-Err "Failed to fetch version information"
164-
exit 1
173+
Write-Muted "Could not resolve the latest version from GitHub (API unavailable) - installing the latest release anyway."
174+
# Reset to $null (not ""): the already-installed short-circuit below compares
175+
# $installedVersion -eq $specificVersion. If the version probe of a missing or
176+
# corrupt binary also yields "", an "" -eq "" match would falsely report
177+
# "already installed" and skip the reinstall. $null -eq "" is $false, so the
178+
# comparison correctly falls through; the banner still shows "latest" because
179+
# if ($specificVersion) treats $null as falsy.
180+
$specificVersion = $null
165181
}
166182
} else {
167183
$useLatest = $false
@@ -225,7 +241,7 @@ function Install-Target {
225241
$checksumsUrl = "$base/checksums.txt"
226242

227243
Write-Host ""
228-
Write-Host "Installing $App version: $specificVersion"
244+
Write-Host "Installing $App version: $(if ($specificVersion) { $specificVersion } else { 'latest' })"
229245

230246
$tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "altimate_install_$PID"
231247
New-Item -ItemType Directory -Force -Path $tmpDir | Out-Null
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Latest-version resolution must be resilient, in BOTH installers.
3+
*
4+
* The `latest` install path hits api.github.com/.../releases/latest only for the
5+
* version-string display + the already-installed short-circuit — the download
6+
* itself uses releases/latest/download/<file> (server-side latest). A transient
7+
* 504 or the 60/hr/IP unauthenticated rate limit must NOT abort the install:
8+
* retry a few times, then degrade gracefully and install latest anyway.
9+
*/
10+
import { describe, test, expect } from "bun:test"
11+
import { readFileSync } from "node:fs"
12+
import { join } from "node:path"
13+
14+
const REPO_ROOT = join(import.meta.dir, "../../../..")
15+
const BASH = readFileSync(join(REPO_ROOT, "install"), "utf-8")
16+
const PS1 = readFileSync(join(REPO_ROOT, "install.ps1"), "utf-8")
17+
18+
describe("bash installer — latest-version fetch is non-fatal", () => {
19+
test("retries the releases/latest API call", () => {
20+
expect(BASH).toContain("for attempt in 1 2 3")
21+
// --fail so a 504 errors out (and retries) instead of parsing an error body.
22+
expect(BASH).toContain("curl -fsSL --max-time 10 https://api.github.com")
23+
})
24+
25+
test("the retry assignment absorbs curl failure so set -e can't abort it", () => {
26+
// Under `set -euo pipefail`, a failing `curl --fail` propagates through the
27+
// pipeline + assignment and aborts the script before the loop can retry or
28+
// degrade. The trailing `|| true` keeps the retry loop alive.
29+
expect(BASH).toMatch(/curl -fsSL --max-time 10 https:\/\/api\.github\.com[^\n]*\|\| true/)
30+
})
31+
32+
test("bounds the API call with a transfer timeout", () => {
33+
expect(BASH).toContain("--max-time 10")
34+
})
35+
36+
test("degrades gracefully instead of exiting on API failure", () => {
37+
expect(BASH).toContain("installing the latest release anyway")
38+
// The old fatal hard-fail must be gone from the latest path.
39+
expect(BASH).not.toContain("Failed to fetch version information")
40+
})
41+
42+
test("only short-circuits as already-installed on a real version match", () => {
43+
expect(BASH).toContain('[ -n "$specific_version" ] && [[ "$installed_version" == "$specific_version" ]]')
44+
})
45+
})
46+
47+
describe("PowerShell installer — latest-version fetch is non-fatal", () => {
48+
test("retries the releases/latest API call", () => {
49+
expect(PS1).toContain("for ($attempt = 1; $attempt -le 3; $attempt++)")
50+
})
51+
52+
test("bounds the API call with a request timeout", () => {
53+
expect(PS1).toContain("-TimeoutSec 10")
54+
})
55+
56+
test("degrades gracefully instead of exiting on API failure", () => {
57+
expect(PS1).toContain("installing the latest release anyway")
58+
// The old fatal hard-fail must be gone.
59+
expect(PS1).not.toContain("Failed to fetch version information")
60+
})
61+
62+
test("resets the unresolved version to $null so empty==empty can't false-match", () => {
63+
// $installedVersion -eq $specificVersion with both "" would falsely report
64+
// "already installed" for a missing/corrupt binary; $null -eq "" is $false.
65+
expect(PS1).toContain("$specificVersion = $null")
66+
})
67+
})

‎packages/opencode/test/release-validation/windows-installer-930-codex.test.ts‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,17 @@ describe("PR #930 install.ps1 release URL construction", () => {
5555
expect(versionBlock).toContain("exit 1")
5656
})
5757

58-
test("latest-version resolution requires a nonblank GitHub release tag", () => {
58+
test("latest-version resolution retries then degrades instead of hard-failing", () => {
5959
const versionBlock = scriptBlock("# Resolve version (once)", "# Skip if the requested version")
6060
expect(versionBlock).toContain("[string]::IsNullOrWhiteSpace($Version)")
6161
expect(versionBlock).toContain('"User-Agent" = "altimate-install"')
6262
expect(versionBlock).toContain("$specificVersion = ($rel.tag_name -replace '^v', '')")
6363
expect(versionBlock).toContain("[string]::IsNullOrWhiteSpace($specificVersion)")
64-
expect(versionBlock.match(/Failed to fetch version information/g)?.length).toBeGreaterThanOrEqual(2)
64+
// A transient releases/latest API blip must not abort the install: retry a few
65+
// times, then degrade gracefully (the download resolves "latest" server-side).
66+
expect(versionBlock).toContain("for ($attempt = 1; $attempt -le 3; $attempt++)")
67+
expect(versionBlock).toContain("installing the latest release anyway")
68+
expect(versionBlock).not.toContain("Failed to fetch version information")
6569
})
6670
})
6771

@@ -110,7 +114,10 @@ describe("PR #930 install.ps1 error handling and idempotency", () => {
110114
expect(INSTALL_PS1).toContain('$ErrorActionPreference = "Stop"')
111115
expect(INSTALL_PS1.match(/\btry\s*\{/g)?.length).toBeGreaterThanOrEqual(4)
112116
expect(INSTALL_PS1.match(/\bcatch\s*\{/g)?.length).toBeGreaterThanOrEqual(4)
113-
expect(INSTALL_PS1.match(/exit 1/g)?.length).toBeGreaterThanOrEqual(3)
117+
// The unsupported-arch and pinned-version-not-found paths still hard-fail
118+
// (exit 1). The latest-version path no longer does: it degrades gracefully on
119+
// a transient API blip rather than aborting the install.
120+
expect(INSTALL_PS1.match(/exit 1/g)?.length).toBeGreaterThanOrEqual(2)
114121
})
115122

116123
test("skips reinstall when altimate or altimate-code already reports the target version", () => {

0 commit comments

Comments
 (0)