From 41b87f181b69830819d26e39ee5a9a9e03ee6ee4 Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Sat, 3 Oct 2026 04:13:34 -0400 Subject: [PATCH] Fall back to the releases/latest redirect in install.ps1 too install.ps1 resolved the version through the GitHub API only, so a spent unauthenticated rate limit (60 requests an hour per IP) failed the install on Windows, as it did on macOS before #37. It now tries the API, then reads the tag from the releases/latest redirect, with redirects disabled so the Location header reads the same on PowerShell 5.1 and 7. The Windows installer was parsed in CI and never run. The test job now runs it on windows-latest twice: once as shipped, and once with the API URL broken so the fallback is what resolves the version. The step refuses to run the second case if the URL substitution matched nothing. --- .github/workflows/ci.yml | 23 +++++++++++++++++++++++ src/scripts/install.ps1 | 27 ++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9486514..256edb9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,6 +73,29 @@ jobs: "$dir/sshmgr" version test -x "$dir/sshmgr" + # The Windows installer is an `irm | iex` entry point and was only ever + # parsed, never run. Run it, then run it again with the API URL broken so + # the releases/latest fallback is what resolves the version. + - name: The Windows installer actually installs + if: runner.os == 'Windows' + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + foreach ($case in 'api', 'redirect-fallback') { + $script = Get-Content -Raw src/scripts/install.ps1 + if ($case -eq 'redirect-fallback') { + $broken = $script -replace 'api\.github\.com/repos/\$owner/\$repo/releases/latest', 'api.github.com/repos/$owner/$repo-forced-404/releases/latest' + if ($broken -eq $script) { throw 'could not break the API URL; the fallback would go untested' } + $script = $broken + } + $path = Join-Path $env:RUNNER_TEMP "install-$case.ps1" + Set-Content -Path $path -Value $script + $dir = Join-Path $env:RUNNER_TEMP "bin-$case" + & $path -InstallDir $dir + & (Join-Path $dir 'sshmgr.exe') version + if ($LASTEXITCODE -ne 0) { throw "sshmgr version failed ($case)" } + } + - name: E2E smoke if: runner.os != 'Windows' shell: bash diff --git a/src/scripts/install.ps1 b/src/scripts/install.ps1 index 6d243e6..8bac48e 100644 --- a/src/scripts/install.ps1 +++ b/src/scripts/install.ps1 @@ -40,9 +40,30 @@ switch ($env:PROCESSOR_ARCHITECTURE) { if (-not $InstallDir) { $InstallDir = Join-Path $env:LOCALAPPDATA "Programs\$repo" } if (-not $Version) { - $rel = Invoke-RestMethod -Headers @{ 'Accept' = 'application/vnd.github+json' } ` - "https://api.github.com/repos/$owner/$repo/releases/latest" - $Version = $rel.tag_name + try { + $rel = Invoke-RestMethod -Headers @{ 'Accept' = 'application/vnd.github+json' } ` + "https://api.github.com/repos/$owner/$repo/releases/latest" + $Version = $rel.tag_name + } catch { $Version = $null } + + # The API allows 60 unauthenticated requests an hour per IP, and anyone behind a + # shared NAT - or on a CI runner - can find that spent and get a 403. The + # releases/latest web redirect is not counted against it and names the same tag, + # so fall back to it rather than failing the install (install.sh does the same). + # HttpWebRequest with redirects off reads the Location header identically on + # Windows PowerShell 5.1 and PowerShell 7; a 3xx can surface as an exception on + # the latter, so its response is read from there too. + if (-not $Version) { + $req = [System.Net.HttpWebRequest]::Create("https://github.com/$owner/$repo/releases/latest") + $req.AllowAutoRedirect = $false + $req.Method = 'HEAD' + try { $resp = $req.GetResponse() } catch [System.Net.WebException] { $resp = $_.Exception.Response } + if ($resp) { + try { $location = [string]$resp.Headers['Location'] } finally { $resp.Close() } + if ($location -match '/releases/tag/([^/?#]+)$') { $Version = $Matches[1] } + } + } + if (-not $Version) { throw 'could not determine the version to install.' } } if ($Version -notlike 'v*') { $Version = "v$Version" }