Fixes for version selection

This commit is contained in:
Philip Peterson 2026-05-28 16:59:09 -07:00
parent 12b6654c2f
commit f26928306b
3 changed files with 121 additions and 0 deletions

45
CLAUDE.md Normal file
View file

@ -0,0 +1,45 @@
# hyper-quineglobal-com
Website and auto-update feed server for [Quine Hyper](https://github.com/quine-global/hyper), a fork of the Hyper terminal.
## Commands
```bash
make dev # start Go server + Tailwind watcher (Ctrl-C stops both)
make start # Go server only
make test # go test -shuffle on ./...
make css # one-shot Tailwind build
make lint # golangci-lint
make build # Docker image (linux/amd64)
make push # build + push to forge.quinefoundation.com
```
## Architecture
Pure Go HTTP server (`go-chi/chi`). HTML is rendered server-side with `maragu.dev/gomponents` (no templates). CSS is Tailwind, compiled to `public/styles/app.css`.
```
cmd/app/ entry point
http/ route handlers, server, release cache, update-check logic
html/ gomponents page renderers and shared types (Release, DownloadProps, etc.)
public/ static assets (images, compiled CSS)
scripts/ release refresh helper script
```
## Key concepts
**Release cache** (`http/releases.go`): fetches GitHub releases from the `quine-global/hyper` repo via the GitHub API, caches them in memory. `TryRefresh` is debounced (called per-request, no-ops if cache is fresh or a fetch is already running). `ForceRefresh` is used by the internal `/internal/refresh` endpoint.
**Asset classification** (`classifyAsset` in `http/releases.go`): maps GitHub asset filenames to `(os, arch)` keys used in `Release.Assets`. Supported platforms: `mac/{arm64,x64}`, `windows/x64`, `linux/{arm64,x64}` (AppImage), `linux-rpm/{arm64,x64}`. armv7l and other unrecognised ARM variants are explicitly excluded (return `ok=false`) so they don't get misclassified as x64.
**Auto-update feed** (`http/updatecheck.go`): implements the Hazel/Squirrel update protocol originally served at `releases.hyper.is`. Responds to `GET /api/update-check` and `GET /update` with 204 (no update) or 200 JSON `{name, notes, pub_date, url}`. Handles stable and canary tracks; platform strings from the Electron client (`darwin`, `win32`, `deb`, etc.) are normalised via `normalizePlatformForAssets`.
**Download page** (`http/home.go`, `html/download.go`): detects platform from `User-Agent`, supports manual override via `?os=` and `?arch=` query params. Linux format toggle (AppImage vs RPM) is a separate `?os=linux-rpm` variant.
## Testing
Tests live alongside source in `package http` (internal) or `package http_test` (integration). Run with `make test`. Notable test files:
- `http/releases_test.go` — `classifyAsset` and `parseTag`
- `http/updatecheck_test.go` — `semverCmp`, `newerCanary`, `normalizePlatformForAssets`
- `http/server_test.go` — integration test that starts the real server

View file

@ -184,6 +184,8 @@ func classifyAsset(name string) (os, arch string, ok bool) {
return "mac", "x64", true
case strings.Contains(l, "arm64") && strings.HasSuffix(l, ".appimage"):
return "linux", "arm64", true
case strings.Contains(l, "armv7l") && strings.HasSuffix(l, ".appimage"):
return "", "", false
case strings.HasSuffix(l, ".appimage"):
return "linux", "x64", true
case strings.Contains(l, "arm64") && strings.HasSuffix(l, ".rpm"):

74
http/releases_test.go Normal file
View file

@ -0,0 +1,74 @@
package http
import (
"testing"
"maragu.dev/is"
)
func TestClassifyAsset(t *testing.T) {
cases := []struct {
name string
wantOS string
wantArch string
wantOK bool
}{
// Windows
{"QuineHyper.Setup.4.0.0.exe", "windows", "x64", true},
{"QuineHyper.Setup.4.0.0-q.exe", "windows", "x64", true},
// macOS
{"QuineHyper-4.0.0.dmg", "mac", "x64", true},
{"QuineHyper-4.0.0-arm64.dmg", "mac", "arm64", true},
// Linux AppImage x64
{"QuineHyper-4.0.0.AppImage", "linux", "x64", true},
{"quinehyper-4.0.0.appimage", "linux", "x64", true},
// Linux AppImage arm64
{"QuineHyper-4.0.0-arm64.AppImage", "linux", "arm64", true},
// Linux AppImage armv7l — must NOT be classified as x64
{"QuineHyper-4.0.0-armv7l.AppImage", "", "", false},
{"quinehyper-4.0.0-armv7l.appimage", "", "", false},
// Linux RPM
{"QuineHyper-4.0.0.rpm", "linux-rpm", "x64", true},
{"QuineHyper-4.0.0-arm64.rpm", "linux-rpm", "arm64", true},
// Unknown / unrecognised
{"QuineHyper-4.0.0.zip", "", "", false},
{"latest.yml", "", "", false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
gotOS, gotArch, gotOK := classifyAsset(c.name)
is.Equal(t, c.wantOK, gotOK)
is.Equal(t, c.wantOS, gotOS)
is.Equal(t, c.wantArch, gotArch)
})
}
}
func TestParseTag(t *testing.T) {
cases := []struct {
tag string
wantVer string
wantBuild int
}{
{"4.0.0", "4.0.0", 0},
{"4.0.0-q", "4.0.0", 0},
{"4.0.0q", "4.0.0", 0},
{"4.0.0-q-canary.12", "4.0.0", 12},
{"4.0.0-q-canary.1", "4.0.0", 1},
}
for _, c := range cases {
t.Run(c.tag, func(t *testing.T) {
gotVer, gotBuild := parseTag(c.tag)
is.Equal(t, c.wantVer, gotVer)
is.Equal(t, c.wantBuild, gotBuild)
})
}
}