mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-21 21:05:09 +00:00
* feat: add browseros-cli self-updater * fix: address review comments for 0327-cli_self_updater * fix: address PR review comments for 0327-cli_self_updater * fix: replace goreleaser with Makefile-based release build Remove .goreleaser.yml (required Pro license for monorepo field) and consolidate cross-compilation into `make release`. CI now uses the same Makefile target, fixing a bug where POSTHOG_API_KEY was missing from release ldflags. * fix: address critical self-updater bugs from code review - Fix SHA256 checksum mismatch: verify archive checksum before extraction instead of verifying extracted binary against archive hash (was always failing). Add VerifyChecksum() and integration test. - Fix JSON field name mismatch: TypeScript was emitting camelCase (publishedAt, archiveFormat) but Go expected snake_case (published_at, archive_format). Manifest parsing was silently broken. - Add decompression size limit (256 MB) to prevent zip/gzip bombs. - Don't update LastCheckedAt on transient errors so retry happens on next CLI invocation instead of waiting 24h.
145 lines
3.1 KiB
Go
145 lines
3.1 KiB
Go
package update
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"golang.org/x/mod/semver"
|
|
)
|
|
|
|
const maxManifestSize = 1 << 20
|
|
|
|
type Manifest struct {
|
|
Version string `json:"version"`
|
|
PublishedAt string `json:"published_at"`
|
|
Tag string `json:"tag"`
|
|
Assets map[string]Asset `json:"assets"`
|
|
}
|
|
|
|
type Asset struct {
|
|
Filename string `json:"filename"`
|
|
URL string `json:"url"`
|
|
ArchiveFormat string `json:"archive_format"`
|
|
SHA256 string `json:"sha256"`
|
|
}
|
|
|
|
func FetchManifest(
|
|
ctx context.Context,
|
|
client *http.Client,
|
|
url string,
|
|
) (*Manifest, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("update manifest returned HTTP %d", resp.StatusCode)
|
|
}
|
|
|
|
var manifest Manifest
|
|
if err := json.NewDecoder(io.LimitReader(resp.Body, maxManifestSize)).Decode(&manifest); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := manifest.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &manifest, nil
|
|
}
|
|
|
|
func (m *Manifest) Validate() error {
|
|
if m == nil {
|
|
return fmt.Errorf("update manifest is nil")
|
|
}
|
|
if !IsReleaseVersion(m.Version) {
|
|
return fmt.Errorf("invalid manifest version %q", m.Version)
|
|
}
|
|
if len(m.Assets) == 0 {
|
|
return fmt.Errorf("update manifest has no assets")
|
|
}
|
|
|
|
for key, asset := range m.Assets {
|
|
if asset.URL == "" {
|
|
return fmt.Errorf("asset %q is missing url", key)
|
|
}
|
|
if asset.SHA256 == "" {
|
|
return fmt.Errorf("asset %q is missing sha256", key)
|
|
}
|
|
if asset.ArchiveFormat != "tar.gz" && asset.ArchiveFormat != "zip" {
|
|
return fmt.Errorf("asset %q has unsupported archive format %q", key, asset.ArchiveFormat)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func NormalizeVersion(version string) string {
|
|
value := strings.TrimSpace(version)
|
|
if value == "" {
|
|
return ""
|
|
}
|
|
if !strings.HasPrefix(value, "v") {
|
|
value = "v" + value
|
|
}
|
|
return semver.Canonical(value)
|
|
}
|
|
|
|
func IsReleaseVersion(version string) bool {
|
|
return NormalizeVersion(version) != ""
|
|
}
|
|
|
|
func CompareVersions(current, latest string) (int, error) {
|
|
normalizedCurrent := NormalizeVersion(current)
|
|
if normalizedCurrent == "" {
|
|
return 0, fmt.Errorf("invalid current version %q", current)
|
|
}
|
|
|
|
normalizedLatest := NormalizeVersion(latest)
|
|
if normalizedLatest == "" {
|
|
return 0, fmt.Errorf("invalid latest version %q", latest)
|
|
}
|
|
|
|
return semver.Compare(normalizedCurrent, normalizedLatest), nil
|
|
}
|
|
|
|
func PlatformKey(goos, goarch string) (string, error) {
|
|
switch goos {
|
|
case "darwin", "linux", "windows":
|
|
default:
|
|
return "", fmt.Errorf("unsupported os %q", goos)
|
|
}
|
|
|
|
switch goarch {
|
|
case "amd64", "arm64":
|
|
default:
|
|
return "", fmt.Errorf("unsupported arch %q", goarch)
|
|
}
|
|
|
|
return goos + "/" + goarch, nil
|
|
}
|
|
|
|
func SelectAsset(manifest *Manifest, goos, goarch string) (Asset, error) {
|
|
key, err := PlatformKey(goos, goarch)
|
|
if err != nil {
|
|
return Asset{}, err
|
|
}
|
|
|
|
asset, ok := manifest.Assets[key]
|
|
if !ok {
|
|
return Asset{}, fmt.Errorf("no update asset for %s", key)
|
|
}
|
|
|
|
return asset, nil
|
|
}
|