- windows/installer.nsi: installs to Program Files, manages system PATH, registers uninstaller - relspec update [--check] [--yes]: compares against the latest release and prompts; on Windows downloads and runs the installer - release workflow builds and uploads relspec-setup-windows-amd64.exe - make installer-windows
116 lines
2.9 KiB
Go
116 lines
2.9 KiB
Go
// Package updatecheck looks up the latest RelSpec release on the project's
|
|
// Gitea instance and compares it against the running version.
|
|
package updatecheck
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// DefaultAPIURL is the Gitea endpoint that returns the latest release.
|
|
const DefaultAPIURL = "https://git.warky.dev/api/v1/repos/wdevs/relspecgo/releases/latest"
|
|
|
|
// Asset is a downloadable file attached to a release.
|
|
type Asset struct {
|
|
Name string `json:"name"`
|
|
URL string `json:"browser_download_url"`
|
|
}
|
|
|
|
// Release is the subset of the Gitea release payload that is needed.
|
|
type Release struct {
|
|
Tag string `json:"tag_name"`
|
|
URL string `json:"html_url"`
|
|
Assets []Asset `json:"assets"`
|
|
}
|
|
|
|
// Latest fetches the latest release from apiURL. A nil client uses a client
|
|
// with a 10 second timeout.
|
|
func Latest(ctx context.Context, client *http.Client, apiURL string) (*Release, error) {
|
|
if client == nil {
|
|
client = &http.Client{Timeout: 10 * time.Second}
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("checking for updates: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("checking for updates: unexpected status %s", resp.Status)
|
|
}
|
|
|
|
var rel Release
|
|
if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil {
|
|
return nil, fmt.Errorf("decoding release: %w", err)
|
|
}
|
|
if rel.Tag == "" {
|
|
return nil, fmt.Errorf("release has no tag")
|
|
}
|
|
return &rel, nil
|
|
}
|
|
|
|
// FindAsset returns the first asset with the given name.
|
|
func (r *Release) FindAsset(name string) (Asset, bool) {
|
|
for _, a := range r.Assets {
|
|
if a.Name == name {
|
|
return a, true
|
|
}
|
|
}
|
|
return Asset{}, false
|
|
}
|
|
|
|
// IsNewer reports whether latest is a higher version than current. Versions
|
|
// that are not dotted numbers (for example "dev" or a commit hash) are never
|
|
// considered outdated, so development builds are not nagged.
|
|
func IsNewer(current, latest string) bool {
|
|
cur, ok := parseVersion(current)
|
|
if !ok {
|
|
return false
|
|
}
|
|
lat, ok := parseVersion(latest)
|
|
if !ok {
|
|
return false
|
|
}
|
|
for i := range cur {
|
|
if lat[i] != cur[i] {
|
|
return lat[i] > cur[i]
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// parseVersion parses "v1.2.3" style versions into three numeric parts.
|
|
// Missing parts are zero; any pre-release or build suffix is ignored.
|
|
func parseVersion(v string) ([3]int, bool) {
|
|
var out [3]int
|
|
v = strings.TrimPrefix(strings.TrimSpace(v), "v")
|
|
if i := strings.IndexAny(v, "-+ "); i >= 0 {
|
|
v = v[:i]
|
|
}
|
|
if v == "" {
|
|
return out, false
|
|
}
|
|
parts := strings.Split(v, ".")
|
|
if len(parts) > 3 {
|
|
return out, false
|
|
}
|
|
for i, p := range parts {
|
|
n, err := strconv.Atoi(p)
|
|
if err != nil || n < 0 {
|
|
return out, false
|
|
}
|
|
out[i] = n
|
|
}
|
|
return out, true
|
|
}
|