Files
mantrae/pkg/build/update.go
2025-11-12 17:24:59 +01:00

248 lines
4.9 KiB
Go

package build
import (
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/mizuchilabs/mantrae/pkg/meta"
)
const (
RepoURL = "https://api.github.com/repos/mizuchilabs/mantrae"
)
type releaseAsset struct {
Name string `json:"name"`
DownloadURL string `json:"browser_download_url"`
ID int `json:"id"`
Size int `json:"size"`
}
type release struct {
Name string `json:"name"`
Tag string `json:"tag_name"`
Published string `json:"published_at"`
URL string `json:"html_url"`
Body string `json:"body"`
Assets []*releaseAsset `json:"assets"`
ID int `json:"id"`
}
func Update(update bool) {
if runningInDocker() {
slog.Info("Running in docker, skipping update")
return
}
latest, err := fetchLatestRelease()
if err != nil {
slog.Error("Update failed", "Error", err)
return
}
if !update {
if compareVersions(
strings.TrimPrefix(meta.Version, "v"),
strings.TrimPrefix(latest.Tag, "v"),
) <= 0 {
slog.Info("You are running the latest version!")
return
}
slog.Info("New version available!", "latest", latest.Tag, "current", meta.Version)
return
}
asset := latest.findBinary(filepath.Base(os.Args[0]))
if asset == nil {
slog.Info("Unsupported platform", "platform", runtime.GOOS+"/"+runtime.GOARCH)
return
}
exec, err := os.Executable()
if err != nil {
slog.Error("Update failed", "Error", err)
return
}
if err := os.Remove(exec); err != nil {
slog.Error("Failed to remove current executable", "Error", err)
return
}
slog.Info("Downloading...", "release", latest.Tag, "binary", asset.Name)
if err := downloadFile(asset.DownloadURL, exec); err != nil {
slog.Error("Failed to download", "Error", err)
return
}
slog.Info("Update success!")
}
func runningInDocker() bool {
if _, err := os.Stat("/.dockerenv"); err == nil {
return true
}
return false
}
func fetchLatestRelease() (*release, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
url := fmt.Sprintf("%s/releases/latest", RepoURL)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer func() {
if err = res.Body.Close(); err != nil {
slog.Error("failed to close response body", "error", err)
}
}()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("(%d) failed to send latest release request", res.StatusCode)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
result := &release{}
if err := json.Unmarshal(body, result); err != nil {
return nil, err
}
return result, nil
}
func downloadFile(url string, dest string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer func() {
if err = res.Body.Close(); err != nil {
slog.Error("failed to close response body", "error", err)
}
}()
if res.StatusCode != http.StatusOK {
return fmt.Errorf("(%d) failed to send download file request", res.StatusCode)
}
out, err := os.Create(dest)
if err != nil {
return err
}
defer func() {
if err = out.Close(); err != nil {
slog.Error("failed to close output file", "error", err)
}
}()
if _, err := io.Copy(out, res.Body); err != nil {
return err
}
if err := out.Chmod(0o755); err != nil {
return err
}
return nil
}
func (r *release) findBinary(name string) *releaseAsset {
var assetName string
switch runtime.GOOS {
case "linux":
switch runtime.GOARCH {
case "amd64":
assetName = name + "_linux_amd64"
case "arm64":
assetName = name + "_linux_arm64"
case "arm":
assetName = name + "_linux_armv7"
}
case "darwin":
switch runtime.GOARCH {
case "amd64":
assetName = name + "_darwin_amd64"
case "arm64":
assetName = name + "_darwin_arm64"
}
case "windows":
switch runtime.GOARCH {
case "amd64":
assetName = name + "_windows_amd64"
case "arm64":
assetName = name + "_windows_arm64"
}
}
for _, asset := range r.Assets {
if assetName == asset.Name {
return asset
}
}
return nil
}
func compareVersions(a, b string) int {
aSplit := strings.Split(a, ".")
aTotal := len(aSplit)
bSplit := strings.Split(b, ".")
bTotal := len(bSplit)
limit := max(aTotal, bTotal)
for i := range limit {
var x, y int
if i < aTotal {
x, _ = strconv.Atoi(aSplit[i])
}
if i < bTotal {
y, _ = strconv.Atoi(bSplit[i])
}
if x < y {
return 1 // b is newer
}
if x > y {
return -1 // a is newer
}
}
return 0 // equal
}