Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions install
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,26 @@ else

if [ -z "$requested_version" ]; then
url="https://github.com/AltimateAI/altimate-code/releases/latest/download/$filename"
specific_version=$(curl -s https://api.github.com/repos/AltimateAI/altimate-code/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')

if [[ $? -ne 0 || -z "$specific_version" ]]; then
echo -e "${RED}Failed to fetch version information${NC}"
exit 1
# The download above resolves "latest" server-side, so this API call only
# feeds the version display and the already-installed short-circuit. A
# transient api.github.com blip or the unauthenticated rate limit
# (60/hr/IP) must NOT abort the install — retry a few times with --fail
# (so a 504 retries instead of parsing an error body), then proceed
# without the version string.
#
# --max-time 10 bounds a dead-air socket (curl's default has no transfer
# cap), and the trailing `|| true` is load-bearing: under `set -euo
# pipefail`, a failing `curl --fail` propagates through the pipeline and
# the assignment, so `set -e` would abort the script before the loop can
# retry or degrade. `|| true` lets the failure resolve to an empty string.
specific_version=""
for attempt in 1 2 3; do
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)
[ -n "$specific_version" ] && break
[ "$attempt" -lt 3 ] && sleep "$attempt"
done
if [ -z "$specific_version" ]; then
echo -e "${MUTED}Could not resolve the latest version from GitHub (API unavailable) — installing the latest release anyway.${NC}"
fi
else
# Strip leading 'v' if present
Expand Down Expand Up @@ -255,11 +270,14 @@ check_version() {
if [ -n "$probe" ]; then
installed_version=$("$probe" --version 2>/dev/null || echo "")

if [[ "$installed_version" != "$specific_version" ]]; then
print_message info "${MUTED}Installed version: ${NC}$installed_version."
else
# Only short-circuit on a real version match. When the latest version
# couldn't be resolved (API unavailable → specific_version empty), never
# treat an empty==empty as "already installed" — fall through and reinstall.
if [ -n "$specific_version" ] && [[ "$installed_version" == "$specific_version" ]]; then
print_message info "${MUTED}Version ${NC}$specific_version${MUTED} already installed${NC}"
exit 0
elif [ -n "$installed_version" ]; then
print_message info "${MUTED}Installed version: ${NC}$installed_version."
fi
fi
}
Expand Down Expand Up @@ -357,7 +375,7 @@ download_with_progress() {
}

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

Expand Down
54 changes: 32 additions & 22 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#
# Mirrors ./install (the bash installer for macOS/Linux): it downloads the
# Bun-compiled standalone executable (altimate.exe) from GitHub releases and
# drops it in %USERPROFILE%\.altimate\bin it does NOT depend on npm/Node.
# drops it in %USERPROFILE%\.altimate\bin - it does NOT depend on npm/Node.
#
# Usage:
# powershell -c "irm https://www.altimate.sh/install.ps1 | iex"
Expand Down Expand Up @@ -64,8 +64,8 @@ if ($Help) {
exit 0
}

