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" }