detached 872a312

fix(install): resolve latest version via GitHub API on Windows

The PowerShell installer scraped the tag from the /releases/latest
redirect via $resp.BaseResponse.ResponseUri, which only exists on
Windows PowerShell 5.1. On PowerShell 7 (the default iwr|iex runtime)
BaseResponse is an HttpResponseMessage with no ResponseUri, so the value
was null, the regex never matched, and the script died with 'could not
resolve latest version' even though zot is public.

Resolve via api.github.com/.../releases/latest instead (tag_name is
returned directly, identical across PS 5.1 and 7+), add status-aware
error messages (404 / 401-403 / other), and drop the stale private-repo
note. Verified on PowerShell 7.7.
commit
872a312a065a3e4fc90c030f674aaf4e2f5363d9
author
patriceckhart · · 2026-05-29 09:26 UTC
parent
edbbcc1
signature
unsigned
refs
tag: v0.2.2
1 file · +34 -12
M install.ps1 +34 -12
@@ -13,9 +13,10 @@
1313 # and moves it into $ZOT_PREFIX (defaults to $HOME\bin, added to PATH
1414 # via the User environment if missing).
1515 #
16-# While the repo is private, set $env:GITHUB_TOKEN to a PAT with
17-# `contents:read` scope; the script uses it for every download. Once
18-# the repo goes public, the token becomes optional.
16+# $env:GITHUB_TOKEN is optional for the public repo. Set it to a PAT
17+# with `contents:read` scope if you hit GitHub API rate limits (or if
18+# you are installing from a private fork); the script then uses it for
19+# the version lookup and every download.
1920
2021
2122 [CmdletBinding()]
@@ -58,20 +59,41 @@ if ($arch -eq "arm64") {
5859 }
5960
6061 # ---- resolve version ----
62+#
63+# Resolve "latest" through the GitHub releases API. This works the same
64+# on Windows PowerShell 5.1 and PowerShell 7+, unlike scraping the
65+# /releases/latest redirect target: on PS7 the final URL lives at
66+# $resp.BaseResponse.RequestMessage.RequestUri while on PS5.1 it is
67+# $resp.BaseResponse.ResponseUri, and relying on either breaks on the
68+# other runtime. The API returns the tag directly, so there is nothing
69+# to scrape.
6170
6271 if ($Version -eq "latest") {
63- if ($headers.ContainsKey("Authorization")) {
64- $api = Invoke-RestMethod -Headers $headers "https://api.github.com/repos/$owner/$repo/releases/latest"
65- $Version = $api.tag_name
66- } else {
67- $resp = Invoke-WebRequest -UseBasicParsing -MaximumRedirection 5 `
68- "https://github.com/$owner/$repo/releases/latest"
69- if ($resp.BaseResponse.ResponseUri -match '/tag/([^/]+)') {
70- $Version = $Matches[1]
72+ $apiUrl = "https://api.github.com/repos/$owner/$repo/releases/latest"
73+ # GitHub's API wants a User-Agent; Invoke-RestMethod sets one, but be
74+ # explicit so corporate proxies that strip it don't trip a 403.
75+ $apiHeaders = @{} + $headers
76+ if (-not $apiHeaders.ContainsKey("User-Agent")) { $apiHeaders["User-Agent"] = "zot-installer" }
77+ $apiHeaders["Accept"] = "application/vnd.github+json"
78+
79+ try {
80+ $api = Invoke-RestMethod -UseBasicParsing -Headers $apiHeaders -Uri $apiUrl
81+ } catch {
82+ $status = $null
83+ try { $status = [int]$_.Exception.Response.StatusCode } catch {}
84+ if ($status -eq 404) {
85+ Die "no published release found for $owner/$repo (the repo may have no releases yet)"
86+ } elseif ($status -eq 401 -or $status -eq 403) {
87+ Die "GitHub API request was rejected ($status). If the repo is private, set `$env:GITHUB_TOKEN to a PAT with contents:read; otherwise you may be rate-limited (try again later or set `$env:GITHUB_TOKEN)."
7188 } else {
72- Die "could not resolve latest version (set `$env:GITHUB_TOKEN if the repo is private)"
89+ Die "could not resolve latest version: $($_.Exception.Message)"
7390 }
7491 }
92+
93+ $Version = $api.tag_name
94+ if (-not $Version) {
95+ Die "could not resolve latest version: GitHub API returned no tag_name for $owner/$repo"
96+ }
7597 }
7698
7799 if (-not $Version.StartsWith("v")) { $Version = "v$Version" }
@@ -13,9 +13,10 @@
13 # and moves it into $ZOT_PREFIX (defaults to $HOME\bin, added to PATH13 # and moves it into $ZOT_PREFIX (defaults to $HOME\bin, added to PATH
14 # via the User environment if missing).14 # via the User environment if missing).
15 #15 #
16-# While the repo is private, set $env:GITHUB_TOKEN to a PAT with16+# $env:GITHUB_TOKEN is optional for the public repo. Set it to a PAT
17-# `contents:read` scope; the script uses it for every download. Once17+# with `contents:read` scope if you hit GitHub API rate limits (or if
18-# the repo goes public, the token becomes optional.18+# you are installing from a private fork); the script then uses it for
19+# the version lookup and every download.
19 20
20 21
21 [CmdletBinding()]22 [CmdletBinding()]
@@ -58,20 +59,41 @@ if ($arch -eq "arm64") {
58 }59 }
59 60
60 # ---- resolve version ----61 # ---- resolve version ----
62+#
63+# Resolve "latest" through the GitHub releases API. This works the same
64+# on Windows PowerShell 5.1 and PowerShell 7+, unlike scraping the
65+# /releases/latest redirect target: on PS7 the final URL lives at
66+# $resp.BaseResponse.RequestMessage.RequestUri while on PS5.1 it is
67+# $resp.BaseResponse.ResponseUri, and relying on either breaks on the
68+# other runtime. The API returns the tag directly, so there is nothing
69+# to scrape.
61 70
62 if ($Version -eq "latest") {71 if ($Version -eq "latest") {
63- if ($headers.ContainsKey("Authorization")) {72+ $apiUrl = "https://api.github.com/repos/$owner/$repo/releases/latest"
64- $api = Invoke-RestMethod -Headers $headers "https://api.github.com/repos/$owner/$repo/releases/latest"73+ # GitHub's API wants a User-Agent; Invoke-RestMethod sets one, but be
65- $Version = $api.tag_name74+ # explicit so corporate proxies that strip it don't trip a 403.
66- } else {75+ $apiHeaders = @{} + $headers
67- $resp = Invoke-WebRequest -UseBasicParsing -MaximumRedirection 5 `76+ if (-not $apiHeaders.ContainsKey("User-Agent")) { $apiHeaders["User-Agent"] = "zot-installer" }
68- "https://github.com/$owner/$repo/releases/latest"77+ $apiHeaders["Accept"] = "application/vnd.github+json"
69- if ($resp.BaseResponse.ResponseUri -match '/tag/([^/]+)') {78+
70- $Version = $Matches[1]79+ try {
80+ $api = Invoke-RestMethod -UseBasicParsing -Headers $apiHeaders -Uri $apiUrl
81+ } catch {
82+ $status = $null
83+ try { $status = [int]$_.Exception.Response.StatusCode } catch {}
84+ if ($status -eq 404) {
85+ Die "no published release found for $owner/$repo (the repo may have no releases yet)"
86+ } elseif ($status -eq 401 -or $status -eq 403) {
87+ Die "GitHub API request was rejected ($status). If the repo is private, set `$env:GITHUB_TOKEN to a PAT with contents:read; otherwise you may be rate-limited (try again later or set `$env:GITHUB_TOKEN)."
71 } else {88 } else {
72- Die "could not resolve latest version (set `$env:GITHUB_TOKEN if the repo is private)"89+ Die "could not resolve latest version: $($_.Exception.Message)"
73 }90 }
74 }91 }
92+
93+ $Version = $api.tag_name
94+ if (-not $Version) {
95+ Die "could not resolve latest version: GitHub API returned no tag_name for $owner/$repo"
96+ }
75 }97 }
76 98
77 if (-not $Version.StartsWith("v")) { $Version = "v$Version" }99 if (-not $Version.StartsWith("v")) { $Version = "v$Version" }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close