# A single P/Invoke type carries both native calls we need the AVX2 CPU probe
# (kernel32) and the PATH-change broadcast (user32) so we Add-Type once instead
# A single P/Invoke type carries both native calls we need - the AVX2 CPU probe
# (kernel32) and the PATH-change broadcast (user32) - so we Add-Type once instead
# of compiling a throwaway type per call site.
function Initialize-Native {
if (-not ("Win32.AltimateNative" -as [type])) {
Expand Down Expand Up @@ -101,27 +101,43 @@ function Test-Avx2 {
Initialize-Native
return [bool][Win32.AltimateNative]::IsProcessorFeaturePresent(40)
} catch {
# If detection fails, assume no AVX2 and fall back to the baseline build
# If detection fails, assume no AVX2 and fall back to the baseline build -
# the baseline binary runs everywhere, an AVX2 binary on a non-AVX2 CPU crashes.
return $false
}
}

# ---------------------------------------------------------------------------
# Resolve version (once) latest tag or a pinned release
# Resolve version (once) - latest tag or a pinned release
# ---------------------------------------------------------------------------
if ([string]::IsNullOrWhiteSpace($Version)) {
$useLatest = $true
try {
$rel = Invoke-RestMethod -Uri "https://api.github.com/repos/AltimateAI/altimate-code/releases/latest" -Headers @{ "User-Agent" = "altimate-install" }
$specificVersion = ($rel.tag_name -replace '^v', '')
} catch {
Write-Err "Failed to fetch version information"
exit 1
# The download below resolves "latest" server-side (releases/latest/download),
# so this API call only feeds the version-string display and the
# already-installed short-circuit. A transient api.github.com blip or the
# unauthenticated rate limit (60/hr/IP) must NOT abort the install - retry a
# few times, then proceed without the version string.
$specificVersion = ""
for ($attempt = 1; $attempt -le 3; $attempt++) {
try {
# -TimeoutSec 10 bounds a stuck socket: Invoke-RestMethod defaults to 100s
# on PS 5.1 and is effectively unbounded on PS 7+, so without it three
# back-to-back retries on dead air could freeze for minutes.
$rel = Invoke-RestMethod -Uri "https://api.github.com/repos/AltimateAI/altimate-code/releases/latest" -Headers @{ "User-Agent" = "altimate-install" } -TimeoutSec 10
$specificVersion = ($rel.tag_name -replace '^v', '')
if (-not [string]::IsNullOrWhiteSpace($specificVersion)) { break }
} catch {}
if ($attempt -lt 3) { Start-Sleep -Seconds $attempt }
}
if ([string]::IsNullOrWhiteSpace($specificVersion)) {
Write-Err "Failed to fetch version information"
exit 1
Write-Muted "Could not resolve the latest version from GitHub (API unavailable) - installing the latest release anyway."
# Reset to $null (not ""): the already-installed short-circuit below compares
# $installedVersion -eq $specificVersion. If the version probe of a missing or
# corrupt binary also yields "", an "" -eq "" match would falsely report
# "already installed" and skip the reinstall. $null -eq "" is $false, so the
# comparison correctly falls through; the banner still shows "latest" because
# if ($specificVersion) treats $null as falsy.
$specificVersion = $null
}
} else {
$useLatest = $false
Expand Down Expand Up @@ -177,19 +193,13 @@ function Install-Target {
}

Write-Host ""
Write-Host "Installing $App version: $specificVersion"
Write-Host "Installing $App version: $(if ($specificVersion) { $specificVersion } else { 'latest' })"

$tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "altimate_install_$PID"
New-Item -ItemType Directory -Force -Path $tmpDir | Out-Null
$zipPath = Join-Path $tmpDir $filename

try {
# NOTE: integrity verification (SHA256/signature) of the archive is
# intentionally deferred to match the bash installer's posture — both rely
# on HTTPS from github.com release assets. Releases do not currently publish
# a checksums file; adding one + verifying it in both installers is tracked
# as a follow-up. See PR #930 discussion.
#
# Prefer curl.exe (ships with Windows 10 1803+) for a fast download with
# --fail so HTTP errors don't write an error page to disk; fall back to
# Invoke-WebRequest where curl.exe is unavailable.
Expand All @@ -210,7 +220,7 @@ function Install-Target {

# Windows locks a running .exe, so `altimate upgrade` (which re-runs this
# installer) can't overwrite the binary that is currently executing. Windows
# *does* allow renaming a running exe move the old one aside first, then
# *does* allow renaming a running exe - move the old one aside first, then
# drop the new one in. Best-effort cleanup of the stale copy afterward.
if (Test-Path $InstalledBinary) {
$stale = "$InstalledBinary.old"
Expand All @@ -237,7 +247,7 @@ if (-not $needsBaseline) {
& $InstalledBinary --version *> $null
$code = $LASTEXITCODE
if ($code -eq 3221225501 -or $code -eq 1073741795 -or $code -eq -1073741795) {
Write-Muted "CPU lacks AVX2 reinstalling the baseline build"
Write-Muted "CPU lacks AVX2 - reinstalling the baseline build"
Install-Target -Baseline:$true
}
}
Expand Down
67 changes: 67 additions & 0 deletions packages/opencode/test/install/version-fetch-resilience.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Latest-version resolution must be resilient, in BOTH installers.
*
* The `latest` install path hits api.github.com/.../releases/latest only for the
* version-string display + the already-installed short-circuit — the download
* itself uses releases/latest/download/<file> (server-side latest). A transient
* 504 or the 60/hr/IP unauthenticated rate limit must NOT abort the install:
* retry a few times, then degrade gracefully and install latest anyway.
*/
import { describe, test, expect } from "bun:test"
import { readFileSync } from "node:fs"
import { join } from "node:path"

const REPO_ROOT = join(import.meta.dir, "../../../..")
const BASH = readFileSync(join(REPO_ROOT, "install"), "utf-8")
const PS1 = readFileSync(join(REPO_ROOT, "install.ps1"), "utf-8")

describe("bash installer — latest-version fetch is non-fatal", () => {
test("retries the releases/latest API call", () => {
expect(BASH).toContain("for attempt in 1 2 3")
// --fail so a 504 errors out (and retries) instead of parsing an error body.
expect(BASH).toContain("curl -fsSL --max-time 10 https://api.github.com")
})

test("the retry assignment absorbs curl failure so set -e can't abort it", () => {
// Under `set -euo pipefail`, a failing `curl --fail` propagates through the
// pipeline + assignment and aborts the script before the loop can retry or
// degrade. The trailing `|| true` keeps the retry loop alive.
expect(BASH).toMatch(/curl -fsSL --max-time 10 https:\/\/api\.github\.com[^\n]*\|\| true/)
})

test("bounds the API call with a transfer timeout", () => {
expect(BASH).toContain("--max-time 10")
})

test("degrades gracefully instead of exiting on API failure", () => {
expect(BASH).toContain("installing the latest release anyway")
// The old fatal hard-fail must be gone from the latest path.
expect(BASH).not.toContain("Failed to fetch version information")
})

test("only short-circuits as already-installed on a real version match", () => {
expect(BASH).toContain('[ -n "$specific_version" ] && [[ "$installed_version" == "$specific_version" ]]')
})
})

describe("PowerShell installer — latest-version fetch is non-fatal", () => {
test("retries the releases/latest API call", () => {
expect(PS1).toContain("for ($attempt = 1; $attempt -le 3; $attempt++)")
})

test("bounds the API call with a request timeout", () => {
expect(PS1).toContain("-TimeoutSec 10")
})

test("degrades gracefully instead of exiting on API failure", () => {
expect(PS1).toContain("installing the latest release anyway")
// The old fatal hard-fail must be gone.
expect(PS1).not.toContain("Failed to fetch version information")
})

test("resets the unresolved version to $null so empty==empty can't false-match", () => {
// $installedVersion -eq $specificVersion with both "" would falsely report
// "already installed" for a missing/corrupt binary; $null -eq "" is $false.
expect(PS1).toContain("$specificVersion = $null")
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,17 @@ describe("PR #930 install.ps1 release URL construction", () => {
expect(versionBlock).toContain("exit 1")
})

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

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

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